diff options
Diffstat (limited to 'src/onapsdk/aai/business/customer.py')
-rw-r--r-- | src/onapsdk/aai/business/customer.py | 603 |
1 files changed, 603 insertions, 0 deletions
diff --git a/src/onapsdk/aai/business/customer.py b/src/onapsdk/aai/business/customer.py new file mode 100644 index 0000000..cdefd6f --- /dev/null +++ b/src/onapsdk/aai/business/customer.py @@ -0,0 +1,603 @@ +"""AAI business module.""" +# Copyright 2022 Orange, Deutsche Telekom AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from dataclasses import dataclass +from typing import Iterable, Iterator, Optional +from urllib.parse import urlencode + +from onapsdk.utils.jinja import jinja_env +from onapsdk.exceptions import APIError, ParameterError, ResourceNotFound + +from ..aai_element import AaiResource, Relationship +from ..cloud_infrastructure.cloud_region import CloudRegion +from .service import ServiceInstance + + +@dataclass +class ServiceSubscriptionCloudRegionTenantData: + """Dataclass to store cloud regions and tenants data for service subscription.""" + + cloud_owner: str = None + cloud_region_id: str = None + tenant_id: str = None + + +@dataclass +class ServiceSubscription(AaiResource): + """Service subscription class.""" + + service_type: str + resource_version: str + customer: "Customer" + + def __init__(self, customer: "Customer", service_type: str, resource_version: str) -> None: + """Service subscription object initialization. + + Args: + customer (Customer): Customer object + service_type (str): Service type + resource_version (str): Service subscription resource version + """ + super().__init__() + self.customer: "Customer" = customer + self.service_type: str = service_type + self.resource_version: str = resource_version + + def _get_service_instance_by_filter_parameter(self, + filter_parameter_name: str, + filter_parameter_value: str) -> ServiceInstance: + """Call a request to get service instance with given filter parameter and value. + + Args: + filter_parameter_name (str): Name of parameter to filter + filter_parameter_value (str): Value of filter parameter + + Returns: + ServiceInstance: ServiceInstance object + + """ + service_instance: dict = self.send_message_json( + "GET", + f"Get service instance with {filter_parameter_value} {filter_parameter_name}", + f"{self.url}/service-instances?{filter_parameter_name}={filter_parameter_value}" + )["service-instance"][0] + return ServiceInstance( + service_subscription=self, + instance_id=service_instance.get("service-instance-id"), + instance_name=service_instance.get("service-instance-name"), + service_type=service_instance.get("service-type"), + service_role=service_instance.get("service-role"), + environment_context=service_instance.get("environment-context"), + workload_context=service_instance.get("workload-context"), + created_at=service_instance.get("created-at"), + updated_at=service_instance.get("updated-at"), + description=service_instance.get("description"), + model_invariant_id=service_instance.get("model-invariant-id"), + model_version_id=service_instance.get("model-version-id"), + persona_model_version=service_instance.get("persona-model-version"), + widget_model_id=service_instance.get("widget-model-id"), + widget_model_version=service_instance.get("widget-model-version"), + bandwith_total=service_instance.get("bandwidth-total"), + vhn_portal_url=service_instance.get("vhn-portal-url"), + service_instance_location_id=service_instance.get("service-instance-location-id"), + resource_version=service_instance.get("resource-version"), + selflink=service_instance.get("selflink"), + orchestration_status=service_instance.get("orchestration-status"), + input_parameters=service_instance.get("input-parameters") + ) + + @classmethod + def get_all_url(cls, customer: "Customer") -> str: # pylint: disable=arguments-differ + """Return url to get all customers. + + Returns: + str: Url to get all customers + + """ + return (f"{cls.base_url}{cls.api_version}/business/customers/" + f"customer/{customer.global_customer_id}/service-subscriptions/") + + @classmethod + def create_from_api_response(cls, + api_response: dict, + customer: "Customer") -> "ServiceSubscription": + """Create service subscription using API response dict. + + Returns: + ServiceSubscription: ServiceSubscription object. + + """ + return cls( + service_type=api_response.get("service-type"), + resource_version=api_response.get("resource-version"), + customer=customer + ) + + @property + def url(self) -> str: + """Cloud region object url. + + URL used to call CloudRegion A&AI API + + Returns: + str: CloudRegion object url + + """ + return ( + f"{self.base_url}{self.api_version}/business/customers/" + f"customer/{self.customer.global_customer_id}/service-subscriptions/" + f"service-subscription/{self.service_type}" + ) + + @property + def service_instances(self) -> Iterator[ServiceInstance]: + """Service instances. + + Yields: + Iterator[ServiceInstance]: Service instance + + """ + for service_instance in \ + self.send_message_json("GET", + (f"Get all service instances for {self.service_type} service " + f"subscription"), + f"{self.url}/service-instances").get("service-instance", []): + yield ServiceInstance( + service_subscription=self, + instance_id=service_instance.get("service-instance-id"), + instance_name=service_instance.get("service-instance-name"), + service_type=service_instance.get("service-type"), + service_role=service_instance.get("service-role"), + environment_context=service_instance.get("environment-context"), + workload_context=service_instance.get("workload-context"), + created_at=service_instance.get("created-at"), + updated_at=service_instance.get("updated-at"), + description=service_instance.get("description"), + model_invariant_id=service_instance.get("model-invariant-id"), + model_version_id=service_instance.get("model-version-id"), + persona_model_version=service_instance.get("persona-model-version"), + widget_model_id=service_instance.get("widget-model-id"), + widget_model_version=service_instance.get("widget-model-version"), + bandwith_total=service_instance.get("bandwidth-total"), + vhn_portal_url=service_instance.get("vhn-portal-url"), + service_instance_location_id=service_instance.get("service-instance-location-id"), + resource_version=service_instance.get("resource-version"), + selflink=service_instance.get("selflink"), + orchestration_status=service_instance.get("orchestration-status"), + input_parameters=service_instance.get("input-parameters") + ) + + @property + def tenant_relationships(self) -> Iterator["Relationship"]: + """Tenant related relationships. + + Iterate through relationships and get related to tenant. + + Yields: + Relationship: Relationship related to tenant. + + """ + for relationship in self.relationships: + if relationship.related_to == "tenant": + yield relationship + + @property + def cloud_region(self) -> "CloudRegion": + """Cloud region associated with service subscription. + + IT'S DEPRECATED! `cloud_regions` parameter SHOULD BE USED + + Raises: + ParameterError: Service subscription has no associated cloud region. + + Returns: + CloudRegion: CloudRegion object + + """ + try: + return next(self.cloud_regions) + except StopIteration: + msg = f"No cloud region for service subscription '{self.name}'" + raise ParameterError(msg) + + @property + def tenant(self) -> "Tenant": + """Tenant associated with service subscription. + + IT'S DEPRECATED! `tenants` parameter SHOULD BE USED + + Raises: + ParameterError: Service subscription has no associated tenants + + Returns: + Tenant: Tenant object + + """ + try: + return next(self.tenants) + except StopIteration: + msg = f"No tenants for service subscription '{self.name}'" + raise ParameterError(msg) + + @property + def _cloud_regions_tenants_data(self) -> Iterator["ServiceSubscriptionCloudRegionTenantData"]: + for relationship in self.tenant_relationships: + cr_tenant_data: ServiceSubscriptionCloudRegionTenantData = \ + ServiceSubscriptionCloudRegionTenantData() + for data in relationship.relationship_data: + if data["relationship-key"] == "cloud-region.cloud-owner": + cr_tenant_data.cloud_owner = data["relationship-value"] + if data["relationship-key"] == "cloud-region.cloud-region-id": + cr_tenant_data.cloud_region_id = data["relationship-value"] + if data["relationship-key"] == "tenant.tenant-id": + cr_tenant_data.tenant_id = data["relationship-value"] + if all([cr_tenant_data.cloud_owner, + cr_tenant_data.cloud_region_id, + cr_tenant_data.tenant_id]): + yield cr_tenant_data + else: + self._logger.error("Invalid tenant relationship: %s", relationship) + + @property + def cloud_regions(self) -> Iterator["CloudRegion"]: + """Cloud regions associated with service subscription. + + Yields: + CloudRegion: CloudRegion object + + """ + cloud_region_set: set = set() + for cr_data in self._cloud_regions_tenants_data: + cloud_region_set.add((cr_data.cloud_owner, cr_data.cloud_region_id)) + for cloud_region_data in cloud_region_set: + try: + yield CloudRegion.get_by_id(cloud_owner=cloud_region_data[0], + cloud_region_id=cloud_region_data[1]) + except ResourceNotFound: + self._logger.error("Can't get cloud region %s %s", cloud_region_data[0], \ + cloud_region_data[1]) + + @property + def tenants(self) -> Iterator["Tenant"]: + """Tenants associated with service subscription. + + Yields: + Tenant: Tenant object + + """ + for cr_data in self._cloud_regions_tenants_data: + try: + cloud_region: CloudRegion = CloudRegion.get_by_id(cr_data.cloud_owner, + cr_data.cloud_region_id) + yield cloud_region.get_tenant(cr_data.tenant_id) + except ResourceNotFound: + self._logger.error("Can't get %s tenant", cr_data.tenant_id) + + def get_service_instance_by_id(self, service_instance_id) -> ServiceInstance: + """Get service instance using it's ID. + + Args: + service_instance_id (str): ID of the service instance + + Returns: + ServiceInstance: ServiceInstance object + + """ + return self._get_service_instance_by_filter_parameter( + "service-instance-id", + service_instance_id + ) + + def get_service_instance_by_name(self, service_instance_name: str) -> ServiceInstance: + """Get service instance using it's name. + + Args: + service_instance_name (str): Name of the service instance + + Returns: + ServiceInstance: ServiceInstance object + + """ + return self._get_service_instance_by_filter_parameter( + "service-instance-name", + service_instance_name + ) + + def link_to_cloud_region_and_tenant(self, + cloud_region: "CloudRegion", + tenant: "Tenant") -> None: + """Create relationship between object and cloud region with tenant. + + Args: + cloud_region (CloudRegion): Cloud region to link to + tenant (Tenant): Cloud region tenant to link to + """ + relationship: Relationship = Relationship( + related_to="tenant", + related_link=tenant.url, + relationship_data=[ + { + "relationship-key": "cloud-region.cloud-owner", + "relationship-value": cloud_region.cloud_owner, + }, + { + "relationship-key": "cloud-region.cloud-region-id", + "relationship-value": cloud_region.cloud_region_id, + }, + { + "relationship-key": "tenant.tenant-id", + "relationship-value": tenant.tenant_id, + }, + ], + related_to_property=[ + {"property-key": "tenant.tenant-name", "property-value": tenant.name} + ], + ) + self.add_relationship(relationship) + + +class Customer(AaiResource): + """Customer class.""" + + def __init__(self, + global_customer_id: str, + subscriber_name: str, + subscriber_type: str, + resource_version: str = None) -> None: + """Initialize Customer class object. + + Args: + global_customer_id (str): Global customer id used across ONAP to + uniquely identify customer. + subscriber_name (str): Subscriber name, an alternate way to retrieve a customer. + subscriber_type (str): Subscriber type, a way to provide VID with + only the INFRA customers. + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update + and delete. Defaults to None. + + """ + super().__init__() + self.global_customer_id: str = global_customer_id + self.subscriber_name: str = subscriber_name + self.subscriber_type: str = subscriber_type + self.resource_version: str = resource_version + + def __repr__(self) -> str: # noqa + """Customer description. + + Returns: + str: Customer object description + + """ + return (f"Customer(global_customer_id={self.global_customer_id}, " + f"subscriber_name={self.subscriber_name}, " + f"subscriber_type={self.subscriber_type}, " + f"resource_version={self.resource_version})") + + def get_service_subscription_by_service_type(self, service_type: str) -> ServiceSubscription: + """Get subscribed service by service type. + + Call a request to get service subscriptions filtered by service-type parameter. + + Args: + service_type (str): Service type + + Returns: + ServiceSubscription: Service subscription + + """ + response: dict = self.send_message_json( + "GET", + f"Get service subscription with {service_type} service type", + (f"{self.base_url}{self.api_version}/business/customers/" + f"customer/{self.global_customer_id}/service-subscriptions" + f"?service-type={service_type}") + ) + return ServiceSubscription.create_from_api_response(response["service-subscription"][0], + self) + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return an url to get all customers. + + Returns: + str: URL to get all customers + + """ + return f"{cls.base_url}{cls.api_version}/business/customers" + + @classmethod + def get_all(cls, + global_customer_id: str = None, + subscriber_name: str = None, + subscriber_type: str = None) -> Iterator["Customer"]: + """Get all customers. + + Call an API to retrieve all customers. It can be filtered + by global-customer-id, subscriber-name and/or subsriber-type. + + Args: + global_customer_id (str): global-customer-id to filer customers by. Defaults to None. + subscriber_name (str): subscriber-name to filter customers by. Defaults to None. + subscriber_type (str): subscriber-type to filter customers by. Defaults to None. + + """ + filter_parameters: dict = cls.filter_none_key_values( + { + "global-customer-id": global_customer_id, + "subscriber-name": subscriber_name, + "subscriber-type": subscriber_type, + } + ) + url: str = (f"{cls.get_all_url()}?{urlencode(filter_parameters)}") + for customer in cls.send_message_json("GET", "get customers", url).get("customer", []): + yield Customer( + global_customer_id=customer["global-customer-id"], + subscriber_name=customer["subscriber-name"], + subscriber_type=customer["subscriber-type"], + resource_version=customer["resource-version"], + ) + + @classmethod + def get_by_global_customer_id(cls, global_customer_id: str) -> "Customer": + """Get customer by it's global customer id. + + Args: + global_customer_id (str): global customer ID + + Returns: + Customer: Customer with given global_customer_id + + """ + response: dict = cls.send_message_json( + "GET", + f"Get {global_customer_id} customer", + f"{cls.base_url}{cls.api_version}/business/customers/customer/{global_customer_id}" + ) + return Customer( + global_customer_id=response["global-customer-id"], + subscriber_name=response["subscriber-name"], + subscriber_type=response["subscriber-type"], + resource_version=response["resource-version"], + ) + + @classmethod + def create(cls, + global_customer_id: str, + subscriber_name: str, + subscriber_type: str, + service_subscriptions: Optional[Iterable[str]] = None) -> "Customer": + """Create customer. + + Args: + global_customer_id (str): Global customer id used across ONAP + to uniquely identify customer. + subscriber_name (str): Subscriber name, an alternate way + to retrieve a customer. + subscriber_type (str): Subscriber type, a way to provide + VID with only the INFRA customers. + service_subscriptions (Optional[Iterable[str]], optional): Iterable + of service subscription names should be created for newly + created customer. Defaults to None. + + Returns: + Customer: Customer object. + + """ + url: str = ( + f"{cls.base_url}{cls.api_version}/business/customers/" + f"customer/{global_customer_id}" + ) + cls.send_message( + "PUT", + "declare customer", + url, + data=jinja_env() + .get_template("customer_create.json.j2") + .render( + global_customer_id=global_customer_id, + subscriber_name=subscriber_name, + subscriber_type=subscriber_type, + service_subscriptions=service_subscriptions + ), + ) + response: dict = cls.send_message_json( + "GET", "get created customer", url + ) # Call API one more time to get Customer's resource version + return Customer( + global_customer_id=response["global-customer-id"], + subscriber_name=response["subscriber-name"], + subscriber_type=response["subscriber-type"], + resource_version=response["resource-version"], + ) + + @property + def url(self) -> str: + """Return customer object url. + + Unique url address to get customer's data. + + Returns: + str: Customer object url + + """ + return ( + f"{self.base_url}{self.api_version}/business/customers/customer/" + f"{self.global_customer_id}?resource-version={self.resource_version}" + ) + + @property + def service_subscriptions(self) -> Iterator[ServiceSubscription]: + """Service subscriptions of customer resource. + + Yields: + ServiceSubscription: ServiceSubscription object + + """ + try: + response: dict = self.send_message_json( + "GET", + "get customer service subscriptions", + f"{self.base_url}{self.api_version}/business/customers/" + f"customer/{self.global_customer_id}/service-subscriptions" + ) + for service_subscription in response.get("service-subscription", []): + yield ServiceSubscription.create_from_api_response( + service_subscription, + self + ) + except ResourceNotFound as exc: + self._logger.info( + "Subscriptions are not " \ + "found for a customer: %s", exc) + except APIError as exc: + self._logger.error( + "API returned an error: %s", exc) + + def subscribe_service(self, service_type: str) -> "ServiceSubscription": + """Create SDC Service subscription. + + If service subscription with given service_type already exists it won't create + a new resource but use the existing one. + + Args: + service_type (str): Value defined by orchestration to identify this service + across ONAP. + """ + try: + return self.get_service_subscription_by_service_type(service_type) + except ResourceNotFound: + self._logger.info("Create service subscription for %s customer", + self.global_customer_id) + self.send_message( + "PUT", + "Create service subscription", + (f"{self.base_url}{self.api_version}/business/customers/" + f"customer/{self.global_customer_id}/service-subscriptions/" + f"service-subscription/{service_type}") + ) + return self.get_service_subscription_by_service_type(service_type) + + def delete(self) -> None: + """Delete customer. + + Sends request to A&AI to delete customer object. + + """ + self.send_message( + "DELETE", + "Delete customer", + self.url + ) |