From: Pedro Gonzalez Gomez Date: Mon, 18 Nov 2024 10:29:40 +0000 (+0100) Subject: mgr/dashboard: add smb endpoints X-Git-Tag: v20.0.0~612^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F60759%2Fhead;p=ceph.git mgr/dashboard: add smb endpoints Adds following SMB endpoints: - cluster: list, get, create - share: list, delete Fixes: https://tracker.ceph.com/issues/69044 Signed-off-by: Pedro Gonzalez Gomez --- diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index 9f9b7501f44c..d05b7551365d 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -2,7 +2,6 @@ # pylint: disable=too-many-lines import errno import json -import logging import os from collections import defaultdict from typing import Any, Dict, List @@ -30,8 +29,6 @@ GET_STATFS_SCHEMA = { 'subdirs': (int, '') } -logger = logging.getLogger("controllers.rgw") - # pylint: disable=R0904 @APIRouter('/cephfs', Scope.CEPHFS) diff --git a/src/pybind/mgr/dashboard/controllers/smb.py b/src/pybind/mgr/dashboard/controllers/smb.py new file mode 100644 index 000000000000..97eff8c3dfec --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/smb.py @@ -0,0 +1,186 @@ + +# -*- coding: utf-8 -*- + +import json +import logging +from typing import List + +from smb.enums import Intent +from smb.proto import Simplified +from smb.resources import Cluster, Share + +from dashboard.controllers._docs import EndpointDoc +from dashboard.controllers._permissions import CreatePermission, DeletePermission +from dashboard.exceptions import DashboardException + +from .. import mgr +from ..security import Scope +from . import APIDoc, APIRouter, ReadPermission, RESTController + +logger = logging.getLogger('controllers.smb') + +CLUSTER_SCHEMA = { + "resource_type": (str, "ceph.smb.cluster"), + "cluster_id": (str, "Unique identifier for the cluster"), + "auth_mode": (str, "Either 'active-directory' or 'user'"), + "intent": (str, "Desired state of the resource, e.g., 'present' or 'removed'"), + "domain_settings": ({ + "realm": (str, "Domain realm, e.g., 'DOMAIN1.SINK.TEST'"), + "join_sources": ([{ + "source_type": (str, "resource"), + "ref": (str, "Reference identifier for the join auth resource") + }], "List of join auth sources for domain settings") + }, "Domain-specific settings for active-directory auth mode"), + "user_group_settings": ([{ + "source_type": (str, "resource"), + "ref": (str, "Reference identifier for the user group resource") + }], "User group settings for user auth mode"), + "custom_dns": ([str], "List of custom DNS server addresses"), + "placement": ({ + "count": (int, "Number of instances to place") + }, "Placement configuration for the resource") +} + +CLUSTER_SCHEMA_RESULTS = { + "results": ([{ + "resource": ({ + "resource_type": (str, "ceph.smb.cluster"), + "cluster_id": (str, "Unique identifier for the cluster"), + "auth_mode": (str, "Either 'active-directory' or 'user'"), + "intent": (str, "Desired state of the resource, e.g., 'present' or 'removed'"), + "domain_settings": ({ + "realm": (str, "Domain realm, e.g., 'DOMAIN1.SINK.TEST'"), + "join_sources": ([{ + "source_type": (str, "resource"), + "ref": (str, "Reference identifier for the join auth resource") + }], "List of join auth sources for domain settings") + }, "Domain-specific settings for active-directory auth mode"), + "user_group_settings": ([{ + "source_type": (str, "resource"), + "ref": (str, "Reference identifier for the user group resource") + }], "User group settings for user auth mode (optional)"), + "custom_dns": ([str], "List of custom DNS server addresses (optional)"), + "placement": ({ + "count": (int, "Number of instances to place") + }, "Placement configuration for the resource (optional)"), + }, "Resource details"), + "state": (str, "State of the resource"), + "success": (bool, "Indicates whether the operation was successful") + }], "List of results with resource details"), + "success": (bool, "Overall success status of the operation") +} + +LIST_CLUSTER_SCHEMA = [CLUSTER_SCHEMA] + +SHARE_SCHEMA = { + "resource_type": (str, "ceph.smb.share"), + "cluster_id": (str, "Unique identifier for the cluster"), + "share_id": (str, "Unique identifier for the share"), + "intent": (str, "Desired state of the resource, e.g., 'present' or 'removed'"), + "name": (str, "Name of the share"), + "readonly": (bool, "Indicates if the share is read-only"), + "browseable": (bool, "Indicates if the share is browseable"), + "cephfs": ({ + "volume": (str, "Name of the CephFS file system"), + "path": (str, "Path within the CephFS file system"), + "provider": (str, "Provider of the CephFS share, e.g., 'samba-vfs'") + }, "Configuration for the CephFS share") +} + + +@APIRouter('/smb/cluster', Scope.SMB) +@APIDoc("SMB Cluster Management API", "SMB") +class SMBCluster(RESTController): + _resource: str = 'ceph.smb.cluster' + + @ReadPermission + @EndpointDoc("List smb clusters", + responses={200: LIST_CLUSTER_SCHEMA}) + def list(self) -> List[Cluster]: + """ + List smb clusters + """ + res = mgr.remote('smb', 'show', [self._resource]) + return res['resources'] if 'resources' in res else [res] + + @ReadPermission + @EndpointDoc("Get an smb cluster", + parameters={ + 'cluster_id': (str, 'Unique identifier for the cluster') + }, + responses={200: CLUSTER_SCHEMA}) + def get(self, cluster_id: str) -> Cluster: + """ + Get an smb cluster by cluster id + """ + return mgr.remote('smb', 'show', [f'{self._resource}.{cluster_id}']) + + @CreatePermission + @EndpointDoc("Create smb cluster", + parameters={ + 'cluster_resource': (str, 'cluster_resource') + }, + responses={201: CLUSTER_SCHEMA_RESULTS}) + def create(self, cluster_resource: Cluster) -> Simplified: + """ + Create an smb cluster + + :param cluster_resource: Dict cluster data + :return: Returns cluster resource. + :rtype: Dict[str, Any] + """ + try: + return mgr.remote( + 'smb', + 'apply_resources', + json.dumps(cluster_resource)).to_simplified() + except RuntimeError as e: + raise DashboardException(e, component='smb') + + +@APIRouter('/smb/share', Scope.SMB) +@APIDoc("SMB Share Management API", "SMB") +class SMBShare(RESTController): + _resource: str = 'ceph.smb.share' + + @ReadPermission + @EndpointDoc("List smb shares", + parameters={ + 'cluster_id': (str, 'Unique identifier for the cluster') + }, + responses={200: SHARE_SCHEMA}) + def list(self, cluster_id: str = '') -> List[Share]: + """ + List all smb shares or all shares for a given cluster + + :param cluster_id: Dict containing cluster information + :return: Returns list of shares. + :rtype: List[Dict] + """ + res = mgr.remote( + 'smb', + 'show', + [f'{self._resource}.{cluster_id}' if cluster_id else self._resource]) + return res['resources'] if 'resources' in res else res + + @DeletePermission + @EndpointDoc("Remove smb shares", + parameters={ + 'cluster_id': (str, 'Unique identifier for the cluster'), + 'share_id': (str, 'Unique identifier for the share') + }, + responses={204: None}) + def delete(self, cluster_id: str, share_id: str): + """ + Remove an smb share from a given cluster + + :param cluster_id: Cluster identifier + :param share_id: Share identifier + :return: None. + """ + resource = {} + resource['resource_type'] = self._resource + resource['cluster_id'] = cluster_id + resource['share_id'] = share_id + resource['intent'] = Intent.REMOVED + return mgr.remote('smb', 'apply_resources', json.dumps(resource)).one().to_simplified() diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index b464344e27a2..de8362980642 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -14055,6 +14055,500 @@ paths: - jwt: [] tags: - Settings + /api/smb/cluster: + get: + description: "\n List smb clusters\n " + parameters: [] + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + schema: + items: + properties: + auth_mode: + description: Either 'active-directory' or 'user' + type: string + cluster_id: + description: Unique identifier for the cluster + type: string + custom_dns: + description: List of custom DNS server addresses + items: + type: string + type: array + domain_settings: + description: Domain-specific settings for active-directory auth + mode + properties: + join_sources: + description: List of join auth sources for domain settings + items: + properties: + ref: + description: Reference identifier for the join auth + resource + type: string + source_type: + description: resource + type: string + required: + - source_type + - ref + type: object + type: array + realm: + description: Domain realm, e.g., 'DOMAIN1.SINK.TEST' + type: string + required: + - realm + - join_sources + type: object + intent: + description: Desired state of the resource, e.g., 'present' + or 'removed' + type: string + placement: + description: Placement configuration for the resource + properties: + count: + description: Number of instances to place + type: integer + required: + - count + type: object + resource_type: + description: ceph.smb.cluster + type: string + user_group_settings: + description: User group settings for user auth mode + items: + properties: + ref: + description: Reference identifier for the user group resource + type: string + source_type: + description: resource + type: string + required: + - source_type + - ref + type: object + type: array + type: object + required: + - resource_type + - cluster_id + - auth_mode + - intent + - domain_settings + - user_group_settings + - custom_dns + - placement + type: array + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: List smb clusters + tags: + - SMB + post: + description: "\n Create an smb cluster\n\n :param cluster_resource:\ + \ Dict cluster data\n :return: Returns cluster resource.\n :rtype:\ + \ Dict[str, Any]\n " + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + cluster_resource: + description: cluster_resource + type: string + required: + - cluster_resource + type: object + responses: + '201': + content: + application/vnd.ceph.api.v1.0+json: + schema: + properties: + results: + description: List of results with resource details + items: + properties: + resource: + description: Resource details + properties: + auth_mode: + description: Either 'active-directory' or 'user' + type: string + cluster_id: + description: Unique identifier for the cluster + type: string + custom_dns: + description: List of custom DNS server addresses (optional) + items: + type: string + type: array + domain_settings: + description: Domain-specific settings for active-directory + auth mode + properties: + join_sources: + description: List of join auth sources for domain + settings + items: + properties: + ref: + description: Reference identifier for the + join auth resource + type: string + source_type: + description: resource + type: string + required: + - source_type + - ref + type: object + type: array + realm: + description: Domain realm, e.g., 'DOMAIN1.SINK.TEST' + type: string + required: + - realm + - join_sources + type: object + intent: + description: Desired state of the resource, e.g., 'present' + or 'removed' + type: string + placement: + description: Placement configuration for the resource + (optional) + properties: + count: + description: Number of instances to place + type: integer + required: + - count + type: object + resource_type: + description: ceph.smb.cluster + type: string + user_group_settings: + description: User group settings for user auth mode + (optional) + items: + properties: + ref: + description: Reference identifier for the user + group resource + type: string + source_type: + description: resource + type: string + required: + - source_type + - ref + type: object + type: array + required: + - resource_type + - cluster_id + - auth_mode + - intent + - domain_settings + - user_group_settings + - custom_dns + - placement + type: object + state: + description: State of the resource + type: string + success: + description: Indicates whether the operation was successful + type: boolean + required: + - resource + - state + - success + type: object + type: array + success: + description: Overall success status of the operation + type: boolean + required: + - results + - success + type: object + description: Resource created. + '202': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: Create smb cluster + tags: + - SMB + /api/smb/cluster/{cluster_id}: + get: + description: "\n Get an smb cluster by cluster id\n " + parameters: + - description: Unique identifier for the cluster + in: path + name: cluster_id + required: true + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + schema: + properties: + auth_mode: + description: Either 'active-directory' or 'user' + type: string + cluster_id: + description: Unique identifier for the cluster + type: string + custom_dns: + description: List of custom DNS server addresses + items: + type: string + type: array + domain_settings: + description: Domain-specific settings for active-directory auth + mode + properties: + join_sources: + description: List of join auth sources for domain settings + items: + properties: + ref: + description: Reference identifier for the join auth + resource + type: string + source_type: + description: resource + type: string + required: + - source_type + - ref + type: object + type: array + realm: + description: Domain realm, e.g., 'DOMAIN1.SINK.TEST' + type: string + required: + - realm + - join_sources + type: object + intent: + description: Desired state of the resource, e.g., 'present' or + 'removed' + type: string + placement: + description: Placement configuration for the resource + properties: + count: + description: Number of instances to place + type: integer + required: + - count + type: object + resource_type: + description: ceph.smb.cluster + type: string + user_group_settings: + description: User group settings for user auth mode + items: + properties: + ref: + description: Reference identifier for the user group resource + type: string + source_type: + description: resource + type: string + required: + - source_type + - ref + type: object + type: array + required: + - resource_type + - cluster_id + - auth_mode + - intent + - domain_settings + - user_group_settings + - custom_dns + - placement + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: Get an smb cluster + tags: + - SMB + /api/smb/share: + get: + description: "\n List all smb shares or all shares for a given cluster\n\ + \n :param cluster_id: Dict containing cluster information\n \ + \ :return: Returns list of shares.\n :rtype: List[Dict]\n " + parameters: + - default: '' + description: Unique identifier for the cluster + in: query + name: cluster_id + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + schema: + properties: + browseable: + description: Indicates if the share is browseable + type: boolean + cephfs: + description: Configuration for the CephFS share + properties: + path: + description: Path within the CephFS file system + type: string + provider: + description: Provider of the CephFS share, e.g., 'samba-vfs' + type: string + volume: + description: Name of the CephFS file system + type: string + required: + - volume + - path + - provider + type: object + cluster_id: + description: Unique identifier for the cluster + type: string + intent: + description: Desired state of the resource, e.g., 'present' or + 'removed' + type: string + name: + description: Name of the share + type: string + readonly: + description: Indicates if the share is read-only + type: boolean + resource_type: + description: ceph.smb.share + type: string + share_id: + description: Unique identifier for the share + type: string + required: + - resource_type + - cluster_id + - share_id + - intent + - name + - readonly + - browseable + - cephfs + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: List smb shares + tags: + - SMB + /api/smb/share/{cluster_id}/{share_id}: + delete: + description: "\n Remove an smb share from a given cluster\n\n \ + \ :param cluster_id: Cluster identifier\n :param share_id: Share identifier\n\ + \ :return: None.\n " + parameters: + - description: Unique identifier for the cluster + in: path + name: cluster_id + required: true + schema: + type: string + - description: Unique identifier for the share + in: path + name: share_id + required: true + schema: + type: string + responses: + '202': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. + '204': + content: + application/vnd.ceph.api.v1.0+json: + schema: + properties: {} + type: object + description: Resource deleted. + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: Remove smb shares + tags: + - SMB /api/summary: get: parameters: [] @@ -15478,6 +15972,8 @@ tags: name: RgwZonegroup - description: Role Management API name: Role +- description: SMB Cluster Management API + name: SMB - description: Service Management API name: Service - description: Settings Management API diff --git a/src/pybind/mgr/dashboard/security.py b/src/pybind/mgr/dashboard/security.py index 2b624aabcc72..c329d24e1b34 100644 --- a/src/pybind/mgr/dashboard/security.py +++ b/src/pybind/mgr/dashboard/security.py @@ -27,6 +27,7 @@ class Scope(object): DASHBOARD_SETTINGS = "dashboard-settings" NFS_GANESHA = "nfs-ganesha" NVME_OF = "nvme-of" + SMB = "smb" @classmethod def all_scopes(cls): diff --git a/src/pybind/mgr/dashboard/services/access_control.py b/src/pybind/mgr/dashboard/services/access_control.py index 21c1a9572bb6..6319802b6cc7 100644 --- a/src/pybind/mgr/dashboard/services/access_control.py +++ b/src/pybind/mgr/dashboard/services/access_control.py @@ -278,6 +278,16 @@ GANESHA_MGR_ROLE = Role( Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.GRAFANA: [_P.READ], + Scope.SMB: [_P.READ] + }) + +SMB_MGR_ROLE = Role( + 'smb-manager', 'allows full permissions for the smb scope', { + Scope.SMB: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], + Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], + Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], + Scope.GRAFANA: [_P.READ], + Scope.NFS_GANESHA: [_P.READ] }) @@ -290,6 +300,7 @@ SYSTEM_ROLES = { POOL_MGR_ROLE.name: POOL_MGR_ROLE, CEPHFS_MGR_ROLE.name: CEPHFS_MGR_ROLE, GANESHA_MGR_ROLE.name: GANESHA_MGR_ROLE, + SMB_MGR_ROLE.name: SMB_MGR_ROLE, } # static name-like roles list for role mapping diff --git a/src/pybind/mgr/dashboard/tests/test_smb.py b/src/pybind/mgr/dashboard/tests/test_smb.py new file mode 100644 index 000000000000..754df482add9 --- /dev/null +++ b/src/pybind/mgr/dashboard/tests/test_smb.py @@ -0,0 +1,197 @@ +import json +from unittest.mock import Mock + +from dashboard.controllers.smb import SMBCluster, SMBShare + +from .. import mgr +from ..tests import ControllerTestCase + + +class SMBClusterTest(ControllerTestCase): + _endpoint = '/api/smb/cluster' + + _clusters = { + "resources": [{ + "resource_type": "ceph.smb.cluster", + "cluster_id": "clusterADTest", + "auth_mode": "active-directory", + "intent": "present", + "domain_settings": { + "realm": "DOMAIN1.SINK.TEST", + "join_sources": [ + { + "source_type": "resource", + "ref": "join1-admin" + }] + }, + "custom_dns": [ + "192.168.76.204" + ], + "placement": { + "count": 1 + } + }, + { + "resource_type": "ceph.smb.cluster", + "cluster_id": "clusterUserTest", + "auth_mode": "user", + "intent": "present", + "user_group_settings": [ + { + "source_type": "resource", + "ref": "ug1" + } + ] + }] + } + + @classmethod + def setup_server(cls): + cls.setup_controllers([SMBCluster]) + + def test_list_one_cluster(self): + mgr.remote = Mock(return_value=self._clusters['resources'][0]) + + self._get(self._endpoint) + self.assertStatus(200) + self.assertJsonBody([self._clusters['resources'][0]]) + + def test_list_multiple_clusters(self): + mgr.remote = Mock(return_value=self._clusters) + + self._get(self._endpoint) + self.assertStatus(200) + self.assertJsonBody(self._clusters['resources']) + + def test_get_cluster(self): + mgr.remote = Mock(return_value=self._clusters['resources'][0]) + cluster_id = self._clusters['resources'][0]['cluster_id'] + self._get(f'{self._endpoint}/{cluster_id}') + self.assertStatus(200) + self.assertJsonBody(self._clusters['resources'][0]) + mgr.remote.assert_called_once_with('smb', 'show', [f'ceph.smb.cluster.{cluster_id}']) + + def test_create_ad(self): + mock_simplified = Mock() + mock_simplified.to_simplified.return_value = json.dumps(self._clusters['resources'][0]) + mgr.remote = Mock(return_value=mock_simplified) + + _cluster_data = """ + { "cluster_resource": { + "resource_type": "ceph.smb.cluster", + "cluster_id": "clusterADTest", + "auth_mode": "active-directory", + "domain_settings": { + "realm": "DOMAIN1.SINK.TEST", + "join_sources": [ + { + "source_type": "resource", + "ref": "join1-admin" + } + ] + } + } + } + """ + + self._post(self._endpoint, _cluster_data) + self.assertStatus(201) + self.assertInJsonBody(json.dumps(self._clusters['resources'][0])) + + def test_create_user(self): + mock_simplified = Mock() + mock_simplified.to_simplified.return_value = json.dumps(self._clusters['resources'][1]) + mgr.remote = Mock(return_value=mock_simplified) + + _cluster_data = """ + { "cluster_resource": { + "resource_type": "ceph.smb.cluster", + "cluster_id": "clusterUser123Test", + "auth_mode": "user", + "user_group_settings": [ + { + "source_type": "resource", + "ref": "ug1" + } + ] + } + } + """ + self._post(self._endpoint, _cluster_data) + self.assertStatus(201) + self.assertInJsonBody(json.dumps(self._clusters['resources'][1])) + + +class SMBShareTest(ControllerTestCase): + _endpoint = '/api/smb/share' + + _shares = [{ + "resource_type": "ceph.smb.share", + "cluster_id": "clusterUserTest", + "share_id": "share1", + "intent": "present", + "name": "share1name", + "readonly": "false", + "browseable": "true", + "cephfs": { + "volume": "fs1", + "path": "/", + "provider": "samba-vfs" + } + }, + { + "resource_type": "ceph.smb.share", + "cluster_id": "clusterADTest", + "share_id": "share2", + "intent": "present", + "name": "share2name", + "readonly": "false", + "browseable": "true", + "cephfs": { + "volume": "fs2", + "path": "/", + "provider": "samba-vfs" + } + } + ] + + @classmethod + def setup_server(cls): + cls.setup_controllers([SMBShare]) + + def test_list_all(self): + mgr.remote = Mock(return_value=self._shares) + + self._get(self._endpoint) + self.assertStatus(200) + self.assertJsonBody(self._shares) + + def test_list_from_cluster(self): + mgr.remote = Mock(return_value=self._shares[0]) + + self._get(self._endpoint) + self.assertStatus(200) + self.assertJsonBody(self._shares[0]) + + def test_delete(self): + _res = { + "resource": { + "resource_type": "ceph.smb.share", + "cluster_id": "smbCluster1", + "share_id": "share1", + "intent": "removed" + }, + "state": "removed", + "success": "true" + } + _res_simplified = { + "resource_type": "ceph.smb.share", + "cluster_id": "smbCluster1", + "share_id": "share1", + "intent": "removed" + } + mgr.remote = Mock(return_value=Mock(return_value=_res)) + mgr.remote.return_value.one.return_value.to_simplified = Mock(return_value=_res_simplified) + self._delete(f'{self._endpoint}/smbCluster1/share1') + self.assertStatus(204) + mgr.remote.assert_called_once_with('smb', 'apply_resources', json.dumps(_res_simplified))