diff --git a/src/spaceone/inventory_v2/__init__.py b/src/spaceone/inventory_v2/__init__.py index e69de29..22636ee 100644 --- a/src/spaceone/inventory_v2/__init__.py +++ b/src/spaceone/inventory_v2/__init__.py @@ -0,0 +1 @@ +name = 'inventory_v2' \ No newline at end of file diff --git a/src/spaceone/inventory_v2/error/__init__.py b/src/spaceone/inventory_v2/error/__init__.py new file mode 100644 index 0000000..84ee522 --- /dev/null +++ b/src/spaceone/inventory_v2/error/__init__.py @@ -0,0 +1 @@ +from spaceone.inventory_v2.error.region import * \ No newline at end of file diff --git a/src/spaceone/inventory_v2/error/region.py b/src/spaceone/inventory_v2/error/region.py new file mode 100644 index 0000000..8499bee --- /dev/null +++ b/src/spaceone/inventory_v2/error/region.py @@ -0,0 +1,9 @@ +from spaceone.core.error import * + + +class ERROR_NOT_FOUND_USER_IN_REGION(ERROR_BASE): + _message = 'A user "{user_id}" is not exist in region ({region_id}).' + + +class ERROR_ALREADY_EXIST_USER_IN_REGION(ERROR_BASE): + _message = 'A user "{user_id}" is already exist in region ({region_id}).' \ No newline at end of file diff --git a/src/spaceone/inventory_v2/info/__init__.py b/src/spaceone/inventory_v2/info/__init__.py new file mode 100644 index 0000000..96f2f5f --- /dev/null +++ b/src/spaceone/inventory_v2/info/__init__.py @@ -0,0 +1,2 @@ +from spaceone.inventory_v2.info.common_info import * +from spaceone.inventory_v2.info.region_info import * \ No newline at end of file diff --git a/src/spaceone/inventory_v2/info/common_info.py b/src/spaceone/inventory_v2/info/common_info.py new file mode 100644 index 0000000..2a2b7dc --- /dev/null +++ b/src/spaceone/inventory_v2/info/common_info.py @@ -0,0 +1,20 @@ +from google.protobuf.empty_pb2 import Empty +from spaceone.core.pygrpc.message_type import * + +__all__ = ['EmptyInfo', 'StatisticsInfo', 'AnalyzeInfo', 'ExportInfo'] + + +def EmptyInfo(): + return Empty() + + +def StatisticsInfo(result): + return change_struct_type(result) + + +def AnalyzeInfo(result): + return change_struct_type(result) + + +def ExportInfo(result): + return change_struct_type(result) diff --git a/src/spaceone/inventory_v2/info/region_info.py b/src/spaceone/inventory_v2/info/region_info.py new file mode 100644 index 0000000..a44baf4 --- /dev/null +++ b/src/spaceone/inventory_v2/info/region_info.py @@ -0,0 +1,38 @@ +import functools +import logging +from spaceone.api.inventory.v2 import region_pb2 +from spaceone.core.pygrpc.message_type import * +from spaceone.core import utils +from spaceone.inventory_v2.model.region_model import Region + +__all__ = ["RegionInfo", "RegionsInfo"] + +_LOGGER = logging.getLogger(__name__) + +def RegionInfo(region_vo: Region, minimal=False): + info = { + "region_id": region_vo.region_id, + "name": region_vo.name, + "region_code": region_vo.region_code, + "provider": region_vo.provider, + } + + if not minimal: + info.update( + { + "region_key": region_vo.region_key, + "tags": change_struct_type(region_vo.tags), + "domain_id": region_vo.domain_id, + "created_at": utils.datetime_to_iso8601(region_vo.created_at), + "updated_at": utils.datetime_to_iso8601(region_vo.updated_at), + } + ) + + return region_pb2.RegionInfo(**info) + + +def RegionsInfo(region_vos, total_count, **kwargs): + return region_pb2.RegionsInfo( + results=list(map(functools.partial(RegionInfo, **kwargs), region_vos)), + total_count=total_count, + ) \ No newline at end of file diff --git a/src/spaceone/inventory_v2/interface/grpc/__init__.py b/src/spaceone/inventory_v2/interface/grpc/__init__.py index c4a66ff..444bcfd 100644 --- a/src/spaceone/inventory_v2/interface/grpc/__init__.py +++ b/src/spaceone/inventory_v2/interface/grpc/__init__.py @@ -1,5 +1,7 @@ from spaceone.core.pygrpc.server import GRPCServer +from .region import Region _all_ = ["app"] app = GRPCServer() +app.add_service(Region) \ No newline at end of file diff --git a/src/spaceone/inventory_v2/interface/grpc/region.py b/src/spaceone/inventory_v2/interface/grpc/region.py new file mode 100644 index 0000000..88ab2af --- /dev/null +++ b/src/spaceone/inventory_v2/interface/grpc/region.py @@ -0,0 +1,45 @@ +from spaceone.api.inventory.v2 import region_pb2, region_pb2_grpc +from spaceone.core.pygrpc import BaseAPI + +class Region(BaseAPI, region_pb2_grpc.RegionServicer): + + pb2 = region_pb2 + pb2_grpc = region_pb2_grpc + + def create(self, request, context): + params, metadata = self.parse_request(request, context) + + with self.locator.get_service('RegionService', metadata) as region_service: + return self.locator.get_info('RegionInfo', region_service.create(params)) + + def update(self, request, context): + params, metadata = self.parse_request(request, context) + + with self.locator.get_service('RegionService', metadata) as region_service: + return self.locator.get_info('RegionInfo', region_service.update(params)) + + def delete(self, request, context): + params, metadata = self.parse_request(request, context) + + with self.locator.get_service('RegionService', metadata) as region_service: + region_service.delete(params) + return self.locator.get_info('EmptyInfo') + + def get(self, request, context): + params, metadata = self.parse_request(request, context) + + with self.locator.get_service('RegionService', metadata) as region_service: + return self.locator.get_info('RegionInfo', region_service.get(params)) + + def list(self, request, context): + params, metadata = self.parse_request(request, context) + + with self.locator.get_service('RegionService', metadata) as region_service: + region_vos, total_count = region_service.list(params) + return self.locator.get_info('RegionsInfo', region_vos, total_count, minimal=self.get_minimal(params)) + + def stat(self, request, context): + params, metadata = self.parse_request(request, context) + + with self.locator.get_service('RegionService', metadata) as region_service: + return self.locator.get_info('StatisticsInfo', region_service.stat(params)) \ No newline at end of file diff --git a/src/spaceone/inventory_v2/lib/__init__.py b/src/spaceone/inventory_v2/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/spaceone/inventory_v2/lib/resource_manager.py b/src/spaceone/inventory_v2/lib/resource_manager.py new file mode 100644 index 0000000..bec85e3 --- /dev/null +++ b/src/spaceone/inventory_v2/lib/resource_manager.py @@ -0,0 +1,43 @@ +from typing import Tuple +from spaceone.core.error import * + + +class ResourceManager(object): + resource_keys: list = None + query_method = None + + """ + This is used by collector + """ + + def find_resources(self, query: dict) -> Tuple[list, int]: + self._check_resource_finder_state() + query["only"] = self.resource_keys + + resources = [] + vos, total_count = getattr(self, self.query_method)(query) + + for vo in vos: + data = {} + for key in self.resource_keys: + data[key] = getattr(vo, key) + + resources.append(data) + + return resources, total_count + + def delete_resources(self, query: dict) -> int: + self._check_resource_finder_state() + query["only"] = self.resource_keys + ["updated_at"] + + vos, total_count = getattr(self, self.query_method)(query) + vos.delete() + + return total_count + + def _check_resource_finder_state(self) -> None: + if not (self.resource_keys and self.query_method): + raise ERROR_UNKNOWN(message="ResourceManager is not set.") + + if getattr(self, self.query_method, None) is None: + raise ERROR_UNKNOWN(message="ResourceManager is not set.") diff --git a/src/spaceone/inventory_v2/manager/__init__.py b/src/spaceone/inventory_v2/manager/__init__.py new file mode 100644 index 0000000..93bbe5b --- /dev/null +++ b/src/spaceone/inventory_v2/manager/__init__.py @@ -0,0 +1 @@ +from spaceone.inventory_v2.manager.region_manager import RegionManager diff --git a/src/spaceone/inventory_v2/manager/region_manager.py b/src/spaceone/inventory_v2/manager/region_manager.py new file mode 100644 index 0000000..365d6c8 --- /dev/null +++ b/src/spaceone/inventory_v2/manager/region_manager.py @@ -0,0 +1,59 @@ +import logging +from typing import Tuple + +from spaceone.core.model.mongo_model import QuerySet +from spaceone.core.manager import BaseManager +from spaceone.inventory_v2.lib.resource_manager import ResourceManager +from spaceone.inventory_v2.model.region_model import Region + +_LOGGER = logging.getLogger(__name__) + + +class RegionManager(BaseManager, ResourceManager): + resource_keys = ["region_id"] + query_method = "list_regions" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.region_model: Region = self.locator.get_model("Region") + + def create_region(self, params: dict) -> Region: + def _rollback(vo: Region): + _LOGGER.info(f"[ROLLBACK] Delete region : {vo.name} ({vo.region_id})") + vo.delete() + + region_vo: Region = self.region_model.create(params) + self.transaction.add_rollback(_rollback, region_vo) + + return region_vo + + def update_region_by_vo(self, params: dict, region_vo: Region) -> Region: + def _rollback(old_data): + _LOGGER.info( + f'[ROLLBACK] Revert Data : {old_data["name"]} ({old_data["region_id"]})' + ) + region_vo.update(old_data) + + self.transaction.add_rollback(_rollback, region_vo.to_dict()) + return region_vo.update(params) + + @staticmethod + def delete_region_by_vo(region_vo: Region) -> None: + region_vo.delete() + + def get_region(self, region_id: str, domain_id: str) -> Region: + conditions = {"region_id": region_id, "domain_id": domain_id} + + # if workspace_id: + # conditions.update({"workspace_id": workspace_id}) + + return self.region_model.get(**conditions) + + def filter_regions(self, **conditions) -> QuerySet: + return self.region_model.filter(**conditions) + + def list_regions(self, query: dict) -> Tuple[QuerySet, int]: + return self.region_model.query(**query) + + def stat_regions(self, query: dict) -> dict: + return self.region_model.stat(**query) diff --git a/src/spaceone/inventory_v2/model/__init__.py b/src/spaceone/inventory_v2/model/__init__.py new file mode 100644 index 0000000..3eb57ed --- /dev/null +++ b/src/spaceone/inventory_v2/model/__init__.py @@ -0,0 +1 @@ +from spaceone.inventory_v2.model.region_model import Region \ No newline at end of file diff --git a/src/spaceone/inventory_v2/model/region_model.py b/src/spaceone/inventory_v2/model/region_model.py new file mode 100644 index 0000000..2b51044 --- /dev/null +++ b/src/spaceone/inventory_v2/model/region_model.py @@ -0,0 +1,49 @@ +from mongoengine import * +from spaceone.core.model.mongo_model import MongoModel + + +class Region(MongoModel): + region_id = StringField(max_length=40, generate_id="region", unique=True) + name = StringField(max_length=255) + region_key = StringField(max_length=255) + region_code = StringField( + max_length=255, unique_with=["provider", "domain_id"] + ) + provider = StringField(max_length=255) + ref_region = StringField(max_length=255) + tags = DictField() + domain_id = StringField(max_length=40) + updated_by = StringField(default=None, null=True) + created_at = DateTimeField(auto_now_add=True) + updated_at = DateTimeField(auto_now=True) + + meta = { + "updatable_fields": ["name", "tags", "updated_by", "updated_at"], + "minimal_fields": ["region_id", "name", "region_code", "provider"], + "ordering": ["name"], + "indexes": [ + { + "fields": ["domain_id", "-updated_at", "updated_by"], + "name": "COMPOUND_INDEX_FOR_GC_1", + }, + { + "fields": ["domain_id", "region_id"], + "name": "COMPOUND_INDEX_FOR_SEARCH_1", + }, + { + "fields": ["domain_id", "provider", "region_code"], + "name": "COMPOUND_INDEX_FOR_SEARCH_2", + }, + { + "fields": ["domain_id", "region_key"], + "name": "COMPOUND_INDEX_FOR_SEARCH_3", + }, + {"fields": ["region_id", "ref_region"], "name": "COMPOUND_INDEX_FOR_REF_1"}, + { + "fields": ["region_code", "provider", "ref_region"], + "name": "COMPOUND_INDEX_FOR_REF_2", + }, + "ref_region", + "domain_id", + ], + } diff --git a/src/spaceone/inventory_v2/service/__init__.py b/src/spaceone/inventory_v2/service/__init__.py new file mode 100644 index 0000000..02d7275 --- /dev/null +++ b/src/spaceone/inventory_v2/service/__init__.py @@ -0,0 +1 @@ +from spaceone.inventory_v2.service.region_service import RegionService diff --git a/src/spaceone/inventory_v2/service/region_service.py b/src/spaceone/inventory_v2/service/region_service.py new file mode 100644 index 0000000..43c3db9 --- /dev/null +++ b/src/spaceone/inventory_v2/service/region_service.py @@ -0,0 +1,194 @@ +import logging +from typing import Tuple +from spaceone.core.service import * +from spaceone.core import utils +from spaceone.core.model.mongo_model import QuerySet +from spaceone.inventory_v2.manager.region_manager import RegionManager +from spaceone.inventory_v2.model.region_model import Region + +_LOGGER = logging.getLogger(__name__) +_KEYWORD_FILTER = ["region_id", "name", "region_code"] + + +@authentication_handler +@authorization_handler +@mutation_handler +@event_handler +class RegionService(BaseService): + resource = "Region" + + def __init__(self, metadata): + super().__init__(metadata) + self.region_mgr: RegionManager = self.locator.get_manager("RegionManager") + + @transaction( + permission="inventory:Region.write", + role_types=["DOMAIN_ADMIN"], + ) + def create(self, params: dict) -> Region: + """ + Args: + params (dict): { + 'name': 'str', # required + 'region_code': 'str', # required + 'provider': 'str', # required + 'tags': 'dict', + 'domain_id': 'str', # injected from auth (required) + } + Returns: + region_vo (object) + """ + + return self.create_resource(params) + + @check_required(["name", "region_code", "provider", "domain_id"]) + def create_resource(self, params: dict) -> Region: + if "tags" in params: + if isinstance(params["tags"], list): + params["tags"] = utils.tags_to_dict(params["tags"]) + + domain_id = params["domain_id"] + + params["updated_by"] = self.transaction.get_meta("collector_id") or "manual" + params["region_key"] = f'{params["provider"]}.{params["region_code"]}' + # params["ref_region"] = f'{domain_id}.{workspace_id}.{params["region_key"]}' + + return self.region_mgr.create_region(params) + + @transaction( + permission="inventory:Region.write", + role_types=["DOMAIN_ADMIN"], + ) + def update(self, params: dict) -> Region: + """ + Args: + params (dict): { + 'region_id': 'str', # required + 'name': 'str', + 'tags': 'dict', + 'domain_id': 'str', # injected from auth (required) + } + Returns: + region_vo (object) + """ + + return self.update_resource(params) + + @check_required(["region_id", "domain_id"]) + def update_resource(self, params: dict) -> Region: + if "tags" in params: + if isinstance(params["tags"], list): + params["tags"] = utils.tags_to_dict(params["tags"]) + + params["updated_by"] = self.transaction.get_meta("collector_id") or "manual" + + region_vo = self.region_mgr.get_region( + params["region_id"], params["domain_id"] + ) + return self.region_mgr.update_region_by_vo(params, region_vo) + + @transaction( + permission="inventory:Region.write", + role_types=["DOMAIN_ADMIN"], + ) + def delete(self, params: dict) -> None: + """ + Args: + params (dict): { + 'region_id': 'str', # required + 'domain_id': 'str' # injected from auth (required) + } + Returns: + None + """ + + self.delete_resource(params) + + @check_required(["region_id", "domain_id"]) + def delete_resource(self, params: dict) -> None: + region_vo = self.region_mgr.get_region( + params["region_id"], params["domain_id"] + ) + self.region_mgr.delete_region_by_vo(region_vo) + + @transaction( + permission="inventory:Region.read", + role_types=["DOMAIN_ADMIN", "WORKSPACE_OWNER", "WORKSPACE_MEMBER"], + ) + @check_required(["region_id", "domain_id"]) + def get(self, params: dict) -> Region: + """ + Args: + params (dict): { + 'region_id': 'str', # required + 'domain_id': 'str', # injected from auth (required) + } + + Returns: + region_vo (object) + + """ + + return self.region_mgr.get_region( + params["region_id"], params["domain_id"] + ) + + @transaction( + permission="inventory:Region.read", + role_types=["DOMAIN_ADMIN", "WORKSPACE_OWNER", "WORKSPACE_MEMBER"], + ) + @check_required(["domain_id"]) + @append_query_filter( + [ + "region_id", + "name", + "region_key", + "region_code", + "provider", + "domain_id", + ] + ) + @append_keyword_filter(_KEYWORD_FILTER) + def list(self, params: dict) -> Tuple[QuerySet, int]: + """ + Args: + params (dict): { + 'query': 'dict (spaceone.api.core.v1.Query)', + 'region_id': 'str', + 'name': 'str', + 'region_key': 'str', + 'region_code': 'str', + 'provider': 'str', + 'domain_id': 'str', # injected from auth (required) + } + + Returns: + results (list) + total_count (int) + + """ + + return self.region_mgr.list_regions(params.get("query", {})) + + @transaction( + permission="inventory:Region.read", + role_types=["DOMAIN_ADMIN", "WORKSPACE_OWNER", "WORKSPACE_MEMBER"], + ) + @check_required(["query", "domain_id"]) + @append_query_filter(["domain_id"]) + @append_keyword_filter(_KEYWORD_FILTER) + def stat(self, params: dict) -> dict: + """ + Args: + params (dict): { + 'query': 'dict (spaceone.api.core.v1.StatisticsQuery)', # required + 'domain_id': 'str', # injected from auth (required) + } + + Returns: + values (list) : 'list of statistics data' + + """ + + query = params.get("query", {}) + return self.region_mgr.stat_regions(query) \ No newline at end of file