diff options
Diffstat (limited to 'src/onapsdk')
168 files changed, 19107 insertions, 0 deletions
diff --git a/src/onapsdk/__init__.py b/src/onapsdk/__init__.py new file mode 100644 index 0000000..ce228b1 --- /dev/null +++ b/src/onapsdk/__init__.py @@ -0,0 +1,14 @@ +"""ONAP SDK master package.""" +# 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. diff --git a/src/onapsdk/aai/__init__.py b/src/onapsdk/aai/__init__.py new file mode 100644 index 0000000..e340efb --- /dev/null +++ b/src/onapsdk/aai/__init__.py @@ -0,0 +1,14 @@ +"""ONAP SDK AAI package.""" +# 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. diff --git a/src/onapsdk/aai/aai_element.py b/src/onapsdk/aai/aai_element.py new file mode 100644 index 0000000..9472165 --- /dev/null +++ b/src/onapsdk/aai/aai_element.py @@ -0,0 +1,192 @@ +"""AAI Element 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, field +from typing import Dict, Iterator, List, Optional + +from onapsdk.configuration import settings +from onapsdk.onap_service import OnapService +from onapsdk.utils.headers_creator import headers_aai_creator +from onapsdk.utils.jinja import jinja_env +from onapsdk.utils.gui import GuiItem, GuiList + +from onapsdk.exceptions import RelationshipNotFound, ResourceNotFound + + +@dataclass +class Relationship: + """Relationship class. + + A&AI elements could have relationship with other A&AI elements. + Relationships are represented by this class objects. + """ + + related_to: str + related_link: str + relationship_data: List[Dict[str, str]] + relationship_label: str = "" + related_to_property: List[Dict[str, str]] = field(default_factory=list) + + def get_relationship_data(self, relationship_key: str) -> Optional[str]: + """Get relationship data for given relationship key. + + From list of relationship data get the value for + given key + + Args: + relationship_key (str): Key to get relationship data value + + Returns: + Optional[str]: Relationship value or None if relationship data + with provided ket doesn't exist + + """ + for data in self.relationship_data: + if data["relationship-key"] == relationship_key: + return data["relationship-value"] + return None + + +class AaiElement(OnapService): + """Mother Class of all A&AI elements.""" + + name: str = "AAI" + server: str = "AAI" + base_url = settings.AAI_URL + api_version = "/aai/" + settings.AAI_API_VERSION + headers = headers_aai_creator(OnapService.headers) + + @classmethod + def get_guis(cls) -> GuiItem: + """Retrieve the status of the AAI GUIs. + + Only one GUI is referenced for AAI + the AAI sparky GUI + + Return the list of GUIs + """ + gui_url = settings.AAI_GUI_SERVICE + aai_gui_response = cls.send_message( + "GET", "Get AAI GUI Status", gui_url) + guilist = GuiList([]) + guilist.add(GuiItem( + gui_url, + aai_gui_response.status_code)) + return guilist + + +class AaiResource(AaiElement): + """A&AI resource class.""" + + @classmethod + def filter_none_key_values(cls, dict_to_filter: Dict[str, Optional[str]]) -> Dict[str, str]: + """Filter out None key values from dictionary. + + Iterate through given dictionary and filter None values. + + Args: + dict_to_filter (Dict): Dictionary to filter out None + + Returns:dataclasse init a field + Dict[str, str]: Filtered dictionary + + """ + return dict( + filter(lambda key_value_tuple: key_value_tuple[1] is not None, dict_to_filter.items(),) + ) + + @property + def url(self) -> str: + """Resource's url. + + Returns: + str: Resource's url + + """ + raise NotImplementedError + + @property + def relationships(self) -> Iterator[Relationship]: + """Resource relationships iterator. + + Yields: + Relationship: resource relationship + + Raises: + RelationshipNotFound: if request for relationships returned 404 + + """ + try: + generator = self.send_message_json("GET", + "Get object relationships", + f"{self.url}/relationship-list")\ + .get("relationship", []) + for relationship in generator: + yield Relationship( + related_to=relationship.get("related-to"), + relationship_label=relationship.get("relationship-label"), + related_link=relationship.get("related-link"), + relationship_data=relationship.get("relationship-data"), + ) + except ResourceNotFound as exc: + self._logger.error("Getting object relationships failed: %s", exc) + + msg = (f'{self.name} relationships not found.' + f'Server: {self.server}. Url: {self.url}') + raise RelationshipNotFound(msg) from exc + + @classmethod + def get_all_url(cls, *args, **kwargs) -> str: + """Return an url for all objects of given class. + + Returns: + str: URL to get all objects of given class + + """ + raise NotImplementedError + + @classmethod + def count(cls, *args, **kwargs) -> int: + """Get the count number of all objects of given class. + + Get the response, iterate through response (each class has different response) + -- the first key value is the count. + + Returns: + int: Count of the objects + + """ + return next(iter(cls.send_message_json( + "GET", + f"Get count of {cls.__name__} class instances", + f"{cls.get_all_url(*args, **kwargs)}?format=count" + )["results"][0].values())) + + def add_relationship(self, relationship: Relationship) -> None: + """Add relationship to aai resource. + + Add relationship to resource using A&AI API + + Args: + relationship (Relationship): Relationship to add + + """ + self.send_message( + "PUT", + f"add relationship to {self.__class__.__name__}", + f"{self.url}/relationship-list/relationship", + data=jinja_env() + .get_template("aai_add_relationship.json.j2") + .render(relationship=relationship), + ) diff --git a/src/onapsdk/aai/bulk.py b/src/onapsdk/aai/bulk.py new file mode 100644 index 0000000..435a0b4 --- /dev/null +++ b/src/onapsdk/aai/bulk.py @@ -0,0 +1,90 @@ +"""A&AI bulk 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 Any, Dict, Iterable + +from more_itertools import chunked + +from onapsdk.configuration import settings +from onapsdk.utils.jinja import jinja_env + +from .aai_element import AaiElement + + +@dataclass +class AaiBulkRequest: + """Class to store information about a request to be sent in A&AI bulk request.""" + + action: str + uri: str + body: Dict[Any, Any] + + +@dataclass +class AaiBulkResponse: + """Class to store A&AI bulk response.""" + + action: str + uri: str + status_code: int + body: str + + +class AaiBulk(AaiElement): + """A&AI bulk class. + + Use it to send bulk request to A&AI. With bulk request you can send + multiple requests at once. + """ + + @property + def url(self) -> str: + """Bulk url. + + Returns: + str: A&AI bulk API url. + + """ + return f"{self.base_url}{self.api_version}/bulk" + + @classmethod + def single_transaction(cls, aai_requests: Iterable[AaiBulkRequest])\ + -> Iterable[AaiBulkResponse]: + """Singe transaction bulk request. + + Args: + aai_requests (Iterable[AaiBulkRequest]): Iterable object of requests to be sent + as a bulk request. + + Yields: + Iterator[Iterable[AaiBulkResponse]]: Bulk request responses. Each object + correspond to the sent request. + + """ + for requests_chunk in chunked(aai_requests, settings.AAI_BULK_CHUNK): + for response in cls.send_message_json(\ + "POST",\ + "Send bulk A&AI request",\ + f"{cls.base_url}{cls.api_version}/bulk/single-transaction",\ + data=jinja_env().get_template(\ + "aai_bulk.json.j2").render(operations=requests_chunk)\ + )["operation-responses"]: + yield AaiBulkResponse( + action=response["action"], + uri=response["uri"], + status_code=response["response-status-code"], + body=response["response-body"] + ) diff --git a/src/onapsdk/aai/business/__init__.py b/src/onapsdk/aai/business/__init__.py new file mode 100644 index 0000000..41f9671 --- /dev/null +++ b/src/onapsdk/aai/business/__init__.py @@ -0,0 +1,27 @@ +"""A&AI business package.""" +# 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 .customer import Customer, ServiceSubscription +from .instance import Instance +from .line_of_business import LineOfBusiness +from .network import NetworkInstance +from .owning_entity import OwningEntity +from .platform import Platform +from .pnf import PnfInstance +from .project import Project +from .service import ServiceInstance +from .sp_partner import SpPartner +from .vf_module import VfModuleInstance +from .vnf import VnfInstance 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 + ) diff --git a/src/onapsdk/aai/business/instance.py b/src/onapsdk/aai/business/instance.py new file mode 100644 index 0000000..146aee9 --- /dev/null +++ b/src/onapsdk/aai/business/instance.py @@ -0,0 +1,55 @@ +"""Base instance 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 abc import ABC, abstractmethod + +from ..aai_element import AaiResource + + +class Instance(AaiResource, ABC): + """Abstract instance class.""" + + def __init__(self, # pylint: disable=too-many-arguments + resource_version: str = None, + model_invariant_id: str = None, + model_version_id: str = None) -> None: + """Instance initialization. + + Args: + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update and delete. Defaults to None. + model_invariant_id (str, optional): The ASDC model id for this resource or + service model. Defaults to None. + model_version_id (str, optional): The ASDC model version for this resource or + service model. Defaults to None. + """ + super().__init__() + self.resource_version: str = resource_version + self.model_invariant_id: str = model_invariant_id + self.model_version_id: str = model_version_id + + @abstractmethod + def delete(self, a_la_carte: bool = True) -> "DeletionRequest": + """Create instance deletion request. + + Send request to delete instance + + Args: + a_la_carte (boolean): deletion mode + + Returns: + DeletionRequest: Deletion request + + """ diff --git a/src/onapsdk/aai/business/line_of_business.py b/src/onapsdk/aai/business/line_of_business.py new file mode 100644 index 0000000..61fc0f8 --- /dev/null +++ b/src/onapsdk/aai/business/line_of_business.py @@ -0,0 +1,123 @@ +"""A&AI line of 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 typing import Any, Dict, Iterator + +from onapsdk.utils.jinja import jinja_env + +from ..aai_element import AaiResource + + +class LineOfBusiness(AaiResource): + """Line of business class.""" + + def __init__(self, name: str, resource_version: str) -> None: + """Line of business object initialization. + + Args: + name (str): Line of business name + resource_version (str): resource version + """ + super().__init__() + self.name: str = name + self.resource_version: str = resource_version + + @property + def url(self) -> str: + """Line of business's url. + + Returns: + str: Resource's url + + """ + return (f"{self.base_url}{self.api_version}/business/lines-of-business/" + f"line-of-business/{self.name}") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all lines of business. + + Returns: + str: Url to get all lines of business + + """ + return f"{cls.base_url}{cls.api_version}/business/lines-of-business/" + + def __repr__(self) -> str: + """Line of business object representation. + + Returns: + str: Line of business object representation + + """ + return f"LineOfBusiness(name={self.name})" + + @classmethod + def get_all(cls) -> Iterator["LineOfBusiness"]: + """Get all line of business. + + Yields: + LineOfBusiness: LineOfBusiness object + + """ + url: str = f"{cls.base_url}{cls.api_version}/business/lines-of-business" + for line_of_business in cls.send_message_json("GET", + "Get A&AI lines of business", + url).get("line-of-business", []): + yield cls( + line_of_business.get("line-of-business-name"), + line_of_business.get("resource-version") + ) + + @classmethod + def create(cls, name: str) -> "LineOfBusiness": + """Create line of business A&AI resource. + + Args: + name (str): line of business name + + Returns: + LineOfBusiness: Created LineOfBusiness object + + """ + cls.send_message( + "PUT", + "Declare A&AI line of business", + (f"{cls.base_url}{cls.api_version}/business/lines-of-business/" + f"line-of-business/{name}"), + data=jinja_env().get_template("aai_line_of_business_create.json.j2").render( + line_of_business_name=name + ) + ) + return cls.get_by_name(name) + + @classmethod + def get_by_name(cls, name: str) -> "LineOfBusiness": + """Get line of business resource by it's name. + + Raises: + ResourceNotFound: Line of business requested by a name does not exist. + + Returns: + LineOfBusiness: Line of business requested by a name. + + """ + url = (f"{cls.base_url}{cls.api_version}/business/lines-of-business/" + f"line-of-business/{name}") + response: Dict[str, Any] = \ + cls.send_message_json("GET", + f"Get {name} line of business", + url) + return cls(response["line-of-business-name"], response["resource-version"]) diff --git a/src/onapsdk/aai/business/network.py b/src/onapsdk/aai/business/network.py new file mode 100644 index 0000000..e36cf62 --- /dev/null +++ b/src/onapsdk/aai/business/network.py @@ -0,0 +1,223 @@ +"""Network instance 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 onapsdk.so.deletion import NetworkDeletionRequest + +from .instance import Instance + + +class NetworkInstance(Instance): # pylint: disable=too-many-instance-attributes + """Network instance class.""" + + def __init__(self, # pylint: disable=too-many-arguments, too-many-locals + service_instance: "ServiceInstance", + network_id: str, + is_bound_to_vpn: bool, + is_provider_network: bool, + is_shared_network: bool, + is_external_network: bool, + network_name: str = None, + network_type: str = None, + network_role: str = None, + network_technology: str = None, + neutron_network_id: str = None, + service_id: str = None, + network_role_instance: str = None, + resource_version: str = None, + orchestration_status: str = None, + heat_stack_id: str = None, + mso_catalog_key: str = None, + model_invariant_id: str = None, + contrail_network_fqdn: str = None, + persona_model_version: str = None, + model_version_id: str = None, + model_customization_id: str = None, + widget_model_id: str = None, + physical_network_name: str = None, + widget_model_version: str = None, + selflink: str = None, + operational_status: str = None, + is_trunked: bool = None): + """Network instance object initialization. + + Args: + service_instance (ServiceInstance): Service instance object + network_id (str): Network ID, should be uuid. Unique across A&AI. + is_bound_to_vpn (bool): Set to true if bound to VPN + is_provider_network (bool): boolean indicatating whether or not network + is a provider network. + is_shared_network (bool): boolean indicatating whether + or not network is a shared network. + is_external_network (bool): boolean indicatating whether + or not network is an external network. + network_name (str, optional): Name of the network, governed by some naming convention. + Defaults to None. + network_type (str, optional): Type of the network. Defaults to None. + network_role (str, optional): Role the network. Defaults to None. + network_technology (str, optional): Network technology. Defaults to None. + neutron_network_id (str, optional): Neutron network id of this Interface. + Defaults to None. + service_id (str, optional): Unique identifier of service from ASDC. + Does not strictly map to ASDC services. Defaults to None. + network_role_instance (str, optional): network role instance. Defaults to None. + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update and delete. Defaults to None. + orchestration_status (str, optional): Orchestration status of this VNF, + mastered by MSO. Defaults to None. + heat_stack_id (str, optional): Heat stack id corresponding to this instance, + managed by MSO. Defaults to None. + mso_catalog_key (str, optional): Corresponds to the SDN-C catalog id used to + configure this VCE. Defaults to None. + contrail_network_fqdn (str, optional): Contrail FQDN for the network. Defaults to None. + model_invariant_id (str, optional): the ASDC model id for this resource + or service model. Defaults to None. + model_version_id (str, optional): the ASDC model version for this resource + or service model. Defaults to None. + persona_model_version (str, optional): the ASDC model version for this resource + or service model. Defaults to None. + model_customization_id (str, optional): captures the id of all the configuration + used to customize the resource for the service. Defaults to None. + widget_model_id (str, optional): the ASDC data dictionary widget model. + This maps directly to the A&AI widget. Defaults to None. + widget_model_version (str, optional): the ASDC data dictionary version of + the widget model. This maps directly to the A&AI version of the widget. + Defaults to None. + physical_network_name (str, optional): Name associated with the physical network. + Defaults to None. + selflink (str, optional): Path to the controller object. Defaults to None. + operational_status (str, optional): Indicator for whether the resource is considered + operational. Defaults to None. + is_trunked (bool, optional): Trunked network indication. Defaults to None. + """ + super().__init__(resource_version=resource_version, + model_version_id=model_version_id, + model_invariant_id=model_invariant_id) + self.service_instance: "ServiceInstance" = service_instance + self.network_id: str = network_id + self.is_bound_to_vpn: bool = is_bound_to_vpn + self.is_provider_network: bool = is_provider_network + self.is_shared_network: bool = is_shared_network + self.is_external_network: bool = is_external_network + self.network_name: str = network_name + self.network_type: str = network_type + self.network_role: str = network_role + self.network_technology: str = network_technology + self.neutron_network_id: str = neutron_network_id + self.service_id: str = service_id + self.network_role_instance: str = network_role_instance + self.orchestration_status: str = orchestration_status + self.heat_stack_id: str = heat_stack_id + self.mso_catalog_key: str = mso_catalog_key + self.contrail_network_fqdn: str = contrail_network_fqdn + self.model_customization_id: str = model_customization_id + self.physical_network_name: str = physical_network_name + self.selflink: str = selflink + self.operational_status: str = operational_status + self.is_trunked: bool = is_trunked + self.persona_model_version: str = persona_model_version + self.widget_model_id: str = widget_model_id + self.widget_model_version: str = widget_model_version + + def __repr__(self) -> str: + """Network instance object representation. + + Returns: + str: Human readable network instance representation + + """ + return (f"NetworkInstance(network_id={self.network_id}, " + f"network_name={self.network_name}, " + f"is_bound_to_vpn={self.is_bound_to_vpn}, " + f"is_provider_network={self.is_provider_network}, " + f"is_shared_network={self.is_shared_network}, " + f"is_external_network={self.is_external_network}, " + f"orchestration_status={self.orchestration_status})") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all networks. + + Returns: + str: Url to get all networks + + """ + return f"{cls.base_url}{cls.api_version}/network/l3-networks/" + + @property + def url(self) -> str: + """Network instance url. + + Returns: + str: NetworkInstance url + + """ + return f"{self.base_url}{self.api_version}/network/l3-networks/l3-network/{self.network_id}" + + @classmethod + def create_from_api_response(cls, api_response: dict, + service_instance: "ServiceInstance") -> "NetworkInstance": + """Create network instance object using HTTP API response dictionary. + + Args: + api_response (dict): A&AI API response dictionary + service_instance (ServiceInstance): Service instance with which network is related + + Returns: + VnfInstance: VnfInstance object + + """ + return cls(service_instance=service_instance, + network_id=api_response["network-id"], + is_bound_to_vpn=api_response["is-bound-to-vpn"], + is_provider_network=api_response["is-provider-network"], + is_shared_network=api_response["is-shared-network"], + is_external_network=api_response["is-external-network"], + network_name=api_response.get("network-name"), + network_type=api_response.get("network-type"), + network_role=api_response.get("network-role"), + network_technology=api_response.get("network-technology"), + neutron_network_id=api_response.get("neutron-network-id"), + service_id=api_response.get("service-id"), + network_role_instance=api_response.get("network-role-instance"), + resource_version=api_response.get("resource-version"), + orchestration_status=api_response.get("orchestration-status"), + heat_stack_id=api_response.get("heat-stack-id"), + mso_catalog_key=api_response.get("mso-catalog-key"), + model_invariant_id=api_response.get("model-invariant-id"), + contrail_network_fqdn=api_response.get("contrail-network-fqdn"), + model_version_id=api_response.get("model-version-id"), + model_customization_id=api_response.get("model-customization-id"), + widget_model_id=api_response.get("widget-model-id"), + persona_model_version=api_response.get("persona-model-version"), + physical_network_name=api_response.get("physical-network-name"), + selflink=api_response.get("selflink"), + widget_model_version=api_response.get("widget-model-version"), + operational_status=api_response.get("operational-status"), + is_trunked=api_response.get("is-trunked")) + + def delete(self, a_la_carte: bool = True) -> "NetworkDeletionRequest": + """Create network deletion request. + + Send request to delete network instance + + Args: + a_la_carte (boolean): deletion mode + + Returns: + NetworkDeletionRequest: Deletion request + + """ + self._logger.debug("Delete %s network", self.network_id) + return NetworkDeletionRequest.send_request(self, a_la_carte) diff --git a/src/onapsdk/aai/business/owning_entity.py b/src/onapsdk/aai/business/owning_entity.py new file mode 100644 index 0000000..bf1e7c1 --- /dev/null +++ b/src/onapsdk/aai/business/owning_entity.py @@ -0,0 +1,154 @@ +"""A&AI owning entity 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 uuid import uuid4 +from typing import Iterator + +from onapsdk.utils.jinja import jinja_env +from onapsdk.exceptions import ResourceNotFound + +from ..aai_element import AaiResource + + +class OwningEntity(AaiResource): + """Owning entity class.""" + + def __init__(self, name: str, owning_entity_id: str, resource_version: str) -> None: + """Owning entity object initialization. + + Args: + name (str): Owning entity name + owning_entity_id (str): owning entity ID + resource_version (str): resource version + """ + super().__init__() + self.name: str = name + self.owning_entity_id: str = owning_entity_id + self.resource_version: str = resource_version + + def __repr__(self) -> str: + """Owning entity object representation. + + Returns: + str: Owning entity object representation + + """ + return f"OwningEntity(name={self.name}, owning_entity_id={self.owning_entity_id})" + + @property + def url(self) -> str: + """Owning entity object url. + + Returns: + str: Url + + """ + return (f"{self.base_url}{self.api_version}/business/owning-entities/owning-entity/" + f"{self.owning_entity_id}") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all owning entities. + + Returns: + str: Url to get all owning entities + + """ + return f"{cls.base_url}{cls.api_version}/business/owning-entities" + + @classmethod + def get_all(cls) -> Iterator["OwningEntity"]: + """Get all owning entities. + + Yields: + OwningEntity: OwningEntity object + + """ + url: str = cls.get_all_url() + for owning_entity in cls.send_message_json("GET", + "Get A&AI owning entities", + url).get("owning-entity", []): + yield cls( + owning_entity.get("owning-entity-name"), + owning_entity.get("owning-entity-id"), + owning_entity.get("resource-version") + ) + + @classmethod + def get_by_owning_entity_id(cls, owning_entity_id: str) -> "OwningEntity": + """Get owning entity by it's ID. + + Args: + owning_entity_id (str): owning entity object id + + Returns: + OwningEntity: OwningEntity object + + """ + response: dict = cls.send_message_json( + "GET", + "Get A&AI owning entity", + (f"{cls.base_url}{cls.api_version}/business/owning-entities/" + f"owning-entity/{owning_entity_id}") + ) + return cls( + response.get("owning-entity-name"), + response.get("owning-entity-id"), + response.get("resource-version") + ) + + @classmethod + def get_by_owning_entity_name(cls, owning_entity_name: str) -> "OwningEntity": + """Get owning entity resource by it's name. + + Raises: + ResourceNotFound: Owning entity requested by a name does not exist. + + Returns: + OwningEntity: Owning entity requested by a name. + + """ + for owning_entity in cls.get_all(): + if owning_entity.name == owning_entity_name: + return owning_entity + + msg = f'Owning entity "{owning_entity_name}" does not exist.' + raise ResourceNotFound(msg) + + @classmethod + def create(cls, name: str, owning_entity_id: str = None) -> "OwningEntity": + """Create owning entity A&AI resource. + + Args: + name (str): owning entity name + owning_entity_id (str): owning entity ID. Defaults to None. + + Returns: + OwningEntity: Created OwningEntity object + + """ + if not owning_entity_id: + owning_entity_id = str(uuid4()) + cls.send_message( + "PUT", + "Declare A&AI owning entity", + (f"{cls.base_url}{cls.api_version}/business/owning-entities/" + f"owning-entity/{owning_entity_id}"), + data=jinja_env().get_template("aai_owning_entity_create.json.j2").render( + owning_entity_name=name, + owning_entity_id=owning_entity_id + ) + ) + return cls.get_by_owning_entity_id(owning_entity_id) diff --git a/src/onapsdk/aai/business/platform.py b/src/onapsdk/aai/business/platform.py new file mode 100644 index 0000000..5c12ba8 --- /dev/null +++ b/src/onapsdk/aai/business/platform.py @@ -0,0 +1,123 @@ +"""A&AI platform 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 typing import Any, Dict, Iterator + +from onapsdk.utils.jinja import jinja_env + +from ..aai_element import AaiResource + + +class Platform(AaiResource): + """Platform class.""" + + def __init__(self, name: str, resource_version: str) -> None: + """Platform object initialization. + + Args: + name (str): Platform name + resource_version (str): resource version + """ + super().__init__() + self.name: str = name + self.resource_version: str = resource_version + + def __repr__(self) -> str: + """Platform object representation. + + Returns: + str: Platform object representation + + """ + return f"Platform(name={self.name})" + + @property + def url(self) -> str: + """Platform's url. + + Returns: + str: Resource's url + + """ + return (f"{self.base_url}{self.api_version}/business/platforms/" + f"platform/{self.name}") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all platforms. + + Returns: + str: Url to get all platforms + + """ + return f"{cls.base_url}{cls.api_version}/business/platforms" + + @classmethod + def get_all(cls) -> Iterator["Platform"]: + """Get all platform. + + Yields: + Platform: Platform object + + """ + url: str = cls.get_all_url() + for platform in cls.send_message_json("GET", + "Get A&AI platforms", + url).get("platform", []): + yield cls( + platform.get("platform-name"), + platform.get("resource-version") + ) + + @classmethod + def create(cls, name: str) -> "Platform": + """Create platform A&AI resource. + + Args: + name (str): platform name + + Returns: + Platform: Created Platform object + + """ + cls.send_message( + "PUT", + "Declare A&AI platform", + (f"{cls.base_url}{cls.api_version}/business/platforms/" + f"platform/{name}"), + data=jinja_env().get_template("aai_platform_create.json.j2").render( + platform_name=name + ) + ) + return cls.get_by_name(name) + + @classmethod + def get_by_name(cls, name: str) -> "Platform": + """Get platform resource by it's name. + + Raises: + ResourceNotFound: Platform requested by a name does not exist. + + Returns: + Platform: Platform requested by a name. + + """ + url = (f"{cls.base_url}{cls.api_version}/business/platforms/" + f"platform/{name}") + response: Dict[str, Any] = \ + cls.send_message_json("GET", + f"Get {name} platform", + url) + return cls(response["platform-name"], response["resource-version"]) diff --git a/src/onapsdk/aai/business/pnf.py b/src/onapsdk/aai/business/pnf.py new file mode 100644 index 0000000..9061ebf --- /dev/null +++ b/src/onapsdk/aai/business/pnf.py @@ -0,0 +1,267 @@ +"""Pnf instance 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 typing import Iterator, Optional, TYPE_CHECKING + +from onapsdk.exceptions import ResourceNotFound +from .instance import Instance + +if TYPE_CHECKING: + from .service import ServiceInstance # pylint: disable=cyclic-import + +class PnfInstance(Instance): # pylint: disable=too-many-instance-attributes + """Pnf instance class.""" + + def __init__(self, # pylint: disable=too-many-arguments, too-many-locals + service_instance: "ServiceInstance", + pnf_name: str, + in_maint: bool, + selflink: str = None, + pnf_id: str = None, + equip_type: str = None, + equip_vendor: str = None, + equip_model: str = None, + management_option: str = None, + orchestration_status: str = None, + ipaddress_v4_oam: str = None, + sw_version: str = None, + frame_id: str = None, + serial_number: str = None, + ipaddress_v4_loopback_0: str = None, + ipaddress_v6_loopback_0: str = None, + ipaddress_v4_aim: str = None, + ipaddress_v6_aim: str = None, + ipaddress_v6_oam: str = None, + inv_status: str = None, + resource_version: str = None, + prov_status: str = None, + nf_role: str = None, + admin_status: str = None, + operational_status: str = None, + model_customization_id: str = None, + model_invariant_id: str = None, + model_version_id: str = None, + pnf_ipv4_address: str = None, + pnf_ipv6_address: str = None) -> None: + """Pnf instance object initialization. + + Args: + service_instance (ServiceInstance): Service instance object + pnf_name (str): unique name of Physical Network Function + in_maint (bool): Used to indicate whether or not this object is in maintenance mode + (maintenance mode = True). This field (in conjunction with prov_status) + is used to suppress alarms and vSCL on VNFs/VMs. + selflink (str, optional): URL to endpoint where AAI can get more details. + Defaults to None. + pnf_id (str, optional): id of pnf. Defaults to None. + equip_type (str, optional): Equipment type. Source of truth should define valid values. + Defaults to None. + equip_vendor (str, optional): Equipment vendor. Source of truth should define + valid values. Defaults to None. + equip_model (str, optional): Equipment model. Source of truth should define + valid values. Defaults to None. + management_option (str, optional): identifier of managed customer. Defaults to None. + orchestration_status (str, optional): Orchestration status of this pnf. + Defaults to None. + ipaddress_v4_oam (str, optional): ipv4-oam-address with new naming + convention for IP addresses. Defaults to None. + sw_version (str, optional): sw-version is the version of SW for the hosted + application on the PNF. Defaults to None. + frame_id (str, optional): ID of the physical frame (relay rack) where pnf is installed. + Defaults to None. + serial_number (str, optional): Serial number of the device. Defaults to None. + ipaddress_v4_loopback_0 (str, optional): IPV4 Loopback 0 address. Defaults to None. + ipaddress_v6_loopback_0 (str, optional): IPV6 Loopback 0 address. Defaults to None. + ipaddress_v4_aim (str, optional): IPV4 AIM address. Defaults to None. + ipaddress_v6_aim (str, optional): IPV6 AIM address. Defaults to None. + ipaddress_v6_oam (str, optional): IPV6 OAM address. Defaults to None. + inv_status (str, optional): CANOPI's inventory status. Only set with values exactly + as defined by CANOPI. Defaults to None. + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update and delete. Defaults to None. + prov_status (str, optional): Prov Status of this device (not under canopi control) + Valid values [PREPROV/NVTPROV/PROV]. Defaults to None. + nf_role (str, optional): Nf Role is the role performed by this instance in the network. + Defaults to None. + admin_status (str, optional): admin Status of this PNF. Defaults to None. + operational_status (str, optional): Store the operational-status for this object. + Defaults to None. + model_customization_id (str, optional): Store the model-customization-id + for this object. Defaults to None. + model_invariant_id (str, optional): The ASDC model id for this resource model. + Defaults to None. + model_version_id (str, optional): The ASDC model version for this resource model. + Defaults to None. + pnf_ipv4_address (str, optional): This is the IP address (IPv4) for the PNF itself. + This is the IPv4 address that the PNF iself can be accessed at. Defaults to None. + pnf_ipv6_address (str, optional): This is the IP address (IPv6) for the PNF itself. + This is the IPv6 address that the PNF iself can be accessed at. Defaults to None. + """ + super().__init__(resource_version=resource_version, + model_invariant_id=model_invariant_id, + model_version_id=model_version_id) + self.service_instance: "ServiceInstance" = service_instance + self.pnf_name: str = pnf_name + self.in_maint: bool = in_maint + self.selflink: Optional[str] = selflink + self.pnf_id: Optional[str] = pnf_id + self.equip_type: Optional[str] = equip_type + self.equip_vendor: Optional[str] = equip_vendor + self.equip_model: Optional[str] = equip_model + self.management_option: Optional[str] = management_option + self.orchestration_status: Optional[str] = orchestration_status + self.ipaddress_v4_oam: Optional[str] = ipaddress_v4_oam + self.sw_version: Optional[str] = sw_version + self.frame_id: Optional[str] = frame_id + self.serial_number: Optional[str] = serial_number + self.ipaddress_v4_loopback_0: Optional[str] = ipaddress_v4_loopback_0 + self.ipaddress_v6_loopback_0: Optional[str] = ipaddress_v6_loopback_0 + self.ipaddress_v4_aim: Optional[str] = ipaddress_v4_aim + self.ipaddress_v6_aim: Optional[str] = ipaddress_v6_aim + self.ipaddress_v6_oam: Optional[str] = ipaddress_v6_oam + self.inv_status: Optional[str] = inv_status + self.prov_status: Optional[str] = prov_status + self.nf_role: Optional[str] = nf_role + self.admin_status: Optional[str] = admin_status + self.operational_status: Optional[str] = operational_status + self.model_customization_id: Optional[str] = model_customization_id + self.pnf_ipv4_address: Optional[str] = pnf_ipv4_address + self.pnf_ipv6_address: Optional[str] = pnf_ipv6_address + + self._pnf: "Pnf" = None + + def __repr__(self) -> str: + """Pnf instance object representation. + + Returns: + str: Human readable pnf instance representation + + """ + return f"PnfInstance(pnf_name={self.pnf_name})" + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return an url to get all pnfs. + + Returns: + str: Url to get all pnfs + + """ + return f"{cls.base_url}{cls.api_version}/network/pnfs/" + + @classmethod + def get_all(cls) -> Iterator["PnfInstance"]: + """Get all PNF instances. + + Yields: + PnfInstance: Pnf instance + + """ + for pnf_data in cls.send_message_json( \ + "GET", \ + "Get all pnf instances", \ + cls.get_all_url() \ + ).get("pnf", []): + yield cls.create_from_api_response(pnf_data, None) + + @property + def url(self) -> str: + """Network instance url. + + Returns: + str: NetworkInstance url + + """ + return f"{self.base_url}{self.api_version}/network/pnfs/pnf/{self.pnf_name}" + + @property + def pnf(self) -> "Pnf": + """Pnf associated with that pnf instance. + + Raises: + ResourceNotFound: Could not find PNF for that PNF instance + + Returns: + Pnf: Pnf object associated with Pnf instance + + """ + if not self._pnf: + for pnf in self.service_instance.sdc_service.pnfs: + if pnf.model_version_id == self.model_version_id: + self._pnf = pnf + return self._pnf + + msg = ( + f'Could not find PNF for the PNF instance' + f' with model version ID "{self.model_version_id}"' + ) + raise ResourceNotFound(msg) + return self._pnf + + @classmethod + def create_from_api_response(cls, api_response: dict, + service_instance: "ServiceInstance") -> "PnfInstance": + """Create pnf instance object using HTTP API response dictionary. + + Args: + api_response (dict): A&AI API response dictionary + service_instance (ServiceInstance): Service instance with which network is related + + Returns: + PnfInstance: PnfInstance object + + """ + return cls(service_instance=service_instance, + pnf_name=api_response["pnf-name"], + in_maint=api_response["in-maint"], + selflink=api_response.get("selflink"), + pnf_id=api_response.get("pnf-id"), + equip_type=api_response.get("equip-type"), + equip_vendor=api_response.get("equip-vendor"), + equip_model=api_response.get("equip-model"), + management_option=api_response.get("management-option"), + orchestration_status=api_response.get("orchestration-status"), + ipaddress_v4_oam=api_response.get("ipaddress-v4-oam"), + sw_version=api_response.get("sw-version"), + frame_id=api_response.get("frame-id"), + serial_number=api_response.get("serial-number"), + ipaddress_v4_loopback_0=api_response.get("ipaddress-v4-loopback-0"), + ipaddress_v6_loopback_0=api_response.get("ipaddress-v6-loopback-0"), + ipaddress_v4_aim=api_response.get("ipaddress-v4-aim"), + ipaddress_v6_aim=api_response.get("ipaddress-v6-aim"), + ipaddress_v6_oam=api_response.get("ipaddress-v6-oam"), + inv_status=api_response.get("inv-status"), + resource_version=api_response.get("resource-version"), + prov_status=api_response.get("prov-status"), + nf_role=api_response.get("nf-role"), + admin_status=api_response.get("admin-status"), + operational_status=api_response.get("operational-status"), + model_customization_id=api_response.get("model-customization-id"), + model_invariant_id=api_response.get("model-invariant-id"), + model_version_id=api_response.get("model-version-id"), + pnf_ipv4_address=api_response.get("pnf-ipv4-address"), + pnf_ipv6_address=api_response.get("pnf-ipv6-address")) + + def delete(self, a_la_carte: bool = True) -> None: + """Delete Pnf instance. + + PNF deletion it's just A&AI resource deletion. That's difference between another instances. + You don't have to wait for that task finish, because it's not async task. + + """ + self._logger.debug("Delete %s pnf", self.pnf_name) + self.send_message("DELETE", + f"Delete {self.pnf_name} PNF", + f"{self.url}?resource-version={self.resource_version}") diff --git a/src/onapsdk/aai/business/project.py b/src/onapsdk/aai/business/project.py new file mode 100644 index 0000000..989444a --- /dev/null +++ b/src/onapsdk/aai/business/project.py @@ -0,0 +1,123 @@ +"""A&AI project 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 typing import Any, Dict, Iterator + +from onapsdk.utils.jinja import jinja_env + +from ..aai_element import AaiResource + + +class Project(AaiResource): + """Project class.""" + + def __init__(self, name: str, resource_version: str) -> None: + """Project object initialization. + + Args: + name (str): Project name + resource_version (str): resource version + """ + super().__init__() + self.name: str = name + self.resource_version: str = resource_version + + @classmethod + def get_all(cls) -> Iterator["Project"]: + """Get all project. + + Yields: + Project: Project object + + """ + url: str = cls.get_all_url() + for project in cls.send_message_json("GET", + "Get A&AI projects", + url).get("project", []): + yield cls( + project.get("project-name"), + project.get("resource-version") + ) + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all projects. + + Returns: + str: Url to get all projects + + """ + return f"{cls.base_url}{cls.api_version}/business/projects" + + def __repr__(self) -> str: + """Project object representation. + + Returns: + str: Project object representation + + """ + return f"Project(name={self.name})" + + @property + def url(self) -> str: + """Project's url. + + Returns: + str: Resource's url + + """ + return (f"{self.base_url}{self.api_version}/business/projects/" + f"project/{self.name}") + + @classmethod + def create(cls, name: str) -> "Project": + """Create project A&AI resource. + + Args: + name (str): project name + + Returns: + Project: Created Project object + + """ + cls.send_message( + "PUT", + "Declare A&AI project", + (f"{cls.base_url}{cls.api_version}/business/projects/" + f"project/{name}"), + data=jinja_env().get_template("aai_project_create.json.j2").render( + project_name=name + ) + ) + return cls.get_by_name(name) + + @classmethod + def get_by_name(cls, name: str) -> "Project": + """Get project resource by it's name. + + Raises: + ResourceNotFound: Project requested by a name does not exist. + + Returns: + Project: Project requested by a name. + + """ + url = (f"{cls.base_url}{cls.api_version}/business/projects/" + f"project/{name}") + response: Dict[str, Any] = \ + cls.send_message_json("GET", + f"Get {name} project", + url) + return cls(response["project-name"], response["resource-version"]) diff --git a/src/onapsdk/aai/business/service.py b/src/onapsdk/aai/business/service.py new file mode 100644 index 0000000..fe3b34d --- /dev/null +++ b/src/onapsdk/aai/business/service.py @@ -0,0 +1,484 @@ +"""Service instance 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 typing import Iterator, Type, Union, Iterable, Optional + +from onapsdk.exceptions import StatusError, ParameterError +from onapsdk.sdc.service import Service +from onapsdk.so.deletion import ServiceDeletionRequest +from onapsdk.so.instantiation import NetworkInstantiation, VnfInstantiation +from onapsdk.utils.jinja import jinja_env + +from .instance import Instance +from .network import NetworkInstance +from .pnf import PnfInstance +from .vnf import VnfInstance + + +class ServiceInstance(Instance): # pylint: disable=too-many-instance-attributes + """Service instanve class.""" + + def __init__(self, # pylint: disable=too-many-arguments, too-many-locals + service_subscription: "ServiceSubscription", + instance_id: str, + instance_name: str = None, + service_type: str = None, + service_role: str = None, + environment_context: str = None, + workload_context: str = None, + created_at: str = None, + updated_at: str = None, + resource_version: str = None, + description: str = None, + model_invariant_id: str = None, + model_version_id: str = None, + persona_model_version: str = None, + widget_model_id: str = None, + widget_model_version: str = None, + bandwith_total: str = None, + vhn_portal_url: str = None, + service_instance_location_id: str = None, + selflink: str = None, + orchestration_status: str = None, + input_parameters: str = None) -> None: + """Service instance object initialization. + + Args: + service_subscription (ServiceSubscription): service subscription which is belongs to + instance_id (str): Uniquely identifies this instance of a service + instance_name (str, optional): This field will store a name assigned to + the service-instance. Defaults to None. + service_type (str, optional): String capturing type of service. Defaults to None. + service_role (str, optional): String capturing the service role. Defaults to None. + environment_context (str, optional): This field will store the environment context + assigned to the service-instance. Defaults to None. + workload_context (str, optional): This field will store the workload context assigned to + the service-instance. Defaults to None. + created_at (str, optional): Create time of Network Service. Defaults to None. + updated_at (str, optional): Last update of Network Service. Defaults to None. + description (str, optional): Short description for service-instance. Defaults to None. + model_invariant_id (str, optional): The ASDC model id for this resource or + service model. Defaults to None. + model_version_id (str, optional): The ASDC model version for this resource or + service model. Defaults to None. + persona_model_version (str, optional): The ASDC model version for this resource or + service model. Defaults to None. + widget_model_id (str, optional): The ASDC data dictionary widget model. This maps + directly to the A&AI widget. Defaults to None. + widget_model_version (str, optional): The ASDC data dictionary version of the widget + model. This maps directly to the A&AI version of the widget. Defaults to None. + bandwith_total (str, optional): Indicates the total bandwidth to be used for this + service. Defaults to None. + vhn_portal_url (str, optional): URL customers will use to access the vHN Portal. + Defaults to None. + service_instance_location_id (str, optional): An identifier that customers assign to + the location where this service is being used. Defaults to None. + resource_version (str, optional): Used for optimistic concurrency. Must be empty on + create, valid on update and delete. Defaults to None. + selflink (str, optional): Path to the controller object. Defaults to None. + orchestration_status (str, optional): Orchestration status of this service. + Defaults to None. + input_parameters (str, optional): String capturing request parameters from SO to + pass to Closed Loop. Defaults to None. + """ + super().__init__(resource_version=resource_version, + model_invariant_id=model_invariant_id, + model_version_id=model_version_id) + self.service_subscription: "ServiceSubscription" = service_subscription + self.instance_id: str = instance_id + self.instance_name: str = instance_name + self.service_type: str = service_type + self.service_role: str = service_role + self.environment_context: str = environment_context + self.workload_context: str = workload_context + self.created_at: str = created_at + self.updated_at: str = updated_at + self.description: str = description + self.bandwith_total: str = bandwith_total + self.vhn_portal_url: str = vhn_portal_url + self.service_instance_location_id: str = service_instance_location_id + self.selflink: str = selflink + self.orchestration_status: str = orchestration_status + self.input_parameters: str = input_parameters + self.persona_model_version: str = persona_model_version + self.widget_model_id: str = widget_model_id + self.widget_model_version: str = widget_model_version + self._sdc_service: Optional[Service] = None + + def __repr__(self) -> str: + """Service instance object representation. + + Returns: + str: Human readable service instance representation + + """ + return (f"ServiceInstance(instance_id={self.instance_id}, " + f"instance_name={self.instance_name})") + + def _get_related_instance(self, + related_instance_class: Union[Type[NetworkInstance], + Type[VnfInstance]], + relationship_related_to_type: str) -> Iterator[\ + Union[NetworkInstance, + VnfInstance]]: + """Iterate through related service instances. + + This is method which for given `relationship_related_to_type` creates iterator + it iterate through objects which are related with service. + + Args: + related_instance_class (Union[Type[NetworkInstance], Type[VnfInstance]]): Class object + to create required object instances + relationship_related_to_type (str): Has to be "generic-vnf" or "l3-network" + + Raises: + ParameterError: relationship_related_to_type does not satisfy the requirements + + Yields: + Iterator[ Union[NetworkInstance, VnfInstance]]: [description] + + """ + if not relationship_related_to_type in ["l3-network", "generic-vnf", "pnf"]: + msg = ( + f'Invalid "relationship_related_to_type" value. ' + f'Provided "{relationship_related_to_type}". ' + f'Has to be "l3-network" or "generic-vnf".' + ) + raise ParameterError(msg) + for relationship in self.relationships: + if relationship.related_to == relationship_related_to_type: + yield related_instance_class.create_from_api_response(\ + self.send_message_json("GET", + (f"Get {self.instance_id} " + f"{related_instance_class.__class__.__name__}"), + f"{self.base_url}{relationship.related_link}"), + self) + + @classmethod + def create(cls, service_subscription: "ServiceSubscription", # pylint: disable=too-many-arguments, too-many-locals + instance_id: str, + instance_name: str = None, + service_type: str = None, + service_role: str = None, + environment_context: str = None, + workload_context: str = None, + created_at: str = None, + updated_at: str = None, + resource_version: str = None, + description: str = None, + model_invariant_id: str = None, + model_version_id: str = None, + persona_model_version: str = None, + widget_model_id: str = None, + widget_model_version: str = None, + bandwith_total: str = None, + vhn_portal_url: str = None, + service_instance_location_id: str = None, + selflink: str = None, + orchestration_status: str = None, + input_parameters: str = None): + """Service instance creation. + + Args: + service_subscription (ServiceSubscription): service subscription which is belongs to + instance_id (str): Uniquely identifies this instance of a service + instance_name (str, optional): This field will store a name assigned to + the service-instance. Defaults to None. + service_type (str, optional): String capturing type of service. Defaults to None. + service_role (str, optional): String capturing the service role. Defaults to None. + environment_context (str, optional): This field will store the environment context + assigned to the service-instance. Defaults to None. + workload_context (str, optional): This field will store the workload context assigned to + the service-instance. Defaults to None. + created_at (str, optional): Create time of Network Service. Defaults to None. + updated_at (str, optional): Last update of Network Service. Defaults to None. + description (str, optional): Short description for service-instance. Defaults to None. + model_invariant_id (str, optional): The ASDC model id for this resource or + service model. Defaults to None. + model_version_id (str, optional): The ASDC model version for this resource or + service model. Defaults to None. + persona_model_version (str, optional): The ASDC model version for this resource or + service model. Defaults to None. + widget_model_id (str, optional): The ASDC data dictionary widget model. This maps + directly to the A&AI widget. Defaults to None. + widget_model_version (str, optional): The ASDC data dictionary version of the widget + model. This maps directly to the A&AI version of the widget. Defaults to None. + bandwith_total (str, optional): Indicates the total bandwidth to be used for this + service. Defaults to None. + vhn_portal_url (str, optional): URL customers will use to access the vHN Portal. + Defaults to None. + service_instance_location_id (str, optional): An identifier that customers assign to + the location where this service is being used. Defaults to None. + resource_version (str, optional): Used for optimistic concurrency. Must be empty on + create, valid on update and delete. Defaults to None. + selflink (str, optional): Path to the controller object. Defaults to None. + orchestration_status (str, optional): Orchestration status of this service. + Defaults to None. + input_parameters (str, optional): String capturing request parameters from SO to + pass to Closed Loop. Defaults to None. + """ + service_instance: "ServiceInstance" = cls( + service_subscription, + instance_id, + instance_name, + service_type, + service_role, + environment_context, + workload_context, + created_at, + updated_at, + resource_version, + description, + model_invariant_id, + model_version_id, + persona_model_version, + widget_model_id, + widget_model_version, + bandwith_total, + vhn_portal_url, + service_instance_location_id, + selflink, + orchestration_status, + input_parameters + ) + cls.send_message("PUT", + f"Create service instance {instance_id} for "\ + f"{service_subscription.service_type} service subscription", + f"{service_subscription.url}/service-instances/service-instance/"\ + f"{instance_id}", + data=jinja_env() + .get_template("aai_service_instance_create.json.j2") + .render( + service_instance=service_instance + )) + return service_instance + + @classmethod + def get_all_url(cls, service_subscription: "ServiceSubscription") -> str: # pylint: disable=arguments-differ + """Return an url to get all service instances for service subscription. + + Args: + service_subscription (ServiceSubscription): Service subscription object + + Returns: + str: Url to get all service instances for service subscription + + """ + return f"{service_subscription.url}/service-instances/" + + @property + def url(self) -> str: + """Service instance resource URL. + + Returns: + str: Service instance url + + """ + return ( + f"{self.service_subscription.url}/service-instances/service-instance/{self.instance_id}" + ) + + @property + def vnf_instances(self) -> Iterator[VnfInstance]: + """Vnf instances associated with service instance. + + Returns iterator of VnfInstances representing VNF instantiated for that service + + Yields: + VnfInstance: VnfInstance object + + """ + return self._get_related_instance(VnfInstance, "generic-vnf") + + @property + def network_instances(self) -> Iterator[NetworkInstance]: + """Network instances associated with service instance. + + Returns iterator of NetworkInstance representing network instantiated for that service + + Yields: + NetworkInstance: NetworkInstance object + + """ + return self._get_related_instance(NetworkInstance, "l3-network") + + @property + def pnfs(self) -> Iterator[PnfInstance]: + """Pnfs associated with service instance. + + Returns iterator of PnfInstance representing pnfs instantiated for that service + + Yields: + PnfInstance: PnfInstance object + + """ + return self._get_related_instance(PnfInstance, "pnf") + + @property + def sdc_service(self) -> Service: + """Sdc service related with that instance. + + Sdc service model which was used to create that instance. + + Raises: + ResourceNotFound: Service model not found + + """ + if not self._sdc_service: + self._sdc_service = Service.get_by_unique_uuid(self.model_invariant_id) + return self._sdc_service + + @property + def active(self) -> bool: + """Information if service instance's orchestration status is active.""" + return self.orchestration_status == "Active" + + def add_vnf(self, # pylint: disable=too-many-arguments + vnf: "Vnf", + line_of_business: "LineOfBusiness", + platform: "Platform", + cloud_region: "CloudRegion" = None, + tenant: "Tenant" = None, + vnf_instance_name: str = None, + vnf_parameters: Iterable["InstantiationParameter"] = None, + so_vnf: "SoServiceVnf" = None, + a_la_carte: bool = True + ) -> "VnfInstantiation": + """Add vnf into service instance. + + Instantiate VNF. + + Args: + vnf (Vnf): Vnf from service configuration to instantiate + line_of_business (LineOfBusiness): LineOfBusiness to use in instantiation request + platform (Platform): Platform to use in instantiation request + cloud_region (CloudRegion, optional): Cloud region to use in instantiation request. + Defaults to None. + THAT PROPERTY WILL BE REQUIRED IN ONE OF THE FUTURE RELEASE. REFACTOR YOUR CODE + TO USE IT!. + tenant (Tenant, optional): Tenant to use in instnatiation request. + Defaults to None. + THAT PROPERTY WILL BE REQUIRED IN ONE OF THE FUTURE RELEASE. REFACTOR YOUR CODE + TO USE IT!. + vnf_instance_name (str, optional): VNF instantion name. + If no value is provided it's going to be + "Python_ONAP_SDK_vnf_instance_{str(uuid4())}". + Defaults to None. + vnf_parameters (Iterable[InstantiationParameter], optional): InstantiationParameter to + be passed as "userParams". Defaults to None. + so_vnf: (SoServiceVnf, optional): object with vnf instance parameters. Defaults to None. + a_la_carte (bool): instantiation type for vnf. Defaults to True. + + Raises: + StatusError: Service orchestration status is not "Active". + + Returns: + VnfInstantiation: VnfInstantiation request object + + """ + if not self.active: + raise StatusError('Service orchestration status must be "Active"') + + if a_la_carte: + return VnfInstantiation.instantiate_ala_carte( + self, + vnf, + line_of_business, + platform, + cloud_region=cloud_region, + tenant=tenant, + vnf_instance_name=vnf_instance_name, + vnf_parameters=vnf_parameters, + sdc_service=self.sdc_service + ) + + return VnfInstantiation.instantiate_macro( + self, + vnf, + line_of_business, + platform, + cloud_region=cloud_region, + tenant=tenant, + vnf_instance_name=vnf_instance_name, + vnf_parameters=vnf_parameters, + so_vnf=so_vnf, + sdc_service=self.sdc_service + ) + + def add_network(self, # pylint: disable=too-many-arguments + network: "Network", + line_of_business: "LineOfBusiness", + platform: "Platform", + cloud_region: "CloudRegion" = None, + tenant: "Tenant" = None, + network_instance_name: str = None, + subnets: Iterator["Subnet"] = None) -> "NetworkInstantiation": + """Add network into service instance. + + Instantiate vl. + + Args: + network (Network): Network from service configuration to instantiate + line_of_business (LineOfBusiness): LineOfBusiness to use in instantiation request + platform (Platform): Platform to use in instantiation request + cloud_region (CloudRegion, optional): Cloud region to use in instantiation request. + Defaults to None. + THAT PROPERTY WILL BE REQUIRED IN ONE OF THE FUTURE RELEASE. REFACTOR YOUR CODE + TO USE IT!. + tenant (Tenant, optional): Tenant to use in instnatiation request. + Defaults to None. + THAT PROPERTY WILL BE REQUIRED IN ONE OF THE FUTURE RELEASE. REFACTOR YOUR CODE + TO USE IT!. + network_instance_name (str, optional): Network instantion name. + If no value is provided it's going to be + "Python_ONAP_SDK_network_instance_{str(uuid4())}". + Defaults to None. + + Raises: + StatusError: Service orchestration status is not "Active" + + Returns: + NetworkInstantiation: NetworkInstantiation request object + + """ + if not self.active: + msg = f'Service orchestration status must be "Active"' + raise StatusError(msg) + + return NetworkInstantiation.instantiate_ala_carte( + self, + network, + line_of_business, + platform, + cloud_region=cloud_region, + tenant=tenant, + network_instance_name=network_instance_name, + subnets=subnets + ) + + def delete(self, a_la_carte: bool = True) -> "ServiceDeletionRequest": + """Create service deletion request. + + Send a request to delete service instance + + Args: + a_la_carte (boolean): deletion mode + + Returns: + ServiceDeletionRequest: Deletion request object + + """ + self._logger.debug("Delete %s service instance", self.instance_id) + return ServiceDeletionRequest.send_request(self, a_la_carte) diff --git a/src/onapsdk/aai/business/sp_partner.py b/src/onapsdk/aai/business/sp_partner.py new file mode 100644 index 0000000..05d6a05 --- /dev/null +++ b/src/onapsdk/aai/business/sp_partner.py @@ -0,0 +1,176 @@ +"""A&AI sp-partner 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 typing import Iterator, Optional + +from onapsdk.utils.jinja import jinja_env + +from ..aai_element import AaiResource + + +class SpPartner(AaiResource): # pylint: disable=too-many-instance-attributes + """Sp partner class.""" + + def __init__(self, sp_partner_id: str, resource_version: str, url: str = None, # pylint: disable=too-many-arguments, too-many-locals + callsource: str = None, operational_status: str = None, + model_customization_id: str = None, model_invariant_id: str = None, + model_version_id: str = None) -> None: + """Sp partner object initialization. + + Args: + sp_partner_id (str): Uniquely identifies this sp-partner by id + resource_version (str): resource version + url (str, optional): Store the URL of this sp-partner. Defaults to None + callsource (str, optional): Store the callsource of this sp-partner. Defaults to None + operational_status (str, optional): Store the operational-status of this sp-partner. + Defaults to None + model_customization_id (str, optional): Store the model-customization-id + of this sp-partner. Defaults to None + model_invariant_id (str, optional): The ASDC model id for this sp-partner model. + Defaults to None + model_version_id (str, optional): The ASDC model version for this sp-partner model. + Defaults to None + + """ + super().__init__() + self.sp_partner_id: str = sp_partner_id + self.resource_version: str = resource_version + self.sp_partner_url: Optional[str] = url + self.callsource: Optional[str] = callsource + self.operational_status: Optional[str] = operational_status + self.model_customization_id: Optional[str] = model_customization_id + self.model_invariant_id: Optional[str] = model_invariant_id + self.model_version_id: Optional[str] = model_version_id + + def __repr__(self) -> str: + """Sp partner object representation. + + Returns: + str: SpPartner object representation + + """ + return f"SpPartner(sp_partner_id={self.sp_partner_id})" + + @property + def url(self) -> str: + """Sp partner's url. + + Returns: + str: Resource's url + + """ + return (f"{self.base_url}{self.api_version}/business/sp-partners/" + f"sp-partner/{self.sp_partner_id}") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all sp partners. + + Returns: + str: Url to get all sp partners + + """ + return f"{cls.base_url}{cls.api_version}/business/sp-partners" + + @classmethod + def get_all(cls) -> Iterator["SpPartner"]: + """Get all sp partners. + + Yields: + SpPartner: SpPartner object + + """ + url: str = cls.get_all_url() + for sp_partner in cls.send_message_json("GET", + "Get A&AI sp-partners", + url).get("sp-partner", []): + yield cls( + sp_partner["sp-partner-id"], + sp_partner["resource-version"], + sp_partner.get("url"), + sp_partner.get("callsource"), + sp_partner.get("operational-status"), + sp_partner.get("model-customization-id"), + sp_partner.get("model-invariant-id"), + sp_partner.get("model-version-id"), + ) + + @classmethod + def create(cls, sp_partner_id: str, url: str = "", callsource: str = "", # pylint: disable=too-many-arguments + operational_status: str = "", model_customization_id: str = "", + model_invariant_id: str = "", model_version_id: str = "") -> "SpPartner": + """Create sp partner A&AI resource. + + Args: + sp_partner_id (str): sp partner unique ID + url (str, optional): Store the URL of this sp-partner. Defaults to None + callsource (str, optional): Store the callsource of this sp-partner. Defaults to None + operational_status (str, optional): Store the operational-status of this sp-partner. + Defaults to None + model_customization_id (str, optional): Store the model-customization-id + of this sp-partner. Defaults to None + model_invariant_id (str, optional): The ASDC model id for this sp-partner model. + Defaults to None + model_version_id (str, optional): The ASDC model version for this sp-partner model. + Defaults to None + + Returns: + SpPartner: Created SpPartner object + + """ + cls.send_message( + "PUT", + "Declare A&AI sp partner", + (f"{cls.base_url}{cls.api_version}/business/sp-partners/" + f"sp-partner/{sp_partner_id}"), + data=jinja_env().get_template("aai_sp_partner_create.json.j2").render( + sp_partner_id=sp_partner_id, + url=url, + callsource=callsource, + operational_status=operational_status, + model_customization_id=model_customization_id, + model_invariant_id=model_invariant_id, + model_version_id=model_version_id + ) + ) + return cls.get_by_sp_partner_id(sp_partner_id) + + @classmethod + def get_by_sp_partner_id(cls, sp_partner_id: str) -> "SpPartner": + """Get sp partner by it's ID. + + Args: + sp_partner_id (str): sp partner object id + + Returns: + SpPartner: SpPartner object + + """ + response: dict = cls.send_message_json( + "GET", + "Get A&AI sp partner", + (f"{cls.base_url}{cls.api_version}/business/sp-partners/" + f"sp-partner/{sp_partner_id}") + ) + return cls( + response["sp-partner-id"], + response["resource-version"], + response.get("url"), + response.get("callsource"), + response.get("operational-status"), + response.get("model-customization-id"), + response.get("model-invariant-id"), + response.get("model-version-id") + ) diff --git a/src/onapsdk/aai/business/vf_module.py b/src/onapsdk/aai/business/vf_module.py new file mode 100644 index 0000000..ac91560 --- /dev/null +++ b/src/onapsdk/aai/business/vf_module.py @@ -0,0 +1,199 @@ +"""VF module instance.""" +# 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 onapsdk.so.deletion import VfModuleDeletionRequest +from onapsdk.exceptions import ResourceNotFound + +from .instance import Instance + + +class VfModuleInstance(Instance): # pylint: disable=too-many-instance-attributes + """Vf module instance class.""" + + def __init__(self, # pylint: disable=too-many-arguments, too-many-locals + vnf_instance: "VnfInstance", + vf_module_id: str, + is_base_vf_module: bool, + automated_assignment: bool, + vf_module_name: str = None, + heat_stack_id: str = None, + resource_version: str = None, + model_invariant_id: str = None, + orchestration_status: str = None, + persona_model_version: str = None, + model_version_id: str = None, + model_customization_id: str = None, + widget_model_id: str = None, + widget_model_version: str = None, + contrail_service_instance_fqdn: str = None, + module_index: int = None, + selflink: str = None) -> None: + """Vf module initialization. + + Args: + vnf_instance (VnfInstance): VnfInstance + vf_module_id (str): Unique ID of vf-module + is_base_vf_module (bool): used to indicate whether or not this object is base vf module + automated_assignment (bool): ndicates whether vf-module assignment was done via + automation or manually + vf_module_name (str, optional): Name of vf-module. Defaults to None. + heat_stack_id (str, optional): Heat stack id corresponding to this instance. + Defaults to None. + orchestration_status (str, optional): orchestration status of this vf-module, + mastered by MSO. Defaults to None. + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update and delete. Defaults to None. + model_invariant_id (str, optional): the ASDC model id for this resource or + service model. Defaults to None. + model_version_id (str, optional): the ASDC model version for this resource or + service model. Defaults to None. + persona_model_version (str, optional): the ASDC model version for this resource or + service model. Defaults to None. + model_customization_id (str, optional): captures the id of all the configuration + used to customize the resource for the service. Defaults to None. + widget_model_id (str, optional): the ASDC data dictionary widget model. + This maps directly to the A&AI widget. Defaults to None. + widget_model_version (str, optional): the ASDC data dictionary version of + the widget model. This maps directly to the A&AI version of the widget. + Defaults to None. + contrail_service_instance_fqdn (str, optional): the Contrail unique ID + for a service-instance. Defaults to None. + module_index (int, optional): the index will track the number of modules + of a given type that have been deployed in a VNF, starting with 0, + and always choosing the lowest available digit. Defaults to None. + selflink (str, optional): Path to the controller object. Defaults to None. + """ + super().__init__(resource_version=resource_version, model_version_id=model_version_id, + model_invariant_id=model_invariant_id) + self.vnf_instance: "VnfInstance" = vnf_instance + self.vf_module_id: str = vf_module_id + self.is_base_vf_module: bool = is_base_vf_module + self.automated_assignment: bool = automated_assignment + self.vf_module_name: str = vf_module_name + self.heat_stack_id: str = heat_stack_id + self.orchestration_status: str = orchestration_status + self.model_customization_id: str = model_customization_id + self.contrail_service_instance_fqdn: str = contrail_service_instance_fqdn + self.module_index: int = module_index + self.selflink: str = selflink + self.persona_model_version: str = persona_model_version + self.widget_model_id: str = widget_model_id + self.widget_model_version: str = widget_model_version + + self._vf_module: "VfModule" = None + + def __repr__(self) -> str: + """Object represetation. + + Returns: + str: Human readble VfModuleInstance representation + + """ + return (f"VfModuleInstance(vf_module_id={self.vf_module_id}, " + f"is_base_vf_module={self.is_base_vf_module}, " + f"automated_assignment={self.automated_assignment})") + + @classmethod + def get_all_url(cls, vnf_instance: "VnfInstance") -> str: # pylint: disable=arguments-differ + """Return url to get all vf modules for vnf instance. + + Args: + vnf_instance (VnfInstance): VNF instance object + + Returns: + str: Url to get all vf modules for vnf instance + + """ + return f"{vnf_instance.url}/vf-modules/" + + @property + def url(self) -> str: + """Resource url. + + Returns: + str: VfModuleInstance url + + """ + return f"{self.vnf_instance.url}/vf-modules/vf-module/{self.vf_module_id}" + + @property + def vf_module(self) -> "VfModule": + """Vf module associated with that vf module instance. + + Returns: + VfModule: VfModule object associated with vf module instance + + """ + if not self._vf_module: + for vf_module in self.vnf_instance.vnf.vf_modules: + if vf_module.model_version_id == self.model_version_id: + self._vf_module = vf_module + return self._vf_module + + msg = ( + f'Could not find VF modules for the VF Module instance' + f' with model version ID "{self.model_version_id}"' + ) + raise ResourceNotFound(msg) + return self._vf_module + + @classmethod + def create_from_api_response(cls, + api_response: dict, + vnf_instance: "VnfInstance") -> "VfModuleInstance": + """Create vf module instance object using HTTP API response dictionary. + + Args: + api_response (dict): HTTP API response content + vnf_instance (VnfInstance): VnfInstance associated with VfModuleInstance + + Returns: + VfModuleInstance: VfModuleInstance object + + """ + return cls( + vnf_instance=vnf_instance, + vf_module_id=api_response.get("vf-module-id"), + is_base_vf_module=api_response.get("is-base-vf-module"), + automated_assignment=api_response.get("automated-assignment"), + vf_module_name=api_response.get("vf-module-name"), + heat_stack_id=api_response.get("heat-stack-id"), + orchestration_status=api_response.get("orchestration-status"), + resource_version=api_response.get("resource-version"), + model_invariant_id=api_response.get("model-invariant-id"), + model_version_id=api_response.get("model-version-id"), + persona_model_version=api_response.get("persona-model-version"), + model_customization_id=api_response.get("model-customization-id"), + widget_model_id=api_response.get("widget-model-id"), + widget_model_version=api_response.get("widget-model-version"), + contrail_service_instance_fqdn=api_response.get("contrail-service-instance-fqdn"), + module_index=api_response.get("module-index"), + selflink=api_response.get("selflink") + ) + + def delete(self, a_la_carte: bool = True) -> "VfModuleDeletionRequest": + """Create deletion request. + + Send request to delete VF module instance + + Args: + a_la_carte (boolean): deletion mode + + Returns: + VfModuleDeletionRequest: Deletion request object + + """ + self._logger.debug("Delete %s VF module", self.vf_module_id) + return VfModuleDeletionRequest.send_request(self, a_la_carte) diff --git a/src/onapsdk/aai/business/vnf.py b/src/onapsdk/aai/business/vnf.py new file mode 100644 index 0000000..2045291 --- /dev/null +++ b/src/onapsdk/aai/business/vnf.py @@ -0,0 +1,536 @@ +"""Vnf instance 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 typing import Iterable, Iterator + +from onapsdk.exceptions import ResourceNotFound, StatusError +from onapsdk.so.deletion import VnfDeletionRequest +from onapsdk.so.instantiation import VfModuleInstantiation, VnfInstantiation, SoService, \ + InstantiationParameter, VnfOperation +from onapsdk.configuration import settings + +from .instance import Instance +from .vf_module import VfModuleInstance + + +class VnfInstance(Instance): # pylint: disable=too-many-instance-attributes + """VNF Instance class.""" + + def __init__(self, # pylint: disable=too-many-arguments, too-many-locals + service_instance: "ServiceInstance", + vnf_id: str, + vnf_type: str, + in_maint: bool, + is_closed_loop_disabled: bool, + vnf_name: str = None, + service_id: str = None, + regional_resource_zone: str = None, + prov_status: str = None, + operational_status: str = None, + equipment_role: str = None, + orchestration_status: str = None, + vnf_package_name: str = None, + vnf_discriptor_name: str = None, + job_id: str = None, + heat_stack_id: str = None, + mso_catalog_key: str = None, + management_option: str = None, + ipv4_oam_address: str = None, + ipv4_loopback0_address: str = None, + nm_lan_v6_address: str = None, + management_v6_address: str = None, + vcpu: int = None, + vcpu_units: str = None, + vmemory: int = None, + vmemory_units: str = None, + vdisk: int = None, + vdisk_units: str = None, + nshd: int = None, + nvm: int = None, + nnet: int = None, + resource_version: str = None, + encrypted_access_flag: bool = None, + model_invariant_id: str = None, + model_version_id: str = None, + persona_model_version: str = None, + model_customization_id: str = None, + widget_model_id: str = None, + widget_model_version: str = None, + as_number: str = None, + regional_resource_subzone: str = None, + nf_type: str = None, + nf_function: str = None, + nf_role: str = None, + nf_naming_code: str = None, + selflink: str = None, + ipv4_oam_gateway_address: str = None, + ipv4_oam_gateway_address_prefix_length: int = None, + vlan_id_outer: int = None, + nm_profile_name: str = None) -> None: + """Vnf instance object initialization. + + Args: + vnf_id (str): Unique id of VNF. This is unique across the graph. + vnf_type (str): String capturing type of vnf, that was intended to identify + the ASDC resource. This field has been overloaded in service-specific ways and + clients should expect changes to occur in the future to this field as ECOMP + matures. + in_maint (bool): used to indicate whether or not this object is in maintenance mode + (maintenance mode = true). This field (in conjunction with prov-status) + is used to suppress alarms and vSCL on VNFs/VMs. + is_closed_loop_disabled (bool): used to indicate whether closed loop function is + enabled on this node + vnf_name (str, optional): Name of VNF. Defaults to None. + service_id (str, optional): Unique identifier of service, does not necessarily map to + ASDC service models. Defaults to None. + regional_resource_zone (str, optional): Regional way of organizing pservers, source of + truth should define values. Defaults to None. + prov_status (str, optional): Trigger for operational monitoring of this resource by + Service Assurance systems. Defaults to None. + operational_status (str, optional): Indicator for whether the resource is considered + operational. Valid values are in-service-path and out-of-service-path. + Defaults to None. + equipment_role (str, optional): Client should send valid enumerated value. + Defaults to None. + orchestration_status (str, optional): Orchestration status of this VNF, used by MSO. + Defaults to None. + vnf_package_name (str, optional): vnf package name. Defaults to None. + vnf_discriptor_name (str, optional): vnf discriptor name. Defaults to None. + job_id (str, optional): job id corresponding to vnf. Defaults to None. + heat_stack_id (str, optional): Heat stack id corresponding to this instance, + managed by MSO. Defaults to None. + mso_catalog_key (str, optional): Corresponds to the SDN-C catalog id used to + configure this VCE. Defaults to None. + management_option (str, optional): identifier of managed customer. Defaults to None. + ipv4_oam_address (str, optional): Address tail-f uses to configure generic-vnf, + also used for troubleshooting and is IP used for traps generated by generic-vnf. + Defaults to None. + ipv4_loopback0_address (str, optional): v4 Loopback0 address. Defaults to None. + nm_lan_v6_address (str, optional): v6 Loopback address. Defaults to None. + management_v6_address (str, optional): v6 management address. Defaults to None. + vcpu (int, optional): number of vcpus ordered for this instance of VNF, + used for VNFs with no vservers/flavors, to be used only by uCPE. Defaults to None. + vcpu_units (str, optional): units associated with vcpu, used for VNFs with no + vservers/flavors, to be used only by uCPE. Defaults to None. + vmemory (int, optional): number of GB of memory ordered for this instance of VNF, + used for VNFs with no vservers/flavors, to be used only by uCPE. Defaults to None. + vmemory_units (str, optional): units associated with vmemory, used for VNFs with + no vservers/flavors, to be used only by uCPE. Defaults to None. + vdisk (int, optional): number of vdisks ordered for this instance of VNF, + used for VNFs with no vservers/flavors, to be used only uCPE. Defaults to None. + vdisk_units (str, optional): units associated with vdisk, used for VNFs with + no vservers/flavors, to be used only by uCPE. Defaults to None. + nshd (int, optional): number of associated SHD in vnf. Defaults to None. + nvm (int, optional): number of vms in vnf. Defaults to None. + nnet (int, optional): number of network in vnf. Defaults to None. + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update and delete. Defaults to None. + encrypted_access_flag (bool, optional): indicates whether generic-vnf access uses SSH. + Defaults to None. + model_invariant_id (str, optional): the ASDC model id for this resource or + service model. Defaults to None. + model_version_id (str, optional): the ASDC model version for this resource or + service model. Defaults to None. + persona_model_version (str, optional): the ASDC model version for this resource or + service model. Defaults to None. + model_customization_id (str, optional): captures the id of all the configuration used + to customize the resource for the service. Defaults to None. + widget_model_id (str, optional): the ASDC data dictionary widget model. This maps + directly to the A&AI widget. Defaults to None. + widget_model_version (str, optional): the ASDC data dictionary version of + the widget model.This maps directly to the A&AI version of the widget. + Defaults to None. + as_number (str, optional): as-number of the VNF. Defaults to None. + regional_resource_subzone (str, optional): represents sub zone of the rr plane. + Defaults to None. + nf_type (str, optional): Generic description of the type of NF. Defaults to None. + nf_function (str, optional): English description of Network function that + the specific VNF deployment is providing. Defaults to None. + nf_role (str, optional): role in the network that this model will be providing. + Defaults to None. + nf_naming_code (str, optional): string assigned to this model used for naming purposes. + Defaults to None. + selflink (str, optional): Path to the controller object. Defaults to None. + ipv4_oam_gateway_address (str, optional): Gateway address. Defaults to None. + ipv4_oam_gateway_address_prefix_length (int, optional): Prefix length for oam-address. + Defaults to None. + vlan_id_outer (int, optional): Temporary location for S-TAG to get to VCE. + Defaults to None. + nm_profile_name (str, optional): Network Management profile of this VNF. + Defaults to None. + + """ + super().__init__(resource_version=resource_version, + model_invariant_id=model_invariant_id, + model_version_id=model_version_id) + self.service_instance: "ServiceInstance" = service_instance + self.vnf_id: str = vnf_id + self.vnf_type: str = vnf_type + self.in_maint: bool = in_maint + self.is_closed_loop_disabled: bool = is_closed_loop_disabled + self.vnf_name: str = vnf_name + self.service_id: str = service_id + self.regional_resource_zone: str = regional_resource_zone + self.prov_status: str = prov_status + self.operational_status: str = operational_status + self.equipment_role: str = equipment_role + self.orchestration_status: str = orchestration_status + self.vnf_package_name: str = vnf_package_name + self.vnf_discriptor_name: str = vnf_discriptor_name + self.job_id: str = job_id + self.heat_stack_id: str = heat_stack_id + self.mso_catalog_key: str = mso_catalog_key + self.management_option: str = management_option + self.ipv4_oam_address: str = ipv4_oam_address + self.ipv4_loopback0_address: str = ipv4_loopback0_address + self.nm_lan_v6_address: str = nm_lan_v6_address + self.management_v6_address: str = management_v6_address + self.vcpu: int = vcpu + self.vcpu_units: str = vcpu_units + self.vmemory: int = vmemory + self.vmemory_units: str = vmemory_units + self.vdisk: int = vdisk + self.vdisk_units: str = vdisk_units + self.nshd: int = nshd + self.nvm: int = nvm + self.nnet: int = nnet + self.encrypted_access_flag: bool = encrypted_access_flag + self.model_customization_id: str = model_customization_id + self.as_number: str = as_number + self.regional_resource_subzone: str = regional_resource_subzone + self.nf_type: str = nf_type + self.nf_function: str = nf_function + self.nf_role: str = nf_role + self.nf_naming_code: str = nf_naming_code + self.selflink: str = selflink + self.ipv4_oam_gateway_address: str = ipv4_oam_gateway_address + self.ipv4_oam_gateway_address_prefix_length: int = ipv4_oam_gateway_address_prefix_length + self.vlan_id_outer: int = vlan_id_outer + self.nm_profile_name: str = nm_profile_name + self.persona_model_version: str = persona_model_version + self.widget_model_id: str = widget_model_id + self.widget_model_version: str = widget_model_version + + self._vnf: "Vnf" = None + + def __repr__(self) -> str: + """Vnf instance object representation. + + Returns: + str: Human readable vnf instance representation + + """ + return (f"VnfInstance(vnf_id={self.vnf_id}, vnf_type={self.vnf_type}, " + f"in_maint={self.in_maint}, " + f"is_closed_loop_disabled={self.is_closed_loop_disabled})") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all vnfs. + + Returns: + str: Url to get all vnfs + + """ + return f"{cls.base_url}{cls.api_version}/network/generic-vnfs/" + + @property + def url(self) -> str: + """Vnf instance url. + + Returns: + str: VnfInstance url + + """ + return f"{self.base_url}{self.api_version}/network/generic-vnfs/generic-vnf/{self.vnf_id}" + + @property + def vf_modules(self) -> Iterator[VfModuleInstance]: + """Vf modules associated with vnf instance. + + Yields: + VfModuleInstance: VfModuleInstance associated with VnfInstance + + """ + for vf_module in self.send_message_json("GET", + f"GET VNF {self.vnf_name} VF modules", + f"{self.url}/vf-modules").get("vf-module", []): + yield VfModuleInstance.create_from_api_response(vf_module, self) + + @property + def vnf(self) -> "Vnf": + """Vnf associated with that vnf instance. + + Raises: + ResourceNotFound: Could not find VNF for that VNF instance + + Returns: + Vnf: Vnf object associated with vnf instance + + """ + if not self._vnf: + for vnf in self.service_instance.sdc_service.vnfs: + if vnf.model_version_id == self.model_version_id: + self._vnf = vnf + return self._vnf + + msg = ( + f'Could not find VNF for the VNF instance' + f' with model version ID "{self.model_version_id}"' + ) + raise ResourceNotFound(msg) + return self._vnf + + @classmethod + def create_from_api_response(cls, api_response: dict, + service_instance: "ServiceInstance") -> "VnfInstance": + """Create vnf instance object using HTTP API response dictionary. + + Returns: + VnfInstance: VnfInstance object + + """ + return cls(service_instance=service_instance, + vnf_id=api_response.get("vnf-id"), + vnf_type=api_response.get("vnf-type"), + in_maint=api_response.get("in-maint"), + is_closed_loop_disabled=api_response.get("is-closed-loop-disabled"), + vnf_name=api_response.get("vnf-name"), + service_id=api_response.get("service-id"), + regional_resource_zone=api_response.get("regional-resource-zone"), + prov_status=api_response.get("prov-status"), + operational_status=api_response.get("operational-status"), + equipment_role=api_response.get("equipment-role"), + orchestration_status=api_response.get("orchestration-status"), + vnf_package_name=api_response.get("vnf-package-name"), + vnf_discriptor_name=api_response.get("vnf-discriptor-name"), + job_id=api_response.get("job-id"), + heat_stack_id=api_response.get("heat-stack-id"), + mso_catalog_key=api_response.get("mso-catalog-key"), + management_option=api_response.get("management-option"), + ipv4_oam_address=api_response.get("ipv4-oam-address"), + ipv4_loopback0_address=api_response.get("ipv4-loopback0-address"), + nm_lan_v6_address=api_response.get("nm-lan-v6-address"), + management_v6_address=api_response.get("management-v6-address"), + vcpu=api_response.get("vcpu"), + vcpu_units=api_response.get("vcpu-units"), + vmemory=api_response.get("vmemory"), + vmemory_units=api_response.get("vmemory-units"), + vdisk=api_response.get("vdisk"), + vdisk_units=api_response.get("vdisk-units"), + nshd=api_response.get("nshd"), + nvm=api_response.get("nvm"), + nnet=api_response.get("nnet"), + resource_version=api_response.get("resource-version"), + model_invariant_id=api_response.get("model-invariant-id"), + model_version_id=api_response.get("model-version-id"), + encrypted_access_flag=api_response.get("encrypted-access-flag"), + persona_model_version=api_response.get("persona-model-version"), + model_customization_id=api_response.get("model-customization-id"), + widget_model_id=api_response.get("widget-model-id"), + widget_model_version=api_response.get("widget-model-version"), + as_number=api_response.get("as-number"), + regional_resource_subzone=api_response.get("regional-resource-subzone"), + nf_type=api_response.get("nf-type"), + nf_function=api_response.get("nf-function"), + nf_role=api_response.get("nf-role"), + nf_naming_code=api_response.get("nf-naming-code"), + selflink=api_response.get("selflink"), + ipv4_oam_gateway_address=api_response.get("ipv4-oam-gateway-address"), + ipv4_oam_gateway_address_prefix_length=\ + api_response.get("ipv4-oam-gateway-address-prefix-length"), + vlan_id_outer=api_response.get("vlan-id-outer"), + nm_profile_name=api_response.get("nm-profile-name")) + + def add_vf_module(self, # pylint: disable=too-many-arguments + vf_module: "VfModule", + cloud_region: "CloudRegion" = None, + tenant: "Tenant" = None, + vf_module_instance_name: str = None, + vnf_parameters: Iterable["InstantiationParameter"] = None, + use_preload: bool = True + ) -> "VfModuleInstantiation": + """Instantiate vf module for that VNF instance. + + Args: + vf_module (VfModule): VfModule to instantiate + cloud_region (CloudRegion, optional): Cloud region to use in instantiation request. + Defaults to None. + THAT PROPERTY WILL BE REQUIRED IN ONE OF THE FUTURE RELEASE. REFACTOR YOUR CODE + TO USE IT!. + tenant (Tenant, optional): Tenant to use in instnatiation request. + Defaults to None. + THAT PROPERTY WILL BE REQUIRED IN ONE OF THE FUTURE RELEASE. REFACTOR YOUR CODE + TO USE IT!. + vf_module_instance_name (str, optional): VfModule instance name. Defaults to None. + vnf_parameters (Iterable[InstantiationParameter], optional): InstantiationParameter + to use for preloading or to be passed as "userParams". Defaults to None. + use_preload (bool, optional): Based on this flag InstantiationParameters are passed + in preload or as "userParam" in the request. Defaults to True + + Returns: + VfModuleInstantiation: VfModuleInstantiation request object + + """ + return VfModuleInstantiation.instantiate_ala_carte( + vf_module, + self, + cloud_region=cloud_region, + tenant=tenant, + vf_module_instance_name=vf_module_instance_name, + vnf_parameters=vnf_parameters, + use_preload=use_preload + ) + + def update(self, + vnf_parameters: Iterable["InstantiationParameter"] = None + ) -> VnfInstantiation: + """Update vnf instance. + + Args: + vnf_parameters (Iterable["InstantiationParameter"], Optional): list of instantiation + parameters for update operation. + Raises: + StatusError: Skip post instantiation configuration flag for VF to True. + It might cause problems with SO component. + + Returns: + VnfInstantiation: VnfInstantiation object. + + """ + skip_flag = next(p for p in self.vnf.properties + if p.name == 'skip_post_instantiation_configuration') + if not skip_flag.value or skip_flag.value != "false": + raise StatusError("Operation for the vnf is not supported! " + "Skip_post_instantiation_configuration flag for VF should be False") + + return self._execute_so_action(operation_type=VnfOperation.UPDATE, + vnf_parameters=vnf_parameters) + + def healthcheck(self) -> VnfInstantiation: + """Execute healthcheck operation for vnf instance. + + Returns: + VnfInstantiation: VnfInstantiation object. + + """ + return self._execute_so_action(operation_type=VnfOperation.HEALTHCHECK) + + def _execute_so_action(self, + operation_type: VnfOperation, + vnf_parameters: Iterable["InstantiationParameter"] = None + ) -> VnfInstantiation: + """Execute SO workflow for selected operation. + + Args: + operation_type (str): Name of the operation to execute. + vnf_parameters (Iterable["InstantiationParameter"], Optional): list of instantiation + parameters for update operation. + + Returns: + VnfInstantiation: VnfInstantiation object. + + """ + if not self.service_instance.active: + msg = f'Service orchestration status must be "Active"' + raise StatusError(msg) + + lob = settings.LOB + platform = settings.PLATFORM + + for relationship in self.relationships: + if relationship.related_to == "line-of-business": + lob = relationship.relationship_data.pop().get("relationship-value") + if relationship.related_to == "platform": + platform = relationship.relationship_data.pop().get("relationship-value") + + so_input = self._build_so_input(vnf_params=vnf_parameters) + + return VnfInstantiation.so_action( + vnf_instance=self, + operation_type=operation_type, + aai_service_instance=self.service_instance, + line_of_business=lob, + platform=platform, + sdc_service=self.service_instance.sdc_service, + so_service=so_input + ) + + def _build_so_input(self, vnf_params: Iterable[InstantiationParameter] = None) -> SoService: + """Prepare so_input with params retrieved from existing service instance. + + Args: + vnf_params (Iterable[InstantiationParameter], Optional): list of instantiation + parameters for update operation. + + Returns: + SoService: SoService object to store SO Service parameters used for macro instantiation. + + """ + so_vnfs = [] + so_pnfs = [] + + if not vnf_params: + vnf_params = [] + + for pnf in self.service_instance.pnfs: + _pnf = { + "model_name": pnf.pnf.model_name, + "instance_name": pnf.pnf_name + } + + so_pnfs.append(_pnf) + + for vnf in self.service_instance.vnf_instances: + _vnf = {"model_name": vnf.vnf.model_name, + "instance_name": vnf.vnf_name, + "parameters": {}} + if vnf.vnf_name == self.vnf_name: + for _param in vnf_params: + _vnf["parameters"][_param.name] = _param.value + + _vf_modules = [] + for vf_module in vnf.vf_modules: + _vf_module = { + "model_name": vf_module.vf_module.model_name.split('..')[1], + "instance_name": vf_module.vf_module_name, + "parameters": {} + } + + _vf_modules.append(_vf_module) + + _vnf["vf_modules"] = _vf_modules + so_vnfs.append(_vnf) + + return SoService.load(data={ + 'subscription_service_type': self.service_instance.service_subscription.service_type, + 'vnfs': so_vnfs, + 'pnfs': so_pnfs + }) + + def delete(self, a_la_carte: bool = True) -> "VnfDeletionRequest": + """Create VNF deletion request. + + Send request to delete VNF instance + + Args: + a_la_carte (boolean): deletion mode + + Returns: + VnfDeletionRequest: Deletion request + + """ + self._logger.debug("Delete %s VNF", self.vnf_id) + return VnfDeletionRequest.send_request(self, a_la_carte) diff --git a/src/onapsdk/aai/cloud_infrastructure/__init__.py b/src/onapsdk/aai/cloud_infrastructure/__init__.py new file mode 100644 index 0000000..a380ce3 --- /dev/null +++ b/src/onapsdk/aai/cloud_infrastructure/__init__.py @@ -0,0 +1,18 @@ +"""A&AI cloud infrastructure package.""" +# 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 .cloud_region import AvailabilityZone, CloudRegion, EsrSystemInfo +from .complex import Complex +from .geo_region import GeoRegion +from .tenant import Tenant diff --git a/src/onapsdk/aai/cloud_infrastructure/cloud_region.py b/src/onapsdk/aai/cloud_infrastructure/cloud_region.py new file mode 100644 index 0000000..d57a025 --- /dev/null +++ b/src/onapsdk/aai/cloud_infrastructure/cloud_region.py @@ -0,0 +1,621 @@ +"""Cloud region 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 Any, Dict, Iterator, List, Optional +from urllib.parse import urlencode + +from onapsdk.msb.multicloud import Multicloud +from onapsdk.utils.jinja import jinja_env +from onapsdk.exceptions import ResourceNotFound + +from ..aai_element import AaiResource, Relationship +from .complex import Complex +from .tenant import Tenant + + +@dataclass +class AvailabilityZone: + """Availability zone. + + A collection of compute hosts/pservers + """ + + name: str + hypervisor_type: str + operational_status: str = None + resource_version: str = None + + +@dataclass +class EsrSystemInfo: # pylint: disable=too-many-instance-attributes + """Persist common address information of external systems.""" + + esr_system_info_id: str + user_name: str + password: str + system_type: str + resource_version: str + system_name: str = None + esr_type: str = None + vendor: str = None + version: str = None + service_url: str = None + protocol: str = None + ssl_cacert: str = None + ssl_insecure: Optional[bool] = None + ip_address: str = None + port: str = None + cloud_domain: str = None + default_tenant: str = None + passive: Optional[bool] = None + remote_path: str = None + system_status: str = None + openstack_region_id: str = None + + +class CloudRegion(AaiResource): # pylint: disable=too-many-instance-attributes + """Cloud region class. + + Represents A&AI cloud region object. + """ + + def __init__(self, + cloud_owner: str, + cloud_region_id: str, + orchestration_disabled: bool, + in_maint: bool, + *, # rest of parameters are keyword + cloud_type: str = "", + owner_defined_type: str = "", + cloud_region_version: str = "", + identity_url: str = "", + cloud_zone: str = "", + complex_name: str = "", + sriov_automation: str = "", + cloud_extra_info: str = "", + upgrade_cycle: str = "", + resource_version: str = "") -> None: + """Cloud region object initialization. + + Args: + cloud_owner (str): Identifies the vendor and cloud name. + cloud_region_id (str): Identifier used by the vendor for the region. + orchestration_disabled (bool): Used to indicate whether orchestration is + enabled for this cloud-region. + in_maint (bool): Used to indicate whether or not cloud-region object + is in maintenance mode. + owner_defined_type (str, optional): Cloud-owner defined type + indicator (e.g., dcp, lcp). Defaults to "". + cloud_region_version (str, optional): Software version employed at the site. + Defaults to "". + identity_url (str, optional): URL of the keystone identity service. Defaults to "". + cloud_zone (str, optional): Zone where the cloud is homed. Defaults to "". + complex_name (str, optional): Complex name for cloud-region instance. Defaults to "". + sriov_automation (str, optional): Whether the cloud region supports (true) or does + not support (false) SR-IOV automation. Defaults to "". + cloud_extra_info (str, optional): ESR inputs extra information about the VIM or Cloud + which will be decoded by MultiVIM. Defaults to "". + upgrade_cycle (str, optional): Upgrade cycle for the cloud region. + For AIC regions upgrade cycle is designated by A,B,C etc. Defaults to "". + resource_version (str, optional): Used for optimistic concurrency. + Must be empty on create, valid on update and delete. Defaults to "". + + """ + super().__init__() + self.cloud_owner = cloud_owner + self.cloud_region_id = cloud_region_id + self.orchestration_disabled = orchestration_disabled + self.in_maint = in_maint + self.cloud_type = cloud_type + self.owner_defined_type = owner_defined_type + self.cloud_region_version = cloud_region_version + self.identity_url = identity_url + self.cloud_zone = cloud_zone + self.complex_name = complex_name + self.sriov_automation = sriov_automation + self.cloud_extra_info = cloud_extra_info + self.upgrade_cycle = upgrade_cycle + self.resource_version = resource_version + + def __repr__(self) -> str: + """Cloud region object representation. + + Returns: + str: Human readable string contains most important information about cloud region. + + """ + return ( + f"CloudRegion(cloud_owner={self.cloud_owner}, cloud_region_id={self.cloud_region_id})" + ) + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all cloud regions. + + Returns: + str: Url to get all cloud regions + + """ + return f"{cls.base_url}{cls.api_version}/cloud-infrastructure/cloud-regions" + + @classmethod + def get_all(cls, + cloud_owner: str = None, + cloud_region_id: str = None, + cloud_type: str = None, + owner_defined_type: str = None) -> Iterator["CloudRegion"]: + """Get all A&AI cloud regions. + + Cloud regions can be filtered by 4 parameters: cloud-owner, + cloud-region-id, cloud-type and owner-defined-type. + + Yields: + CloudRegion -- CloudRegion object. Can not yield anything + if cloud region with given filter parameters doesn't exist + + """ + # Filter request parameters - use only these which are not None + filter_parameters: dict = cls.filter_none_key_values( + { + "cloud-owner": cloud_owner, + "cloud-region-id": cloud_region_id, + "cloud-type": cloud_type, + "owner-defined-type": owner_defined_type, + } + ) + url: str = (f"{cls.get_all_url()}?{urlencode(filter_parameters)}") + response_json: Dict[str, List[Dict[str, Any]]] = cls.send_message_json( + "GET", "get cloud regions", url + ) + for cloud_region in response_json.get("cloud-region", []): # typing: dict + yield CloudRegion( + cloud_owner=cloud_region["cloud-owner"], # required + cloud_region_id=cloud_region["cloud-region-id"], # required + cloud_type=cloud_region.get("cloud-type"), + owner_defined_type=cloud_region.get("owner-defined-type"), + cloud_region_version=cloud_region.get("cloud-region-version"), + identity_url=cloud_region.get("identity_url"), + cloud_zone=cloud_region.get("cloud-zone"), + complex_name=cloud_region.get("complex-name"), + sriov_automation=cloud_region.get("sriov-automation"), + cloud_extra_info=cloud_region.get("cloud-extra-info"), + upgrade_cycle=cloud_region.get("upgrade-cycle"), + orchestration_disabled=cloud_region["orchestration-disabled"], # required + in_maint=cloud_region["in-maint"], # required + resource_version=cloud_region.get("resource-version"), + ) + + @classmethod + def get_by_id(cls, cloud_owner: str, cloud_region_id: str) -> "CloudRegion": + """Get CloudRegion object by cloud_owner and cloud-region-id field value. + + This method calls A&AI cloud region API filtering them by cloud_owner and + cloud-region-id field value. + + Raises: + ResourceNotFound: Cloud region with given id does not exist. + + Returns: + CloudRegion: CloudRegion object with given cloud-region-id. + + """ + try: + return next(cls.get_all(cloud_owner=cloud_owner, cloud_region_id=cloud_region_id)) + except StopIteration: + msg = ( + f'CloudRegion with {cloud_owner}, ' + f'{cloud_region_id} cloud-id not found. ' + ) + raise ResourceNotFound(msg) + + @classmethod + def create(cls, # pylint: disable=too-many-locals + cloud_owner: str, + cloud_region_id: str, + orchestration_disabled: bool, + in_maint: bool, + *, # rest of parameters are keyword + cloud_type: str = "", + owner_defined_type: str = "", + cloud_region_version: str = "", + identity_url: str = "", + cloud_zone: str = "", + complex_name: str = "", + sriov_automation: str = "", + cloud_extra_info: str = "", + upgrade_cycle: str = "") -> "CloudRegion": + """Create CloudRegion object. + + Create cloud region with given values. + + Returns: + CloudRegion: Created cloud region. + + """ + cloud_region: "CloudRegion" = CloudRegion( + cloud_owner=cloud_owner, + cloud_region_id=cloud_region_id, + orchestration_disabled=orchestration_disabled, + in_maint=in_maint, + cloud_type=cloud_type, + owner_defined_type=owner_defined_type, + cloud_region_version=cloud_region_version, + identity_url=identity_url, + cloud_zone=cloud_zone, + complex_name=complex_name, + sriov_automation=sriov_automation, + cloud_extra_info=cloud_extra_info, + upgrade_cycle=upgrade_cycle, + ) + url: str = ( + f"{cls.base_url}{cls.api_version}/cloud-infrastructure/cloud-regions/cloud-region/" + f"{cloud_region.cloud_owner}/{cloud_region.cloud_region_id}" + ) + cls.send_message( + "PUT", + "Create cloud region", + url, + data=jinja_env() + .get_template("cloud_region_create.json.j2") + .render(cloud_region=cloud_region), + ) + return cloud_region + + @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}/cloud-infrastructure/cloud-regions/cloud-region/" + f"{self.cloud_owner}/{self.cloud_region_id}" + ) + + @property + def tenants(self) -> Iterator["Tenant"]: + """Tenants iterator. + + Cloud region tenants iterator. + + Returns: + Iterator[Tenant]: Iterate through cloud region tenants + + """ + response: dict = self.send_message_json("GET", "get tenants", f"{self.url}/tenants") + return ( + Tenant( + cloud_region=self, + tenant_id=tenant["tenant-id"], + tenant_name=tenant["tenant-name"], + tenant_context=tenant.get("tenant-context"), + resource_version=tenant.get("resource-version"), + ) + for tenant in response.get("tenant", []) + ) + + @property + def availability_zones(self) -> Iterator[AvailabilityZone]: + """Cloud region availability zones. + + Iterate over CloudRegion availability zones. Relationship list is given using A&AI API call. + + Returns: + Iterator[AvailabilityZone]: CloudRegion availability zone + + """ + response: dict = self.send_message_json( + "GET", "get cloud region availability zones", f"{self.url}/availability-zones" + ) + return ( + AvailabilityZone( + name=availability_zone["availability-zone-name"], + hypervisor_type=availability_zone["hypervisor-type"], + operational_status=availability_zone.get("operational-status"), + resource_version=availability_zone.get("resource-version") + ) + for availability_zone in response.get("availability-zone", []) + ) + + @property + def esr_system_infos(self) -> Iterator[EsrSystemInfo]: + """Cloud region collection of persistent block-level external system auth info. + + Returns: + Iterator[EsrSystemInfo]: Cloud region external system address information. + + """ + response: dict = self.send_message_json( + "GET", "get cloud region external systems info list", f"{self.url}/esr-system-info-list" + ) + return ( + EsrSystemInfo( + esr_system_info_id=esr_system_info.get("esr-system-info-id"), + user_name=esr_system_info.get("user-name"), + password=esr_system_info.get("password"), + system_type=esr_system_info.get("system-type"), + system_name=esr_system_info.get("system-name"), + esr_type=esr_system_info.get("type"), + vendor=esr_system_info.get("vendor"), + version=esr_system_info.get("version"), + service_url=esr_system_info.get("service-url"), + protocol=esr_system_info.get("protocol"), + ssl_cacert=esr_system_info.get("ssl-cacert"), + ssl_insecure=esr_system_info.get("ssl-insecure"), + ip_address=esr_system_info.get("ip-address"), + port=esr_system_info.get("port"), + cloud_domain=esr_system_info.get("cloud-domain"), + default_tenant=esr_system_info.get("default-tenant"), + passive=esr_system_info.get("passive"), + remote_path=esr_system_info.get("remote-path"), + system_status=esr_system_info.get("system-status"), + openstack_region_id=esr_system_info.get("openstack-region-id"), + resource_version=esr_system_info.get("resource-version"), + ) + for esr_system_info in response.get("esr-system-info", []) + ) + + @property + def complex(self) -> Optional[Complex]: + """Complex related with cloud region. + + Returns: + Optional[Complex]: Complex object related with CloudRegion or None if + CloudRegion has no relationship with any Complex + + """ + try: + for relationship in self.relationships: + if relationship.related_to == "complex": + physical_location_id: Optional[str] = relationship.get_relationship_data( + "complex.physical-location-id" + ) + if physical_location_id is not None: + try: + return Complex.get_by_physical_location_id( + physical_location_id + ) + except ResourceNotFound: + self._logger.error("Complex with %s physical location id does " + "not exist", physical_location_id) + self._logger.error("Invalid Complex relationship!") + return None + except ResourceNotFound: + self._logger.debug("Cloud region %s has no relationships", self.cloud_region_id) + self._logger.debug("Cloud region %s has no related complex", self.cloud_region_id) + return None + + def add_tenant(self, tenant_id: str, tenant_name: str, tenant_context: str = None) -> None: + """Add tenant to cloud region. + + Args: + tenant_id (str): Unique id relative to the cloud-region. + tenant_name (str): Readable name of tenant + tenant_context (str, optional): This field will store + the tenant context.. Defaults to None. + + """ + self.send_message( + "PUT", + "add tenant to cloud region", + f"{self.url}/tenants/tenant/{tenant_id}", + data=jinja_env() + .get_template("cloud_region_add_tenant.json.j2") + .render(tenant_id=tenant_id, tenant_name=tenant_name, + tenant_context=tenant_context) + ) + + def get_tenant(self, tenant_id: str) -> "Tenant": + """Get tenant with provided ID. + + Args: + tenant_id (str): Tenant ID + + Returns: + Tenant: Tenant object + + """ + response: dict = self.send_message_json( + "GET", + "get tenants", + f"{self.url}/tenants/tenant/{tenant_id}" + ) + return Tenant( + cloud_region=self, + tenant_id=response["tenant-id"], + tenant_name=response["tenant-name"], + tenant_context=response.get("tenant-context"), + resource_version=response.get("resource-version"), + ) + + def get_tenants_by_name(self, tenant_name: str) -> Iterator["Tenant"]: + """Get tenants with given name. + + Args: + tenant_name (str): Tenant name + + Returns: + Iterator[Tenant]: Iterate through cloud region tenants with given name + + """ + return (tenant for tenant in self.tenants if tenant.name == tenant_name) + + + def get_availability_zone_by_name(self, + zone_name: str) -> "AvailabilityZone": + """Get availability zone with provided Name. + + Args: + availability_zone name (str): The name of the availibilty zone + + Returns: + AvailabilityZone: AvailabilityZone object + + """ + response: dict = self.send_message_json( + "GET", + "get availability_zones", + f"{self.url}/availability-zones/availability-zone/{zone_name}" + ) + return AvailabilityZone( + name=response["availability-zone-name"], + hypervisor_type=response["hypervisor-type"], + resource_version=response["resource-version"] + ) + + def add_availability_zone(self, + availability_zone_name: str, + availability_zone_hypervisor_type: str, + availability_zone_operational_status: str = None) -> None: + """Add avaiability zone to cloud region. + + Args: + availability_zone_name (str): Name of the availability zone. + Unique across a cloud region + availability_zone_hypervisor_type (str): Type of hypervisor + availability_zone_operational_status (str, optional): State that indicates whether + the availability zone should be used. Defaults to None. + """ + self.send_message( + "PUT", + "Add availability zone to cloud region", + f"{self.url}/availability-zones/availability-zone/{availability_zone_name}", + data=jinja_env() + .get_template("cloud_region_add_availability_zone.json.j2") + .render(availability_zone_name=availability_zone_name, + availability_zone_hypervisor_type=availability_zone_hypervisor_type, + availability_zone_operational_status=availability_zone_operational_status) + ) + + def add_esr_system_info(self, # pylint: disable=too-many-arguments, too-many-locals + esr_system_info_id: str, + user_name: str, + password: str, + system_type: str, + system_name: str = None, + esr_type: str = None, + vendor: str = None, + version: str = None, + service_url: str = None, + protocol: str = None, + ssl_cacert: str = None, + ssl_insecure: Optional[bool] = None, + ip_address: str = None, + port: str = None, + cloud_domain: str = None, + default_tenant: str = None, + passive: Optional[bool] = None, + remote_path: str = None, + system_status: str = None, + openstack_region_id: str = None, + resource_version: str = None) -> None: + """Add external system info to cloud region. + + Args: + esr_system_info_id (str): Unique ID of esr system info + user_name (str): username used to access external system + password (str): password used to access external system + system_type (str): it could be vim/vnfm/thirdparty-sdnc/ + ems-resource/ems-performance/ems-alarm + system_name (str, optional): name of external system. Defaults to None. + esr_type (str, optional): type of external system. Defaults to None. + vendor (str, optional): vendor of external system. Defaults to None. + version (str, optional): version of external system. Defaults to None. + service_url (str, optional): url used to access external system. Defaults to None. + protocol (str, optional): protocol of third party SDNC, + for example netconf/snmp. Defaults to None. + ssl_cacert (str, optional): ca file content if enabled ssl on auth-url. + Defaults to None. + ssl_insecure (bool, optional): Whether to verify VIM's certificate. Defaults to True. + ip_address (str, optional): service IP of ftp server. Defaults to None. + port (str, optional): service port of ftp server. Defaults to None. + cloud_domain (str, optional): domain info for authentication. Defaults to None. + default_tenant (str, optional): default tenant of VIM. Defaults to None. + passive (bool, optional): ftp passive mode or not. Defaults to False. + remote_path (str, optional): resource or performance data file path. Defaults to None. + system_status (str, optional): he status of external system. Defaults to None. + openstack_region_id (str, optional): OpenStack region ID used by MultiCloud plugin to + interact with an OpenStack instance. Defaults to None. + """ + self.send_message( + "PUT", + "Add external system info to cloud region", + f"{self.url}/esr-system-info-list/esr-system-info/{esr_system_info_id}", + data=jinja_env() + .get_template("cloud_region_add_esr_system_info.json.j2") + .render(esr_system_info_id=esr_system_info_id, + user_name=user_name, + password=password, + system_type=system_type, + system_name=system_name, + esr_type=esr_type, + vendor=vendor, + version=version, + service_url=service_url, + protocol=protocol, + ssl_cacert=ssl_cacert, + ssl_insecure=ssl_insecure, + ip_address=ip_address, + port=port, + cloud_domain=cloud_domain, + default_tenant=default_tenant, + passive=passive, + remote_path=remote_path, + system_status=system_status, + openstack_region_id=openstack_region_id, + resource_version=resource_version) + ) + + def register_to_multicloud(self, default_tenant: str = None) -> None: + """Register cloud to multicloud using MSB API. + + Args: + default_tenant (str, optional): Default tenant. Defaults to None. + """ + Multicloud.register_vim(self.cloud_owner, self.cloud_region_id, default_tenant) + + def unregister_from_multicloud(self) -> None: + """Unregister cloud from mutlicloud.""" + Multicloud.unregister_vim(self.cloud_owner, self.cloud_region_id) + + def delete(self) -> None: + """Delete cloud region.""" + self.send_message( + "DELETE", + f"Delete cloud region {self.cloud_region_id}", + self.url, + params={"resource-version": self.resource_version} + ) + + def link_to_complex(self, complex_object: Complex) -> None: + """Link cloud region to comples. + + It creates relationhip object and add it into cloud region. + """ + relationship = Relationship( + related_to="complex", + related_link=(f"aai/v13/cloud-infrastructure/complexes/" + f"complex/{complex_object.physical_location_id}"), + relationship_data={ + "relationship-key": "complex.physical-location-id", + "relationship-value": f"{complex_object.physical_location_id}", + }, + relationship_label="org.onap.relationships.inventory.LocatedIn", + ) + self.add_relationship(relationship) diff --git a/src/onapsdk/aai/cloud_infrastructure/complex.py b/src/onapsdk/aai/cloud_infrastructure/complex.py new file mode 100644 index 0000000..a854f02 --- /dev/null +++ b/src/onapsdk/aai/cloud_infrastructure/complex.py @@ -0,0 +1,300 @@ +"""A&AI Complex 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 typing import Any, Dict, Iterator +from urllib.parse import urlencode + +from onapsdk.utils.jinja import jinja_env + +from ..aai_element import AaiResource + + +class Complex(AaiResource): # pylint: disable=too-many-instance-attributes + """Complex class. + + Collection of physical locations that can house cloud-regions. + """ + + def __init__(self, # pylint: disable=too-many-locals + physical_location_id: str, + *, + name: str = "", + data_center_code: str = "", + identity_url: str = "", + resource_version: str = "", + physical_location_type: str = "", + street1: str = "", + street2: str = "", + city: str = "", + state: str = "", + postal_code: str = "", + country: str = "", + region: str = "", + latitude: str = "", + longitude: str = "", + elevation: str = "", + lata: str = "", + timezone: str = "", + data_owner: str = "", + data_source: str = "", + data_source_version: str = "") -> None: + """Complex object initialization. + + Args: + name (str): complex name + physical_location_id (str): complex ID + data_center_code (str, optional): complex data center code. Defaults to "". + identity_url (str, optional): complex identity url. Defaults to "". + resource_version (str, optional): complex resource version. Defaults to "". + physical_location_type (str, optional): complex physical location type. Defaults to "". + street1 (str, optional): complex address street part one. Defaults to "". + street2 (str, optional): complex address street part two. Defaults to "". + city (str, optional): complex address city. Defaults to "". + state (str, optional): complex address state. Defaults to "". + postal_code (str, optional): complex address postal code. Defaults to "". + country (str, optional): complex address country. Defaults to "". + region (str, optional): complex address region. Defaults to "". + latitude (str, optional): complex geographical location latitude. Defaults to "". + longitude (str, optional): complex geographical location longitude. Defaults to "". + elevation (str, optional): complex elevation. Defaults to "". + lata (str, optional): complex lata. Defaults to "". + timezone (str, optional): the time zone where the complex is located. Defaults to "". + data_owner (str, optional): Identifies the entity that is responsible managing this + inventory object. Defaults to "". + data_source (str, optional): Identifies the upstream source of the data. Defaults to "". + data_source_version (str, optional): Identifies the version of the upstream source. + Defaults to "". + + """ + super().__init__() + self.name: str = name + self.physical_location_id: str = physical_location_id + self.data_center_code: str = data_center_code + self.identity_url: str = identity_url + self.resource_version: str = resource_version + self.physical_location_type: str = physical_location_type + self.street1: str = street1 + self.street2: str = street2 + self.city: str = city + self.state: str = state + self.postal_code: str = postal_code + self.country: str = country + self.region: str = region + self.latitude: str = latitude + self.longitude: str = longitude + self.elevation: str = elevation + self.lata: str = lata + self.timezone: str = timezone + self.data_owner: str = data_owner + self.data_source: str = data_source + self.data_source_version: str = data_source_version + + def __repr__(self) -> str: + """Complex object description. + + Returns: + str: Complex object description + + """ + return (f"Complex(name={self.name}, " + f"physical_location_id={self.physical_location_id}, " + f"resource_version={self.resource_version})") + + @property + def url(self) -> str: + """Complex url. + + Returns: + str: Complex url + + """ + return (f"{self.base_url}{self.api_version}/cloud-infrastructure/complexes/complex/" + f"{self.physical_location_id}") + + @classmethod + def create(cls, # pylint: disable=too-many-locals + physical_location_id: str, + *, + name: str = "", + data_center_code: str = "", + identity_url: str = "", + resource_version: str = "", + physical_location_type: str = "", + street1: str = "", + street2: str = "", + city: str = "", + state: str = "", + postal_code: str = "", + country: str = "", + region: str = "", + latitude: str = "", + longitude: str = "", + elevation: str = "", + lata: str = "", + timezone: str = "", + data_owner: str = "", + data_source: str = "", + data_source_version: str = "") -> "Complex": + """Create complex. + + Create complex object by calling A&AI API. + If API request doesn't fail it returns Complex object. + + Returns: + Complex: Created complex object + + """ + complex_object: Complex = Complex( + name=name, + physical_location_id=physical_location_id, + data_center_code=data_center_code, + identity_url=identity_url, + resource_version=resource_version, + physical_location_type=physical_location_type, + street1=street1, + street2=street2, + city=city, + state=state, + postal_code=postal_code, + country=country, + region=region, + latitude=latitude, + longitude=longitude, + elevation=elevation, + lata=lata, + timezone=timezone, + data_owner=data_owner, + data_source=data_source, + data_source_version=data_source_version + ) + payload: str = jinja_env().get_template("complex_create.json.j2").render( + complex=complex_object) + url: str = ( + f"{cls.base_url}{cls.api_version}/cloud-infrastructure/complexes/complex/" + f"{complex_object.physical_location_id}" + ) + cls.send_message("PUT", "create complex", url, data=payload) + return complex_object + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return an url to get all complexes. + + Returns: + str: URL to get all complexes + + """ + return f"{cls.base_url}{cls.api_version}/cloud-infrastructure/complexes" + + @classmethod + def get_all(cls, + physical_location_id: str = None, + data_center_code: str = None, + complex_name: str = None, + identity_url: str = None) -> Iterator["Complex"]: + """Get all complexes from A&AI. + + Call A&AI API to get all complex objects. + + Args: + physical_location_id (str, optional): Unique identifier for physical location, + e.g., CLLI. Defaults to None. + data_center_code (str, optional): Data center code which can be an alternate way + to identify a complex. Defaults to None. + complex_name (str, optional): Gamma complex name for LCP instance. Defaults to None. + identity_url (str, optional): URL of the keystone identity service. Defaults to None. + + Yields: + Complex -- Complex object. Can not yield anything if any complex with given filter + parameters doesn't exist + + """ + filter_parameters: dict = cls.filter_none_key_values( + { + "physical-location-id": physical_location_id, + "data-center-code": data_center_code, + "complex-name": complex_name, + "identity-url": identity_url, + } + ) + url: str = (f"{cls.get_all_url()}?{urlencode(filter_parameters)}") + for complex_json in cls.send_message_json("GET", + "get cloud regions", + url).get("complex", []): + yield cls.create_from_api_response(complex_json) + + @classmethod + def get_by_physical_location_id(cls, physical_location_id: str) -> "Complex": + """Get complex by physical location id. + + Args: + physical_location_id (str): Physical location id of Complex + + Returns: + Complex: Complex object + + Raises: + ResourceNotFound: Complex with given physical location id not found + + """ + response = cls.send_message_json("GET", + "Get complex with physical location id: " + f"{physical_location_id}", + f"{cls.base_url}{cls.api_version}/cloud-infrastructure/" + f"complexes/complex/{physical_location_id}") + return cls.create_from_api_response(response) + + @classmethod + def create_from_api_response(cls, + api_response: Dict[str, Any]) -> "Complex": + """Create complex object using given A&AI API response JSON. + + Args: + api_response (Dict[str, Any]): Complex A&AI API response + + Returns: + Complex: Complex object created from given response + + """ + return cls( + name=api_response.get("complex-name"), + physical_location_id=api_response["physical-location-id"], + data_center_code=api_response.get("data-center-code"), + identity_url=api_response.get("identity-url"), + resource_version=api_response.get("resource-version"), + physical_location_type=api_response.get("physical-location-type"), + street1=api_response.get("street1"), + street2=api_response.get("street2"), + city=api_response.get("city"), + state=api_response.get("state"), + postal_code=api_response.get("postal-code"), + country=api_response.get("country"), + region=api_response.get("region"), + latitude=api_response.get("latitude"), + longitude=api_response.get("longitude"), + elevation=api_response.get("elevation"), + lata=api_response.get("lata"), + timezone=api_response.get("time-zone"), + data_owner=api_response.get("data-owner"), + data_source=api_response.get("data-source"), + data_source_version=api_response.get("data-source-version") + ) + + def delete(self) -> None: + """Delete complex.""" + self.send_message( + "DELETE", + f"Delete {self.physical_location_id} complex", + f"{self.url}?resource-version={self.resource_version}" + ) diff --git a/src/onapsdk/aai/cloud_infrastructure/geo_region.py b/src/onapsdk/aai/cloud_infrastructure/geo_region.py new file mode 100644 index 0000000..32ff820 --- /dev/null +++ b/src/onapsdk/aai/cloud_infrastructure/geo_region.py @@ -0,0 +1,191 @@ +"""Geo region 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 typing import Iterator, Optional + +from onapsdk.utils.jinja import jinja_env + +from ..aai_element import AaiResource + + +class GeoRegion(AaiResource): # pylint: disable=too-many-instance-attributes + """Geo region class.""" + + def __init__(self, + geo_region_id: str, + *, + geo_region_name: str = "", + geo_region_type: str = "", + geo_region_role: str = "", + geo_region_function: str = "", + data_owner: str = "", + data_source: str = "", + data_source_version: str = "", + resource_version: str = "", + ) -> None: + """Geo region init. + + Args: + geo_region_id (str): UUID, key for geo-region object. + geo_region_name (str, optional): Name of geo-region. Defaults to "". + geo_region_type (str, optional): Type of geo-region. Defaults to "". + geo_region_role (str, optional): Role of geo-region. Defaults to "". + geo_region_function (str, optional): Function of geo-region. Defaults to "". + data_owner (str, optional): Identifies the entity that is responsible managing + this inventory object. Defaults to "". + data_source (str, optional): Identifies the upstream source of the data. + Defaults to "". + data_source_version (str, optional): Identifies the version of + the upstream source. Defaults to "". + resource_version (str, optional): Resource version. Defaults to "". + + """ + super().__init__() + self.geo_region_id: str = geo_region_id + self.geo_region_name: str = geo_region_name + self.geo_region_type: str = geo_region_type + self.geo_region_role: str = geo_region_role + self.geo_region_function: str = geo_region_function + self.data_owner: str = data_owner + self.data_source: str = data_source + self.data_source_version: str = data_source_version + self.resource_version: str = resource_version + + def __repr__(self) -> str: + """Geo region object representation. + + Returns: + str: Human readable string contains most important information about geo region. + + """ + return ( + f"GeoRegion(geo_region_id={self.geo_region_id})" + ) + + @property + def url(self) -> str: + """Geo region's url. + + Returns: + str: Geo Region's url + + """ + return (f"{self.base_url}{self.api_version}/cloud-infrastructure/" + f"geo-regions/geo-region/{self.geo_region_id}") + + @classmethod + def get_all_url(cls, *args, **kwargs) -> str: # pylint: disable=arguments-differ + """Return url to get all geo regions. + + Returns: + str: Url to get all geo regions + + Raises: + ResourceNotFound: No geo regions found + + """ + return f"{cls.base_url}{cls.api_version}/cloud-infrastructure/geo-regions" + + @classmethod + def get_all(cls) -> Iterator["GeoRegion"]: + """Get all geo regions. + + Yields: + GeoRegion: Geo region + + """ + for geo_region_data in cls.send_message_json("GET", + "Get all geo regions", + cls.get_all_url()).get("geo-region", []): + yield cls(geo_region_id=geo_region_data["geo-region-id"], + geo_region_name=geo_region_data.get("geo-region-name", ""), + geo_region_type=geo_region_data.get("geo-region-type", ""), + geo_region_role=geo_region_data.get("geo-region-role", ""), + geo_region_function=geo_region_data.get("geo-region-function", ""), + data_owner=geo_region_data.get("data-owner", ""), + data_source=geo_region_data.get("data-source", ""), + data_source_version=geo_region_data.get("data-source-version", ""), + resource_version=geo_region_data.get("resource-version", "")) + + @classmethod + def get_by_geo_region_id(cls, geo_region_id: str) -> "GeoRegion": + """Get geo region by it's id. + + Args: + geo_region_id (str): Geo region id + + Returns: + GeoRegion: Geo region + + """ + resp = cls.send_message_json("GET", + f"Get geo region with {geo_region_id} id", + f"{cls.get_all_url()}/geo-region/{geo_region_id}") + return GeoRegion(resp["geo-region-id"], + geo_region_name=resp.get("geo-region-name", ""), + geo_region_type=resp.get("geo-region-type", ""), + geo_region_role=resp.get("geo-region-role", ""), + geo_region_function=resp.get("geo-region-function", ""), + data_owner=resp.get("data-owner", ""), + data_source=resp.get("data-source", ""), + data_source_version=resp.get("data-source-version", ""), + resource_version=resp["resource-version"]) + + @classmethod + def create(cls, # pylint: disable=too-many-arguments + geo_region_id: str, + geo_region_name: Optional[str] = None, + geo_region_type: Optional[str] = None, + geo_region_role: Optional[str] = None, + geo_region_function: Optional[str] = None, + data_owner: Optional[str] = None, + data_source: Optional[str] = None, + data_source_version: Optional[str] = None) -> "GeoRegion": + """Create geo region. + + Args: + geo_region_id (str): UUID, key for geo-region object. + geo_region_name (Optional[str], optional): Name of geo-region. Defaults to None. + geo_region_type (Optional[str], optional): Type of geo-region. Defaults to None. + geo_region_role (Optional[str], optional): Role of geo-region. Defaults to None. + geo_region_function (Optional[str], optional): Function of geo-region. + Defaults to None. + data_owner (Optional[str], optional): Identifies the entity that is + responsible managing this inventory object.. Defaults to None. + data_source (Optional[str], optional): Identifies the upstream source of the data. + Defaults to None. + data_source_version (Optional[str], optional): Identifies the version of + the upstream source. Defaults to None. + + Returns: + GeoRegion: Geo region object + + """ + cls.send_message( + "PUT", + "Create geo region", + f"{cls.get_all_url()}/geo-region/{geo_region_id}", + data=jinja_env() + .get_template("geo_region_create.json.j2") + .render(geo_region_id=geo_region_id, + geo_region_name=geo_region_name, + geo_region_type=geo_region_type, + geo_region_role=geo_region_role, + geo_region_function=geo_region_function, + data_owner=data_owner, + data_source=data_source, + data_source_version=data_source_version), + ) + return cls.get_by_geo_region_id(geo_region_id) diff --git a/src/onapsdk/aai/cloud_infrastructure/tenant.py b/src/onapsdk/aai/cloud_infrastructure/tenant.py new file mode 100644 index 0000000..13d9aec --- /dev/null +++ b/src/onapsdk/aai/cloud_infrastructure/tenant.py @@ -0,0 +1,101 @@ +"""A&AI Tenant 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 onapsdk.aai.cloud_infrastructure.cloud_region import CloudRegion +from ..aai_element import AaiResource + + +class Tenant(AaiResource): + """Tenant class.""" + + def __init__(self, # pylint: disable=too-many-arguments + cloud_region: "CloudRegion", + tenant_id: str, + tenant_name: str, + tenant_context: str = None, + resource_version: str = None): + """Tenant object initialization. + + Tenant object represents A&AI Tenant resource. + + Args: + cloud_region (str): Cloud region object + tenant_id (str): Unique Tenant ID + tenant_name (str): Tenant name + tenant_context (str, optional): Tenant context. Defaults to None. + resource_version (str, optional): Tenant resource version. Defaults to None. + + """ + super().__init__() + self.cloud_region: "CloudRegion" = cloud_region + self.tenant_id: str = tenant_id + self.name: str = tenant_name + self.context: str = tenant_context + self.resource_version: str = resource_version + + def __repr__(self) -> str: + """Tenant repr. + + Returns: + str: Human readable Tenant object description + + """ + return ( + f"Tenant(tenant_id={self.tenant_id}, tenant_name={self.name}, " + f"tenant_context={self.context}, " + f"resource_version={self.resource_version}, " + f"cloud_region={self.cloud_region.cloud_region_id})" + ) + + @classmethod + def get_all_url(cls, cloud_region: "CloudRegion") -> str: # pylint: disable=arguments-differ + """Return an url to get all tenants for given cloud region. + + Args: + cloud_region (CloudRegion): Cloud region object + + Returns: + str: Url to get all tenants + + """ + return (f"{cls.base_url}{cls.api_version}/cloud-infrastructure/cloud-regions/cloud-region/" + f"{cloud_region.cloud_owner}/{cloud_region.cloud_region_id}" + f"/tenants/") + + @property + def url(self) -> str: + """Tenant url. + + Returns: + str: Url which can be used to update or delete tenant. + + """ + return ( + f"{self.base_url}{self.api_version}/cloud-infrastructure/cloud-regions/cloud-region/" + f"{self.cloud_region.cloud_owner}/{self.cloud_region.cloud_region_id}" + f"/tenants/tenant/{self.tenant_id}?" + f"resource-version={self.resource_version}" + ) + + def delete(self) -> None: + """Delete tenant. + + Remove tenant from cloud region. + + """ + return self.send_message( + "DELETE", + f"Remove tenant {self.name} from {self.cloud_region.cloud_region_id} cloud region", + url=self.url, + ) diff --git a/src/onapsdk/aai/network/__init__.py b/src/onapsdk/aai/network/__init__.py new file mode 100644 index 0000000..c3795c1 --- /dev/null +++ b/src/onapsdk/aai/network/__init__.py @@ -0,0 +1,16 @@ +"""A&AI network package.""" +# 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 .site_resource import SiteResource diff --git a/src/onapsdk/aai/network/site_resource.py b/src/onapsdk/aai/network/site_resource.py new file mode 100644 index 0000000..3ac3c20 --- /dev/null +++ b/src/onapsdk/aai/network/site_resource.py @@ -0,0 +1,244 @@ +"""A&AI site resource 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 typing import Iterable, Optional + +from onapsdk.utils.jinja import jinja_env +from ..aai_element import AaiResource + + +class SiteResource(AaiResource): # pylint: disable=too-many-instance-attributes + """Site resource class.""" + + def __init__(self, # pylint: disable=too-many-locals + site_resource_id: str, + *, + site_resource_name: str = "", + description: str = "", + site_resource_type: str = "", + role: str = "", + generated_site_id: str = "", + selflink: str = "", + operational_status: str = "", + model_customization_id: str = "", + model_invariant_id: str = "", + model_version_id: str = "", + data_owner: str = "", + data_source: str = "", + data_source_version: str = "", + resource_version: str = "") -> None: + """Site resource object init. + + Args: + site_resource_id (str): Uniquely identifies this site-resource by id. + site_resource_name (str, optional): Store the name of this site-resource. + Defaults to "". + description (str, optional): Store the description of this site-resource. + Defaults to "". + site_resource_type (str, optional): Store the type of this site-resource. + Defaults to "". + role (str, optional): Store the role of this site-resource. Defaults to "". + generated_site_id (str, optional): Store the generated-site-id of this site-resource. + Defaults to "". + selflink (str, optional): Store the link to get more information for this object. + Defaults to "". + operational_status (str, optional): Store the operational-status for this object. + Defaults to "". + model_customization_id (str, optional): Store the model-customization-id + for this object. Defaults to "". + model_invariant_id (str, optional): The ASDC model id for this resource or + service model. Defaults to "". + model_version_id (str, optional): The ASDC model version for this resource or service + model. Defaults to "". + data_owner (str, optional): Identifies the entity that is responsible managing + this inventory object. Defaults to "". + data_source (str, optional): Identifies the upstream source of the data. + Defaults to "". + data_source_version (str, optional): Identifies the version of the upstream source. + Defaults to "". + resource_version (str, optional): Used for optimistic concurrency. Must be empty on + create, valid on update and delete. Defaults to "". + + """ + super().__init__() + self.site_resource_id: str = site_resource_id + self.site_resource_name: str = site_resource_name + self.description: str = description + self.site_resource_type: str = site_resource_type + self.role: str = role + self.generated_site_id: str = generated_site_id + self.selflink: str = selflink + self.operational_status: str = operational_status + self.model_customization_id: str = model_customization_id + self.model_invariant_id: str = model_invariant_id + self.model_version_id: str = model_version_id + self.data_owner: str = data_owner + self.data_source: str = data_source + self.data_source_version: str = data_source_version + self.resource_version: str = resource_version + + @property + def url(self) -> str: + """Site resource's url. + + Returns: + str: Site resources's url + + """ + return (f"{self.base_url}{self.api_version}/network/site-resources" + f"/site-resource/{self.site_resource_id}") + + @classmethod + def get_all_url(cls, *args, **kwargs) -> str: + """Get all site resources request url. + + Returns: + str: Url used on get all site resources request + + """ + return f"{cls.base_url}{cls.api_version}/network/site-resources" + + @classmethod + def get_all(cls) -> Iterable["SiteResource"]: + """Get all site resources. + + Yields: + SiteResource: Site resource object + + """ + for site_resource_data in cls.send_message_json("GET", + "Get all site resources", + cls.get_all_url()).get("site-resource", []): + yield SiteResource(site_resource_id=site_resource_data["site-resource-id"], + site_resource_name=site_resource_data.get("site-resource-name", ""), + description=site_resource_data.get("description", ""), + site_resource_type=site_resource_data.get("type", ""), + role=site_resource_data.get("role", ""), + generated_site_id=site_resource_data.get("generated-site-id", ""), + selflink=site_resource_data.get("selflink", ""), + operational_status=site_resource_data.get("operational-status", ""), + model_customization_id=site_resource_data.\ + get("model-customization-id", ""), + model_invariant_id=site_resource_data.get("model-invariant-id", ""), + model_version_id=site_resource_data.get("model-version-id", ""), + data_owner=site_resource_data.get("data-owner", ""), + data_source=site_resource_data.get("data-source", ""), + data_source_version=site_resource_data.get("data-source-version", + ""), + resource_version=site_resource_data.get("resource-version", "")) + + @classmethod + def get_by_site_resource_id(cls, site_resource_id: str) -> "SiteResource": + """Get site resource by it's id. + + Args: + site_resource_id (str): Site resource id. + + Returns: + SiteResource: Site resource object. + + """ + site_resource_data = cls.send_message_json("GET", + f"Get site resource with {site_resource_id} id", + f"{cls.get_all_url()}" + f"/site-resource/{site_resource_id}") + return SiteResource(site_resource_id=site_resource_data["site-resource-id"], + site_resource_name=site_resource_data.get("site-resource-name", ""), + description=site_resource_data.get("description", ""), + site_resource_type=site_resource_data.get("type", ""), + role=site_resource_data.get("role", ""), + generated_site_id=site_resource_data.get("generated-site-id", ""), + selflink=site_resource_data.get("selflink", ""), + operational_status=site_resource_data.get("operational-status", ""), + model_customization_id=site_resource_data.get("model-customization-id", + ""), + model_invariant_id=site_resource_data.get("model-invariant-id", ""), + model_version_id=site_resource_data.get("model-version-id", ""), + data_owner=site_resource_data.get("data-owner", ""), + data_source=site_resource_data.get("data-source", ""), + data_source_version=site_resource_data.get("data-source-version", ""), + resource_version=site_resource_data.get("resource-version", "")) + + @classmethod + def create(cls, # pylint: disable=too-many-arguments + site_resource_id: str, + site_resource_name: Optional[str] = None, + description: Optional[str] = None, + site_resource_type: Optional[str] = None, + role: Optional[str] = None, + generated_site_id: Optional[str] = None, + selflink: Optional[str] = None, + operational_status: Optional[str] = None, + model_customization_id: Optional[str] = None, + model_invariant_id: Optional[str] = None, + model_version_id: Optional[str] = None, + data_owner: Optional[str] = None, + data_source: Optional[str] = None, + data_source_version: Optional[str] = None) -> "SiteResource": + """Create site resource. + + Args: + site_resource_id (str): Uniquely identifies this site-resource by id + site_resource_name (Optional[str], optional): Store the name of this site-resource. + Defaults to None. + description (Optional[str], optional): Store the description of this site-resource. + Defaults to None. + site_resource_type (Optional[str], optional): Store the type of this site-resource. + Defaults to None. + role (Optional[str], optional): Store the role of this site-resource. + Defaults to None. + generated_site_id (Optional[str], optional): Store the generated-site-id of + this site-resource. Defaults to None. + selflink (Optional[str], optional): Store the link to get more information + for this object. Defaults to None. + operational_status (Optional[str], optional): Store the operational-status + for this object. Defaults to None. + model_customization_id (Optional[str], optional): Store the model-customization-id + for this object. Defaults to None. + model_invariant_id (Optional[str], optional): The ASDC model id for + this resource or service model. Defaults to None. + model_version_id (Optional[str], optional): The ASDC model version for this + resource or service model. Defaults to None. + data_owner (Optional[str], optional): Identifies the entity that is responsible + managing this inventory object. Defaults to None. + data_source (Optional[str], optional): Identifies the upstream source of the data. + Defaults to None. + data_source_version (Optional[str], optional): Identifies the version of the upstream + source. Defaults to None. + + Returns: + SiteResource: Site resource object + + """ + cls.send_message("PUT", + f"Create site resource {site_resource_id}", + f"{cls.get_all_url()}/site-resource/{site_resource_id}", + data=jinja_env() + .get_template("site_resource_create.json.j2") + .render(site_resource_id=site_resource_id, + site_resource_name=site_resource_name, + description=description, + site_resource_type=site_resource_type, + role=role, + generated_site_id=generated_site_id, + selflink=selflink, + operational_status=operational_status, + model_customization_id=model_customization_id, + model_invariant_id=model_invariant_id, + model_version_id=model_version_id, + data_owner=data_owner, + data_source=data_source, + data_source_version=data_source_version)) + return cls.get_by_site_resource_id(site_resource_id) diff --git a/src/onapsdk/aai/service_design_and_creation.py b/src/onapsdk/aai/service_design_and_creation.py new file mode 100644 index 0000000..5bf2c6f --- /dev/null +++ b/src/onapsdk/aai/service_design_and_creation.py @@ -0,0 +1,186 @@ +"""AAI service-design-and-creation 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 typing import Iterator +from urllib.parse import urlencode + +from onapsdk.utils.jinja import jinja_env + +from .aai_element import AaiResource + + +class Service(AaiResource): + """SDC service class.""" + + def __init__(self, service_id: str, service_description: str, resource_version: str) -> None: + """Service model initialization. + + Args: + service_id (str): This gets defined by others to provide a unique ID for the service. + service_description (str): Description of the service. + resource_version (str): Used for optimistic concurrency. + + """ + super().__init__() + self.service_id = service_id + self.service_description = service_description + self.resource_version = resource_version + + def __repr__(self) -> str: + """Service object description. + + Returns: + str: Service object description + + """ + return ( + f"Service(service_id={self.service_id}, " + f"service_description={self.service_description}, " + f"resource_version={self.resource_version})" + ) + + @property + def url(self) -> str: + """Service object url. + + Returns: + str: Service object url address + + """ + return (f"{self.base_url}{self.api_version}/service-design-and-creation/services/service/" + f"{self.service_id}?resource-version={self.resource_version}") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all services. + + Returns: + str: Url to get all services + + """ + return f"{cls.base_url}{cls.api_version}/service-design-and-creation/services" + + @classmethod + def get_all(cls, + service_id: str = None, + service_description: str = None) -> Iterator["Service"]: + """Services iterator. + + Stand-in for service model definitions. + + Returns: + Iterator[Service]: Service + + """ + filter_parameters: dict = cls.filter_none_key_values( + {"service-id": service_id, "service-description": service_description} + ) + url: str = (f"{cls.get_all_url()}?{urlencode(filter_parameters)}") + for service in cls.send_message_json("GET", "get subscriptions", url).get("service", []): + yield Service( + service_id=service["service-id"], + service_description=service["service-description"], + resource_version=service["resource-version"], + ) + + @classmethod + def create(cls, + service_id: str, + service_description: str) -> None: + """Create service. + + Args: + service_id (str): service ID + service_description (str): service description + + """ + cls.send_message( + "PUT", + "Create A&AI service", + f"{cls.base_url}{cls.api_version}/service-design-and-creation/" + f"services/service/{service_id}", + data=jinja_env() + .get_template("aai_service_create.json.j2") + .render( + service_id=service_id, + service_description=service_description + ) + ) + + +class Model(AaiResource): + """Model resource class.""" + + def __init__(self, invariant_id: str, model_type: str, resource_version: str) -> None: + """Model object initialization. + + Args: + invariant_id (str): invariant id + model_type (str): model type + resource_version (str): resource version + + """ + super().__init__() + self.invariant_id: str = invariant_id + self.model_type: str = model_type + self.resource_version: str = resource_version + + def __repr__(self) -> str: + """Model object representation. + + Returns: + str: model object representation + + """ + return (f"Model(invatiant_id={self.invariant_id}, " + f"model_type={self.model_type}, " + f"resource_version={self.resource_version}") + + @property + def url(self) -> str: + """Model instance url. + + Returns: + str: Model's url + + """ + return (f"{self.base_url}{self.api_version}/service-design-and-creation/models/" + f"model/{self.invariant_id}?resource-version={self.resource_version}") + + @classmethod + def get_all_url(cls) -> str: # pylint: disable=arguments-differ + """Return url to get all models. + + Returns: + str: Url to get all models + + """ + return f"{cls.base_url}{cls.api_version}/service-design-and-creation/models" + + @classmethod + def get_all(cls) -> Iterator["Model"]: + """Get all models. + + Yields: + Model: Model object + + """ + for model in cls.send_message_json("GET", + "Get A&AI sdc models", + cls.get_all_url()).get("model", []): + yield Model( + invariant_id=model.get("model-invariant-id"), + model_type=model.get("model-type"), + resource_version=model.get("resource-version") + ) diff --git a/src/onapsdk/aai/templates/aai_add_relationship.json.j2 b/src/onapsdk/aai/templates/aai_add_relationship.json.j2 new file mode 100644 index 0000000..5d7acb8 --- /dev/null +++ b/src/onapsdk/aai/templates/aai_add_relationship.json.j2 @@ -0,0 +1,11 @@ +{ + "related-to": "{{ relationship.related_to }}", + "related-link": "{{ relationship.related_link }}", + {% if relationship.relationship_label %} + "relationship-label": "{{ relationship.relationship_label }}", + {% endif %} + {% if relationship.related_to_property %} + "related-to-property": {{ relationship.related_to_property | tojson }}, + {% endif %} + "relationship-data": {{ relationship.relationship_data | tojson }} +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_bulk.json.j2 b/src/onapsdk/aai/templates/aai_bulk.json.j2 new file mode 100644 index 0000000..40a97bd --- /dev/null +++ b/src/onapsdk/aai/templates/aai_bulk.json.j2 @@ -0,0 +1,11 @@ +{ + "operations": [ + {% for operation in operations %} + { + "action": "{{ operation.action }}", + "uri": "{{ operation.uri }}", + "body": {{ operation.body }} + }{% if not loop.last %},{% endif %} + {% endfor %} + ] +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_line_of_business_create.json.j2 b/src/onapsdk/aai/templates/aai_line_of_business_create.json.j2 new file mode 100644 index 0000000..adab1fa --- /dev/null +++ b/src/onapsdk/aai/templates/aai_line_of_business_create.json.j2 @@ -0,0 +1,3 @@ +{ + "line-of-business-name": "{{ line_of_business_name }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_owning_entity_create.json.j2 b/src/onapsdk/aai/templates/aai_owning_entity_create.json.j2 new file mode 100644 index 0000000..2877a3d --- /dev/null +++ b/src/onapsdk/aai/templates/aai_owning_entity_create.json.j2 @@ -0,0 +1,4 @@ +{ + "owning-entity-name": "{{ owning_entity_name }}", + "owning-entity-id": "{{ owning_entity_id }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_platform_create.json.j2 b/src/onapsdk/aai/templates/aai_platform_create.json.j2 new file mode 100644 index 0000000..afe339a --- /dev/null +++ b/src/onapsdk/aai/templates/aai_platform_create.json.j2 @@ -0,0 +1,3 @@ +{ + "platform-name": "{{ platform_name }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_project_create.json.j2 b/src/onapsdk/aai/templates/aai_project_create.json.j2 new file mode 100644 index 0000000..3c7a426 --- /dev/null +++ b/src/onapsdk/aai/templates/aai_project_create.json.j2 @@ -0,0 +1,3 @@ +{ + "project-name": "{{ project_name }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_service_create.json.j2 b/src/onapsdk/aai/templates/aai_service_create.json.j2 new file mode 100644 index 0000000..ee360cc --- /dev/null +++ b/src/onapsdk/aai/templates/aai_service_create.json.j2 @@ -0,0 +1,4 @@ +{ + "service-id": "{{ service_id }}", + "service-description": "{{ service_description }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_service_instance_create.json.j2 b/src/onapsdk/aai/templates/aai_service_instance_create.json.j2 new file mode 100644 index 0000000..91f0046 --- /dev/null +++ b/src/onapsdk/aai/templates/aai_service_instance_create.json.j2 @@ -0,0 +1,22 @@ +{ + "service-instance-id": "{{ service_instance.instance_id }}" + {% if service_instance.instance_name %}, "service-instance-name": "{{ service_instance.instance_name }}"{% endif %} + {% if service_instance.service_type %}, "service-type": "{{ service_instance.service_type }}"{% endif %} + {% if service_instance.service_role %}, "service-role": "{{ service_instance.service_role }}"{% endif %} + {% if service_instance.environment_context %}, "environment-context": "{{ service_instance.environment_context }}"{% endif %} + {% if service_instance.workload_context %}, "workload-context": "{{ service_instance.workload_context }}"{% endif %} + {% if service_instance.created_at %}, "created-at": "{{ service_instance.created_at }}"{% endif %} + {% if service_instance.updated_at %}, "updated-at": "{{ service_instance.updated_at }}"{% endif %} + {% if service_instance.description %}, "description": "{{ service_instance.description }}"{% endif %} + {% if service_instance.model_invariant_id %}, "model-invariant-id": "{{ service_instance.model_invariant_id }}"{% endif %} + {% if service_instance.model_version_id %}, "model-version-id": "{{ service_instance.model_version_id }}"{% endif %} + {% if service_instance.persona_model_version %}, "persona-model-version": "{{ service_instance.persona_model_version }}"{% endif %} + {% if service_instance.widget_model_id %}, "widget-model-id": "{{ service_instance.widget_model_id }}"{% endif %} + {% if service_instance.widget_model_version %}, "widget-model-version": "{{ service_instance.widget_model_version }}"{% endif %} + {% if service_instance.bandwith_total %}, "bandwidth-total": "{{ service_instance.bandwith_total }}"{% endif %} + {% if service_instance.vhn_portal_url %}, "vhn-portal-url": "{{ service_instance.vhn_portal_url }}"{% endif %} + {% if service_instance.service_instance_location_id %}, "service-instance-location-id": "{{ service_instance.service_instance_location_id }}"{% endif %} + {% if service_instance.selflink %}, "selflink": "{{ service_instance.selflink }}"{% endif %} + {% if service_instance.orchestration_status %}, "orchestration-status": "{{ service_instance.orchestration_status }}"{% endif %} + {% if service_instance.input_parameters %}, "input-parameters": "{{ service_instance.input_parameters }}"{% endif %} +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/aai_sp_partner_create.json.j2 b/src/onapsdk/aai/templates/aai_sp_partner_create.json.j2 new file mode 100644 index 0000000..40ba1d7 --- /dev/null +++ b/src/onapsdk/aai/templates/aai_sp_partner_create.json.j2 @@ -0,0 +1,21 @@ +{ + "sp-partner-id": "{{ sp_partner_id }}" + {% if url %} + , "url": "{{ url }}" + {% endif %} + {% if callsource %} + , "callsource": "{{ callsource }}" + {% endif %} + {% if operational_status %} + , "operational-status": "{{ operational_status }}" + {% endif %} + {% if model_customization_id %} + , "model-customization-id": "{{ model_customization_id }}" + {% endif %} + {% if model_invariant_id %} + , "model-invariant-id": "{{ model_invariant_id }}" + {% endif %} + {% if model_version_id %} + , "model-version-id": "{{ model_version_id }}" + {% endif %} +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/cloud_region_add_availability_zone.json.j2 b/src/onapsdk/aai/templates/cloud_region_add_availability_zone.json.j2 new file mode 100644 index 0000000..be6ebc5 --- /dev/null +++ b/src/onapsdk/aai/templates/cloud_region_add_availability_zone.json.j2 @@ -0,0 +1,7 @@ +{ + "availability-zone-name": "{{ availability_zone_name }}", + "hypervisor-type": "{{ availability_zone_hypervisor_type }}" + {% if availability_zone_operational_status %} + , "operational-status": "{{ availability_zone_operational_status }}" + {% endif %} +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/cloud_region_add_esr_system_info.json.j2 b/src/onapsdk/aai/templates/cloud_region_add_esr_system_info.json.j2 new file mode 100644 index 0000000..ab03de3 --- /dev/null +++ b/src/onapsdk/aai/templates/cloud_region_add_esr_system_info.json.j2 @@ -0,0 +1,54 @@ +{ + "esr-system-info-id": "{{ esr_system_info_id }}", + "user-name": "{{ user_name }}", + "password": "{{ password }}", + "system-type": "{{ system_type }}" + {% if system_name %} + , "system-name": "{{ system_name }}" + {% endif %} + {% if esr_type %} + , "type": "{{ esr_type }}" + {% endif %} + {% if vendor %} + , "vendor": "{{ vendor }}" + {% endif %} + {% if version %} + , "version": "{{ version }}" + {% endif %} + {% if service_url %} + , "service-url": "{{ service_url }}" + {% endif %} + {% if protocol %} + , "protocol": "{{ protocol }}" + {% endif %} + {% if ssl_cacert %} + , "ssl-cacert": "{{ ssl_cacert }}" + {% endif %} + {% if ssl_insecure is not none %} + , "ssl-insecure": {{ ssl_insecure | tojson }} + {% endif %} + {% if ip_address %} + , "ip-address": "{{ ip_address }}" + {% endif %} + {% if port %} + , "port": "{{ port }}" + {% endif %} + {% if cloud_domain %} + , "cloud-domain": "{{ cloud_domain }}" + {% endif %} + {% if default_tenant %} + , "default-tenant": "{{ default_tenant }}" + {% endif %} + {% if passive is not none %} + , "passive": {{ passive | tojson }} + {% endif %} + {% if remote_path %} + , "remote-path": "{{ remote_path }}" + {% endif %} + {% if system_status %} + , "system-status": "{{ system_status }}" + {% endif %} + {% if openstack_region_id %} + , "openstack-region-id": "{{ openstack_region_id }}" + {% endif %} +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/cloud_region_add_tenant.json.j2 b/src/onapsdk/aai/templates/cloud_region_add_tenant.json.j2 new file mode 100644 index 0000000..fd7bcb7 --- /dev/null +++ b/src/onapsdk/aai/templates/cloud_region_add_tenant.json.j2 @@ -0,0 +1,5 @@ +{ + "tenant-id": "{{ tenant_id }}", + "tenant-name": "{{ tenant_name }}", + "tenant-context": "{{ tenant_context }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/cloud_region_create.json.j2 b/src/onapsdk/aai/templates/cloud_region_create.json.j2 new file mode 100644 index 0000000..65a7057 --- /dev/null +++ b/src/onapsdk/aai/templates/cloud_region_create.json.j2 @@ -0,0 +1,16 @@ +{ + "cloud-owner": "{{ cloud_region.cloud_owner }}", + "cloud-region-id": "{{ cloud_region.cloud_region_id }}", + "orchestration-disabled": "{{ cloud_region.orchestration_disabled }}", + "in-maint": "{{ cloud_region.in_maint }}", + "cloud-type": "{{ cloud_region.cloud_type }}", + "owner-defined-type": "{{ cloud_region.owner_defined_type }}", + "cloud-region-version": "{{ cloud_region.cloud_region_version }}", + "identity-url": "{{ cloud_region.identity_url }}", + "cloud-zone": "{{ cloud_region.cloud_zone }}", + "complex-name": "{{ cloud_region.complex_name }}", + "sriov-automation": "{{ cloud_region.sriov_automation }}", + "cloud-extra-info": "{{ cloud_region.cloud_extra_info }}", + "upgrade-cycle": "{{ cloud_region.upgrade_cycle }}", + "resource-version": "{{ cloud_region.resource_version }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/complex_create.json.j2 b/src/onapsdk/aai/templates/complex_create.json.j2 new file mode 100644 index 0000000..681fdad --- /dev/null +++ b/src/onapsdk/aai/templates/complex_create.json.j2 @@ -0,0 +1,23 @@ +{ + "physical-location-id": "{{ complex.physical_location_id }}", + "data-center-code": "{{ complex.data_center_code }}", + "complex-name": "{{ complex.name }}", + "identity-url": "{{ complex.identity_url }}", + "resource-version": "{{ complex.resource_version }}", + "physical-location-type": "{{ complex.physical_location_type }}", + "street1": "{{ complex.street1 }}", + "street2": "{{ complex.street2 }}", + "city": "{{ complex.city }}", + "state": "{{ complex.state }}", + "postal-code": "{{ complex.postal_code }}", + "country": "{{ complex.country }}", + "region": "{{ complex.region }}", + "latitude": "{{ complex.latitude }}", + "longitude": "{{ complex.longitude }}", + "elevation": "{{ complex.elevation }}", + "lata": "{{ complex.lata }}", + "time-zone": "{{ complex.timezone }}", + "data-owner": "{{ complex.data_owner }}", + "data-source": "{{ complex.data_source }}", + "data-source-version": "{{ complex.data_source_version }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/customer_create.json.j2 b/src/onapsdk/aai/templates/customer_create.json.j2 new file mode 100644 index 0000000..0eea2ed --- /dev/null +++ b/src/onapsdk/aai/templates/customer_create.json.j2 @@ -0,0 +1,15 @@ +{ + "global-customer-id": "{{ global_customer_id }}", + "subscriber-name": "{{ subscriber_name }}", + "subscriber-type": "{{ subscriber_type }}"{% if service_subscriptions %}, + "service-subscriptions": { + "service-subscription": [ + {% for service_subscription in service_subscriptions %} + { + "service-type": "{{ service_subscription }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + } + {% endif %} +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/customer_service_subscription_create.json.j2 b/src/onapsdk/aai/templates/customer_service_subscription_create.json.j2 new file mode 100644 index 0000000..c1ee61e --- /dev/null +++ b/src/onapsdk/aai/templates/customer_service_subscription_create.json.j2 @@ -0,0 +1,3 @@ +{ + "service-id": "{{ service_id }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/geo_region_create.json.j2 b/src/onapsdk/aai/templates/geo_region_create.json.j2 new file mode 100644 index 0000000..d84b427 --- /dev/null +++ b/src/onapsdk/aai/templates/geo_region_create.json.j2 @@ -0,0 +1,10 @@ +{ + {% if geo_region_name is not none %}"geo-region-name": "{{ geo_region_name }}",{% endif %} + {% if geo_region_type is not none %}"geo-region-type": "{{ geo_region_type }}",{% endif %} + {% if geo_region_role is not none %}"geo-region-role": "{{ geo_region_role }}",{% endif %} + {% if geo_region_function is not none %}"geo-region-function": "{{ geo_region_function }}",{% endif %} + {% if data_owner is not none %}"data-owner": "{{ data_owner }}",{% endif %} + {% if data_source is not none %}"data-source": "{{ data_source }}",{% endif %} + {% if data_source_version is not none %}"data-source-version": "{{ data_source_version }}",{% endif %} + "geo-region-id": "{{ geo_region_id }}" +}
\ No newline at end of file diff --git a/src/onapsdk/aai/templates/site_resource_create.json.j2 b/src/onapsdk/aai/templates/site_resource_create.json.j2 new file mode 100644 index 0000000..caaf291 --- /dev/null +++ b/src/onapsdk/aai/templates/site_resource_create.json.j2 @@ -0,0 +1,16 @@ +{ + {% if site_resource_name is not none %}"site-resource-name": "{{ site_resource_name }}",{% endif %} + {% if description is not none %}"description": "{{ description }}",{% endif %} + {% if site_resource_type is not none %}"type": "{{ site_resource_type }}",{% endif %} + {% if role is not none %}"role": "{{ role }}",{% endif %} + {% if generated_site_id is not none %}"generated-site-id": "{{ generated_site_id }}",{% endif %} + {% if selflink is not none %}"selflink": "{{ selflink }}",{% endif %} + {% if operational_status is not none %}"operational-status": "{{ operational_status }}",{% endif %} + {% if model_customization_id is not none %}"model-customization-id": "{{ model_customization_id }}",{% endif %} + {% if model_invariant_id is not none %}"model-invariant-id": "{{ model_invariant_id }}",{% endif %} + {% if model_version_id is not none %}"model-version-id": "{{ model_version_id }}",{% endif %} + {% if data_owner is not none %}"data-owner": "{{ data_owner }}",{% endif %} + {% if data_source is not none %}"data-source": "{{ data_source }}",{% endif %} + {% if data_source_version is not none %}"data-source-version": "{{ data_source_version }}",{% endif %} + "site-resource-id": "{{ site_resource_id }}" +}
\ No newline at end of file diff --git a/src/onapsdk/cds/README.md b/src/onapsdk/cds/README.md new file mode 100644 index 0000000..5875e43 --- /dev/null +++ b/src/onapsdk/cds/README.md @@ -0,0 +1,71 @@ +# CDS module # + +## Load blueprint ## + +``` +>>> from onapsdk.cds import Blueprint +>>> blueprint = Blueprint.load_from_file("<< path to CBA file >>") # load a blueprint from ZIP file +``` + +## Enrich, publish blueprint + +``` +>>> enriched_blueprint = blueprint.enrich() # returns enriched blueprint object +>>> enriched_blueprint.publish() +``` + +## Execute blueprint workflow + +``` +>>> blueprint.workflows +[Workflow(name='resource-assignment', blueprint_name='vDNS-CDS-test1)', Workflow(name='config-assign', blueprint_name='vDNS-CDS-test1)', Workflow(name='config-deploy', blueprint_name='vDNS-CDS-test1)'] +>>> workflow = blueprint.workflows[0] # get the first workflow named 'resource-assignment` +>>> workflow.inputs # display what workflow needs as an input +[Workflow.WorkflowInput(name='template-prefix', required=True, type='list', description=None), Workflow.WorkflowInput(name='resource-assignment-properties', required=True, type='dt-resource-assignment-properties', description='Dynamic PropertyDefinition for workflow(resource-assignment).')] +>>> response = workflow.execute({"template-prefix": ["vpkg"], "resource-assignment-properties": {}}) # execute workflow with required inputs +``` + +## Generate data dictionary for blueprint + +Generated data dictionaries have to be manually filled for "source-rest" and "source-db" input types. + +``` +>>> blueprint.get_data_dictionaries().save_to_file("/tmp/dd.json") # generate data dictionaries for blueprint and save it to "/tmp/dd.json" file +``` + +## Manage Blueprint Models in CDS + +### Retrieve Blueprint Models from CDS + - All +``` +>>> from onapsdk.cds import BlueprintModel +>>> all_blueprint_models = BlueprintModel.get_all() +``` + - Selected by **id** of Blueprint Model +``` +>>> blueprint_model = BlueprintModel.get_by_id(blueprint_model_id='11111111-1111-1111-1111-111111111111') +>>> blueprint_model +BlueprintModel(artifact_name='test_name', blueprint_model_id='11111111-1111-1111-1111-111111111111') +``` +- Selected by **name and version** of Blueprint Model +``` +>>> blueprint_model = BlueprintModel.get_by_name_and_version(blueprint_name='test_name', blueprint_version='1.0.0') +>>> blueprint_model +BlueprintModel(artifact_name='test_name', blueprint_model_id='11111111-1111-1111-1111-111111111111') +``` + +### Delete Blueprint Model +``` +>>> blueprint_model.delete() +``` + +### Download Blueprint Model +``` +>>> blueprint_model.save(dst_file_path='/tmp/blueprint.zip') +``` + +### Get Blueprint object for Blueprint Model +``` +>>> blueprint = blueprint_model.get_blueprint() +``` +After that, all operation for blueprint object, like execute blueprint workflow etc. can be executed. diff --git a/src/onapsdk/cds/__init__.py b/src/onapsdk/cds/__init__.py new file mode 100644 index 0000000..f58e7e1 --- /dev/null +++ b/src/onapsdk/cds/__init__.py @@ -0,0 +1,18 @@ +"""ONAP SDK CDS package.""" +# 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 .blueprint import Blueprint +from .blueprint_model import BlueprintModel +from .data_dictionary import DataDictionarySet diff --git a/src/onapsdk/cds/blueprint.py b/src/onapsdk/cds/blueprint.py new file mode 100644 index 0000000..1286375 --- /dev/null +++ b/src/onapsdk/cds/blueprint.py @@ -0,0 +1,830 @@ +"""CDS Blueprint 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. +import json +import re +from dataclasses import dataclass, field +from datetime import datetime +from io import BytesIO +from typing import Any, Dict, Generator, Iterator, List, Optional +from urllib.parse import urlencode +from uuid import uuid4 +from zipfile import ZipFile + +import oyaml as yaml + +from onapsdk.utils.jinja import jinja_env +from onapsdk.exceptions import FileError, ParameterError, ValidationError + +from .cds_element import CdsElement +from .data_dictionary import DataDictionary, DataDictionarySet + + +@dataclass +class CbaMetadata: + """Class to hold CBA metadata values.""" + + tosca_meta_file_version: str + csar_version: str + created_by: str + entry_definitions: str + template_name: str + template_version: str + template_tags: str + + +@dataclass +class Mapping: + """Blueprint's template mapping. + + Stores mapping data: + - name, + - type, + - name of dictionary from which value should be get, + - dictionary source of value. + """ + + name: str + mapping_type: str + dictionary_name: str + dictionary_sources: List[str] = field(default_factory=list) + + def __hash__(self) -> int: # noqa: D401 + """Mapping object hash. + + Based on mapping name. + + Returns: + int: Mapping hash + + """ + return hash(self.name) + + def __eq__(self, mapping: "Mapping") -> bool: + """Compare two mapping objects. + + Mappings are equal if have the same name. + + Args: + mapping (Mapping): Mapping object to compare with. + + Returns: + bool: True if objects have the same name, False otherwise. + + """ + return self.name == mapping.name + + def merge(self, mapping: "Mapping") -> None: + """Merge mapping objects. + + Merge objects dictionary sources. + + Args: + mapping (Mapping): Mapping object to merge. + + """ + self.dictionary_sources = list( + set(self.dictionary_sources) | set(mapping.dictionary_sources) + ) + + def generate_data_dictionary(self) -> dict: + """Generate data dictionary for mapping. + + Data dictionary with required data sources, type and name for mapping will be created from + Jinja2 template. + + Returns: + dict: Data dictionary + + """ + return json.loads( + jinja_env().get_template("data_dictionary_base.json.j2").render(mapping=self) + ) + + +class MappingSet: + """Set of mapping objects. + + Mapping objects will be stored in dictionary where mapping name is a key. + No two mappings with the same name can be stored in this collection. + """ + + def __init__(self) -> None: + """Initialize mappings collection. + + Create dictionary to store mappings. + """ + self.mappings = {} + + def __len__(self) -> int: # noqa: D401 + """Mapping set length. + + Returns: + int: Number of stored mapping objects. + + """ + return len(self.mappings) + + def __iter__(self) -> Iterator[Mapping]: + """Iterate through mapping stored in set. + + Returns: + Iterator[Mapping]: Stored mappings iterator. + + """ + return iter(list(self.mappings.values())) + + def __getitem__(self, index: int) -> Mapping: + """Get item stored on given index. + + Args: + index (int): Index number. + + Returns: + Mapping: Mapping stored on given index. + + """ + return list(self.mappings.values())[index] + + def add(self, mapping: Mapping) -> None: + """Add mapping to set. + + If there is already mapping object with the same name in collection + they will be merged. + + Args: + mapping (Mapping): Mapping to add to collection. + + """ + if mapping.name not in self.mappings: + self.mappings.update({mapping.name: mapping}) + else: + self.mappings[mapping.name].merge(mapping) + + def extend(self, iterable: Iterator[Mapping]) -> None: + """Extend set with an iterator of mappings. + + Args: + iterable (Iterator[Mapping]): Mappings iterator. + + """ + for mapping in iterable: + self.add(mapping) + + +class Workflow(CdsElement): + """Blueprint's workflow. + + Stores workflow steps, inputs, outputs. + Executes workflow using CDS HTTP API. + """ + + @dataclass + class WorkflowStep: + """Workflow step class. + + Stores step name, description, target and optional activities. + """ + + name: str + description: str + target: str + activities: List[Dict[str, str]] = field(default_factory=list) + + @dataclass + class WorkflowInput: + """Workflow input class. + + Stores input name, information if it's required, type, and optional description. + """ + + name: str + required: bool + type: str + description: str = "" + + @dataclass + class WorkflowOutput: + """Workflow output class. + + Stores output name, type na value. + """ + + name: str + type: str + value: Dict[str, Any] + + def __init__(self, + cba_workflow_name: str, + cba_workflow_data: dict, + blueprint: "Blueprint") -> None: + """Workflow initialization. + + Args: + cba_workflow_name (str): Workflow name. + cba_workflow_data (dict): Workflow data. + blueprint (Blueprint): Blueprint object which contains workflow. + + """ + super().__init__() + self.name: str = cba_workflow_name + self.workflow_data: dict = cba_workflow_data + self.blueprint: "Blueprint" = blueprint + self._steps: List[self.WorkflowStep] = None + self._inputs: List[self.WorkflowInput] = None + self._outputs: List[self.WorkflowOutput] = None + + def __repr__(self) -> str: + """Representation of object. + + Returns: + str: Object's string representation + + """ + return (f"Workflow(name='{self.name}', " + f"blueprint_name='{self.blueprint.metadata.template_name})'") + + @property + def steps(self) -> List["Workflow.WorkflowStep"]: + """Workflow's steps property. + + Returns: + List[Workflow.WorkflowStep]: List of workflow's steps. + + """ + if self._steps is None: + self._steps = [] + for step_name, step_data in self.workflow_data.get("steps", {}).items(): + self._steps.append( + self.WorkflowStep( + name=step_name, + description=step_data.get("description"), + target=step_data.get("target"), + activities=step_data.get("activities", []), + ) + ) + return self._steps + + @property + def inputs(self) -> List["Workflow.WorkflowInput"]: + """Workflow's inputs property. + + Returns: + List[Workflow.WorkflowInput]: List of workflows's inputs. + + """ + if self._inputs is None: + self._inputs = [] + for input_name, input_data in self.workflow_data.get("inputs", {}).items(): + self._inputs.append( + self.WorkflowInput( + name=input_name, + required=input_data.get("required"), + type=input_data.get("type"), + description=input_data.get("description"), + ) + ) + return self._inputs + + @property + def outputs(self) -> List["Workflow.WorkflowOutput"]: + """Workflow's outputs property. + + Returns: + List[Workflow.WorkflowOutput]: List of workflows's outputs. + + """ + if self._outputs is None: + self._outputs = [] + for output_name, output_data in self.workflow_data.get("outputs", {}).items(): + self._outputs.append( + self.WorkflowOutput( + name=output_name, + type=output_data.get("type"), + value=output_data.get("value"), + ) + ) + return self._outputs + + @property + def url(self) -> str: + """Workflow execution url. + + Returns: + str: Url to call warkflow execution. + + """ + return f"{self._url}/api/v1/execution-service/process" + + def execute(self, inputs: dict) -> dict: + """Execute workflow. + + Call CDS HTTP API to execute workflow. + + Args: + inputs (dict): Inputs dictionary. + + Returns: + dict: Response's payload. + + """ + # There should be some flague to check if CDS UI API is used or blueprintprocessor. + # For CDS UI API there is no endporint to execute workflow, so it has to be turned off. + execution_service_input: dict = { + "commonHeader": { + "originatorId": "onapsdk", + "requestId": str(uuid4()), + "subRequestId": str(uuid4()), + "timestamp": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + }, + "actionIdentifiers": { + "blueprintName": self.blueprint.metadata.template_name, + "blueprintVersion": self.blueprint.metadata.template_version, + "actionName": self.name, + "mode": "SYNC", # Has to be SYNC for REST call + }, + "payload": {f"{self.name}-request": inputs}, + } + response: "requests.Response" = self.send_message_json( + "POST", + f"Execute {self.blueprint.metadata.template_name} blueprint {self.name} workflow", + self.url, + auth=self.auth, + data=json.dumps(execution_service_input) + ) + return response["payload"] + + +class ResolvedTemplate(CdsElement): + """Resolved template class. + + Store and retrieve rendered template results. + """ + + def __init__(self, blueprint: "Blueprint", # pylint: disable=too-many-arguments + artifact_name: Optional[str] = None, + resolution_key: Optional[str] = None, + resource_id: Optional[str] = None, + resource_type: Optional[str] = None, + occurrence: Optional[str] = None, + response_format: str = "application/json") -> None: + """Init resolved template class instance. + + Args: + blueprint (Blueprint): Blueprint object. + artifact_name (Optional[str], optional): Artifact name for which to retrieve + a resolved resource. Defaults to None. + resolution_key (Optional[str], optional): Resolution Key associated with + the resolution. Defaults to None. + resource_id (Optional[str], optional): Resource Id associated with + the resolution. Defaults to None. + resource_type (Optional[str], optional): Resource Type associated + with the resolution. Defaults to None. + occurrence (Optional[str], optional): Occurrence of the template resolution (1-n). + Defaults to None. + response_format (str): Expected format of the template being retrieved. + + """ + super().__init__() + self.blueprint: "Blueprint" = blueprint + self.artifact_name: Optional[str] = artifact_name + self.resolution_key: Optional[str] = resolution_key + self.resource_id: Optional[str] = resource_id + self.resource_type: Optional[str] = resource_type + self.occurrence: Optional[str] = occurrence + self.response_format: str = response_format + + @property + def url(self) -> str: + """Url property. + + Returns: + str: Url + + """ + return f"{self._url}/api/v1/template" + + @property + def resolved_template_url(self) -> str: + """Url to retrieve resolved template. + + Filter None parameters. + + Returns: + str: Retrieve resolved template url + + """ + params_dict: Dict[str, str] = urlencode(dict(filter(lambda item: item[1] is not None, { + "bpName": self.blueprint.metadata.template_name, + "bpVersion": self.blueprint.metadata.template_version, + "artifactName": self.artifact_name, + "resolutionKey": self.resolution_key, + "resourceType": self.resource_type, + "resourceId": self.resource_id, + "occurrence": self.occurrence, + "format": self.response_format + }.items()))) + return f"{self.url}?{params_dict}" + + def get_resolved_template(self) -> Dict[str, str]: + """Get resolved template. + + Returns: + Dict[str, str]: Resolved template + + """ + return self.send_message_json( + "GET", + f"Get resolved template {self.artifact_name} for " + f"{self.blueprint.metadata.template_name} version " + f"{self.blueprint.metadata.template_version}", + self.resolved_template_url, + auth=self.auth + ) + + def store_resolved_template(self, resolved_template: str) -> None: + """Store resolved template. + + Args: + resolved_template (str): Template to store + + Raises: + ParameterError: To store template it's needed to pass artifact name and: + - resolution key, or + - resource type and resource id. + If not all needed parameters are given that exception will be raised. + + """ + if self.artifact_name and self.resolution_key: + return self.store_resolved_template_with_resolution_key(resolved_template) + if self.artifact_name and self.resource_type and self.resource_id: + return self.store_resolved_template_with_resource_type_and_id(resolved_template) + raise ParameterError("To store template artifact name with resolution key or both " + "resource type and id is needed") + + def store_resolved_template_with_resolution_key(self, resolved_template: str) -> None: + """Store template using resolution key. + + Args: + resolved_template (str): Template to store + + """ + return self.send_message( + "POST", + f"Store resolved template {self.artifact_name} for " + f"{self.blueprint.metadata.template_name} version " + f"{self.blueprint.metadata.template_version}", + f"{self.url}/{self.blueprint.metadata.template_name}/" + f"{self.blueprint.metadata.template_version}/{self.artifact_name}/" + f"{self.resolution_key}", + auth=self.auth, + data=resolved_template + ) + + def store_resolved_template_with_resource_type_and_id(self, resolved_template: str) -> None: + """Store template using resource type and resource ID. + + Args: + resolved_template (str): Template to store + + """ + return self.send_message( + "POST", + f"Store resolved template {self.artifact_name} for " + f"{self.blueprint.metadata.template_name} version " + f"{self.blueprint.metadata.template_version}", + f"{self.url}/{self.blueprint.metadata.template_name}/" + f"{self.blueprint.metadata.template_version}/{self.artifact_name}/" + f"{self.resource_type}/{self.resource_id}", + auth=self.auth, + data=resolved_template + ) + +class Blueprint(CdsElement): + """CDS blueprint representation.""" + + TEMPLATES_RE = r"Templates\/.*json$" + TOSCA_META = "TOSCA-Metadata/TOSCA.meta" + + def __init__(self, cba_file_bytes: bytes) -> None: + """Blueprint initialization. + + Save blueprint zip file bytes. + You can create that object using opened file or bytes: + blueprint = Blueprint(open("path/to/CBA.zip", "rb")) + or + with open("path/to/CBA.zip", "rb") as cba: + blueprint = Blueprint(cba.read()) + It is even better to use second example due to CBA file will be correctly closed for sure. + + Args: + cba_file_bytes (bytes): CBA ZIP file bytes + + """ + super().__init__() + self.cba_file_bytes: bytes = cba_file_bytes + self._cba_metadata: CbaMetadata = None + self._cba_mappings: MappingSet = None + self._cba_workflows: List[Workflow] = None + + @property + def url(self) -> str: + """URL address to use for CDS API call. + + Returns: + str: URL to CDS blueprintprocessor. + + """ + return f"{self._url}/api/v1/blueprint-model" + + @property + def metadata(self) -> CbaMetadata: + """Blueprint metadata. + + Data from TOSCA.meta file. + + Returns: + CbaMetadata: Blueprint metadata object. + + """ + if not self._cba_metadata: + with ZipFile(BytesIO(self.cba_file_bytes)) as cba_zip_file: + self._cba_metadata = self.get_cba_metadata(cba_zip_file.read(self.TOSCA_META)) + return self._cba_metadata + + @property + def mappings(self) -> MappingSet: + """Blueprint mappings collection. + + Returns: + MappingSet: Mappings collection. + + """ + if not self._cba_mappings: + with ZipFile(BytesIO(self.cba_file_bytes)) as cba_zip_file: + self._cba_mappings = self.get_mappings(cba_zip_file) + return self._cba_mappings + + @property + def workflows(self) -> List["Workflow"]: + """Blueprint's workflows property. + + Returns: + List[Workflow]: Blueprint's workflow list. + + """ + if not self._cba_workflows: + with ZipFile(BytesIO(self.cba_file_bytes)) as cba_zip_file: + self._cba_workflows = list( + self.get_workflows(cba_zip_file.read(self.metadata.entry_definitions)) + ) + return self._cba_workflows + + @classmethod + def load_from_file(cls, cba_file_path: str) -> "Blueprint": + """Load blueprint from file. + + Raises: + FileError: File to load blueprint from doesn't exist. + + Returns: + Blueprint: Blueprint object + + """ + try: + with open(cba_file_path, "rb") as cba_file: + return Blueprint(cba_file.read()) + except FileNotFoundError as exc: + msg = "The requested file with a blueprint doesn't exist." + raise FileError(msg) from exc + + def enrich(self) -> "Blueprint": + """Call CDS API to get enriched blueprint file. + + Returns: + Blueprint: Enriched blueprint object + + """ + response: "requests.Response" = self.send_message( + "POST", + "Enrich CDS blueprint", + f"{self.url}/enrich", + files={"file": self.cba_file_bytes}, + headers={}, # Leave headers empty to fill it correctly by `requests` library + auth=self.auth + ) + return Blueprint(response.content) + + def publish(self) -> None: + """Publish blueprint.""" + self.send_message( + "POST", + "Publish CDS blueprint", + f"{self.url}/publish", + files={"file": self.cba_file_bytes}, + headers={}, # Leave headers empty to fill it correctly by `requests` library + auth=self.auth + ) + + def deploy(self) -> None: + """Deploy blueprint.""" + self.send_message( + "POST", + "Deploy CDS blueprint", + f"{self.url}", + files={"file": self.cba_file_bytes}, + headers={}, # Leave headers empty to fill it correctly by `requests` library + auth=self.auth + ) + + def save(self, dest_file_path: str) -> None: + """Save blueprint to file. + + Args: + dest_file_path (str): Path of file where blueprint is going to be saved + + """ + with open(dest_file_path, "wb") as cba_file: + cba_file.write(self.cba_file_bytes) + + def get_data_dictionaries(self) -> DataDictionarySet: + """Get the generated data dictionaries required by blueprint. + + If mapping reqires other source than input it should be updated before upload to CDS. + + Returns: + Generator[DataDictionary, None, None]: DataDictionary objects. + + """ + dd_set: DataDictionarySet = DataDictionarySet() + for mapping in self.mappings: + dd_set.add(DataDictionary(mapping.generate_data_dictionary())) + return dd_set + + @staticmethod + def get_cba_metadata(cba_tosca_meta_bytes: bytes) -> CbaMetadata: + """Parse CBA TOSCA.meta file and get values from it. + + Args: + cba_tosca_meta_bytes (bytes): TOSCA.meta file bytes. + + Raises: + ValidationError: TOSCA Meta file has invalid format. + + Returns: + CbaMetadata: Dataclass with CBA metadata + + """ + meta_dict: dict = yaml.safe_load(cba_tosca_meta_bytes) + if not isinstance(meta_dict, dict): + raise ValidationError("Invalid TOSCA Meta file") + return CbaMetadata( + tosca_meta_file_version=meta_dict.get("TOSCA-Meta-File-Version"), + csar_version=meta_dict.get("CSAR-Version"), + created_by=meta_dict.get("Created-By"), + entry_definitions=meta_dict.get("Entry-Definitions"), + template_name=meta_dict.get("Template-Name"), + template_version=meta_dict.get("Template-Version"), + template_tags=meta_dict.get("Template-Tags"), + ) + + @staticmethod + def get_mappings_from_mapping_file(cba_mapping_file_bytes: bytes + ) -> Generator[Mapping, None, None]: + """Read mapping file and create Mappping object for it. + + Args: + cba_mapping_file_bytes (bytes): CBA mapping file bytes. + + Yields: + Generator[Mapping, None, None]: Mapping object. + + """ + mapping_file_json = json.loads(cba_mapping_file_bytes) + for mapping in mapping_file_json: + yield Mapping( + name=mapping["name"], + mapping_type=mapping["property"]["type"], + dictionary_name=mapping["dictionary-name"], + dictionary_sources=[mapping["dictionary-source"]], + ) + + def get_mappings(self, cba_zip_file: ZipFile) -> MappingSet: + """Read mappings from CBA file. + + Search mappings in CBA file and create Mapping object for each of them. + + Args: + cba_zip_file (ZipFile): CBA file to get mappings from. + + Returns: + MappingSet: Mappings set object. + + """ + mapping_set = MappingSet() + for info in cba_zip_file.infolist(): + if re.match(self.TEMPLATES_RE, info.filename): + mapping_set.extend( + self.get_mappings_from_mapping_file(cba_zip_file.read(info.filename)) + ) + return mapping_set + + def get_workflows(self, + cba_entry_definitions_file_bytes: bytes) -> Generator[Workflow, None, None]: + """Get worfklows from entry_definitions file. + + Parse entry_definitions file and create Workflow objects for workflows stored in. + + Args: + cba_entry_definitions_file_bytes (bytes): entry_definition file. + + Yields: + Generator[Workflow, None, None]: Workflow object. + + """ + entry_definitions_json: dict = json.loads(cba_entry_definitions_file_bytes) + workflows: dict = entry_definitions_json.get("topology_template", {}).get("workflows", {}) + for workflow_name, workflow_data in workflows.items(): + yield Workflow(workflow_name, workflow_data, self) + + def get_workflow_by_name(self, workflow_name: str) -> Workflow: + """Get workflow by name. + + If there is no workflow with given name `ParameterError` is going to be raised. + + Args: + workflow_name (str): Name of the workflow + + Returns: + Workflow: Workflow with given name + + """ + try: + return next(filter(lambda workflow: workflow.name == workflow_name, self.workflows)) + except StopIteration: + raise ParameterError("Workflow with given name does not exist") + + def get_resolved_template(self, # pylint: disable=too-many-arguments + artifact_name: str, + resolution_key: Optional[str] = None, + resource_type: Optional[str] = None, + resource_id: Optional[str] = None, + occurrence: Optional[str] = None) -> Dict[str, str]: + """Get resolved template for Blueprint. + + Args: + artifact_name (str): Resolved template's artifact name + resolution_key (Optional[str], optional): Resolved template's resolution key. + Defaults to None. + resource_type (Optional[str], optional): Resolved template's resource type. + Defaults to None. + resource_id (Optional[str], optional): Resolved template's resource ID. + Defaults to None. + occurrence: (Optional[str], optional): Resolved template's occurrence value. + Defaults to None. + + Returns: + Dict[str, str]: Resolved template + + """ + return ResolvedTemplate(blueprint=self, + artifact_name=artifact_name, + resolution_key=resolution_key, + resource_type=resource_type, + resource_id=resource_id, + occurrence=occurrence).get_resolved_template() + + def store_resolved_template(self, # pylint: disable=too-many-arguments + artifact_name: str, + data: str, + resolution_key: Optional[str] = None, + resource_type: Optional[str] = None, + resource_id: Optional[str] = None) -> None: + """Store resolved template for Blueprint. + + Args: + artifact_name (str): Resolved template's artifact name + data (str): Resolved template + resolution_key (Optional[str], optional): Resolved template's resolution key. + Defaults to None. + resource_type (Optional[str], optional): Resolved template's resource type. + Defaults to None. + resource_id (Optional[str], optional): Resolved template's resource ID. + Defaults to None. + """ + ResolvedTemplate(blueprint=self, + artifact_name=artifact_name, + resolution_key=resolution_key, + resource_type=resource_type, + resource_id=resource_id).store_resolved_template(data) diff --git a/src/onapsdk/cds/blueprint_model.py b/src/onapsdk/cds/blueprint_model.py new file mode 100644 index 0000000..7976001 --- /dev/null +++ b/src/onapsdk/cds/blueprint_model.py @@ -0,0 +1,222 @@ +"""CDS Blueprint Models 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 typing import Iterator +from onapsdk.exceptions import ResourceNotFound # for custom exceptions + +from .blueprint import Blueprint +from .cds_element import CdsElement + + +class BlueprintModel(CdsElement): # pylint: disable=too-many-instance-attributes + """Blueprint Model class. + + Represents blueprint models in CDS + """ + + def __init__(self, # pylint: disable=too-many-arguments + blueprint_model_id: str, + artifact_uuid: str = None, + artifact_type: str = None, + artifact_version: str = None, + artifact_description: str = None, + internal_version: str = None, + created_date: str = None, + artifact_name: str = None, + published: str = 'N', + updated_by: str = None, + tags: str = None): + """Blueprint Model initialization. + + Args: + blueprint_model_id (str): Blueprint model identifier + artifact_uuid (str): Blueprint model uuid + artifact_type (str): Blueprint artifact type + artifact_version (str): Blueprint model version + artifact_description (str): Blueprint model description + internal_version (str): Blueprint model internal version + created_date (str): Blueprint model created date + artifact_name (str): Blueprint model name + published (str): Blueprint model publish status - 'N' or 'Y' + updated_by (str): Blueprint model author + tags (str): Blueprint model tags + + """ + super().__init__() + self.blueprint_model_id = blueprint_model_id + self.artifact_uuid = artifact_uuid + self.artifact_type = artifact_type + self.artifact_version = artifact_version + self.artifact_description = artifact_description + self.internal_version = internal_version + self.created_date = created_date + self.artifact_name = artifact_name + self.published = published + self.updated_by = updated_by + self.tags = tags + + def __repr__(self) -> str: + """Representation of object. + + Returns: + str: Object's string representation + + """ + return (f"BlueprintModel(artifact_name='{self.artifact_name}', " + f"blueprint_model_id='{self.blueprint_model_id}')") + + @classmethod + def get_by_id(cls, blueprint_model_id: str) -> "BlueprintModel": + """Retrieve blueprint model with provided ID. + + Args: blueprint_model_id (str): + + Returns: + BlueprintModel: Blueprint model object + + Raises: + ResourceNotFound: Blueprint model with provided ID doesn't exist + + """ + try: + blueprint_model = cls.send_message_json( + "GET", + "Retrieve blueprint", + f"{cls._url}/api/v1/blueprint-model/{blueprint_model_id}", + auth=cls.auth) + + return cls( + blueprint_model_id=blueprint_model["blueprintModel"]['id'], + artifact_uuid=blueprint_model["blueprintModel"]['artifactUUId'], + artifact_type=blueprint_model["blueprintModel"]['artifactType'], + artifact_version=blueprint_model["blueprintModel"]['artifactVersion'], + internal_version=blueprint_model["blueprintModel"]['internalVersion'], + created_date=blueprint_model["blueprintModel"]['createdDate'], + artifact_name=blueprint_model["blueprintModel"]['artifactName'], + published=blueprint_model["blueprintModel"]['published'], + updated_by=blueprint_model["blueprintModel"]['updatedBy'], + tags=blueprint_model["blueprintModel"]['tags'] + ) + + except ResourceNotFound: + raise ResourceNotFound(f"BlueprintModel blueprint_model_id='{blueprint_model_id}" + f" not found") + + @classmethod + def get_by_name_and_version(cls, blueprint_name: str, + blueprint_version: str) -> "BlueprintModel": + """Retrieve blueprint model with provided name and version. + + Args: + blueprint_name (str): Blueprint model name + blueprint_version (str): Blueprint model version + + Returns: + BlueprintModel: Blueprint model object + + Raises: + ResourceNotFound: Blueprint model with provided name and version doesn't exist + + """ + try: + blueprint_model = cls.send_message_json( + "GET", + "Retrieve blueprint", + f"{cls._url}/api/v1/blueprint-model/by-name/{blueprint_name}" + f"/version/{blueprint_version}", + auth=cls.auth) + + return cls( + blueprint_model_id=blueprint_model["blueprintModel"]['id'], + artifact_uuid=blueprint_model["blueprintModel"]['artifactUUId'], + artifact_type=blueprint_model["blueprintModel"]['artifactType'], + artifact_version=blueprint_model["blueprintModel"]['artifactVersion'], + internal_version=blueprint_model["blueprintModel"]['internalVersion'], + created_date=blueprint_model["blueprintModel"]['createdDate'], + artifact_name=blueprint_model["blueprintModel"]['artifactName'], + published=blueprint_model["blueprintModel"]['published'], + updated_by=blueprint_model["blueprintModel"]['updatedBy'], + tags=blueprint_model["blueprintModel"]['tags'] + ) + + except ResourceNotFound: + raise ResourceNotFound(f"BlueprintModel blueprint_name='{blueprint_name}" + f" and blueprint_version='{blueprint_version}' not found") + + @classmethod + def get_all(cls) -> Iterator["BlueprintModel"]: + """Get all blueprint models. + + Yields: + BlueprintModel: BlueprintModel object. + + """ + for blueprint_model in cls.send_message_json( + "GET", + "Retrieve all blueprints", + f"{cls._url}/api/v1/blueprint-model", + auth=cls.auth): + + yield cls( + blueprint_model_id=blueprint_model["blueprintModel"]['id'], + artifact_uuid=blueprint_model["blueprintModel"]['artifactUUId'], + artifact_type=blueprint_model["blueprintModel"]['artifactType'], + artifact_version=blueprint_model["blueprintModel"]['artifactVersion'], + internal_version=blueprint_model["blueprintModel"]['internalVersion'], + created_date=blueprint_model["blueprintModel"]['createdDate'], + artifact_name=blueprint_model["blueprintModel"]['artifactName'], + published=blueprint_model["blueprintModel"]['published'], + updated_by=blueprint_model["blueprintModel"]['updatedBy'], + tags=blueprint_model["blueprintModel"]['tags'] + ) + + def get_blueprint(self) -> Blueprint: + """Get Blueprint object for selected blueprint model. + + Returns: + Blueprint: Blueprint object + + """ + cba_package = self.send_message( + "GET", + "Retrieve selected blueprint object", + f"{self._url}/api/v1/blueprint-model/download/{self.blueprint_model_id}", + auth=self.auth) + + return Blueprint(cba_file_bytes=cba_package.content) + + def save(self, dst_file_path: str): + """Save blueprint model to file. + + Args: + dst_file_path (str): Path of file where blueprint is going to be saved + """ + cba_package = self.send_message( + "GET", + "Retrieve and save selected blueprint", + f"{self._url}/api/v1/blueprint-model/download/{self.blueprint_model_id}", + auth=self.auth) + + with open(dst_file_path, "wb") as content: + for chunk in cba_package.iter_content(chunk_size=128): + content.write(chunk) + + def delete(self): + """Delete blueprint model.""" + self.send_message( + "DELETE", + "Delete blueprint", + f"{self._url}/api/v1/blueprint-model/{self.blueprint_model_id}", + auth=self.auth) diff --git a/src/onapsdk/cds/blueprint_processor.py b/src/onapsdk/cds/blueprint_processor.py new file mode 100644 index 0000000..3763e1b --- /dev/null +++ b/src/onapsdk/cds/blueprint_processor.py @@ -0,0 +1,53 @@ +"""CDS Blueprintprocessor 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 onapsdk.utils.jinja import jinja_env + +from .cds_element import CdsElement + + +class Blueprintprocessor(CdsElement): + """Blueprintprocessor class.""" + + @classmethod + def bootstrap(cls, + load_model_type: bool = True, + load_resource_dictionary: bool = True, + load_cba: bool = True) -> None: + """Bootstrap CDS blueprintprocessor. + + That action in needed to work with CDS. Can be done only once. + + Args: + load_model_type (bool, optional): Datermines if model types should be loaded + on bootstrap. Defaults to True. + load_resource_dictionary (bool, optional): Determines if resource dictionaries + should be loaded on bootstrap. Defaults to True. + load_cba (bool, optional): Determines if cba files should be loaded on + bootstrap. Defaults to True. + + """ + cls.send_message( + "POST", + "Bootstrap CDS blueprintprocessor", + f"{cls._url}/api/v1/blueprint-model/bootstrap", + data=jinja_env().get_template("cds_blueprintprocessor_bootstrap.json.j2").render( + load_model_type=load_model_type, + load_resource_dictionary=load_resource_dictionary, + load_cba=load_cba + ), + auth=cls.auth, + headers=cls.headers + ) diff --git a/src/onapsdk/cds/cds_element.py b/src/onapsdk/cds/cds_element.py new file mode 100644 index 0000000..7a4b9c0 --- /dev/null +++ b/src/onapsdk/cds/cds_element.py @@ -0,0 +1,47 @@ +"""Base CDS 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 abc import ABC + +from onapsdk.configuration import settings +from onapsdk.onap_service import OnapService +from onapsdk.utils.gui import GuiItem, GuiList + +class CdsElement(OnapService, ABC): + """Base CDS class. + + Stores url to CDS API (edit if you want to use other) and authentication tuple + (username, password). + """ + + # These should be stored in configuration. There is even a task in Orange repo. + _url: str = settings.CDS_URL + auth: tuple = settings.CDS_AUTH + + @classmethod + def get_guis(cls) -> GuiItem: + """Retrieve the status of the CDS GUIs. + + Only one GUI is referenced for CDS: CDS UI + + Return the list of GUIs + """ + gui_url = settings.CDS_GUI_SERVICE + cds_gui_response = cls.send_message( + "GET", "Get CDS GUI Status", gui_url) + guilist = GuiList([]) + guilist.add(GuiItem( + gui_url, + cds_gui_response.status_code)) + return guilist diff --git a/src/onapsdk/cds/data_dictionary.py b/src/onapsdk/cds/data_dictionary.py new file mode 100644 index 0000000..b4d8d0e --- /dev/null +++ b/src/onapsdk/cds/data_dictionary.py @@ -0,0 +1,266 @@ +"""CDS data dictionary 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. + +import json +from logging import getLogger, Logger + +from onapsdk.exceptions import FileError, ValidationError + +from .cds_element import CdsElement + + +class DataDictionary(CdsElement): + """Data dictionary class.""" + + logger: Logger = getLogger(__name__) + + def __init__(self, data_dictionary_json: dict, fix_schema: bool = True) -> None: + """Initialize data dictionary. + + Args: + data_dictionary_json (dict): data dictionary json + fix_schema (bool, optional): determines if data dictionary should be fixed if + the invalid schema is detected. Fixing can raise ValidationError if + dictionary is invalid. Defaults to True. + + """ + super().__init__() + self.data_dictionary_json: dict = data_dictionary_json + if not self.has_valid_schema() and fix_schema: + self.fix_schema() + + def __hash__(self) -> int: # noqa: D401 + """Data dictionary object hash. + + Based on data dictionary name + + Returns: + int: Data dictionary hash + + """ + return hash(self.name) + + def __eq__(self, dd: "DataDictionary") -> bool: + """Compare two data dictionaries. + + Data dictionaries are equal if have the same name. + + Args: + dd (DataDictionary): Object to compare with. + + Returns: + bool: True if objects have the same name, False otherwise. + + """ + return self.name == dd.name + + def __repr__(self) -> str: + """Representation of object. + + Returns: + str: Object's string representation + + """ + return f'DataDictionary[name: "{self.name}"]' + + @property + def name(self) -> str: # noqa: D401 + """Data dictionary name. + + Returns: + str: Data dictionary name + + """ + return self.data_dictionary_json["name"] + + @property + def url(self) -> str: + """URL to call. + + Returns: + str: CDS dictionary API url + + """ + return f"{self._url}/api/v1/dictionary" + + @classmethod + def get_by_name(cls, name: str) -> "DataDictionary": + """Get data dictionary by the provided name. + + Returns: + DataDictionary: Data dicionary object with the given name + + """ + cls.logger.debug("Get CDS data dictionary with %s name", name) + return DataDictionary( + data_dictionary_json=cls.send_message_json( + "GET", + f"Get {name} CDS data dictionary", + f"{cls._url}/api/v1/dictionary/{name}", + auth=cls.auth), + fix_schema=False + ) + + def upload(self) -> None: + """Upload data dictionary using CDS API.""" + self.logger.debug("Upload %s data dictionary", self.name) + self.send_message( + "POST", + "Publish CDS data dictionary", + f"{self.url}", + auth=self.auth, + data=json.dumps(self.data_dictionary_json) + ) + + def has_valid_schema(self) -> bool: + """Check data dictionary json schema. + + Check data dictionary JSON and return bool if schema is valid or not. + Valid schema means that data dictionary has given keys: + - "name" + - "tags" + - "data_type" + - "description" + - "entry_schema" + - "updatedBy" + - "definition" + "definition" key value should contains the "raw" data dictionary. + + Returns: + bool: True if data dictionary has valid schema, False otherwise + + """ + return all(key_to_check in self.data_dictionary_json for + key_to_check in ["name", "tags", "data_type", "description", "entry_schema", + "updatedBy", "definition"]) + + def fix_schema(self) -> None: + """Fix data dictionary schema. + + "Raw" data dictionary can be passed during initialization, but + this kind of data dictionary can't be uploaded to blueprintprocessor. + That method tries to fix it. It can be done only if "raw" data dictionary + has a given schema: + { + "name": "string", + "tags": "string", + "updated-by": "string", + "property": { + "description": "string", + "type": "string" + } + } + + Raises: + ValidationError: Data dictionary doesn't have all required keys + + """ + try: + self.data_dictionary_json = { + "name": self.data_dictionary_json["name"], + "tags": self.data_dictionary_json["tags"], + "data_type": self.data_dictionary_json["property"]["type"], + "description": self.data_dictionary_json["property"]["description"], + "entry_schema": self.data_dictionary_json["property"]["type"], + "updatedBy": self.data_dictionary_json["updated-by"], + "definition": self.data_dictionary_json + } + except KeyError: + raise ValidationError("Raw data dictionary JSON has invalid schema") + + +class DataDictionarySet: + """Data dictionary set. + + Stores data dictionary and upload to server. + """ + + logger: Logger = getLogger(__name__) + + def __init__(self) -> None: + """Initialize data dictionary set.""" + self.dd_set = set() + + @property + def length(self) -> int: + """Get the length of data dicitonary set. + + Returns: + int: Number of data dictionaries in set + + """ + return len(self.dd_set) + + def add(self, data_dictionary: DataDictionary) -> None: + """Add data dictionary object to set. + + Based on name it won't add duplications. + + Args: + data_dictionary (DataDictionary): object to add to set. + + """ + self.dd_set.add(data_dictionary) + + def upload(self) -> None: + """Upload all data dictionaries using CDS API. + + Raises: + RuntimeError: Raises if any data dictionary won't be uploaded to server. + Data dictionaries uploaded before the one which raises excepion won't be + deleted from server. + + """ + self.logger.debug("Upload data dictionary") + for data_dictionary in self.dd_set: # type DataDictionary + data_dictionary.upload() # raise a relevant exception + + def save_to_file(self, dd_file_path: str) -> None: + """Save data dictionaries to file. + + Args: + dd_file_path (str): Data dictinary file path. + """ + with open(dd_file_path, "w") as dd_file: + dd_file.write(json.dumps([dd.data_dictionary_json for dd in self.dd_set], indent=4)) + + @classmethod + def load_from_file(cls, dd_file_path: str, fix_schema: bool = True) -> "DataDictionarySet": + """Create data dictionary set from file. + + File has to have valid JSON with data dictionaries list. + + Args: + dd_file_path (str): Data dictionaries file path. + fix_schema (bool): Determines if schema should be fixed or not. + + Raises: + FileError: File to load data dictionaries from doesn't exist. + + Returns: + DataDictionarySet: Data dictionary set with data dictionaries from given file. + + """ + dd_set: DataDictionarySet = DataDictionarySet() + + try: + with open(dd_file_path, "r") as dd_file: # type file + dd_json: dict = json.loads(dd_file.read()) + for data_dictionary in dd_json: # type DataDictionary + dd_set.add(DataDictionary(data_dictionary, fix_schema=fix_schema)) + return dd_set + except FileNotFoundError as exc: + msg = "File with a set of data dictionaries does not exist." + raise FileError(msg) from exc diff --git a/src/onapsdk/cds/templates/cds_blueprintprocessor_bootstrap.json.j2 b/src/onapsdk/cds/templates/cds_blueprintprocessor_bootstrap.json.j2 new file mode 100644 index 0000000..41f43cd --- /dev/null +++ b/src/onapsdk/cds/templates/cds_blueprintprocessor_bootstrap.json.j2 @@ -0,0 +1,5 @@ +{ + "loadModelType" : {{ load_model_type | tojson }}, + "loadResourceDictionary" : {{ load_resource_dictionary | tojson }}, + "loadCBA" : {{ load_cba | tojson }} +}
\ No newline at end of file diff --git a/src/onapsdk/cds/templates/data_dictionary_base.json.j2 b/src/onapsdk/cds/templates/data_dictionary_base.json.j2 new file mode 100644 index 0000000..0ea6752 --- /dev/null +++ b/src/onapsdk/cds/templates/data_dictionary_base.json.j2 @@ -0,0 +1,52 @@ +{ + "name": "{{ mapping.dictionary_name }}", + "tags": "{{ mapping.dictionary_name }}", + "data_type": "{{ mapping.mapping_type }}", + "description": "{{ mapping.dictionary_name }}", + "entry_schema": "{{ mapping.mapping_type }}", + "updatedBy": "Python ONAP SDK", + "definition": { + "tags": "{{ mapping.dictionary_name }}", + "name": "{{ mapping.dictionary_name }}", + "property": { + "description": "{{ mapping.dictionary_name }}", + "type": "{{ mapping.mapping_type }}" + }, + "updated-by": "Python ONAP SDK", + "sources": { + {% for source in mapping.dictionary_sources %} + {% if source == "input" %} + "input": { + "type": "source-input" + }, + {% elif source == "sdnc" %} + "sdnc": {% include "data_dictionary_source_rest.json.j2" %}, + {% elif source == "processor-db" %} + "processor-db": { + "type": "source-db", + "properties": { + "type": "<< FILL >>", + "query": "<< FILL >>", + "input-key-mapping": {}, + "output-key-mapping": {}, + "key-dependencies": [] + } + }, + {% elif source == "aai-data" %} + "aai-data": {% include "data_dictionary_source_rest.json.j2" %}, + {% elif source == "default" %} + {# Do not do anything, default will be always added #} + {% else %} + "{{ source }}": { + "type": "unknown", + "properties": {} + }, + {% endif %} + {% endfor %} + "default": { + "type": "source-default", + "properties": {} + } + } + } +}
\ No newline at end of file diff --git a/src/onapsdk/cds/templates/data_dictionary_source_rest.json.j2 b/src/onapsdk/cds/templates/data_dictionary_source_rest.json.j2 new file mode 100644 index 0000000..088a044 --- /dev/null +++ b/src/onapsdk/cds/templates/data_dictionary_source_rest.json.j2 @@ -0,0 +1,13 @@ +{ + "type": "source-rest", + "properties": { + "verb": "<< FILL >>", + "type": "<< FILL >>", + "url-path": "<< FILL >>", + "path": "<< FILL >>", + "payload": "<< FILL >>", + "input-key-mapping": {}, + "output-key-mapping": {}, + "key-dependencies": [] + } +}
\ No newline at end of file diff --git a/src/onapsdk/clamp/__init__.py b/src/onapsdk/clamp/__init__.py new file mode 100644 index 0000000..621adc9 --- /dev/null +++ b/src/onapsdk/clamp/__init__.py @@ -0,0 +1,14 @@ +"""ONAP SDK CLAMP package.""" +# 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. diff --git a/src/onapsdk/clamp/clamp_element.py b/src/onapsdk/clamp/clamp_element.py new file mode 100644 index 0000000..843db42 --- /dev/null +++ b/src/onapsdk/clamp/clamp_element.py @@ -0,0 +1,79 @@ +"""Clamp 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService as Onap +from onapsdk.sdc.service import Service +from onapsdk.exceptions import ResourceNotFound +from onapsdk.utils.headers_creator import headers_clamp_creator + + +class Clamp(Onap): + """Mother Class of all CLAMP elements.""" + + #class variable + _base_url = settings.CLAMP_URL + name: str = "CLAMP" + headers = headers_clamp_creator(Onap.headers) + + @classmethod + def base_url(cls) -> str: + """Give back the base url of Clamp.""" + return f"{cls._base_url}/restservices/clds/v2" + + @classmethod + def check_loop_template(cls, service: Service) -> str: + """ + Return loop template name if exists. + + Args: + service (Service): the distributed sdc service with tca blueprint artifact + + Raises: + ResourceNotFound: Template not found. + + Returns: + if required template exists in CLAMP or not + + """ + url = f"{cls.base_url()}/templates/" + for template in cls.send_message_json('GET', + 'Get Loop Templates', + url): + if template["modelService"]["serviceDetails"]["name"] == service.name: + return template["name"] + raise ResourceNotFound("Template not found.") + + @classmethod + def check_policies(cls, policy_name: str, req_policies: int = 30) -> bool: + """ + Ensure that a policy is stored in CLAMP. + + Args: + policy_name (str): policy acronym + req_policies (int): number of required policies in CLAMP + + Returns: + if required policy exists in CLAMP or not + + """ + url = f"{cls.base_url()}/policyToscaModels/" + policies = cls.send_message_json('GET', + 'Get stocked policies', + url) + exist_policy = False + for policy in policies: + if policy["policyAcronym"] == policy_name: + exist_policy = True + return (len(policies) >= req_policies) and exist_policy diff --git a/src/onapsdk/clamp/loop_instance.py b/src/onapsdk/clamp/loop_instance.py new file mode 100644 index 0000000..a72f9d1 --- /dev/null +++ b/src/onapsdk/clamp/loop_instance.py @@ -0,0 +1,349 @@ +"""Control Loop 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. +import json +from pathlib import Path +from jsonschema import validate, ValidationError + +from onapsdk.clamp.clamp_element import Clamp +from onapsdk.utils.jinja import jinja_env +from onapsdk.exceptions import ParameterError + +CLAMP_UPDDATE_REFRESH_TIMER = 60 + +class LoopInstance(Clamp): + """Control Loop instantiation class.""" + + # class variable + _loop_schema = None + operational_policies = "" + + def __init__(self, template: str, name: str, details: dict) -> None: + """ + Initialize loop instance object. + + Args: + template (str): template from which we build the loop + name (str) : loop creation name + details (dict) : dictionnary containing all loop details + + """ + super().__init__() + self.template = template + self.name = "LOOP_" + name + self._details = details + + @property + def details(self) -> dict: + """Return and lazy load the details.""" + if not self._details: + self._update_loop_details() + return self._details + + @details.setter + def details(self, details: dict) -> None: + """Set value for details.""" + self._details = details + + def _update_loop_details(self) -> dict: + """ + Update loop details. + + Returns: + the dictionnary of loop details + + """ + url = f"{self.base_url()}/loop/{self.name}" + loop_details = self.send_message_json('GET', + 'Get loop details', + url) + return loop_details + + def refresh_status(self) -> None: + """Reshresh loop status.""" + url = f"{self.base_url()}/loop/getstatus/{self.name}" + loop_details = self.send_message_json('GET', + 'Get loop status', + url) + + self.details = loop_details + + @property + def loop_schema(self) -> dict: + """ + Return and lazy load the details schema. + + Returns: + schema to be respected to accede to loop details + + """ + if not self._loop_schema: + schema_file = Path.cwd() / 'src' / 'onapsdk' / 'clamp' / 'schema_details.json' + with open(schema_file, "rb") as plan: + self._loop_schema = json.load(plan) + return self._loop_schema + + def validate_details(self) -> bool: + """ + Validate Loop Instance details. + + Returns: + schema validation status (True, False) + + """ + try: + validate(self.details, self.loop_schema) + except ValidationError as error: + self._logger.error(error) + self._logger.error("---------") + self._logger.error(error.absolute_path) + self._logger.error("---------") + self._logger.error(error.absolute_schema_path) + return False + return True + + def create(self) -> None: + """Create instance and load loop details.""" + url = f"{self.base_url()}/loop/create/{self.name}?templateName={self.template}" + instance_details = self.send_message_json('POST', + 'Create Loop Instance', + url) + self.details = instance_details + + def add_operational_policy(self, policy_type: str, policy_version: str) -> None: + """ + Add operational policy to the loop instance. + + Args: + policy_type (str): the full policy model type + policy_version (str): policy version + + Raises: + ParameterError : Corrupt response or a key in a dictionary not found. + It will also be raised when the response contains more operational + policies than there are currently. + + """ + url = (f"{self.base_url()}/loop/addOperationaPolicy/{self.name}/" + f"policyModel/{policy_type}/{policy_version}") + add_response = self.send_message_json('PUT', + 'Create Operational Policy', + url) + + key = "operationalPolicies" + + try: + if self.details[key] is None: + self.details[key] = [] + + response_policies = add_response[key] + current_policies = self.details[key] + except KeyError as exc: + msg = 'Corrupt response, current loop details. Key not found.' + raise ParameterError(msg) from exc + + if len(response_policies) > len(current_policies): + self.details = add_response + else: + raise ParameterError("Couldn't add the operational policy.") + + def remove_operational_policy(self, policy_type: str, policy_version: str) -> None: + """ + Remove operational policy from the loop instance. + + Args: + policy_type (str): the full policy model type + policy_version (str): policy version + + """ + url = (f"{self.base_url()}/loop/removeOperationaPolicy/" + f"{self.name}/policyModel/{policy_type}/{policy_version}") + self.details = self.send_message_json('PUT', + 'Remove Operational Policy', + url) + + def update_microservice_policy(self) -> None: + """ + Update microservice policy configuration. + + Update microservice policy configuration using payload data. + + """ + url = f"{self.base_url()}/loop/updateMicroservicePolicy/{self.name}" + template = jinja_env().get_template("clamp_add_tca_config.json.j2") + microservice_name = self.details["globalPropertiesJson"]["dcaeDeployParameters"]\ + ["uniqueBlueprintParameters"]["policy_id"] + data = template.render(name=microservice_name, + LOOP_name=self.name) + + self.send_message('POST', + 'ADD TCA config', + url, + data=data) + + def extract_operational_policy_name(self, policy_type: str) -> str: + """ + Return operational policy name for a closed loop and a given policy. + + Args: + policy_type (str): the policy acronym. + + Raises: + ParameterError : Couldn't load the operational policy name. + + Returns: + Operational policy name in the loop details after adding a policy. + + """ + for policy in filter(lambda x: x["policyModel"]["policyAcronym"] == policy_type, + self.details["operationalPolicies"]): + return policy["name"] + + raise ParameterError("Couldn't load the operational policy name.") + + def add_drools_conf(self) -> dict: + """Add drools configuration.""" + self.validate_details() + vfmodule_dicts = self.details["modelService"]["resourceDetails"]["VFModule"] + entity_ids = {} + #Get the vf module details + for vfmodule in vfmodule_dicts.values(): + entity_ids["resourceID"] = vfmodule["vfModuleModelName"] + entity_ids["modelInvariantId"] = vfmodule["vfModuleModelInvariantUUID"] + entity_ids["modelVersionId"] = vfmodule["vfModuleModelUUID"] + entity_ids["modelName"] = vfmodule["vfModuleModelName"] + entity_ids["modelVersion"] = vfmodule["vfModuleModelVersion"] + entity_ids["modelCustomizationId"] = vfmodule["vfModuleModelCustomizationUUID"] + template = jinja_env().get_template("clamp_add_drools_policy.json.j2") + data = template.render(name=self.extract_operational_policy_name("Drools"), + resourceID=entity_ids["resourceID"], + modelInvariantId=entity_ids["modelInvariantId"], + modelVersionId=entity_ids["modelVersionId"], + modelName=entity_ids["modelName"], + modelVersion=entity_ids["modelVersion"], + modelCustomizationId=entity_ids["modelCustomizationId"], + LOOP_name=self.name) + return data + + def add_minmax_config(self) -> None: + """Add MinMax operational policy config.""" + #must configure start/end time and min/max instances in json file + template = jinja_env().get_template("clamp_MinMax_config.json.j2") + return template.render(name=self.extract_operational_policy_name("MinMax")) + + def add_frequency_limiter(self, limit: int = 1) -> None: + """Add frequency limiter config.""" + template = jinja_env().get_template("clamp_add_frequency.json.j2") + return template.render(name=self.extract_operational_policy_name("FrequencyLimiter"), + LOOP_name=self.name, + limit=limit) + + def add_op_policy_config(self, func, **kwargs) -> None: + """ + Add operational policy configuration. + + Add operation policy configuration using payload data. + + Args: + func (function): policy configuration function in (add_drools_conf, + add_minmax_config, + add_frequency_limiter) + + """ + data = func(**kwargs) + if not data: + raise ParameterError("Payload data from configuration is None.") + if self.operational_policies: + self.operational_policies = self.operational_policies[:-1] + "," + data = data[1:] + self.operational_policies += data + url = f"{self.base_url()}/loop/updateOperationalPolicies/{self.name}" + self.send_message('POST', + 'ADD operational policy config', + url, + data=self.operational_policies) + + self._logger.info(("Files for op policy config %s have been uploaded to loop's" + "Op policy"), self.name) + + def submit(self): + """Submit policies to policy engine.""" + state = self.details["components"]["POLICY"]["componentState"]["stateName"] + return state == "SENT_AND_DEPLOYED" + + def stop(self): + """Undeploy Policies from policy engine.""" + state = self.details["components"]["POLICY"]["componentState"]["stateName"] + return state == "SENT" + + def restart(self): + """Redeploy policies to policy engine.""" + state = self.details["components"]["POLICY"]["componentState"]["stateName"] + return state == "SENT_AND_DEPLOYED" + + def act_on_loop_policy(self, func) -> bool: + """ + Act on loop's policy. + + Args: + func (function): function of action to be done (submit, stop, restart) + + Returns: + action state : failed or done + + """ + url = f"{self.base_url()}/loop/{func.__name__}/{self.name}" + self.send_message('PUT', + f'{func.__name__} policy', + url) + self.refresh_status() + self.validate_details() + return func() + + def deploy_microservice_to_dcae(self) -> bool: + """ + Execute the deploy operation on the loop instance. + + Returns: + loop deploy on DCAE status (True, False) + + """ + url = f"{self.base_url()}/loop/deploy/{self.name}" + self.send_message('PUT', + 'Deploy microservice to DCAE', + url) + self.validate_details() + state = self.details["components"]["DCAE"]["componentState"]["stateName"] + failure = "MICROSERVICE_INSTALLATION_FAILED" + success = "MICROSERVICE_INSTALLED_SUCCESSFULLY" + while state not in (success, failure): + self.refresh_status() + self.validate_details() + state = self.details["components"]["DCAE"]["componentState"]["stateName"] + return state == success + + def undeploy_microservice_from_dcae(self) -> None: + """Stop the deploy operation.""" + url = f"{self.base_url()}/loop/undeploy/{self.name}" + self.send_message('PUT', + 'Undeploy microservice from DCAE', + url) + + def delete(self) -> None: + """Delete the loop instance.""" + self._logger.debug("Delete %s loop instance", self.name) + url = "{}/loop/delete/{}".format(self.base_url(), self.name) + self.send_message('PUT', + 'Delete loop instance', + url) diff --git a/src/onapsdk/clamp/schema_details.json b/src/onapsdk/clamp/schema_details.json new file mode 100644 index 0000000..3caea6c --- /dev/null +++ b/src/onapsdk/clamp/schema_details.json @@ -0,0 +1,138 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "components": { + "type": "object", + "properties": { + "POLICY": { + "type": "object", + "properties": { + "componentState": { + "type": "object", + "properties": { + "stateName": { + "type": "string" + } + }, + "required": [ + "stateName" + ] + } + }, + "required": [ + "componentState" + ] + }, + "DCAE": { + "type": "object", + "properties": { + "componentState": { + "type": "object", + "properties": { + "stateName": { + "type": "string" + } + }, + "required": [ + "stateName" + ] + } + }, + "required": [ + "componentState" + ] + } + }, + "required": [ + "POLICY", + "DCAE" + ] + }, + "modelService": { + "type": "object", + "properties": { + "resourceDetails": { + "type": "object", + "properties": { + "VFModule": { + "type": "object", + "properties": { + "resourceID": { + "type": "object", + "properties": { + "vfModuleModelName": { + "type": "string" + }, + "vfModuleModelInvariantUUID": { + "type": "string" + }, + "vfModuleModelUUID": { + "type": "string" + }, + "vfModuleModelVersion": { + "type": "string" + }, + "vfModuleModelCustomizationUUID": { + "type": "string" + } + }, + "required": [ + "vfModuleModelName", + "vfModuleModelInvariantUUID", + "vfModuleModelUUID", + "vfModuleModelVersion", + "vfModuleModelCustomizationUUID" + ] + } + } + } + }, + "required": [ + "VFModule" + ] + } + }, + "required": [ + "resourceDetails" + ] + }, + "operationalPolicies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + }, + "microServicePolicies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + }, + "required": [ + "name", + "components", + "modelService", + "operationalPolicies", + "microServicePolicies" + ] + }
\ No newline at end of file diff --git a/src/onapsdk/clamp/templates/clamp_MinMax_config.json.j2 b/src/onapsdk/clamp/templates/clamp_MinMax_config.json.j2 new file mode 100644 index 0000000..2402ace --- /dev/null +++ b/src/onapsdk/clamp/templates/clamp_MinMax_config.json.j2 @@ -0,0 +1,94 @@ +[ + { + "name": "{{ name }}", + "jsonRepresentation": { + "title": "onap.policies.controlloop.guard.common.MinMax", + "type": "object", + "description": "Supports Min/Max number of entity for scaling operations. Although min and max fields are marked as not\nrequired, you need to have at least one or the other.\n", + "required": [ + "actor", + "operation", + "target" + ], + "properties": { + "id": { + "type": "string", + "description": "The Control Loop id this applies to." + }, + "actor": { + "type": "string", + "description": "Specifies the Actor the guard applies to." + }, + "operation": { + "type": "string", + "description": "Specified the operation that the actor is performing the guard applies to." + }, + "timeRange": { + "title": "tosca.datatypes.TimeInterval", + "type": "object", + "required": [ + "start_time", + "end_time" + ], + "properties": { + "start_time": { + "type": "string", + "format": "date-time" + }, + "end_time": { + "type": "string", + "format": "date-time" + } + } + }, + "min": { + "type": "integer", + "description": "The minimum instances of this entity" + }, + "max": { + "type": "integer", + "description": "The maximum instances of this entity" + }, + "target": { + "type": "string", + "description": "The target entity that has scaling restricted" + } + } + }, + "configurationsJson": { + "actor": "test", + "operation": "test", + "target": "test", + "timeRange": { + "start_time": "00:00:00", + "end_time": "01:00:00" + }, + "min": 1, + "max": 10 + }, + "policyModel": { + "policyModelType": "onap.policies.controlloop.guard.common.MinMax", + "version": "1.0.0", + "policyAcronym": "MinMax", + "policyPdpGroup": { + "supportedPdpGroups": [ + { + "defaultGroup": [ + "xacml" + ] + } + ] + }, + "createdDate": "2020-07-22T01:37:35.861060Z", + "updatedDate": "2020-07-22T01:37:51.719018Z", + "updatedBy": "Not found", + "createdBy": "Not found" + }, + "createdDate": "2020-07-22T09:01:14.168344Z", + "updatedDate": "2020-07-22T09:01:14.168344Z", + "updatedBy": "clamp@clamp.onap.org", + "createdBy": "clamp@clamp.onap.org", + "pdpGroup": "defaultGroup", + "pdpSubgroup": "xacml" + } +]
\ No newline at end of file diff --git a/src/onapsdk/clamp/templates/clamp_add_drools_policy.json.j2 b/src/onapsdk/clamp/templates/clamp_add_drools_policy.json.j2 new file mode 100644 index 0000000..40ca7cd --- /dev/null +++ b/src/onapsdk/clamp/templates/clamp_add_drools_policy.json.j2 @@ -0,0 +1,325 @@ +[ + { + "name": "{{ name }}", + "jsonRepresentation": { + "title": "onap.policies.controlloop.operational.common.Drools", + "type": "object", + "description": "Operational policies for Drools PDP", + "required": [ + "abatement", + "operations", + "trigger", + "timeout", + "id" + ], + "properties": { + "abatement": { + "type": "boolean", + "description": "Whether an abatement event message will be expected for the control loop from DCAE.", + "default": "false" + }, + "operations": { + "type": "array", + "description": "List of operations to be performed when Control Loop is triggered.", + "items": { + "title": "onap.datatype.controlloop.Operation", + "type": "object", + "description": "An operation supported by an actor", + "required": [ + "id", + "operation", + "retries", + "timeout" + ], + "properties": { + "failure_retries": { + "type": "string", + "description": "Points to the operation to invoke when the current operation has exceeded its max retries.", + "default": "final_failure_retries" + }, + "id": { + "type": "string", + "description": "Unique identifier for the operation" + }, + "failure_timeout": { + "type": "string", + "description": "Points to the operation to invoke when the time out for the operation occurs.", + "default": "final_failure_timeout" + }, + "failure": { + "type": "string", + "description": "Points to the operation to invoke on Actor operation failure.", + "default": "final_failure" + }, + "operation": { + "title": "onap.datatype.controlloop.Actor", + "type": "object", + "description": "An actor/operation/target definition", + "required": [ + "target", + "actor", + "operation" + ], + "properties": { + "payload": { + "type": "object", + "description": "Name/value pairs of payload information passed by Policy to the actor", + "anyOf": [ + { + "title": "User defined", + "properties": { + + } + } + ] + }, + "target": { + "title": "onap.datatype.controlloop.Target", + "type": "object", + "description": "Definition for a entity in A&AI to perform a control loop operation on", + "required": [ + "targetType" + ], + "properties": { + "entityIds": { + "type": "object", + "description": "Map of values that identify the resource. If none are provided, it is assumed that the\nentity that generated the ONSET event will be the target.\n", + "anyOf": [ + { + "title": "User defined", + "properties": { + + } + }, + { + "title": "VNF-ubuntu18agent_VF 0", + "properties": { + "resourceID": { + "title": "Resource ID", + "type": "string", + "default": "6daf6e05-fc26-4aa3-9f0b-d47cf3f37ece", + "readOnly": "True" + } + } + }, + { + "title": "VFMODULE-Ubuntu18agentVf..base_ubuntu18..module-0", + "properties": { + "resourceID": { + "title": "Resource ID", + "type": "string", + "default": "Ubuntu18agentVf..base_ubuntu18..module-0", + "readOnly": "True" + }, + "modelInvariantId": { + "title": "Model Invariant Id (ModelInvariantUUID)", + "type": "string", + "default": "2556faee-75dd-448f-8d2f-d4201a957e7c", + "readOnly": "True" + }, + "modelVersionId": { + "title": "Model Version Id (ModelUUID)", + "type": "string", + "default": "98df9741-530a-486c-b156-b2cb62e6fc6c", + "readOnly": "True" + }, + "modelName": { + "title": "Model Name", + "type": "string", + "default": "Ubuntu18agentVf..base_ubuntu18..module-0", + "readOnly": "True" + }, + "modelVersion": { + "title": "Model Version", + "type": "string", + "default": "1", + "readOnly": "True" + }, + "modelCustomizationId": { + "title": "Customization ID", + "type": "string", + "default": "ba567b66-e46b-4521-8fdd-54185cb21a7f", + "readOnly": "True" + } + } + } + ] + }, + "targetType": { + "type": "string", + "description": "Category for the target type", + "enum": [ + "VNF", + "VM", + "VFMODULE", + "PNF" + ] + } + } + }, + "actor": { + "type": "string", + "description": "The actor performing the operation.", + "enum": [ + "SDNR", + "SDNC", + "VFC", + "SO", + "APPC", + "CDS" + ], + "options": { + "enum_titles": [ + "SDNR", + "SDNC", + "VFC", + "SO", + "APPC" + ] + } + }, + "operation": { + "type": "string", + "description": "The operation the actor is performing.", + "enum": [ + "BandwidthOnDemand", + "VF Module Delete", + "Reroute", + "VF Module Create", + "ModifyConfig", + "Rebuild", + "Restart", + "Migrate", + "Health-Check" + ], + "options": { + "enum_titles": [ + "BandwidthOnDemand (SDNC operation)", + "VF Module Delete (SO operation)", + "Reroute (SDNC operation)", + "VF Module Create (SO operation)", + "ModifyConfig (APPC/VFC operation)", + "Rebuild (APPC operation)", + "Restart (APPC operation)", + "Migrate (APPC operation)", + "Health-Check (APPC operation)" + ] + } + } + } + }, + "failure_guard": { + "type": "string", + "description": "Points to the operation to invoke when the current operation is blocked due to guard policy enforcement.", + "default": "final_failure_guard" + }, + "retries": { + "type": "integer", + "description": "The number of retries the actor should attempt to perform the operation.", + "default": "0" + }, + "timeout": { + "type": "integer", + "description": "The amount of time for the actor to perform the operation." + }, + "failure_exception": { + "type": "string", + "description": "Points to the operation to invoke when the current operation causes an exception.", + "default": "final_failure_exception" + }, + "description": { + "type": "string", + "description": "A user-friendly description of the intent for the operation" + }, + "success": { + "type": "string", + "description": "Points to the operation to invoke on success. A value of \"final_success\" indicates and end to the operation.", + "default": "final_success" + } + } + }, + "format": "tabs-top" + }, + "trigger": { + "type": "string", + "description": "Initial operation to execute upon receiving an Onset event message for the Control Loop." + }, + "timeout": { + "type": "integer", + "description": "Overall timeout for executing all the operations. This timeout should equal or exceed the total\ntimeout for each operation listed.\n" + }, + "id": { + "type": "string", + "description": "The unique control loop id." + }, + "controllerName": { + "type": "string", + "description": "Drools controller properties" + } + } + }, + "configurationsJson": { + "abatement": false, + "operations": [ + { + "failure_retries": "final_failure_retries", + "id": "policy-1-vfmodule-create", + "failure_timeout": "final_failure_timeout", + "failure": "final_failure", + "operation": { + "payload": { + "requestParameters": "{\"usePreload\":false,\"userParams\":[]}", + "configurationParameters": "[{\"ip-addr\":\"$.vf-module-topology.vf-module-parameters.param[16].value\",\"oam-ip-addr\":\"$.vf-module-topology.vf-module-parameters.param[30].value\"}]" + }, + "target": { + "entityIds": { + "resourceID": "{{ resourceID }}", + "modelInvariantId": "{{ modelInvariantId }}", + "modelVersionId": "{{ modelVersionId }}", + "modelName": "{{ modelName }}", + "modelVersion": "{{ modelVersion }}", + "modelCustomizationId": "{{ modelCustomizationId }}" + }, + "targetType": "VFMODULE" + }, + "actor": "SO", + "operation": "VF Module Create" + }, + "failure_guard": "final_failure_guard", + "retries": 1, + "timeout": 300, + "failure_exception": "final_failure_exception", + "description": "test", + "success": "final_success" + } + ], + "trigger": "policy-1-vfmodule-create", + "timeout": 650, + "id": "{{ LOOP_name }}" + }, + "policyModel": { + "policyModelType": "onap.policies.controlloop.operational.common.Drools", + "version": "1.0.0", + "policyAcronym": "Drools", + "policyPdpGroup": { + "supportedPdpGroups": [ + { + "defaultGroup": [ + "drools" + ] + } + ] + }, + "createdDate": "2020-07-22T01:37:38.528901Z", + "updatedDate": "2020-07-22T01:37:51.752302Z", + "updatedBy": "Not found", + "createdBy": "Not found" + }, + "createdDate": "2020-07-22T07:50:00.076714Z", + "updatedDate": "2020-07-22T07:50:00.076714Z", + "updatedBy": "clamp@clamp.onap.org", + "createdBy": "clamp@clamp.onap.org", + "pdpGroup": "defaultGroup", + "pdpSubgroup": "drools" + } +]
\ No newline at end of file diff --git a/src/onapsdk/clamp/templates/clamp_add_frequency.json.j2 b/src/onapsdk/clamp/templates/clamp_add_frequency.json.j2 new file mode 100644 index 0000000..fabf9e6 --- /dev/null +++ b/src/onapsdk/clamp/templates/clamp_add_frequency.json.j2 @@ -0,0 +1,102 @@ +[ + { + "name": "{{ name }}", + "jsonRepresentation": { + "title": "onap.policies.controlloop.guard.common.FrequencyLimiter", + "type": "object", + "description": "Supports limiting the frequency of actions being taken by a Actor.", + "required": [ + "actor", + "operation", + "limit", + "timeWindow", + "timeUnits" + ], + "properties": { + "id": { + "type": "string", + "description": "The Control Loop id this applies to." + }, + "actor": { + "type": "string", + "description": "Specifies the Actor the guard applies to." + }, + "operation": { + "type": "string", + "description": "Specified the operation that the actor is performing the guard applies to." + }, + "timeRange": { + "title": "tosca.datatypes.TimeInterval", + "type": "object", + "required": [ + "start_time", + "end_time" + ], + "properties": { + "start_time": { + "type": "string", + "format": "date-time" + }, + "end_time": { + "type": "string", + "format": "date-time" + } + } + }, + "limit": { + "type": "integer", + "description": "The limit", + "exclusiveMinimum": "0" + }, + "timeWindow": { + "type": "integer", + "description": "The time window to count the actions against." + }, + "timeUnits": { + "type": "string", + "description": "The units of time the window is counting.", + "enum": [ + "second", + "minute", + "hour", + "day", + "week", + "month", + "year" + ] + } + } + }, + "configurationsJson": { + "actor": "SO", + "operation": "VF Module Create", + "limit": {{ limit }}, + "timeWindow": 10, + "timeUnits": "minute" + }, + "policyModel": { + "policyModelType": "onap.policies.controlloop.guard.common.FrequencyLimiter", + "version": "1.0.0", + "policyAcronym": "FrequencyLimiter", + "policyPdpGroup": { + "supportedPdpGroups": [ + { + "defaultGroup": [ + "xacml" + ] + } + ] + }, + "createdDate": "2020-07-22T01:37:35.106757Z", + "updatedDate": "2020-07-22T01:37:51.709386Z", + "updatedBy": "Not found", + "createdBy": "Not found" + }, + "createdDate": "2020-07-22T08:27:34.576868Z", + "updatedDate": "2020-07-22T08:27:34.576868Z", + "updatedBy": "clamp@clamp.onap.org", + "createdBy": "clamp@clamp.onap.org", + "pdpGroup": "defaultGroup", + "pdpSubgroup": "xacml" + } +]
\ No newline at end of file diff --git a/src/onapsdk/clamp/templates/clamp_add_tca_config.json.j2 b/src/onapsdk/clamp/templates/clamp_add_tca_config.json.j2 new file mode 100644 index 0000000..0919a6b --- /dev/null +++ b/src/onapsdk/clamp/templates/clamp_add_tca_config.json.j2 @@ -0,0 +1,30 @@ +{ + "name": "{{ name }}", + "configurationsJson": { + "tca.policy": { + "domain": "measurementsForVfScaling", + "metricsPerEventName": [ + { + "policyScope": "DCAE", + "thresholds": [ + { + "version": "1.0.2", + "severity": "MAJOR", + "thresholdValue": 200, + "closedLoopEventStatus": "ONSET", + "closedLoopControlName": "{{ LOOP_name }}", + "direction": "LESS_OR_EQUAL", + "fieldPath": "$.event.measurementsForVfScalingFields.vNicPerformanceArray[*].receivedTotalPacketsDelta" + } + ], + "eventName": "vLoadBalancer", + "policyVersion": "v0.0.1", + "controlLoopSchemaType": "VM", + "policyName": "DCAE.Config_tca-hi-lo" + } + ] + } + }, + "pdpGroup": "defaultGroup", + "pdpSubgroup": "xacml" +} diff --git a/src/onapsdk/configuration/__init__.py b/src/onapsdk/configuration/__init__.py new file mode 100644 index 0000000..027e7ef --- /dev/null +++ b/src/onapsdk/configuration/__init__.py @@ -0,0 +1,18 @@ +"""Configuration 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 .loader import SettingsLoader, SETTINGS_ENV + + +settings = SettingsLoader() # pylint: disable=invalid-name diff --git a/src/onapsdk/configuration/global_settings.py b/src/onapsdk/configuration/global_settings.py new file mode 100644 index 0000000..6f7e4d7 --- /dev/null +++ b/src/onapsdk/configuration/global_settings.py @@ -0,0 +1,71 @@ +"""Global settings module.""" # pylint: disable=bad-whitespace +# 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. + + +###################### +# # +# ONAP SERVICES URLS # +# # +###################### + +## API +AAI_URL = "https://aai.api.sparky.simpledemo.onap.org:30233" +AAI_API_VERSION = "v23" +AAI_AUTH = "Basic QUFJOkFBSQ==" +AAI_BULK_CHUNK = 30 +CDS_URL = "http://portal.api.simpledemo.onap.org:30449" +CDS_AUTH = ("ccsdkapps", "ccsdkapps") +CPS_URL = "http://portal.api.simpledemo.onap.org:8080" +CPS_AUTH = ("cpsuser", "cpsr0cks!") +MSB_URL = "https://msb.api.simpledemo.onap.org:30283" +SDC_BE_URL = "https://sdc.api.be.simpledemo.onap.org:30204" +SDC_FE_URL = "https://sdc.api.fe.simpledemo.onap.org:30207" +SDC_AUTH = "Basic YWFpOktwOGJKNFNYc3pNMFdYbGhhazNlSGxjc2UyZ0F3ODR2YW9HR21KdlV5MlU=" +SDNC_URL = "https://sdnc.api.simpledemo.onap.org:30267" +SDNC_AUTH = "Basic YWRtaW46S3A4Yko0U1hzek0wV1hsaGFrM2VIbGNzZTJnQXc4NHZhb0dHbUp2VXkyVQ==" +SO_URL = "http://so.api.simpledemo.onap.org:30277" +SO_API_VERSION = "v7" +SO_AUTH = "Basic SW5mcmFQb3J0YWxDbGllbnQ6cGFzc3dvcmQxJA==" +SO_CAT_DB_AUTH = "Basic YnBlbDpwYXNzd29yZDEk" +VID_URL = "https://vid.api.simpledemo.onap.org:30200" +VID_API_VERSION = "/vid" +CLAMP_URL = "https://clamp.api.simpledemo.onap.org:30258" +CLAMP_AUTH = "Basic ZGVtb0BwZW9wbGUub3NhYWYub3JnOmRlbW8xMjM0NTYh" +VES_URL = "http://ves.api.simpledemo.onap.org:30417" +DMAAP_URL = "http://dmaap.api.simpledemo.onap.org:3904" +NBI_URL = "https://nbi.api.simpledemo.onap.org:30274" +NBI_API_VERSION = "/nbi/api/v4" +DCAEMOD_URL = "" +HOLMES_URL = "https://aai.api.sparky.simpledemo.onap.org:30293" +POLICY_URL = "" + +## GUI +AAI_GUI_URL = "https://aai.api.sparky.simpledemo.onap.org:30220" +AAI_GUI_SERVICE = f"{AAI_GUI_URL}/services/aai/webapp/index.html#/browse" +CDS_GUI_SERVICE = f"{CDS_URL}/" +SO_MONITOR_GUI_SERVICE = f"{SO_URL}/" +SDC_GUI_SERVICE = f"{SDC_FE_URL}/sdc1/portal" +SDNC_DG_GUI_SERVICE = f"{SDNC_URL}/nifi/" +SDNC_ODL_GUI_SERVICE = f"{SDNC_URL}/odlux/index.html" + +DCAEMOD_GUI_SERVICE = f"{DCAEMOD_URL}/" +HOLMES_GUI_SERVICE = f"{HOLMES_URL}/iui/holmes/default.html" +POLICY_GUI_SERVICE = f"{POLICY_URL}/onap/login.html" +POLICY_CLAMP_GUI_SERVICE = f"{CLAMP_URL}/" + +# VID OBJECTS DEFAULT VALUES +PROJECT = "Onapsdk_project" +LOB = "Onapsdk_lob" +PLATFORM = "Onapsdk_platform" diff --git a/src/onapsdk/configuration/loader.py b/src/onapsdk/configuration/loader.py new file mode 100644 index 0000000..a9aae6f --- /dev/null +++ b/src/onapsdk/configuration/loader.py @@ -0,0 +1,115 @@ +"""Settings loader 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. +import importlib +import os +from typing import Any + +from onapsdk.exceptions import ModuleError, SettingsError + +from . import global_settings + + +SETTINGS_ENV = "ONAP_PYTHON_SDK_SETTINGS" + + +class SettingsLoader: + """Settings loader class. + + Load global settings and optionally load + custom settings by importing the module + stored in ONAP_PYTHON_SDK_SETTINGS environment + variable. + The module has to be uder PYTHONPATH. + """ + + def __init__(self) -> None: + """Load settings. + + Load global settings and optionally load custom one. + + Raises: + ModuleError: If ONAP_PYTHON_SDK_SETTINGS environment variable + is set and module can't be imported. + + """ + self._settings = {} + + # Load values from global_settings (only uppercase) + self.filter_and_set(global_settings) + + settings_env_value: str = os.environ.get(SETTINGS_ENV) + if settings_env_value: + # Load values from custom settings + try: + module = importlib.import_module(settings_env_value) + except ModuleNotFoundError: + msg = "Can't import custom settings. Is it under PYTHONPATH?" + raise ModuleError(msg) + self.filter_and_set(module) + + def __getattribute__(self, name: str) -> Any: + """Return stored attributes. + + If attribute name is uppercase return it from + _settings dictionary. + Look for attribute in __dict__ otherwise. + + Args: + name (str): Attribute's name + + Raises: + SettingsError: a setting is not found by the key. + + Returns: + Any: Attribute's value + + """ + if name.isupper(): + try: + return self._settings[name] + except KeyError as exc: + msg = f"Requested setting {exc.args[0]} does not exist." + raise SettingsError(msg) from exc + return super().__getattribute__(name) + + def __setattr__(self, name: str, value: Any) -> None: + """Save attribute. + + If attribute name is uppercase save the value + in _settings dictionary. + Use Object class __setattr__ implementation + otherwise. + + Args: + name (str): Attribute's name + value (Any): Attribute's value + + """ + if name.isupper(): + self._settings[name] = value + super().__setattr__(name, value) + + def filter_and_set(self, module: "module") -> None: + """Filter module attributes and save the uppercased. + + Iterate through module's attribures and save the value + of them which name is uppercase. + + Args: + module (module): Module to get attributes from + + """ + for key in filter(lambda x: x.isupper(), dir(module)): + self._settings[key] = getattr(module, key) diff --git a/src/onapsdk/constants.py b/src/onapsdk/constants.py new file mode 100644 index 0000000..191e7aa --- /dev/null +++ b/src/onapsdk/constants.py @@ -0,0 +1,61 @@ +"""Constant package.""" +# 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. + +## +# State Machines +# Vendor: DRAFT --> CERTIFIED +# VSP: DRAFT --> UPLOADED --> VALIDATED --> COMMITED --> CERTIFIED +## + +## +# States +## +DRAFT = "Draft" +CERTIFIED = "Certified" +COMMITED = "Commited" +UPLOADED = "Uploaded" +VALIDATED = "Validated" +APPROVED = "Approved" +UNDER_CERTIFICATION = "Certification in progress" +CHECKED_IN = "Checked In" +SUBMITTED = "Submitted" +DISTRIBUTED = "Distributed" +## +# Actions +## +CERTIFY = "Certify" +COMMIT = "Commit" +CREATE_PACKAGE = "Create_Package" +SUBMIT = "Submit" +SUBMIT_FOR_TESTING = "certificationRequest" +CHECKOUT = "checkout" +UNDOCHECKOUT = "UNDOCHECKOUT" +CHECKIN = "checkin" +APPROVE = "approve" +DISTRIBUTE = "PROD/activate" +TOSCA = "toscaModel" +DISTRIBUTION = "distribution" +START_CERTIFICATION = "startCertification" +NOT_CERTIFIED_CHECKOUT = "NOT_CERTIFIED_CHECKOUT" +NOT_CERTIFIED_CHECKIN = "NOT_CERTIFIED_CHECKIN" +READY_FOR_CERTIFICATION = "READY_FOR_CERTIFICATION" +CERTIFICATION_IN_PROGRESS = "CERTIFICATION_IN_PROGRESS" +DISTRIBUTION_APPROVED = "DISTRIBUTION_APPROVED" +DISTRIBUTION_NOT_APPROVED = "DISTRIBUTION_NOT_APPROVED" +SDC_DISTRIBUTED = "DISTRIBUTED" +## +# Distribution States +## +DOWNLOAD_OK = "DOWNLOAD_OK" diff --git a/src/onapsdk/cps/__init__.py b/src/onapsdk/cps/__init__.py new file mode 100644 index 0000000..63d5dd5 --- /dev/null +++ b/src/onapsdk/cps/__init__.py @@ -0,0 +1,18 @@ +"""ONAP SDK CPS package.""" +# 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 .anchor import Anchor # pylint: disable=unused-import +from .dataspace import Dataspace # pylint: disable=unused-import +from .schemaset import SchemaSet, SchemaSetModuleReference # pylint: disable=unused-import diff --git a/src/onapsdk/cps/anchor.py b/src/onapsdk/cps/anchor.py new file mode 100644 index 0000000..f02687d --- /dev/null +++ b/src/onapsdk/cps/anchor.py @@ -0,0 +1,193 @@ +"""ONAP SDK CPS anchor 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 typing import Any, Dict, TYPE_CHECKING + +from .cps_element import CpsElement + +if TYPE_CHECKING: + from .schemaset import SchemaSet # pylint: disable=cyclic-import + + +class Anchor(CpsElement): + """CPS anchor class.""" + + def __init__(self, name: str, schema_set: "SchemaSet") -> None: + """Initialise CPS anchor object. + + Args: + name (str): Anchor name + schema_set (SchemaSet): Schema set + + """ + super().__init__() + self.name: str = name + self.schema_set: "SchemaSet" = schema_set + + def __repr__(self) -> str: + """Human readable representation of the object. + + Returns: + str: Human readable string + + """ + return f"Anchor(name={self.name}, "\ + f"schema set={self.schema_set.name}, "\ + f"dataspace={self.schema_set.dataspace.name})" + + @property + def url(self) -> str: + """Anchor url. + + Returns: + str: Anchor url + + """ + return f"{self._url}/cps/api/v1/dataspaces/"\ + f"{self.schema_set.dataspace.name}/anchors/{self.name}" + + def delete(self) -> None: + """Delete anchor.""" + self.send_message( + "DELETE", + f"Delete {self.name} anchor", + self.url, + auth=self.auth + ) + + def create_node(self, node_data: str) -> None: + """Create anchor node. + + Fill CPS anchor with a data. + + Args: + node_data (str): Node data. Should be JSON formatted. + + """ + self.send_message( + "POST", + f"Create {self.name} anchor node", + f"{self.url}/nodes", + data=node_data, + auth=self.auth + ) + + def get_node(self, xpath: str, include_descendants: bool = False) -> Dict[Any, Any]: + """Get anchor node data. + + Using XPATH get anchor's node data. + + Args: + xpath (str): Anchor node xpath. + include_descendants (bool, optional): Determies if descendants should be included in + response. Defaults to False. + + Returns: + Dict[Any, Any]: Anchor node data. + + """ + return self.send_message_json( + "GET", + f"Get {self.name} anchor node with {xpath} xpath", + f"{self.url}/node?xpath={xpath}&include-descendants={include_descendants}", + auth=self.auth + ) + + def update_node(self, xpath: str, node_data: str) -> None: + """Update anchor node data. + + Using XPATH update anchor's node data. + + Args: + xpath (str): Anchor node xpath. + node_data (str): Node data. + + """ + self.send_message( + "PATCH", + f"Update {self.name} anchor node with {xpath} xpath", + f"{self.url}/nodes?xpath={xpath}", + data=node_data, + auth=self.auth + ) + + def replace_node(self, xpath: str, node_data: str) -> None: + """Replace anchor node data. + + Using XPATH replace anchor's node data. + + Args: + xpath (str): Anchor node xpath. + node_data (str): Node data. + + """ + self.send_message( + "PUT", + f"Replace {self.name} anchor node with {xpath} xpath", + f"{self.url}/nodes?xpath={xpath}", + data=node_data, + auth=self.auth + ) + + def add_list_node(self, xpath: str, node_data: str) -> None: + """Add an element to the list node of an anchor. + + Args: + xpath (str): Xpath to the list node. + node_data (str): Data to be added. + + """ + self.send_message( + "POST", + f"Add element to {self.name} anchor node with {xpath} xpath", + f"{self.url}/list-nodes?xpath={xpath}", + data=node_data, + auth=self.auth + ) + + def query_node(self, query: str, include_descendants: bool = False) -> Dict[Any, Any]: + """Query CPS anchor data. + + Args: + query (str): Query + include_descendants (bool, optional): Determies if descendants should be included in + response. Defaults to False. + + Returns: + Dict[Any, Any]: Query return values. + + """ + return self.send_message_json( + "GET", + f"Get {self.name} anchor node with {query} query", + f"{self.url}/nodes/query?cps-path={query}&include-descendants={include_descendants}", + auth=self.auth + ) + + def delete_nodes(self, xpath: str) -> None: + """Delete nodes. + + Use XPATH to delete Anchor nodes. + + Args: + xpath (str): Nodes to delete + + """ + self.send_message( + "DELETE", + f"Delete {self.name} anchor nodes with {xpath} xpath", + f"{self.url}/nodes?xpath={xpath}", + auth=self.auth + ) diff --git a/src/onapsdk/cps/cps_element.py b/src/onapsdk/cps/cps_element.py new file mode 100644 index 0000000..27f1faa --- /dev/null +++ b/src/onapsdk/cps/cps_element.py @@ -0,0 +1,24 @@ +"""ONAP SDK CPS element 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService + + +class CpsElement(OnapService): + """Mother Class of all CPS elements.""" + + _url: str = settings.CPS_URL + auth: tuple = settings.CPS_AUTH diff --git a/src/onapsdk/cps/dataspace.py b/src/onapsdk/cps/dataspace.py new file mode 100644 index 0000000..e6340d7 --- /dev/null +++ b/src/onapsdk/cps/dataspace.py @@ -0,0 +1,193 @@ +"""ONAP SDK CPS dataspace 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 typing import Any, Dict, Iterable + +from .anchor import Anchor +from .cps_element import CpsElement +from .schemaset import SchemaSet, SchemaSetModuleReference + + +class Dataspace(CpsElement): + """CPS dataspace class.""" + + def __init__(self, name: str) -> None: + """Initialize dataspace object. + + Args: + name (str): Dataspace name + + """ + super().__init__() + self.name: str = name + + def __repr__(self) -> str: + """Human readable representation of the object. + + Returns: + str: Human readable string + + """ + return f"Dataspace(name={self.name})" + + @property + def url(self) -> str: + """Dataspace url. + + Returns: + str: Dataspace url + + """ + return f"{self._url}/cps/api/v1/dataspaces/{self.name}" + + @classmethod + def create(cls, dataspace_name: str) -> "Dataspace": + """Create dataspace with given name. + + Args: + dataspace_name (str): Dataspace name + + Returns: + Dataspace: Newly created dataspace + + """ + cls.send_message( + "POST", + f"Create {dataspace_name} dataspace", + f"{cls._url}/cps/api/v1/dataspaces?dataspace-name={dataspace_name}", + auth=cls.auth + ) + return Dataspace(dataspace_name) + + def create_anchor(self, schema_set: SchemaSet, anchor_name: str) -> Anchor: + """Create anchor. + + Args: + schema_set (SchemaSet): Schema set object which is going to be used to create anchor. + anchor_name (str): Anchor name + + Returns: + Anchor: Created anchor + + """ + self.send_message( + "POST", + "Get all CPS dataspace schemasets", + f"{self.url}/anchors/?schema-set-name={schema_set.name}&anchor-name={anchor_name}", + auth=self.auth + ) + return Anchor(name=anchor_name, schema_set=schema_set) + + def get_anchors(self) -> Iterable[Anchor]: + """Get all dataspace's anchors. + + Iterable of related with dataspace anchors. + + Yields: + Iterator[Anchor]: Anchor object + + """ + for anchor_data in self.send_message_json(\ + "GET",\ + "Get all CPS dataspace anchors",\ + f"{self.url}/anchors",\ + auth=self.auth\ + ): + yield Anchor(name=anchor_data["name"], + schema_set=SchemaSet(name=anchor_data["schemaSetName"], + dataspace=self)) + + def get_anchor(self, anchor_name: str) -> Anchor: + """Get dataspace anchor by name. + + To get anchor there is no need to use `SchemaSet` object, but to create anchor it it. + + Args: + anchor_name (str): Anchor name. + + Returns: + Anchor: Anchor object + + """ + anchor_data: Dict[str, Any] = self.send_message_json( + "GET", + f"Get {anchor_name} anchor", + f"{self.url}/anchors/{anchor_name}", + auth=self.auth + ) + return Anchor(name=anchor_data["name"], + schema_set=SchemaSet(name=anchor_data["schemaSetName"], + dataspace=self)) + + def get_schema_set(self, schema_set_name: str) -> SchemaSet: + """Get schema set by name. + + Args: + schema_set_name (str): Schema set name + + Returns: + SchemaSet: Schema set object + + """ + schema_set_data: Dict[str, Any] = self.send_message_json( + "GET", + "Get all CPS dataspace schemasets", + f"{self._url}/cps/api/v1/dataspaces/{self.name}/schema-sets/{schema_set_name}", + auth=self.auth + ) + return SchemaSet( + name=schema_set_data["name"], + dataspace=self, + module_references=[ + SchemaSetModuleReference( + name=module_reference_data["name"], + namespace=module_reference_data["namespace"], + revision=module_reference_data["revision"] + ) for module_reference_data in schema_set_data["moduleReferences"] + ] + ) + + def create_schema_set(self, schema_set_name: str, schema_set: bytes) -> SchemaSet: + """Create schema set. + + Create CPS schema set in dataspace + + Args: + schema_set_name (str): Schema set name + schema_set (bytes): Schema set YANG + + Returns: + SchemaSet: Created schema set object + + """ + self.send_message( + "POST", + "Create schema set", + f"{self._url}/cps/api/v1/dataspaces/{self.name}/schema-sets/", + files={"file": schema_set}, + data={"schema-set-name": schema_set_name}, + headers={}, # Leave headers empty to fill it correctly by `requests` library + auth=self.auth + ) + return self.get_schema_set(schema_set_name) + + def delete(self) -> None: + """Delete dataspace.""" + self.send_message( + "DELETE", + f"Delete {self.name} dataspace", + f"{self._url}/cps/api/v1/dataspaces/{self.name}", + auth=self.auth + ) diff --git a/src/onapsdk/cps/schemaset.py b/src/onapsdk/cps/schemaset.py new file mode 100644 index 0000000..650d12d --- /dev/null +++ b/src/onapsdk/cps/schemaset.py @@ -0,0 +1,73 @@ +"""ONAP SDK CPS schemaset 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 List, Optional, TYPE_CHECKING + +from .cps_element import CpsElement + +if TYPE_CHECKING: + from .dataspace import Dataspace # pylint: disable=cyclic-import + + +@dataclass +class SchemaSetModuleReference: + """Schema set module reference dataclass. + + Stores all information about module reference. + """ + + name: str + namespace: str + revision: str + + +class SchemaSet(CpsElement): + """Schema set class.""" + + def __init__(self, + name: str, + dataspace: "Dataspace", + module_references: Optional[List[SchemaSetModuleReference]] = None) -> None: + """Initialize schema set class object. + + Args: + name (str): Schema set name + dataspace (Dataspace): Dataspace on which schema set was created. + module_references (Optional[List[SchemaSetModuleReference]], optional): + List of module references. Defaults to None. + """ + super().__init__() + self.name: str = name + self.dataspace: "Dataspace" = dataspace + self.module_refences: List[SchemaSetModuleReference] = module_references \ + if module_references else [] + + def __repr__(self) -> str: + """Human readable representation of the object. + + Returns: + str: Human readable string + + """ + return f"SchemaSet(name={self.name}, dataspace={self.dataspace.name})" + + def delete(self) -> None: + """Delete schema set.""" + self.send_message( + "DELETE", + f"Delete {self.name} schema set", + f"{self._url}/cps/api/v1/dataspaces/{self.dataspace.name}/schema-sets/{self.name}" + ) diff --git a/src/onapsdk/dmaap/__init__.py b/src/onapsdk/dmaap/__init__.py new file mode 100644 index 0000000..e0e448d --- /dev/null +++ b/src/onapsdk/dmaap/__init__.py @@ -0,0 +1,14 @@ +"""ONAP SDK Dmaap package.""" +# 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. diff --git a/src/onapsdk/dmaap/dmaap.py b/src/onapsdk/dmaap/dmaap.py new file mode 100644 index 0000000..27c72c5 --- /dev/null +++ b/src/onapsdk/dmaap/dmaap.py @@ -0,0 +1,87 @@ +"""Base Dmaap event store.""" +# Copyright 2022 Orange, Deutsche Telekom AG, Nokia +# +# 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 typing import Dict + +from onapsdk.dmaap.dmaap_service import DmaapService + +ACTION = "Get events from Dmaap" +GET_HTTP_METHOD = "GET" + + +class Dmaap(DmaapService): + """Dmaap library provides functions for getting events from Dmaap.""" + + dmaap_url = DmaapService._url + get_all_events_url = f"{dmaap_url}/events" + get_all_topics_url = f"{dmaap_url}/topics" + get_events_from_topic_url = "{}/events/{}/CG1/C1" + + @classmethod + def get_all_events(cls, + basic_auth: Dict[str, str]) -> dict: + """ + Get all events stored in Dmaap. + + Args: + basic_auth: (Dict[str, str]) for example:{ 'username': 'bob', 'password': 'secret' } + Returns: + (dict) Events from Dmaap + + """ + return Dmaap.__get_events(cls.get_all_events_url, basic_auth) + + @classmethod + def get_events_for_topic(cls, + topic: str, + basic_auth: Dict[str, str]) -> dict: + """ + Get all events stored specific topic in Dmaap. + + Args: + topic: (str) topic of events stored in Dmaap + basic_auth: (Dict[str, str]) for example:{ 'username': 'bob', 'password': 'secret' } + + Returns: + (dict) Events from Dmaap + + """ + url = cls.get_events_from_topic_url.format(cls.dmaap_url, topic) + return Dmaap.__get_events(url, basic_auth) + + @classmethod + def get_all_topics(cls, + basic_auth: Dict[str, str]) -> dict: + """ + Get all topics stored in Dmaap. + + Args: + basic_auth: (Dict[str, str]) for example:{ 'username': 'bob', 'password': 'secret' } + + Returns: + (dict) Topics from Dmaap + + """ + return Dmaap.__get_events(cls.get_all_topics_url, basic_auth)['topics'] + + @classmethod + def __get_events(cls, + url: str, + basic_auth: Dict[str, str]) -> dict: + return cls.send_message_json( + GET_HTTP_METHOD, + ACTION, + url, + basic_auth=basic_auth + ) diff --git a/src/onapsdk/dmaap/dmaap_service.py b/src/onapsdk/dmaap/dmaap_service.py new file mode 100644 index 0000000..5fe393b --- /dev/null +++ b/src/onapsdk/dmaap/dmaap_service.py @@ -0,0 +1,26 @@ +"""Base VES module.""" +# Copyright 2022 Orange, Deutsche Telekom AG, Nokia +# +# 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService + + +class DmaapService(OnapService): + """Base DMAAP class. + + Stores url to DMAAP API (edit if you want to use other). + """ + + _url: str = settings.DMAAP_URL diff --git a/src/onapsdk/exceptions.py b/src/onapsdk/exceptions.py new file mode 100644 index 0000000..76af60e --- /dev/null +++ b/src/onapsdk/exceptions.py @@ -0,0 +1,109 @@ +"""ONAP Exception 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 typing import Optional + + +class SDKException(Exception): + """Generic exception for ONAP SDK.""" + + +class RequestError(SDKException): + """Request error occured.""" + + +class ConnectionFailed(RequestError): + """Unable to connect.""" + + +class APIError(RequestError): + """API error occured.""" + + def __init__(self, + message: Optional[str] = None, + response_status_code: Optional[int] = None) -> None: + """Init api error exception. + + Save message and optional response status code. + + Args: + message (Optional[str]): Response error message. Defaults to None. + response_status_code (Optional[int], optional): Response status code. Defaults to None. + + """ + if message: + super().__init__(message) + else: + super().__init__() + self._response_status_code: int = response_status_code if response_status_code else 0 + + @property + def response_status_code(self) -> int: + """Response status code property. + + Returns: + int: Response status code. If not set, returns 0 + + """ + return self._response_status_code + + @response_status_code.setter + def response_status_code(self, status_code: int) -> None: + """Response status code property setter. + + Args: + status_code (int): Response status code + + """ + self._response_status_code = status_code + + +class InvalidResponse(RequestError): + """Unable to decode response.""" + + +class ResourceNotFound(APIError): + """Requested resource does not exist.""" + + +class RelationshipNotFound(ResourceNotFound): + """Required relationship is missing.""" + + +class StatusError(SDKException): + """Invalid status.""" + + +class ParameterError(SDKException): + """Parameter does not satisfy requirements.""" + +class ModuleError(SDKException): + """Unable to import module.""" + + +class ValidationError(SDKException): + """Data validation failed.""" + + +class FileError(ValidationError): + """Reading in a file failed.""" + + +class SettingsError(SDKException): + """Some settings are wrong.""" + + +class NoGuiError(SDKException): + """No GUI available for this component.""" diff --git a/src/onapsdk/msb/__init__.py b/src/onapsdk/msb/__init__.py new file mode 100644 index 0000000..6b23278 --- /dev/null +++ b/src/onapsdk/msb/__init__.py @@ -0,0 +1,17 @@ +"""Microsevice bus package.""" +# 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 .msb_service import MSB +from .esr import ESR +from .multicloud import Multicloud diff --git a/src/onapsdk/msb/esr.py b/src/onapsdk/msb/esr.py new file mode 100644 index 0000000..b29cfa4 --- /dev/null +++ b/src/onapsdk/msb/esr.py @@ -0,0 +1,85 @@ +"""ESR 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 onapsdk.utils.jinja import jinja_env +from .msb_service import MSB + + +class ESR(MSB): + """External system EST module.""" + + base_url = f"{MSB.base_url}/api/aai-esr-server/v1/vims" + + @classmethod + def register_vim(cls, # pylint: disable=too-many-arguments + cloud_owner: str, + cloud_region_id: str, + cloud_type: str, + cloud_region_version: str, + auth_info_cloud_domain: str, + auth_info_username: str, + auth_info_password: str, + auth_info_url: str, + owner_defined_type: str = None, + cloud_zone: str = None, + physical_location_id: str = None, + cloud_extra_info: str = None, + auth_info_ssl_cacert: str = None, + auth_info_ssl_insecure: bool = None) -> None: + """Register VIM. + + Args: + cloud_owner (str): cloud owner name, can be customized, e.g. att-aic + cloud_region_id (str): cloud region info based on deployment, e.g. RegionOne + cloud_type (str): type of the cloud, decides which multicloud plugin to use, + openstack or vio + cloud_region_version (str): cloud version, ocata, mitaka or other + auth_info_cloud_domain (str): domain info for keystone v3 + auth_info_username (str): user name + auth_info_password (str): password + auth_info_url (str): authentication url of the cloud, e.g. keystone url + owner_defined_type (str, optional): cloud-owner defined type indicator (e.g., dcp, lcp). + Defaults to None. + cloud_zone (str, optional): zone where the cloud is homed.. Defaults to None. + physical_location_id (str, optional): complex physical location id for + cloud-region instance. Defaults to None. + cloud_extra_info (str, optional): extra info for Cloud. Defaults to None. + auth_info_ssl_cacert (str, optional): ca file content if enabled ssl on auth-url. + Defaults to None. + auth_info_ssl_insecure (bool, optional): whether to verify VIM's certificate. + Defaults to None. + """ + cls.send_message( + "POST", + "Register VIM instance to ONAP", + cls.base_url, + data=jinja_env() + .get_template("msb_esr_vim_registration.json.j2") + .render( + cloud_owner=cloud_owner, + cloud_region_id=cloud_region_id, + cloud_type=cloud_type, + cloud_region_version=cloud_region_version, + auth_info_cloud_domain=auth_info_cloud_domain, + auth_info_username=auth_info_username, + auth_info_password=auth_info_password, + auth_info_url=auth_info_url, + owner_defined_type=owner_defined_type, + cloud_zone=cloud_zone, + physical_location_id=physical_location_id, + cloud_extra_info=cloud_extra_info, + auth_info_ssl_cacert=auth_info_ssl_cacert, + auth_info_ssl_insecure=auth_info_ssl_insecure, + ), + ) diff --git a/src/onapsdk/msb/k8s/__init__.py b/src/onapsdk/msb/k8s/__init__.py new file mode 100644 index 0000000..655502d --- /dev/null +++ b/src/onapsdk/msb/k8s/__init__.py @@ -0,0 +1,17 @@ +"""K8s package.""" +# 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 .definition import Definition, Profile, ConfigurationTemplate +from .connectivity_info import ConnectivityInfo +from .instance import InstantiationParameter, InstantiationRequest, Instance diff --git a/src/onapsdk/msb/k8s/connectivity_info.py b/src/onapsdk/msb/k8s/connectivity_info.py new file mode 100644 index 0000000..71a43c1 --- /dev/null +++ b/src/onapsdk/msb/k8s/connectivity_info.py @@ -0,0 +1,105 @@ +"""Connectivity-Info 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 onapsdk.utils.jinja import jinja_env +from ..msb_service import MSB + + +class ConnectivityInfo(MSB): + """Connectivity-Info class.""" + + api_version = "/api/multicloud-k8s/v1/v1" + url = f"{MSB.base_url}{api_version}/connectivity-info" + + def __init__(self, cloud_region_id: str, + cloud_owner: str, + other_connectivity_list: dict, + kubeconfig: str) -> None: + """Connectivity-info object initialization. + + Args: + cloud_region_id (str): Cloud region ID + cloud_owner (str): Cloud owner name + other_connectivity_list (dict): Optional other connectivity list + kubeconfig (str): kubernetes cluster kubeconfig + """ + super().__init__() + self.cloud_region_id: str = cloud_region_id + self.cloud_owner: str = cloud_owner + self.other_connectivity_list: dict = other_connectivity_list + self.kubeconfig: str = kubeconfig + + @classmethod + def get_connectivity_info_by_region_id(cls, cloud_region_id: str) -> "ConnectivityInfo": + """Get connectivity-info by its name (cloud region id). + + Args: + cloud_region_id (str): Cloud region ID + + Returns: + ConnectivityInfo: Connectivity-Info object + + """ + url: str = f"{cls.url}/{cloud_region_id}" + connectivity_info: dict = cls.send_message_json( + "GET", + "Get Connectivity Info", + url + ) + return cls( + connectivity_info["cloud-region"], + connectivity_info["cloud-owner"], + connectivity_info.get("other-connectivity-list"), + connectivity_info["kubeconfig"] + ) + + def delete(self) -> None: + """Delete connectivity info.""" + url: str = f"{self.url}/{self.cloud_region_id}" + self.send_message( + "DELETE", + "Delete Connectivity Info", + url + ) + + @classmethod + def create(cls, + cloud_region_id: str, + cloud_owner: str, + kubeconfig: bytes = None) -> "ConnectivityInfo": + """Create Connectivity Info. + + Args: + cloud_region_id (str): Cloud region ID + cloud_owner (str): Cloud owner name + kubeconfig (bytes): kubernetes cluster kubeconfig file + + Returns: + ConnectivityInfo: Created object + + """ + json_file = jinja_env().get_template("multicloud_k8s_add_connectivity_info.json.j2").render( + cloud_region_id=cloud_region_id, + cloud_owner=cloud_owner + ) + url: str = f"{cls.url}" + cls.send_message( + "POST", + "Create Connectivity Info", + url, + files={"file": kubeconfig, + "metadata": (None, json_file)}, + headers={} + ) + return cls.get_connectivity_info_by_region_id(cloud_region_id) diff --git a/src/onapsdk/msb/k8s/definition.py b/src/onapsdk/msb/k8s/definition.py new file mode 100644 index 0000000..6c0def2 --- /dev/null +++ b/src/onapsdk/msb/k8s/definition.py @@ -0,0 +1,424 @@ +"""Definition 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 typing import Iterator +from dataclasses import dataclass + +from onapsdk.utils.jinja import jinja_env +from ..msb_service import MSB + + +# pylint: disable=too-many-arguments, too-few-public-methods +class DefinitionBase(MSB): + """DefinitionBase class.""" + + base_url = f"{MSB.base_url}/api/multicloud-k8s/v1/v1/rb/definition" + + def __init__(self, rb_name: str, + rb_version: str) -> None: + """Definition-Base object initialization. + + Args: + rb_name (str): Definition name + rb_version (str): Definition version + """ + super().__init__() + self.rb_name: str = rb_name + self.rb_version: str = rb_version + + @property + def url(self) -> str: + """URL address for Definition Based calls. + + Returns: + str: URL to RB Definition + + """ + return f"{self.base_url}/{self.rb_name}/{self.rb_version}" + + def delete(self) -> None: + """Delete Definition Based object.""" + self.send_message( + "DELETE", + f"Delete {self.__class__.__name__}", + self.url + ) + + def upload_artifact(self, package: bytes = None): + """Upload artifact. + + Args: + package (bytes): Artifact to be uploaded to multicloud-k8s plugin + + """ + url: str = f"{self.url}/content" + self.send_message( + "POST", + "Upload Artifact content", + url, + data=package, + headers={} + ) + + +class Definition(DefinitionBase): + """Definition class.""" + + def __init__(self, rb_name: str, + rb_version: str, + chart_name: str, + description: str, + labels: dict) -> None: + """Definition object initialization. + + Args: + rb_name (str): Definition name + rb_version (str): Definition version + chart_name (str): Chart name, optional field, will be detected if it is not provided + description (str): Definition description + labels (str): Labels + """ + super().__init__(rb_name, rb_version) + self.rb_name: str = rb_name + self.rb_version: str = rb_version + self.chart_name: str = chart_name + self.description: str = description + self.labels: dict = labels + + @classmethod + def get_all(cls): + """Get all definitions. + + Yields: + Definition: Definition object + + """ + for definition in cls.send_message_json("GET", + "Get definitions", + cls.base_url): + yield cls( + definition["rb-name"], + definition["rb-version"], + definition.get("chart-name"), + definition.get("description"), + definition.get("labels") + ) + + @classmethod + def get_definition_by_name_version(cls, rb_name: str, rb_version: str) -> "Definition": + """Get definition by it's name and version. + + Args: + rb_name (str): definition name + rb_version (str): definition version + + Returns: + Definition: Definition object + + """ + url: str = f"{cls.base_url}/{rb_name}/{rb_version}" + definition: dict = cls.send_message_json( + "GET", + "Get definition", + url + ) + return cls( + definition["rb-name"], + definition["rb-version"], + definition.get("chart-name"), + definition.get("description"), + definition.get("labels") + ) + + @classmethod + def create(cls, rb_name: str, + rb_version: str, + chart_name: str = "", + description: str = "", + labels=None) -> "Definition": + """Create Definition. + + Args: + rb_name (str): Definition name + rb_version (str): Definition version + chart_name (str): Chart name, optional field, will be detected if it is not provided + description (str): Definition description + labels (str): Labels + + Returns: + Definition: Created object + + """ + if labels is None: + labels = {} + url: str = f"{cls.base_url}" + cls.send_message( + "POST", + "Create definition", + url, + data=jinja_env().get_template("multicloud_k8s_add_definition.json.j2").render( + rb_name=rb_name, + rb_version=rb_version, + chart_name=chart_name, + description=description, + labels=labels + ) + ) + return cls.get_definition_by_name_version(rb_name, rb_version) + + def create_profile(self, profile_name: str, + namespace: str, + kubernetes_version: str, + release_name=None) -> "Profile": + """Create Profile for Definition. + + Args: + profile_name (str): Name of profile + namespace (str): Namespace that service is created in + kubernetes_version (str): Required Kubernetes version + release_name (str): Release name + + Returns: + Profile: Created object + + """ + url: str = f"{self.url}/profile" + if release_name is None: + release_name = profile_name + self.send_message( + "POST", + "Create profile for definition", + url, + data=jinja_env().get_template("multicloud_k8s_create_profile_" + "for_definition.json.j2").render( + rb_name=self.rb_name, + rb_version=self.rb_version, + profile_name=profile_name, + release_name=release_name, + namespace=namespace, + kubernetes_version=kubernetes_version + ) + ) + return self.get_profile_by_name(profile_name) + + def get_all_profiles(self) -> Iterator["Profile"]: + """Get all profiles. + + Yields: + Profile: Profile object + + """ + url: str = f"{self.url}/profile" + + for profile in self.send_message_json("GET", + "Get profiles", + url): + yield Profile( + profile["rb-name"], + profile["rb-version"], + profile["profile-name"], + profile["namespace"], + profile.get("kubernetes-version"), + profile.get("labels"), + profile.get("release-name") + ) + + def get_profile_by_name(self, profile_name: str) -> "Profile": + """Get profile by it's name. + + Args: + profile_name (str): profile name + + Returns: + Profile: Profile object + + """ + url: str = f"{self.url}/profile/{profile_name}" + + profile: dict = self.send_message_json( + "GET", + "Get profile", + url + ) + return Profile( + profile["rb-name"], + profile["rb-version"], + profile["profile-name"], + profile["namespace"], + profile.get("kubernetes-version"), + profile.get("labels"), + profile.get("release-name") + ) + + def get_all_configuration_templates(self): + """Get all configuration templates. + + Yields: + ConfigurationTemplate: ConfigurationTemplate object + + """ + url: str = f"{self.url}/config-template" + + for template in self.send_message_json("GET", + "Get configuration templates", + url): + yield ConfigurationTemplate( + self.rb_name, + self.rb_version, + template["template-name"], + template.get("description") + ) + + def create_configuration_template(self, template_name: str, + description="") -> "ConfigurationTemplate": + """Create configuration template. + + Args: + template_name (str): Name of the template + description (str): Description + + Returns: + ConfigurationTemplate: Created object + + """ + url: str = f"{self.url}/config-template" + + self.send_message( + "POST", + "Create configuration template", + url, + data=jinja_env().get_template("multicloud_k8s_create_configuration_" + "template.json.j2").render( + template_name=template_name, + description=description + ) + ) + + return self.get_configuration_template_by_name(template_name) + + def get_configuration_template_by_name(self, template_name: str) -> "ConfigurationTemplate": + """Get configuration template. + + Args: + template_name (str): Name of the template + + Returns: + ConfigurationTemplate: object + + """ + url: str = f"{self.url}/config-template/{template_name}" + + template: dict = self.send_message_json( + "GET", + "Get Configuration template", + url + ) + return ConfigurationTemplate( + self.rb_name, + self.rb_version, + template["template-name"], + template.get("description") + ) + + +class ProfileBase(DefinitionBase): + """ProfileBase class.""" + + def __init__(self, rb_name: str, + rb_version: str, + profile_name: str) -> None: + """Profile-Base object initialization. + + Args: + rb_name (str): Definition name + rb_version (str): Definition version + profile_name (str): Name of profile + """ + super().__init__(rb_name, rb_version) + self.rb_name: str = rb_name + self.rb_version: str = rb_version + self.profile_name: str = profile_name + + @property + def url(self) -> str: + """URL address for Profile calls. + + Returns: + str: URL to RB Profile + + """ + return f"{super().url}/profile/{self.profile_name}" + + +@dataclass +class Profile(ProfileBase): + """Profile class.""" + + def __init__(self, rb_name: str, + rb_version: str, + profile_name: str, + namespace: str, + kubernetes_version: str, + labels=None, + release_name=None) -> None: + """Profile object initialization. + + Args: + rb_name (str): Definition name + rb_version (str): Definition version + profile_name (str): Name of profile + release_name (str): Release name, if release_name is not provided, + namespace (str): Namespace that service is created in + kubernetes_version (str): Required Kubernetes version + labels (dict): Labels + """ + super().__init__(rb_name, rb_version, profile_name) + if release_name is None: + release_name = profile_name + self.release_name: str = release_name + self.namespace: str = namespace + self.kubernetes_version: str = kubernetes_version + self.labels: dict = labels + if self.labels is None: + self.labels = dict() + + +class ConfigurationTemplate(DefinitionBase): + """ConfigurationTemplate class.""" + + @property + def url(self) -> str: + """URL address for ConfigurationTemplate calls. + + Returns: + str: URL to Configuration template in Multicloud-k8s API. + + """ + return f"{super().url}/config-template/{self.template_name}" + + def __init__(self, rb_name: str, + rb_version: str, + template_name: str, + description="") -> None: + """Configuration-Template object initialization. + + Args: + rb_name (str): Definition name + rb_version (str): Definition version + template_name (str): Configuration template name + description (str): Namespace that service is created in + """ + super().__init__(rb_name, rb_version) + self.template_name: str = template_name + self.description: str = description diff --git a/src/onapsdk/msb/k8s/instance.py b/src/onapsdk/msb/k8s/instance.py new file mode 100644 index 0000000..196b9d2 --- /dev/null +++ b/src/onapsdk/msb/k8s/instance.py @@ -0,0 +1,190 @@ +"""Instantiation 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 typing import Iterator +from dataclasses import dataclass + +from onapsdk.msb import MSB +from onapsdk.utils.jinja import jinja_env + + +# pylint: disable=too-many-arguments +@dataclass +class InstantiationRequest: + """Instantiation Request class.""" + + def __init__(self, request: dict) -> None: + """Request object initialization. + + Args: + cloud_region_id (str): Cloud region ID + profile_name (str): Name of profile + rb_name (str): Definition name + rb_version (str): Definition version + override_values (dict): Optional parameters + labels (dict): Optional labels + """ + super().__init__() + self.cloud_region_id: str = request["cloud-region"] + self.profile_name: str = request["profile-name"] + self.rb_name: str = request["rb-name"] + self.rb_version: str = request["rb-version"] + self.override_values: dict = request["override-values"] + self.labels: dict = request["labels"] + + +@dataclass +class InstantiationParameter: + """Class to store instantiation parameters used to pass override_values and labels. + + Contains two values: name of parameter and it's value + """ + + name: str + value: str + + +class Instance(MSB): + """Instance class.""" + + base_url = f"{MSB.base_url}/api/multicloud-k8s/v1/v1/instance" + + def __init__(self, instance_id: str, + namespace: str, + request: InstantiationRequest, + resources: dict = None, + override_values: dict = None) -> None: + """Instance object initialization. + + Args: + instance_id (str): instance ID + namespace (str): namespace that instance is created in + request (InstantiationRequest): datails of the instantiation request + resources (dict): Created resources + override_values (dict): Optional values + """ + super().__init__() + self.instance_id: str = instance_id + self.namespace: str = namespace + self.request: InstantiationRequest = request + self.resources: dict = resources + self.override_values: dict = override_values + + @property + def url(self) -> str: + """URL address. + + Returns: + str: URL to Instance + + """ + return f"{self.base_url}/{self.instance_id}" + + @classmethod + def get_all(cls) -> Iterator["Instance"]: + """Get all instantiated Kubernetes resources. + + Yields: + Instantiation: Instantiation object + + """ + for resource in cls.send_message_json("GET", + "Get Kubernetes resources", + cls.base_url): + yield cls( + instance_id=resource["id"], + namespace=resource["namespace"], + request=InstantiationRequest(resource["request"]) + ) + + @classmethod + def get_by_id(cls, instance_id: str) -> "Instance": + """Get Kubernetes resource by id. + + Args: + instance_id (str): instance ID + + Returns: + Instantiation: Instantiation object + + """ + url: str = f"{cls.base_url}/{instance_id}" + resource: dict = cls.send_message_json( + "GET", + "Get Kubernetes resource by id", + url + ) + return cls( + instance_id=resource["id"], + namespace=resource["namespace"], + request=InstantiationRequest(resource["request"]), + resources=resource["resources"], + override_values=resource.get("override-values") + ) + + @classmethod + def create(cls, + cloud_region_id: str, + profile_name: str, + rb_name: str, + rb_version: str, + override_values: dict = None, + labels: dict = None) -> "Instance": + """Create Instance. + + Args: + cloud_region_id (str): Cloud region ID + profile_name (str): Name of profile to be instantiated + rb_name: (bytes): Definition name + rb_version (str): Definition version + override_values (dict): List of optional override values + labels (dict): List of optional labels + + Returns: + Instance: Created object + + """ + if labels is None: + labels = {} + if override_values is None: + override_values = {} + url: str = f"{cls.base_url}" + response: dict = cls.send_message_json( + "POST", + "Create Instance", + url, + data=jinja_env().get_template("multicloud_k8s_instantiate.json.j2").render( + cloud_region_id=cloud_region_id, + profile_name=profile_name, + rb_name=rb_name, + rb_version=rb_version, + override_values=override_values, + labels=labels), + headers={} + ) + return cls( + instance_id=response["id"], + namespace=response["namespace"], + request=InstantiationRequest(response["request"]), + resources=response["resources"], + override_values=response.get("override-values") + ) + + def delete(self) -> None: + """Delete Instance object.""" + self.send_message( + "DELETE", + f"Delete {self.instance_id} instance", + self.url + ) diff --git a/src/onapsdk/msb/msb_service.py b/src/onapsdk/msb/msb_service.py new file mode 100644 index 0000000..017fed7 --- /dev/null +++ b/src/onapsdk/msb/msb_service.py @@ -0,0 +1,24 @@ +"""Microsevice bus 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService +from onapsdk.utils.headers_creator import headers_msb_creator + + +class MSB(OnapService): + """Microservice Bus base class.""" + + base_url = settings.MSB_URL + headers = headers_msb_creator(OnapService.headers) diff --git a/src/onapsdk/msb/multicloud.py b/src/onapsdk/msb/multicloud.py new file mode 100644 index 0000000..bc8b468 --- /dev/null +++ b/src/onapsdk/msb/multicloud.py @@ -0,0 +1,55 @@ +"""Multicloud 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 .msb_service import MSB + + +class Multicloud(MSB): + """MSB subclass to register/unregister instance to ONAP.""" + + base_url = f"{MSB.base_url}/api/multicloud/v1" + + @classmethod + def register_vim(cls, + cloud_owner: str, + cloud_region_id: str, + default_tenant: str = None) -> None: + """Register a VIM instance to ONAP. + + Args: + cloud_owner (str): Cloud owner name + cloud_region_id (str): Cloud region ID + default_tenant (str, optional): Default tenant name. Defaults to None. + """ + cls.send_message( + "POST", + "Register VIM instance to ONAP", + f"{cls.base_url}/{cloud_owner}/{cloud_region_id}/registry", + data={"defaultTenant": default_tenant} if default_tenant else None + ) + + @classmethod + def unregister_vim(cls, cloud_owner: str, cloud_region_id: str) -> None: + """Unregister a VIM instance from ONAP. + + Args: + cloud_owner (str): Cloud owner name + cloud_region_id (str): Cloud region ID + """ + cls.send_message( + "DELETE", + "Unregister VIM instance from ONAP", + f"{cls.base_url}/{cloud_owner}/{cloud_region_id}" + ) diff --git a/src/onapsdk/msb/templates/msb_esr_vim_registration.json.j2 b/src/onapsdk/msb/templates/msb_esr_vim_registration.json.j2 new file mode 100644 index 0000000..ba19258 --- /dev/null +++ b/src/onapsdk/msb/templates/msb_esr_vim_registration.json.j2 @@ -0,0 +1,31 @@ +{ + "cloudOwner": "{{ cloud_owner }}", + "cloudRegionId": "{{ cloud_region_id }}", + "cloudType": "{{ cloud_type }}", + "cloudRegionVersion": "{{ cloud_region_version }}" + {% if owner_defined_type %} + , "ownerDefinedType": "{{ owner_defined_type }}" + {% endif %} + {% if cloud_zone %} + , "cloudZone": "{{ cloud_zone }}" + {% endif %} + {% if complex_name %} + , "physicalLocationId": "{{ physical_location_id }}" + {% endif %} + {% if cloud_extra_info %} + , "cloudExtraInfo": "{{ cloud_extra_info }}" + {% endif %} + , "vimAuthInfos": + [{ + "userName": "{{ auth_info_username }}", + "password": "{{ auth_info_password }}", + "authUrl": "{{ auth_info_url }}", + "cloudDomain": "{{ auth_info_cloud_domain }}" + {% if auth_info_ssl_cacert %} + , "sslCacert": "{{ auth_info_ssl_cacert }}" + {% endif %} + {% if auth_info_ssl_insecure is not none %} + , "sslInsecure": {{ auth_info_ssl_insecure | tojson }} + {% endif %} + }] +} diff --git a/src/onapsdk/msb/templates/multicloud_k8s_add_connectivity_info.json.j2 b/src/onapsdk/msb/templates/multicloud_k8s_add_connectivity_info.json.j2 new file mode 100644 index 0000000..4a3dc2d --- /dev/null +++ b/src/onapsdk/msb/templates/multicloud_k8s_add_connectivity_info.json.j2 @@ -0,0 +1,8 @@ +{ + "cloud-region" : "{{ cloud_region_id }}", + "cloud-owner" : "{{ cloud_owner }}", + "other-connectivity-list" : { + "connectivity-records" : [ + ] + } +}
\ No newline at end of file diff --git a/src/onapsdk/msb/templates/multicloud_k8s_add_definition.json.j2 b/src/onapsdk/msb/templates/multicloud_k8s_add_definition.json.j2 new file mode 100644 index 0000000..866d577 --- /dev/null +++ b/src/onapsdk/msb/templates/multicloud_k8s_add_definition.json.j2 @@ -0,0 +1,7 @@ +{ + "rb-name": "{{ rb_name }}", + "rb-version": "{{ rb_version }}", + "chart-name": "{{ chart_name }}", + "description": "{{ description }}", + "labels": {{ labels }} +} diff --git a/src/onapsdk/msb/templates/multicloud_k8s_create_configuration_template.json.j2 b/src/onapsdk/msb/templates/multicloud_k8s_create_configuration_template.json.j2 new file mode 100644 index 0000000..61e6d2b --- /dev/null +++ b/src/onapsdk/msb/templates/multicloud_k8s_create_configuration_template.json.j2 @@ -0,0 +1,4 @@ +{ + "template-name": "{{ template_name }}", + "description": "{{ description }}" +}
\ No newline at end of file diff --git a/src/onapsdk/msb/templates/multicloud_k8s_create_profile_for_definition.json.j2 b/src/onapsdk/msb/templates/multicloud_k8s_create_profile_for_definition.json.j2 new file mode 100644 index 0000000..5ea2de1 --- /dev/null +++ b/src/onapsdk/msb/templates/multicloud_k8s_create_profile_for_definition.json.j2 @@ -0,0 +1,8 @@ +{ + "rb-name": "{{ rb_name }}", + "rb-version": "{{ rb_version }}", + "profile-name": "{{ profile_name }}", + "release-name": "{{ release_name }}", + "namespace": "{{ namespace }}", + "kubernetes-version": "{{ kubernetes_version }}" +}
\ No newline at end of file diff --git a/src/onapsdk/msb/templates/multicloud_k8s_instantiate.json.j2 b/src/onapsdk/msb/templates/multicloud_k8s_instantiate.json.j2 new file mode 100644 index 0000000..fa5ef66 --- /dev/null +++ b/src/onapsdk/msb/templates/multicloud_k8s_instantiate.json.j2 @@ -0,0 +1,18 @@ +{ + "cloud-region": "{{ cloud_region_id }}", + "profile-name": "{{ profile_name }}", + "rb-name": "{{ rb_name }}", + "rb-version": "{{ rb_version }}", + "override-values": + { + {% for override_value in override_values %} + "{{ override_value.name }}": "{{ override_value.value }}"{% if not loop.last %},{% endif %} + {% endfor %} + }, + "labels": + { + {% for label in labels %} + "{{ label.name }}": "{{ label.value }}"{% if not loop.last %},{% endif %} + {% endfor %} + } +}
\ No newline at end of file diff --git a/src/onapsdk/nbi/__init__.py b/src/onapsdk/nbi/__init__.py new file mode 100644 index 0000000..d3ceaf6 --- /dev/null +++ b/src/onapsdk/nbi/__init__.py @@ -0,0 +1,16 @@ +"""NBI package.""" +# 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 .nbi import Nbi, Service, ServiceOrder, ServiceSpecification diff --git a/src/onapsdk/nbi/nbi.py b/src/onapsdk/nbi/nbi.py new file mode 100644 index 0000000..17356cb --- /dev/null +++ b/src/onapsdk/nbi/nbi.py @@ -0,0 +1,490 @@ +"""NBI 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 abc import ABC +from enum import Enum +from typing import Iterator +from uuid import uuid4 + +from onapsdk.aai.business.customer import Customer +from onapsdk.exceptions import RequestError +from onapsdk.onap_service import OnapService +from onapsdk.utils import get_zulu_time_isoformat +from onapsdk.utils.jinja import jinja_env +from onapsdk.utils.mixins import WaitForFinishMixin +from onapsdk.configuration import settings + + +class Nbi(OnapService, ABC): + """NBI base class.""" + + base_url = settings.NBI_URL + api_version = settings.NBI_API_VERSION + + @classmethod + def is_status_ok(cls) -> bool: + """Check NBI service status. + + Returns: + bool: True if NBI works fine, False otherwise + + """ + try: + cls.send_message( + "GET", + "Check NBI status", + f"{cls.base_url}{cls.api_version}/status" + ) + except RequestError as exc: + msg = f"An error occured during NBI status check: {exc}" + cls._logger.error(msg) + return False + return True + + +class ServiceSpecification(Nbi): + """NBI service specification class.""" + + def __init__(self, # pylint: disable=too-many-arguments + unique_id: str, + name: str, + invariant_uuid: str, + category: str, + distribution_status: str, + version: str, + lifecycle_status: str) -> None: + """Service specification object initialization. + + Args: + unique_id (str): Unique ID + name (str): Service specification name + invariant_uuid (str): Invariant UUID + category (str): Category + distribution_status (str): Service distribution status + version (str): Service version + lifecycle_status (str): Service lifecycle status + """ + super().__init__() + self.unique_id: str = unique_id + self.name: str = name + self.invariant_uuid: str = invariant_uuid + self.category: str = category + self.distribution_status: str = distribution_status + self.version: str = version + self.lifecycle_status: str = lifecycle_status + + def __repr__(self) -> str: + """Service specification representation. + + Returns: + str: Service specification object human readable representation + + """ + return (f"ServiceSpecification(unique_id={self.unique_id}, name={self.name}, " + f"invariant_uuid={self.invariant_uuid}, category={self.category}, " + f"distribution_status={self.distribution_status}, version={self.version}, " + f"lifecycle_status={self.lifecycle_status})") + + @classmethod + def get_all(cls) -> Iterator["ServiceSpecification"]: + """Get all service specifications. + + Yields: + ServiceSpecification: Service specification object + + """ + for service_specification in cls.send_message_json("GET", + "Get service specifications from NBI", + (f"{cls.base_url}{cls.api_version}/" + "serviceSpecification")): + yield ServiceSpecification( + service_specification.get("id"), + service_specification.get("name"), + service_specification.get("invariantUUID"), + service_specification.get("category"), + service_specification.get("distributionStatus"), + service_specification.get("version"), + service_specification.get("lifecycleStatus"), + ) + + @classmethod + def get_by_id(cls, service_specification_id: str) -> "ServiceSpecification": + """Get service specification by ID. + + Args: + service_specification_id (str): Service specification ID + + Returns: + ServiceSpecification: Service specification object + + """ + service_specification: dict = cls.send_message_json( + "GET", + f"Get service specification with {service_specification_id} ID from NBI", + f"{cls.base_url}{cls.api_version}/serviceSpecification/{service_specification_id}" + ) + return ServiceSpecification( + service_specification.get("id"), + service_specification.get("name"), + service_specification.get("invariantUUID"), + service_specification.get("category"), + service_specification.get("distributionStatus"), + service_specification.get("version"), + service_specification.get("lifecycleStatus"), + ) + + +class Service(Nbi): + """NBI service.""" + + def __init__(self, # pylint: disable=too-many-arguments + name: str, + service_id: str, + service_specification_name: str, + service_specification_id: str, + customer_id: str, + customer_role: str, + href: str) -> None: + """Service object initialization. + + Args: + name (str): Service name + service_id (str): Service ID + service_specification_name (str): Service specification name + service_specification_id (str): Service specification ID + customer_id (str): Global customer ID + customer_role (str): Customer role + href (str): Service object href + """ + super().__init__() + self.name: str = name + self.service_id: str = service_id + self._service_specification_name: str = service_specification_name + self._service_specification_id: str = service_specification_id + self._customer_id: str = customer_id + self.customer_role: str = customer_role + self.href: str = href + + def __repr__(self) -> str: + """Service object representation. + + Returns: + str: Human readable service object representation + + """ + return (f"Service(name={self.name}, service_id={self.service_id}, " + f"service_specification={self.service_specification}, customer={self.customer}, " + f"customer_role={self.customer_role})") + + @classmethod + def get_all(cls, customer_id: str = 'generic') -> Iterator["Service"]: + """Get all services for selected customer. + + Args: + customer_id (str): Global customer ID + + Yields: + Service: Service object + + """ + for service in cls.send_message_json("GET", + "Get service instances from NBI", + f"{cls.base_url}{cls.api_version}/service?" + f"relatedParty.id={customer_id}"): + yield cls(service.get("name"), + service.get("id"), + service.get("serviceSpecification", {}).get("name"), + service.get("serviceSpecification", {}).get("id"), + service.get("relatedParty", {}).get("id"), + service.get("relatedParty", {}).get("role"), + service.get("href")) + + @property + def customer(self) -> Customer: + """Service order Customer object. + + Returns: + Customer: Customer object + + """ + if not self._customer_id: + return None + return Customer.get_by_global_customer_id(self._customer_id) + + @property + def service_specification(self) -> ServiceSpecification: + """Service specification. + + Returns: + ServiceSpecification: Service specification object + + """ + if not self._service_specification_id: + return None + return ServiceSpecification.get_by_id(self._service_specification_id) + + +class ServiceOrder(Nbi, WaitForFinishMixin): # pylint: disable=too-many-instance-attributes + """Service order class.""" + + WAIT_FOR_SLEEP_TIME = 10 + + def __init__(self, # pylint: disable=too-many-arguments + unique_id: str, + href: str, + priority: str, + description: str, + category: str, + external_id: str, + service_instance_name: str, + state: str = None, + customer: Customer = None, + customer_id: str = None, + service_specification: ServiceSpecification = None, + service_specification_id: str = None) -> None: + """Service order object initialization. + + Args: + unique_id (str): unique ID + href (str): object's href + priority (str): order priority + description (str): order description + category (str): category description + external_id (str): external ID + service_instance_name (str): name of service instance + state (str, optional): instantiation state. Defaults to None. + customer (Customer, optional): Customer object. Defaults to None. + customer_id (str, optional): global customer ID. Defaults to None. + service_specification (ServiceSpecification, optional): service specification object. + Defaults to None. + service_specification_id (str, optional): service specification ID. Defaults to None. + """ + super().__init__() + self.unique_id: str = unique_id + self.href: str = href + self.priority: str = priority + self.category: str = category + self.description: str = description + self.external_id: str = external_id + self._customer: Customer = customer + self._customer_id: str = customer_id + self._service_specification: ServiceSpecification = service_specification + self._service_specification_id: str = service_specification_id + self.service_instance_name: str = service_instance_name + self.state: str = state + + class StatusEnum(Enum): + """Status enum. + + Store possible statuses for service order: + - completed, + - failed, + - inProgress. + If instantiation has status which is not covered by these values + `unknown` value is used. + + """ + + ACKNOWLEDGED = "acknowledged" + IN_PROGRESS = "inProgress" + FAILED = "failed" + COMPLETED = "completed" + REJECTED = "rejected" + UNKNOWN = "unknown" + + def __repr__(self) -> str: + """Service order object representation. + + Returns: + str: Service order object representation. + + """ + return (f"ServiceOrder(unique_id={self.unique_id}, href={self.href}, " + f"priority={self.priority}, category={self.category}, " + f"description={self.description}, external_id={self.external_id}, " + f"customer={self.customer}, service_specification={self.service_specification}" + f"service_instance_name={self.service_instance_name}, state={self.state})") + + @property + def customer(self) -> Customer: + """Get customer object used in service order. + + Returns: + Customer: Customer object + + """ + if not self._customer: + if not self._customer_id: + self._logger.error("No customer ID") + return None + self._customer = Customer.get_by_global_customer_id(self._customer_id) + return self._customer + + @property + def service_specification(self) -> ServiceSpecification: + """Service order service specification used in order item. + + Returns: + ServiceSpecification: Service specification + + """ + if not self._service_specification: + if not self._service_specification_id: + self._logger.error("No service specification") + return None + self._service_specification = ServiceSpecification.\ + get_by_id(self._service_specification_id) + return self._service_specification + + @classmethod + def get_all(cls) -> Iterator["ServiceOrder"]: + """Get all service orders. + + Returns: + Iterator[ServiceOrder]: ServiceOrder object + + """ + for service_order in cls.send_message_json("GET", + "Get all service orders", + f"{cls.base_url}{cls.api_version}/serviceOrder"): + service_order_related_party = None + if service_order.get("relatedParty") is not None: + service_order_related_party = service_order.get( + "relatedParty", [{}])[0].get("id") + + yield ServiceOrder( + unique_id=service_order.get("id"), + href=service_order.get("href"), + priority=service_order.get("priority"), + category=service_order.get("category"), + description=service_order.get("description"), + external_id=service_order.get("externalId"), + customer_id=service_order_related_party, + service_specification_id=service_order.get("orderItem", [{}])[0].get("service")\ + .get("serviceSpecification").get("id"), + service_instance_name=service_order.get("orderItem", [{}])[0].\ + get("service", {}).get("name"), + state=service_order.get("state") + ) + + @classmethod + def create(cls, + customer: Customer, + service_specification: ServiceSpecification, + name: str = None, + external_id: str = None) -> "ServiceOrder": + """Create service order. + + Returns: + ServiceOrder: ServiceOrder object + + """ + if external_id is None: + external_id = str(uuid4()) + if name is None: + name = f"Python_ONAP_SDK_service_instance_{str(uuid4())}" + response: dict = cls.send_message_json( + "POST", + "Add service instance via ServiceOrder API", + f"{cls.base_url}{cls.api_version}/serviceOrder", + data=jinja_env() + .get_template("nbi_service_order_create.json.j2") + .render( + customer=customer, + service_specification=service_specification, + service_instance_name=name, + external_id=external_id, + request_time=get_zulu_time_isoformat() + ) + ) + return cls( + unique_id=response.get("id"), + href=response.get("href"), + priority=response.get("priority"), + description=response.get("description"), + category=response.get("category"), + external_id=response.get("externalId"), + customer=customer, + service_specification=service_specification, + service_instance_name=name + ) + + @property + def status(self) -> "StatusEnum": + """Service order instantiation status. + + It's populated by call Service order endpoint. + + Returns: + StatusEnum: Service order status. + + """ + response: dict = self.send_message_json("GET", + "Get service order status", + (f"{self.base_url}{self.api_version}/" + f"serviceOrder/{self.unique_id}")) + try: + return self.StatusEnum(response.get("state")) + except (KeyError, ValueError): + self._logger.exception("Invalid status") + return self.StatusEnum.UNKNOWN + + @property + def completed(self) -> bool: + """Store an information if service order is completed or not. + + Service orded is completed if it's status is COMPLETED. + + Returns: + bool: True if service orded is completed, False otherwise. + + """ + return self.status == self.StatusEnum.COMPLETED + + @property + def rejected(self) -> bool: + """Store an information if service order is rejected or not. + + Service orded is completed if it's status is REJECTED. + + Returns: + bool: True if service orded is rejected, False otherwise. + + """ + return self.status == self.StatusEnum.REJECTED + + @property + def failed(self) -> bool: + """Store an information if service order is failed or not. + + Service orded is completed if it's status is FAILED. + + Returns: + bool: True if service orded is failed, False otherwise. + + """ + return self.status == self.StatusEnum.FAILED + + @property + def finished(self) -> bool: + """Store an information if service order is finished or not. + + Service orded is finished if it's status is not ACKNOWLEDGED or IN_PROGRESS. + + Returns: + bool: True if service orded is finished, False otherwise. + + """ + return self.status not in [self.StatusEnum.ACKNOWLEDGED, + self.StatusEnum.IN_PROGRESS] diff --git a/src/onapsdk/nbi/templates/nbi_service_order_create.json.j2 b/src/onapsdk/nbi/templates/nbi_service_order_create.json.j2 new file mode 100644 index 0000000..aee124d --- /dev/null +++ b/src/onapsdk/nbi/templates/nbi_service_order_create.json.j2 @@ -0,0 +1,28 @@ +{ + "externalId": "{{ external_id }}", + "priority": "1", + "description": "{{ service_specification.name }} order for {{ customer.global_customer_id }} customer via Python ONAP SDK", + "category": "Consumer", + "requestedStartDate": "{{ request_time }}", + "requestedCompletionDate": "{{ request_time }}", + "relatedParty": [ + { + "id": "{{ customer.global_customer_id }}", + "role": "ONAPcustomer", + "name": "{{ customer.global_customer_id }}" + } + ], + "orderItem": [ + { + "id": "1", + "action": "add", + "service": { + "name": "{{ service_instance_name }}", + "serviceState": "active", + "serviceSpecification": { + "id": "{{ service_specification.unique_id }}" + } + } + } + ] +}
\ No newline at end of file diff --git a/src/onapsdk/onap_service.py b/src/onapsdk/onap_service.py new file mode 100644 index 0000000..9298715 --- /dev/null +++ b/src/onapsdk/onap_service.py @@ -0,0 +1,327 @@ +"""ONAP Service 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 abc import ABC +from dataclasses import dataclass, field +from typing import Any, Callable, Dict, Iterator, List, Optional, Union + +import logging +import requests +import urllib3 +from urllib3.util.retry import Retry +import simplejson.errors + +from requests.adapters import HTTPAdapter +from requests import ( # pylint: disable=redefined-builtin + HTTPError, RequestException, ConnectionError +) + +from onapsdk.exceptions import ( + RequestError, APIError, ResourceNotFound, InvalidResponse, + ConnectionFailed, NoGuiError +) + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +class OnapService(ABC): + """ + Mother Class of all ONAP services. + + An important attribute when inheriting from this class is `_jinja_env`. + it allows to fetch simply jinja templates where they are. + by default jinja engine will look for templates in `templates` directory of + the package. + See in Examples to see how to use. + + Attributes: + server (str): nickname of the server we send the request. Used in logs + strings. For example, 'SDC' is the nickame for SDC server. + headers (Dict[str, str]): the headers dictionnary to use. + proxy (Dict[str, str]): the proxy configuration if needed. + permanent_headers (Optional[Dict[str, str]]): optional dictionary of + headers which could be set by the user and which are **always** + added into sended request. Unlike the `headers`, which could be + overrided on `send_message` call these headers are constant. + + """ + + @dataclass + class PermanentHeadersCollection: + """Collection to store permanent headers.""" + + ph_dict: Dict[str, Any] = field(default_factory=dict) + ph_call: List[Callable] = field(default_factory=list) + + def __iter__(self) -> Iterator[Dict[str, any]]: + """Iterate through the headers. + + For dictionary based headers just return the dict and + for the callables iterate through the list of them, + call them and yield the result. + """ + yield self.ph_dict + for ph_call in self.ph_call: + yield ph_call() + + _logger: logging.Logger = logging.getLogger(__qualname__) + server: str = None + headers: Dict[str, str] = { + "Content-Type": "application/json", + "Accept": "application/json", + } + proxy: Dict[str, str] = None + permanent_headers: PermanentHeadersCollection = PermanentHeadersCollection() + + def __init_subclass__(cls): + """Subclass initialization. + + Add _logger property for any OnapService with it's class name as a logger name + """ + super().__init_subclass__() + cls._logger: logging.Logger = logging.getLogger(cls.__qualname__) + + def __init__(self) -> None: + """Initialize the service.""" + + @classmethod + def send_message(cls, method: str, action: str, url: str, # pylint: disable=too-many-locals + **kwargs) -> Union[requests.Response, None]: + """ + Send a message to an ONAP service. + + Args: + method (str): which method to use (GET, POST, PUT, PATCH, ...) + action (str): what action are we doing, used in logs strings. + url (str): the url to use + exception (Exception, optional): if an error occurs, raise the + exception given instead of RequestError + **kwargs: Arbitrary keyword arguments. any arguments used by + requests can be used here. + + Raises: + RequestError: if other exceptions weren't caught or didn't raise, + or if there was an ambiguous exception by a request + ResourceNotFound: 404 returned + APIError: returned an error code within 400 and 599, except 404 + ConnectionFailed: connection can't be established + + Returns: + the request response if OK + + """ + cert = kwargs.pop('cert', None) + basic_auth: Dict[str, str] = kwargs.pop('basic_auth', None) + exception = kwargs.pop('exception', None) + headers = kwargs.pop('headers', cls.headers).copy() + if OnapService.permanent_headers: + for header in OnapService.permanent_headers: + headers.update(header) + data = kwargs.get('data', None) + try: + # build the request with the requested method + session = cls.__requests_retry_session() + if cert: + session.cert = cert + OnapService._set_basic_auth_if_needed(basic_auth, session) + + cls._logger.debug("[%s][%s] sent header: %s", cls.server, action, + headers) + cls._logger.debug("[%s][%s] url used: %s", cls.server, action, url) + cls._logger.debug("[%s][%s] data sent: %s", cls.server, action, + data) + + response = session.request(method, + url, + headers=headers, + verify=False, + proxies=cls.proxy, + **kwargs) + + cls._logger.info( + "[%s][%s] response code: %s", + cls.server, action, + response.status_code if response is not None else "n/a") + cls._logger.debug( + "[%s][%s] response: %s", + cls.server, action, + response.text if (response is not None and + response.headers.get("Content-Type", "") in \ + ["application/json", "text/plain"]) else "n/a") + + response.raise_for_status() + return response + + except HTTPError as cause: + cls._logger.error("[%s][%s] API returned and error: %s", + cls.server, action, headers) + + msg = f'Code: {cause.response.status_code}. Info: {cause.response.text}.' + + if cause.response.status_code == 404: + exc = ResourceNotFound(msg) + else: + exc = APIError(msg) + + exc.response_status_code = cause.response.status_code + + raise exc from cause + + except ConnectionError as cause: + cls._logger.error("[%s][%s] Failed to connect: %s", cls.server, + action, cause) + + msg = f"Can't connect to {url}." + raise ConnectionFailed(msg) from cause + + except RequestException as cause: + cls._logger.error("[%s][%s] Request failed: %s", + cls.server, action, cause) + + if not exception: + msg = f"Ambiguous error while requesting {url}." + raise RequestError(msg) + + raise exception + + @classmethod + def _set_basic_auth_if_needed(cls, basic_auth, session): + if basic_auth: + session.auth = (basic_auth.get('username'), + basic_auth.get('password')) + + @classmethod + def send_message_json(cls, method: str, action: str, url: str, + **kwargs) -> Dict[Any, Any]: + """ + Send a message to an ONAP service and parse the response as JSON. + + Args: + method (str): which method to use (GET, POST, PUT, PATCH, ...) + action (str): what action are we doing, used in logs strings. + url (str): the url to use + exception (Exception, optional): if an error occurs, raise the + exception given + **kwargs: Arbitrary keyword arguments. any arguments used by + requests can be used here. + + Raises: + InvalidResponse: if JSON coudn't be decoded + RequestError: if other exceptions weren't caught or didn't raise + APIError/ResourceNotFound: send_message() got an HTTP error code + ConnectionFailed: connection can't be established + RequestError: send_message() raised an ambiguous exception + + + Returns: + the response body in dict format if OK + + """ + exception = kwargs.get('exception', None) + try: + + response = cls.send_message(method, action, url, **kwargs) + + if response: + return response.json() + + except simplejson.errors.JSONDecodeError as cause: + cls._logger.error("[%s][%s]Failed to decode JSON: %s", cls.server, + action, cause) + raise InvalidResponse from cause + + except RequestError as exc: + cls._logger.error("[%s][%s] request failed: %s", + cls.server, action, exc) + if not exception: + exception = exc + + raise exception + + @staticmethod + def __requests_retry_session(retries: int = 10, + backoff_factor: float = 0.3, + session: requests.Session = None + ) -> requests.Session: + """ + Create a request Session with retries. + + Args: + retries (int, optional): number of retries. Defaults to 10. + backoff_factor (float, optional): backoff_factor. Defaults to 0.3. + session (requests.Session, optional): an existing session to + enhance. Defaults to None. + + Returns: + requests.Session: the session with retries set + + """ + session = session or requests.Session() + retry = Retry( + total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + ) + adapter = HTTPAdapter(max_retries=retry) + session.mount('http://', adapter) + session.mount('https://', adapter) + return session + + @staticmethod + def set_proxy(proxy: Dict[str, str]) -> None: + """ + Set the proxy for Onap Services rest calls. + + Args: + proxy (Dict[str, str]): the proxy configuration + + Examples: + >>> OnapService.set_proxy({ + ... 'http': 'socks5h://127.0.0.1:8082', + ... 'https': 'socks5h://127.0.0.1:8082'}) + + """ + OnapService.proxy = proxy + + @staticmethod + def set_header(header: Optional[Union[Dict[str, Any], Callable]] = None) -> None: + """Set the header which will be always send on request. + + The header can be: + * dictionary - will be used same dictionary for each request + * callable - a method which is going to be called every time on request + creation. Could be useful if you need to connect with ONAP through some API + gateway and you need to take care about authentication. The callable shouldn't + require any parameters + * None - reset headers + + Args: + header (Optional[Union[Dict[str, Any], Callable]]): header to set. Defaults to None + + """ + if not header: + OnapService._logger.debug("Reset headers") + OnapService.permanent_headers = OnapService.PermanentHeadersCollection() + return + if callable(header): + OnapService.permanent_headers.ph_call.append(header) + else: + OnapService.permanent_headers.ph_dict.update(header) + OnapService._logger.debug("Set permanent header %s", header) + + @classmethod + def get_guis(cls): + """Return the list of GUI and its status.""" + raise NoGuiError diff --git a/src/onapsdk/sdc/__init__.py b/src/onapsdk/sdc/__init__.py new file mode 100644 index 0000000..15280d9 --- /dev/null +++ b/src/onapsdk/sdc/__init__.py @@ -0,0 +1,486 @@ +"""SDC Element 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 typing import Any, Dict, List, Optional, Union +from operator import attrgetter +from abc import ABC, abstractmethod + +from requests import Response + +from onapsdk.configuration import settings +from onapsdk.exceptions import APIError, RequestError +from onapsdk.onap_service import OnapService +import onapsdk.constants as const +from onapsdk.utils.jinja import jinja_env +from onapsdk.utils.gui import GuiItem, GuiList + +class SDC(OnapService, ABC): + """Mother Class of all SDC elements.""" + + server: str = "SDC" + base_front_url = settings.SDC_FE_URL + base_back_url = settings.SDC_BE_URL + + def __init__(self, name: str = None) -> None: + """Initialize SDC.""" + super().__init__() + self.name: str = name + + def __eq__(self, other: Any) -> bool: + """ + Check equality for SDC and children. + + Args: + other: another object + + Returns: + bool: True if same object, False if not + + """ + if isinstance(other, type(self)): + return self.name == other.name + return False + + @classmethod + @abstractmethod + def _get_all_url(cls) -> str: + """ + Get URL for all elements in SDC. + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @classmethod + @abstractmethod + def _get_objects_list(cls, + result: List[Dict[str, Any]]) -> List['SdcResource']: + """ + Import objects created in SDC. + + Args: + result (Dict[str, Any]): the result returned by SDC in a Dict + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @classmethod + @abstractmethod + def _base_url(cls) -> str: + """ + Give back the base url of Sdc. + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @classmethod + @abstractmethod + def _base_create_url(cls) -> str: + """ + Give back the base url of Sdc. + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @abstractmethod + def _copy_object(self, obj: 'SDC') -> None: + """ + Copy relevant properties from object. + + Args: + obj (Sdc): the object to "copy" + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @classmethod + @abstractmethod + def import_from_sdc(cls, values: Dict[str, Any]) -> 'SDC': + """ + Import Sdc object from SDC. + + Args: + values (Dict[str, Any]): dict to parse returned from SDC. + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @staticmethod + def _get_mapped_version(item: "SDC") -> Optional[Union[float, str]]: + """Map Sdc objects version to float. + + Mostly we need to get the newest version of the requested objects. To do + so we use the version property of them. In most cases it's string + formatted float value, but in some cases (like VSP objects) it isn't. + That method checks if given object has "version" attribute and if it's not + a None it tries to map it's value to float. If it's not possible it + returns the alrady existing value. + + Args: + item (SDC): SDC item to map version to float + + Returns: + Optional[Union[float, str]]: Float format version if possible, + string otherwise. If object doesn't have "version" + attribut returns None. + + """ + if hasattr(item, "version") and item.version is not None: + try: + return float(item.version) + except ValueError: + return item.version + else: + return None + + @classmethod + def get_all(cls, **kwargs) -> List['SDC']: + """ + Get the objects list created in SDC. + + Returns: + the list of the objects + + """ + cls._logger.info("retrieving all objects of type %s from SDC", + cls.__name__) + url = cls._get_all_url() + objects = [] + + try: + result = \ + cls.send_message_json('GET', "get {}s".format(cls.__name__), + url, **kwargs) + + for obj_info in cls._get_objects_list(result): + objects.append(cls.import_from_sdc(obj_info)) + + except APIError as exc: + cls._logger.debug("Couldn't get %s: %s", cls.__name__, exc) + except KeyError as exc: + cls._logger.debug("Invalid result dictionary: %s", exc) + + cls._logger.debug("number of %s returned: %s", cls.__name__, + len(objects)) + return objects + + def exists(self) -> bool: + """ + Check if object already exists in SDC and update infos. + + Returns: + True if exists, False either + + """ + self._logger.debug("check if %s %s exists in SDC", + type(self).__name__, self.name) + objects = self.get_all() + + self._logger.debug("filtering objects of all versions to be %s", + self.name) + relevant_objects = list(filter(lambda obj: obj == self, objects)) + + if not relevant_objects: + + self._logger.info("%s %s doesn't exist in SDC", + type(self).__name__, self.name) + return False + + if hasattr(self, 'version_filter') and self.version_filter is not None: # pylint: disable=no-member + + self._logger.debug("filtering %s objects by version %s", + self.name, self.version_filter) # pylint: disable=no-member + + all_versioned = filter( + lambda obj: obj.version == self.version_filter, relevant_objects) # pylint: disable=no-member + + try: + versioned_object = next(all_versioned) + except StopIteration: + self._logger.info("Version %s of %s %s, doesn't exist in SDC", + self.version_filter, type(self).__name__, # pylint: disable=no-member + self.name) + return False + + else: + versioned_object = max(relevant_objects, key=self._get_mapped_version) + + self._logger.info("%s found, updating information", type(self).__name__) + self._copy_object(versioned_object) + return True + + @classmethod + def get_guis(cls) -> GuiItem: + """Retrieve the status of the SDC GUIs. + + Only one GUI is referenced for SDC + the SDC Front End + + Return the list of GUIs + """ + gui_url = settings.SDC_GUI_SERVICE + sdc_gui_response = cls.send_message( + "GET", "Get SDC GUI Status", gui_url) + guilist = GuiList([]) + guilist.add(GuiItem( + gui_url, + sdc_gui_response.status_code)) + return guilist + +class SdcOnboardable(SDC, ABC): + """Base class for onboardable SDC resources (Vendors, Services, ...).""" + + ACTION_TEMPLATE: str + ACTION_METHOD: str + + def __init__(self, name: str = None) -> None: + """Initialize the object.""" + super().__init__(name) + self._identifier: str = None + self._status: str = None + self._version: str = None + + @property + def identifier(self) -> str: + """Return and lazy load the identifier.""" + if not self._identifier: + self.load() + return self._identifier + + @property + def status(self) -> str: + """Return and lazy load the status.""" + if self.created() and not self._status: + self.load() + return self._status + + @property + def version(self) -> str: + """Return and lazy load the version.""" + if self.created() and not self._version: + self.load() + return self._version + + @identifier.setter + def identifier(self, value: str) -> None: + """Set value for identifier.""" + self._identifier = value + + @status.setter + def status(self, status: str) -> None: + """Return and lazy load the status.""" + self._status = status + + @version.setter + def version(self, version: str) -> None: + """Return and lazy load the status.""" + self._version = version + + def created(self) -> bool: + """Determine if SDC is created.""" + if self.name and not self._identifier: + return self.exists() + return bool(self._identifier) + + def submit(self) -> None: + """Submit the SDC object in order to enable it.""" + self._logger.info("attempting to certify/sumbit %s %s in SDC", + type(self).__name__, self.name) + if self.status != const.CERTIFIED and self.created(): + self._really_submit() + elif self.status == const.CERTIFIED: + self._logger.warning("%s %s in SDC is already submitted/certified", + type(self).__name__, self.name) + elif not self.created(): + self._logger.warning("%s %s in SDC is not created", + type(self).__name__, self.name) + + def _create(self, template_name: str, **kwargs) -> None: + """Create the object in SDC if not already existing.""" + self._logger.info("attempting to create %s %s in SDC", + type(self).__name__, self.name) + if not self.exists(): + url = "{}/{}".format(self._base_create_url(), self._sdc_path()) + template = jinja_env().get_template(template_name) + data = template.render(**kwargs) + try: + create_result = self.send_message_json('POST', + "create {}".format( + type(self).__name__), + url, + data=data) + except RequestError as exc: + self._logger.error( + "an error occured during creation of %s %s in SDC", + type(self).__name__, self.name) + raise exc + else: + self._logger.info("%s %s is created in SDC", + type(self).__name__, self.name) + self._status = const.DRAFT + self.identifier = self._get_identifier_from_sdc(create_result) + self._version = self._get_version_from_sdc(create_result) + self.update_informations_from_sdc_creation(create_result) + + else: + self._logger.warning("%s %s is already created in SDC", + type(self).__name__, self.name) + + def _action_to_sdc(self, action: str, action_type: str = None, + **kwargs) -> Response: + """ + Really do an action in the SDC. + + Args: + action (str): the action to perform + action_type (str, optional): the type of action + headers (Dict[str, str], optional): headers to use if any + + Returns: + Response: the response + + """ + subpath = self._generate_action_subpath(action) + url = self._action_url(self._base_create_url(), + subpath, + self._version_path(), + action_type=action_type) + template = jinja_env().get_template(self.ACTION_TEMPLATE) + data = template.render(action=action, const=const) + + return self.send_message(self.ACTION_METHOD, + "{} {}".format(action, + type(self).__name__), + url, + data=data, + **kwargs) + + @abstractmethod + def update_informations_from_sdc(self, details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC. + + Args: + details ([type]): [description] + + """ + @abstractmethod + def update_informations_from_sdc_creation(self, + details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC after creation. + + Args: + details ([type]): the details from SDC + + """ + + @abstractmethod + def load(self) -> None: + """ + Load Object information from SDC. + + Raises: + NotImplementedError: this is an abstract method. + + """ + + @abstractmethod + def _get_version_from_sdc(self, sdc_infos: Dict[str, Any]) -> str: + """ + Get version from SDC results. + + Args: + sdc_infos (Dict[str, Any]): the result dict from SDC + + Raises: + NotImplementedError: this is an abstract method. + + """ + @abstractmethod + def _get_identifier_from_sdc(self, sdc_infos: Dict[str, Any]) -> str: + """ + Get identifier from SDC results. + + Args: + sdc_infos (Dict[str, Any]): the result dict from SDC + + Raises: + NotImplementedError: this is an abstract method. + + """ + @abstractmethod + def _generate_action_subpath(self, action: str) -> str: + """ + + Generate subpath part of SDC action url. + + Args: + action (str): the action that will be done + + Raises: + NotImplementedError: this is an abstract method. + + """ + @abstractmethod + def _version_path(self) -> str: + """ + Give the end of the path for a version. + + Raises: + NotImplementedError: this is an abstract method. + + """ + @abstractmethod + def _really_submit(self) -> None: + """Really submit the SDC Vf in order to enable it.""" + @staticmethod + @abstractmethod + def _action_url(base: str, + subpath: str, + version_path: str, + action_type: str = None) -> str: + """ + Generate action URL for SDC. + + Raises: + NotImplementedError: this is an abstract method. + + """ + @classmethod + @abstractmethod + def _sdc_path(cls) -> None: + """Give back the end of SDC path.""" + + @abstractmethod + def onboard(self) -> None: + """Onboard resource. + + Onboarding is a full stack of actions which needs to be done to + make SDC resource ready to use. It depends on the type of object + but most of them needs to be created and submitted. + """ diff --git a/src/onapsdk/sdc/category_management.py b/src/onapsdk/sdc/category_management.py new file mode 100644 index 0000000..c2d63b4 --- /dev/null +++ b/src/onapsdk/sdc/category_management.py @@ -0,0 +1,285 @@ +"""SDC category management 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. + +import json +from abc import ABC, abstractmethod +from typing import Any, Dict, List + +from onapsdk.configuration import settings +from onapsdk.exceptions import ResourceNotFound +from onapsdk.sdc import SDC +from onapsdk.utils.headers_creator import headers_sdc_generic + + +class BaseCategory(SDC, ABC): # pylint: disable=too-many-instance-attributes + """Base SDC category class. + + It's SDC admin resource, has no common properties with + SDC resourcer or elements, so SDC class can't be it's + base class. + + """ + + SDC_ADMIN_USER = "demo" + + def __init__(self, name: str) -> None: + """Service category initialization. + + Args: + name (str): Service category name. + + """ + super().__init__(name) + self.normalized_name: str = None + self.unique_id: str = None + self.icons: List[str] = None + self.subcategories: List[Dict[str, str]] = None + self.version: str = None + self.owner_id: str = None + self.empty: bool = None + self.type: str = None + + @classmethod + def _get_all_url(cls) -> str: + """Get URL for all categories in SDC.""" + return f"{cls.base_front_url}/sdc1/feProxy/rest/v1/setup/ui" + + @classmethod + def _base_url(cls) -> str: + """Give back the base url of Sdc.""" + return f"{settings.SDC_FE_URL}/sdc1/feProxy/rest/v1/category" + + @classmethod + def headers(cls) -> Dict[str, str]: + """Headers used for category management. + + It uses SDC admin user. + + Returns: + Dict[str, str]: Headers + + """ + return headers_sdc_generic(super().headers, user=cls.SDC_ADMIN_USER) + + @classmethod + def get_all(cls, **kwargs) -> List['SDC']: + """ + Get the categories list created in SDC. + + Returns: + the list of the categories + + """ + return super().get_all(headers=cls.headers()) + + @classmethod + def import_from_sdc(cls, values: Dict[str, Any]) -> 'BaseCategory': + """ + Import category object from SDC. + + Args: + values (Dict[str, Any]): dict to parse returned from SDC. + + """ + category_obj = cls(name=values["name"]) + category_obj.normalized_name = values["normalizedName"] + category_obj.unique_id = values["uniqueId"] + category_obj.icons = values["icons"] + category_obj.subcategories = values["subcategories"] + category_obj.version = values["version"] + category_obj.owner_id = values["ownerId"] + category_obj.empty = values["empty"] + return category_obj + + @classmethod + @abstractmethod + def category_name(cls) -> str: + """Class category name. + + Used for logs. + + Returns: + str: Category name + + """ + + @classmethod + def get(cls, name: str) -> "BaseCategory": + """Get category with given name. + + Raises: + ResourceNotFound: Category with given name does not exist + + Returns: + BaseCategory: BaseCategory instance + + """ + category_obj: "BaseCategory" = cls(name) + if category_obj.exists(): + return category_obj + msg = f"{cls.category_name()} with \"{name}\" name does not exist." + raise ResourceNotFound(msg) + + @classmethod + def create(cls, name: str) -> "BaseCategory": + """Create category instance. + + Checks if category with given name exists and if it already + exists just returns category with given name. + + Returns: + BaseCategory: Created category instance + + """ + category_obj: "BaseCategory" = cls(name) + if category_obj.exists(): + return category_obj + cls.send_message_json("POST", + f"Create {name} {cls.category_name()}", + cls._base_create_url(), + data=json.dumps({"name": name}), + headers=cls.headers()) + category_obj.exists() + return category_obj + + def _copy_object(self, obj: 'BaseCategory') -> None: + """ + Copy relevant properties from object. + + Args: + obj (BaseCategory): the object to "copy" + + """ + self.name = obj.name + self.normalized_name = obj.normalized_name + self.unique_id = obj.unique_id + self.icons = obj.icons + self.subcategories = obj.subcategories + self.version = obj.version + self.owner_id = obj.owner_id + self.empty = obj.empty + + +class ResourceCategory(BaseCategory): + """Resource category class.""" + + @classmethod + def _get_objects_list(cls, + result: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + Get list of resource categories. + + Args: + result (List[Dict[str, Any]]): the result returned by SDC + in a list of dicts + + Raises: + KeyError: Invalid result dictionary + + """ + return result["categories"]["resourceCategories"] + + @classmethod + def _base_create_url(cls) -> str: + """Url to create resource category. + + Returns: + str: Creation url + + """ + return f"{cls._base_url()}/resources" + + @classmethod + def category_name(cls) -> str: + """Resource category name. + + Used for logging. + + Returns: + str: Resource category name + + """ + return "Resource Category" + + @classmethod + def get(cls, name: str, subcategory: str = None) -> "ResourceCategory": # pylint: disable=arguments-differ + """Get resource category with given name. + + It returns resource category with all subcategories by default. You can + get resource category with only one subcategory if you provide it's + name as `subcategory` parameter. + + Args: + name (str): Resource category name. + subcategory (str, optional): Name of subcategory. Defaults to None. + + Raises: + ResourceNotFound: Subcategory with given name does not exist + + Returns: + BaseCategory: BaseCategory instance + + """ + category_obj: "ResourceCategory" = super().get(name=name) + if not subcategory: + return category_obj + filtered_subcategories: Dict[str, str] = list(filter(lambda x: x["name"] == subcategory, + category_obj.subcategories)) + if not filtered_subcategories: + raise ResourceNotFound(f"Subcategory {subcategory} does not exist.") + category_obj.subcategories = filtered_subcategories + return category_obj + + +class ServiceCategory(BaseCategory): + """Service category class.""" + + @classmethod + def _get_objects_list(cls, + result: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + Get list of service categories. + + Args: + result (List[Dict[str, Any]]): the result returned by SDC + in a list of dicts + + Raises: + KeyError: Invalid result dictionary + + """ + return result["categories"]["serviceCategories"] + + @classmethod + def _base_create_url(cls) -> str: + """Url to create service category. + + Returns: + str: Creation url + + """ + return f"{cls._base_url()}/services" + + @classmethod + def category_name(cls) -> str: + """Service category name. + + Used for logging. + + Returns: + str: Service category name + + """ + return "Service Category" diff --git a/src/onapsdk/sdc/component.py b/src/onapsdk/sdc/component.py new file mode 100644 index 0000000..9b26bef --- /dev/null +++ b/src/onapsdk/sdc/component.py @@ -0,0 +1,162 @@ +"""SDC Component 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 Any, Dict, Iterator, List, Optional +from onapsdk.exceptions import ParameterError + +from onapsdk.sdc.properties import ComponentProperty +from onapsdk.utils.jinja import jinja_env + + +@dataclass +class Component: # pylint: disable=too-many-instance-attributes + """Component dataclass.""" + + created_from_csar: bool + actual_component_uid: str + unique_id: str + normalized_name: str + name: str + origin_type: str + customization_uuid: str + component_uid: str + component_version: str + tosca_component_name: str + component_name: str + group_instances: Optional[List[Dict[str, Any]]] + sdc_resource: "SdcResource" + parent_sdc_resource: "SdcResource" + + @classmethod + def create_from_api_response(cls, + api_response: Dict[str, Any], + sdc_resource: "SdcResource", + parent_sdc_resource: "SdcResource") -> "Component": + """Create component from api response. + + Args: + api_response (Dict[str, Any]): component API response + sdc_resource (SdcResource): component's SDC resource + parent_sdc_resource (SdcResource): component's parent SDC resource + + Returns: + Component: Component created using api_response and SDC resource + + """ + return cls(created_from_csar=api_response["createdFromCsar"], + actual_component_uid=api_response["actualComponentUid"], + unique_id=api_response["uniqueId"], + normalized_name=api_response["normalizedName"], + name=api_response["name"], + origin_type=api_response["originType"], + customization_uuid=api_response["customizationUUID"], + component_uid=api_response["componentUid"], + component_version=api_response["componentVersion"], + tosca_component_name=api_response["toscaComponentName"], + component_name=api_response["componentName"], + group_instances=api_response["groupInstances"], + sdc_resource=sdc_resource, + parent_sdc_resource=parent_sdc_resource) + + @property + def properties_url(self) -> str: + """Url to get component's properties. + + Returns: + str: Compoent's properties url + + """ + return self.parent_sdc_resource.get_component_properties_url(self) + + @property + def properties_value_url(self) -> str: + """Url to set component property value. + + Returns: + str: Url to set component property value + + """ + return self.parent_sdc_resource.get_component_properties_value_set_url(self) + + @property + def properties(self) -> Iterator["ComponentProperty"]: + """Component properties. + + In SDC it's named as properties, but we uses "inputs" endpoint to fetch them. + Structure is also input's like, but it's a property. + + Yields: + ComponentProperty: Component property object + + """ + for component_property in self.sdc_resource.send_message_json(\ + "GET", + f"Get {self.name} component properties", + self.properties_url): + yield ComponentProperty(unique_id=component_property["uniqueId"], + name=component_property["name"], + property_type=component_property["type"], + _value=component_property.get("value"), + component=self) + + def get_property(self, property_name: str) -> "ComponentProperty": + """Get component property by it's name. + + Args: + property_name (str): property name + + Raises: + ParameterError: Component has no property with given name + + Returns: + ComponentProperty: Component's property object + + """ + for property_obj in self.properties: + if property_obj.name == property_name: + return property_obj + msg = f"Component has no property with {property_name} name" + raise ParameterError(msg) + + def set_property_value(self, property_obj: "ComponentProperty", value: Any) -> None: + """Set property value. + + Set given value to component property + + Args: + property_obj (ComponentProperty): Component property object + value (Any): Property value to set + + """ + self.sdc_resource.send_message_json( + "POST", + f"Set {self.name} component property {property_obj.name} value", + self.properties_value_url, + data=jinja_env().get_template(\ + "sdc_resource_component_set_property_value.json.j2").\ + render( + component=self, + value=value, + property=property_obj + ) + ) + + def delete(self) -> None: + """Delete component.""" + self.sdc_resource.send_message_json( + "DELETE", + f"Delete {self.name} component", + f"{self.parent_sdc_resource.resource_inputs_url}/resourceInstance/{self.unique_id}" + ) diff --git a/src/onapsdk/sdc/pnf.py b/src/onapsdk/sdc/pnf.py new file mode 100644 index 0000000..3fe4657 --- /dev/null +++ b/src/onapsdk/sdc/pnf.py @@ -0,0 +1,74 @@ +"""Pnf 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 typing import Dict, List, Union +from onapsdk.exceptions import ParameterError + +from onapsdk.sdc.sdc_resource import SdcResource +from onapsdk.sdc.properties import NestedInput, Property +import onapsdk.constants as const +from onapsdk.sdc.vendor import Vendor +from onapsdk.sdc.vsp import Vsp + + +class Pnf(SdcResource): + """ + ONAP PNF Object used for SDC operations. + + Attributes: + name (str): the name of the pnf. Defaults to "ONAP-test-PNF". + identifier (str): the unique ID of the pnf from SDC. + status (str): the status of the pnf from SDC. + version (str): the version ID of the vendor from SDC. + uuid (str): the UUID of the PNF (which is different from identifier, + don't ask why...) + unique_identifier (str): Yet Another ID, just to puzzle us... + vendor (optional): the vendor of the PNF + vsp (optional): the vsp related to the PNF + + """ + + def __init__(self, name: str = None, version: str = None, vendor: Vendor = None, # pylint: disable=too-many-arguments + sdc_values: Dict[str, str] = None, vsp: Vsp = None, + properties: List[Property] = None, inputs: Union[Property, NestedInput] = None, + category: str = None, subcategory: str = None): + """ + Initialize pnf object. + + Args: + name (optional): the name of the pnf + version (str, optional): the version of a PNF object + + """ + super().__init__(sdc_values=sdc_values, version=version, properties=properties, + inputs=inputs, category=category, subcategory=subcategory) + self.name: str = name or "ONAP-test-PNF" + self.vendor: Vendor = vendor + self.vsp: Vsp = vsp + + def create(self) -> None: + """Create the PNF in SDC if not already existing.""" + if not self.vsp and not self.vendor: + raise ParameterError("Neither Vsp nor Vendor provided.") + self._create("pnf_create.json.j2", + name=self.name, + vsp=self.vsp, + vendor=self.vendor, + category=self.category) + + def _really_submit(self) -> None: + """Really submit the SDC PNF in order to enable it.""" + result = self._action_to_sdc(const.CERTIFY, "lifecycleState") + if result: + self.load() diff --git a/src/onapsdk/sdc/properties.py b/src/onapsdk/sdc/properties.py new file mode 100644 index 0000000..f7e07a0 --- /dev/null +++ b/src/onapsdk/sdc/properties.py @@ -0,0 +1,202 @@ +"""Service properties 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, field +from typing import Any, Dict, List, Optional + +from onapsdk.exceptions import ParameterError + + +@dataclass +class Input: + """Property input dataclass.""" + + unique_id: str + input_type: str + name: str + sdc_resource: "SdcResource" + _default_value: Optional[Any] = field(repr=False, default=None) + + @property + def default_value(self) -> Any: + """Input default value. + + Returns: + Any: Input default value + + """ + return self._default_value + + @default_value.setter + def default_value(self, value: Any) -> None: + """Set input default value. + + Use related sdc_resource "set_input_default_value" + method to set default value in SDC. We use that + because different types of SDC resources has different + urls to call SDC API. + + Args: + value (Any): Default value to set + + """ + self.sdc_resource.set_input_default_value(self, value) + self._default_value = value + +@dataclass +class NestedInput: + """Dataclass used for nested input declaration.""" + + sdc_resource: "SdcResource" + input_obj: Input + + +class Property: # pylint: disable=too-many-instance-attributes, too-few-public-methods + """Service property class.""" + + def __init__(self, # pylint: disable=too-many-arguments + name: str, + property_type: str, + description: Optional[str] = None, + unique_id: Optional[str] = None, + parent_unique_id: Optional[str] = None, + sdc_resource: Optional["SdcResource"] = None, + value: Optional[Any] = None, + get_input_values: Optional[List[Dict[str, str]]] = None) -> None: + """Property class initialization. + + Args: + property_type (str): [description] + description (Optional[str], optional): [description]. Defaults to None. + unique_id (Optional[str], optional): [description]. Defaults to None. + parent_unique_id (Optional[str], optional): [description]. Defaults to None. + sdc_resource (Optional[, optional): [description]. Defaults to None. + value (Optional[Any], optional): [description]. Defaults to None. + get_input_values (Optional[List[Dict[str, str]]], optional): [description]. + Defaults to None. + """ + self.name: str = name + self.property_type: str = property_type + self.description: str = description + self.unique_id: str = unique_id + self.parent_unique_id: str = parent_unique_id + self.sdc_resource: "SdcResource" = sdc_resource + self._value: Any = value + self.get_input_values: List[Dict[str, str]] = get_input_values + + def __repr__(self) -> str: + """Property object human readable representation. + + Returns: + str: Property human readable representation + + """ + return f"Property(name={self.name}, property_type={self.property_type})" + + def __eq__(self, obj: "Property") -> bool: + """Check if two Property object are equal. + + Args: + obj (Property): Object to compare + + Returns: + bool: True if objects are equal, False otherwise + + """ + return self.name == obj.name and self.property_type == obj.property_type + + @property + def input(self) -> Input: + """Property input. + + Returns property Input object. + Returns None if property has no associated input. + + Raises: + ParameterError: Input has no associated SdcResource + + ParameterError: Input for given property does not exits. + It shouldn't ever happen, but it's possible if after you + get property object someone delete input. + + Returns: + Input: Property input object. + + """ + if not self.sdc_resource: + raise ParameterError("Property has no associated SdcResource") + if not self.get_input_values: + return None + try: + return next(filter(lambda x: x.unique_id == self.get_input_values[0].get("inputId"), + self.sdc_resource.inputs)) + except StopIteration: + raise ParameterError("Property input does not exist") + + @property + def value(self) -> Any: + """Value property. + + Get property value. + + Returns: + Any: Property value + + """ + return self._value + + @value.setter + def value(self, val: Any) -> Any: + if self.sdc_resource: + self.sdc_resource.set_property_value(self, val) + self._value = val + + +@dataclass +class ComponentProperty: + """Component property dataclass. + + Component properties are inputs objects in SDC, but in logic + it's a property. + + """ + + unique_id: str + property_type: str + name: str + component: "Component" + _value: Optional[Any] = field(repr=False, default=None) + + @property + def value(self) -> Any: + """Property value getter. + + Returns: + Any: Property value + + """ + return self._value + + @value.setter + def value(self, val: Any) -> None: + """Property value setter. + + Set value both in an object and in SDC using it's HTTP API. + + Args: + val (Any): Property value to set + + """ + self.component.set_property_value(self, val) + self._value = val diff --git a/src/onapsdk/sdc/sdc_element.py b/src/onapsdk/sdc/sdc_element.py new file mode 100644 index 0000000..df513b7 --- /dev/null +++ b/src/onapsdk/sdc/sdc_element.py @@ -0,0 +1,227 @@ +"""SDC Element 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 abc import ABC, abstractmethod +from operator import itemgetter +from typing import Any, Dict, List, Optional + +from onapsdk.sdc import SdcOnboardable +import onapsdk.constants as const + + +class SdcElement(SdcOnboardable, ABC): + """Mother Class of all SDC elements.""" + + ACTION_TEMPLATE = 'sdc_element_action.json.j2' + ACTION_METHOD = 'PUT' + + def __init__(self, name: str = None) -> None: + """Initialize the object.""" + super().__init__(name=name) + self.human_readable_version: Optional[str] = None + + def _get_item_details(self) -> Dict[str, Any]: + """ + Get item details. + + Returns: + Dict[str, Any]: the description of the item + + """ + if self.created(): + url = "{}/items/{}/versions".format(self._base_url(), + self.identifier) + results: Dict[str, Any] = self.send_message_json('GET', 'get item', url) + if results["listCount"] > 1: + items: List[Dict[str, Any]] = results["results"] + return sorted(items, key=itemgetter("creationTime"), reverse=True)[0] + return results["results"][0] + return {} + + def load(self) -> None: + """Load Object information from SDC.""" + vsp_details = self._get_item_details() + if vsp_details: + self._logger.debug("details found, updating") + self.version = vsp_details['id'] + self.human_readable_version = vsp_details["name"] + self.update_informations_from_sdc(vsp_details) + else: + # exists() method check if exists AND update identifier + self.exists() + + def update_informations_from_sdc(self, details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC. + + Args: + details ([type]): [description] + + """ + def update_informations_from_sdc_creation(self, + details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC after creation. + + Args: + details ([type]): the details from SDC + + """ + @classmethod + def _base_url(cls) -> str: + """ + Give back the base url of Sdc. + + Returns: + str: the base url + + """ + return "{}/sdc1/feProxy/onboarding-api/v1.0".format(cls.base_front_url) + + @classmethod + def _base_create_url(cls) -> str: + """ + Give back the base url of Sdc. + + Returns: + str: the base url + + """ + return "{}/sdc1/feProxy/onboarding-api/v1.0".format(cls.base_front_url) + + def _generate_action_subpath(self, action: str) -> str: + """ + + Generate subpath part of SDC action url. + + Args: + action (str): the action that will be done + + Returns: + str: the subpath part + + """ + subpath = self._sdc_path() + if action == const.COMMIT: + subpath = "items" + return subpath + + def _version_path(self) -> str: + """ + Give the end of the path for a version. + + Returns: + str: the end of the path + + """ + return "{}/versions/{}".format(self.identifier, self.version) + + @staticmethod + def _action_url(base: str, + subpath: str, + version_path: str, + action_type: str = None) -> str: + """ + Generate action URL for SDC. + + Args: + base (str): base part of url + subpath (str): subpath of url + version_path (str): version path of the url + action_type (str, optional): the type of action. UNUSED here + + Returns: + str: the URL to use + + """ + return "{}/{}/{}/actions".format(base, subpath, version_path) + + @classmethod + def _get_objects_list(cls, result: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """ + Import objects created in SDC. + + Args: + result (Dict[str, Any]): the result returned by SDC in a Dict + + Return: + List[Dict[str, Any]]: the list of objects + + """ + return result['results'] + + @classmethod + def _get_all_url(cls) -> str: + """ + Get URL for all elements in SDC. + + Returns: + str: the url + + """ + return "{}/{}".format(cls._base_url(), cls._sdc_path()) + + def _copy_object(self, obj: 'SdcElement') -> None: + """ + Copy relevant properties from object. + + Args: + obj (SdcElement): the object to "copy" + + """ + self.identifier = obj.identifier + + def _get_version_from_sdc(self, sdc_infos: Dict[str, Any]) -> str: + """ + Get version from SDC results. + + Args: + sdc_infos (Dict[str, Any]): the result dict from SDC + + Returns: + str: the version + + """ + return sdc_infos['version']['id'] + + def _get_identifier_from_sdc(self, sdc_infos: Dict[str, Any]) -> str: + """ + Get identifier from SDC results. + + Args: + sdc_infos (Dict[str, Any]): the result dict from SDC + + Returns: + str: the identifier + + """ + return sdc_infos['itemId'] + + @classmethod + @abstractmethod + def import_from_sdc(cls, values: Dict[str, Any]) -> 'SdcElement': + """ + Import SdcElement from SDC. + + Args: + values (Dict[str, Any]): dict to parse returned from SDC. + + Raises: + NotImplementedError: this is an abstract method. + + """ + raise NotImplementedError("SdcElement is an abstract class") diff --git a/src/onapsdk/sdc/sdc_resource.py b/src/onapsdk/sdc/sdc_resource.py new file mode 100644 index 0000000..7e7dbb9 --- /dev/null +++ b/src/onapsdk/sdc/sdc_resource.py @@ -0,0 +1,960 @@ +"""SDC Element 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. +import logging +from abc import ABC +from typing import Any, Dict, Iterator, List, Union +import base64 +import time + +import onapsdk.constants as const +from onapsdk.exceptions import ParameterError, ResourceNotFound, StatusError +from onapsdk.sdc import SdcOnboardable +from onapsdk.sdc.category_management import ResourceCategory, ServiceCategory +from onapsdk.sdc.component import Component +from onapsdk.sdc.properties import Input, NestedInput, Property +from onapsdk.utils.headers_creator import (headers_sdc_creator, + headers_sdc_tester, + headers_sdc_artifact_upload) +from onapsdk.utils.jinja import jinja_env + + +# For an unknown reason, pylint keeps seeing _unique_uuid and +# _unique_identifier as attributes along with unique_uuid and unique_identifier +class SdcResource(SdcOnboardable, ABC): # pylint: disable=too-many-instance-attributes, too-many-public-methods + """Mother Class of all SDC resources.""" + + RESOURCE_PATH = 'resources' + ACTION_TEMPLATE = 'sdc_resource_action.json.j2' + ACTION_METHOD = 'POST' + headers = headers_sdc_creator(SdcOnboardable.headers) + + def __init__(self, name: str = None, version: str = None, # pylint: disable=too-many-arguments + sdc_values: Dict[str, str] = None, properties: List[Property] = None, + inputs: Union[Property, NestedInput] = None, + category: str = None, subcategory: str = None): + """Initialize the object.""" + super().__init__(name) + self.version_filter: str = version + self._unique_uuid: str = None + self._unique_identifier: str = None + self._resource_type: str = "resources" + self._properties_to_add: List[Property] = properties or [] + self._inputs_to_add: Union[Property, NestedInput] = inputs or [] + self._time_wait: int = 10 + self._category_name: str = category + self._subcategory_name: str = subcategory + if sdc_values: + self._logger.debug("SDC values given, using them") + self.identifier = sdc_values['uuid'] + self.version = sdc_values['version'] + self.unique_uuid = sdc_values['invariantUUID'] + distribitution_state = None + if 'distributionStatus' in sdc_values: + distribitution_state = sdc_values['distributionStatus'] + self.status = self._parse_sdc_status(sdc_values['lifecycleState'], + distribitution_state, + self._logger) + self._logger.debug("SDC resource %s status: %s", self.name, + self.status) + + def __repr__(self) -> str: + """SDC resource description. + + Returns: + str: SDC resource object description + + """ + return f"{self.__class__.__name__.upper()}(name={self.name})" + + @property + def unique_uuid(self) -> str: + """Return and lazy load the unique_uuid.""" + if not self._unique_uuid: + self.load() + return self._unique_uuid + + @property + def unique_identifier(self) -> str: + """Return and lazy load the unique_identifier.""" + if not self._unique_identifier: + self.deep_load() + return self._unique_identifier + + @unique_uuid.setter + def unique_uuid(self, value: str) -> None: + """Set value for unique_uuid.""" + self._unique_uuid = value + + @unique_identifier.setter + def unique_identifier(self, value: str) -> None: + """Set value for unique_identifier.""" + self._unique_identifier = value + + def load(self) -> None: + """Load Object information from SDC.""" + self.exists() + + def deep_load(self) -> None: + """Deep load Object informations from SDC.""" + url = ( + f"{self.base_front_url}/sdc1/feProxy/rest/v1/" + "screen?excludeTypes=VFCMT&excludeTypes=Configuration" + ) + headers = headers_sdc_creator(SdcResource.headers) + if self.status == const.UNDER_CERTIFICATION: + headers = headers_sdc_tester(SdcResource.headers) + + response = self.send_message_json("GET", + "Deep Load {}".format( + type(self).__name__), + url, + headers=headers) + + for resource in response[self._sdc_path()]: + if resource["invariantUUID"] == self.unique_uuid: + if resource["uuid"] == self.identifier: + self._logger.debug("Resource %s found in %s list", + resource["name"], self._sdc_path()) + self.unique_identifier = resource["uniqueId"] + self._category_name = resource["categories"][0]["name"] + subcategories = resource["categories"][0].get("subcategories", [{}]) + self._subcategory_name = None if subcategories is None else \ + subcategories[0].get("name") + return + if self._sdc_path() == "services": + for dependency in self.send_message_json("GET", + "Get service dependecies", + f"{self._base_create_url()}/services/" + f"{resource['uniqueId']}/" + "dependencies"): + if dependency["version"] == self.version: + self.unique_identifier = dependency["uniqueId"] + return + + def _generate_action_subpath(self, action: str) -> str: + """ + + Generate subpath part of SDC action url. + + Args: + action (str): the action that will be done + + Returns: + str: the subpath part + + """ + return action + + def _version_path(self) -> str: + """ + Give the end of the path for a version. + + Returns: + str: the end of the path + + """ + return self.unique_identifier + + def _action_url(self, + base: str, + subpath: str, + version_path: str, + action_type: str = None) -> str: + """ + Generate action URL for SDC. + + Args: + base (str): base part of url + subpath (str): subpath of url + version_path (str): version path of the url + action_type (str, optional): the type of action + ('distribution', 'distribution-state' + or 'lifecycleState'). Default to + 'lifecycleState'). + + Returns: + str: the URL to use + + """ + if not action_type: + action_type = "lifecycleState" + return "{}/{}/{}/{}/{}".format(base, self._resource_type, version_path, + action_type, subpath) + + @classmethod + def _base_create_url(cls) -> str: + """ + Give back the base url of Sdc. + + Returns: + str: the base url + + """ + return "{}/sdc1/feProxy/rest/v1/catalog".format(cls.base_front_url) + + @classmethod + def _base_url(cls) -> str: + """ + Give back the base url of Sdc. + + Returns: + str: the base url + + """ + return "{}/sdc/v1/catalog".format(cls.base_back_url) + + @classmethod + def _get_all_url(cls) -> str: + """ + Get URL for all elements in SDC. + + Returns: + str: the url + + """ + return "{}/{}?resourceType={}".format(cls._base_url(), cls._sdc_path(), + cls.__name__.upper()) + + @classmethod + def _get_objects_list(cls, result: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """ + Import objects created in SDC. + + Args: + result (Dict[str, Any]): the result returned by SDC in a Dict + + Return: + List[Dict[str, Any]]: the list of objects + + """ + return result + + def _get_version_from_sdc(self, sdc_infos: Dict[str, Any]) -> str: + """ + Get version from SDC results. + + Args: + sdc_infos (Dict[str, Any]): the result dict from SDC + + Returns: + str: the version + + """ + return sdc_infos['version'] + + def _get_identifier_from_sdc(self, sdc_infos: Dict[str, Any]) -> str: + """ + Get identifier from SDC results. + + Args: + sdc_infos (Dict[str, Any]): the result dict from SDC + + Returns: + str: the identifier + + """ + return sdc_infos['uuid'] + + @classmethod + def import_from_sdc(cls, values: Dict[str, Any]) -> 'SdcResource': + """ + Import SdcResource from SDC. + + Args: + values (Dict[str, Any]): dict to parse returned from SDC. + + Return: + SdcResource: the created resource + + """ + cls._logger.debug("importing SDC Resource %s from SDC", values['name']) + return cls(name=values['name'], sdc_values=values) + + def _copy_object(self, obj: 'SdcResource') -> None: + """ + Copy relevant properties from object. + + Args: + obj (SdcResource): the object to "copy" + + """ + self.identifier = obj.identifier + self.unique_uuid = obj.unique_uuid + self.status = obj.status + self.version = obj.version + self.unique_identifier = obj.unique_identifier + self._specific_copy(obj) + + def _specific_copy(self, obj: 'SdcResource') -> None: + """ + Copy specific properties from object. + + Args: + obj (SdcResource): the object to "copy" + + """ + + def update_informations_from_sdc(self, details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC. + + Args: + details ([type]): [description] + + """ + def update_informations_from_sdc_creation(self, + details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC after creation. + + Args: + details ([type]): the details from SDC + + """ + self.unique_uuid = details['invariantUUID'] + distribution_state = None + + if 'distributionStatus' in details: + distribution_state = details['distributionStatus'] + self.status = self._parse_sdc_status(details['lifecycleState'], + distribution_state, self._logger) + self.version = details['version'] + self.unique_identifier = details['uniqueId'] + + # Not my fault if SDC has so many states... + # pylint: disable=too-many-return-statements + @staticmethod + def _parse_sdc_status(sdc_status: str, distribution_state: str, + logger: logging.Logger) -> str: + """ + Parse SDC status in order to normalize it. + + Args: + sdc_status (str): the status found in SDC + distribution_state (str): the distribution status found in SDC. + Can be None. + + Returns: + str: the normalized status + + """ + logger.debug("Parse status for SDC Resource") + if sdc_status.capitalize() == const.CERTIFIED: + if distribution_state and distribution_state == const.SDC_DISTRIBUTED: + return const.DISTRIBUTED + return const.CERTIFIED + if sdc_status == const.NOT_CERTIFIED_CHECKOUT: + return const.DRAFT + if sdc_status == const.NOT_CERTIFIED_CHECKIN: + return const.CHECKED_IN + if sdc_status == const.READY_FOR_CERTIFICATION: + return const.SUBMITTED + if sdc_status == const.CERTIFICATION_IN_PROGRESS: + return const.UNDER_CERTIFICATION + if sdc_status != "": + return sdc_status + return None + + def _really_submit(self) -> None: + """Really submit the SDC Vf in order to enable it.""" + raise NotImplementedError("SDC is an abstract class") + + def onboard(self) -> None: + """Onboard resource in SDC.""" + if not self.status: + self.create() + time.sleep(self._time_wait) + self.onboard() + elif self.status == const.DRAFT: + for property_to_add in self._properties_to_add: + self.add_property(property_to_add) + for input_to_add in self._inputs_to_add: + self.declare_input(input_to_add) + self.submit() + time.sleep(self._time_wait) + self.onboard() + elif self.status == const.CHECKED_IN: + # Checked in status check added + self.certify() + time.sleep(self._time_wait) + self.onboard() + elif self.status == const.CERTIFIED: + self.load() + + @classmethod + def _sdc_path(cls) -> None: + """Give back the end of SDC path.""" + return cls.RESOURCE_PATH + + @property + def deployment_artifacts_url(self) -> str: + """Deployment artifacts url. + + Returns: + str: SdcResource Deployment artifacts url + + """ + return (f"{self._base_create_url()}/resources/" + f"{self.unique_identifier}/filteredDataByParams?include=deploymentArtifacts") + + @property + def add_deployment_artifacts_url(self) -> str: + """Add deployment artifacts url. + + Returns: + str: Url used to add deployment artifacts + + """ + return (f"{self._base_create_url()}/resources/" + f"{self.unique_identifier}/artifacts") + + @property + def properties_url(self) -> str: + """Properties url. + + Returns: + str: SdcResource properties url + + """ + return (f"{self._base_create_url()}/resources/" + f"{self.unique_identifier}/filteredDataByParams?include=properties") + + @property + def add_property_url(self) -> str: + """Add property url. + + Returns: + str: Url used to add property + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}/properties") + + @property + def set_input_default_value_url(self) -> str: + """Url to set input default value. + + Returns: + str: SDC API url used to set input default value + + """ + return (f"{self._base_create_url()}/resources/" + f"{self.unique_identifier}/update/inputs") + + @property + def origin_type(self) -> str: + """Resource origin type. + + Value needed for composition. It's used for adding SDC resource + as an another SDC resource component. + + Returns: + str: SDC resource origin type + + """ + return type(self).__name__.upper() + + @property + def properties(self) -> Iterator[Property]: + """SDC resource properties. + + Iterate resource properties. + + Yields: + Property: Resource property + + """ + for property_data in self.send_message_json(\ + "GET", + f"Get {self.name} resource properties", + self.properties_url).get("properties", []): + yield Property( + sdc_resource=self, + unique_id=property_data["uniqueId"], + name=property_data["name"], + property_type=property_data["type"], + parent_unique_id=property_data["parentUniqueId"], + value=property_data.get("value"), + description=property_data.get("description"), + get_input_values=property_data.get("getInputValues"), + ) + + def get_property(self, property_name: str) -> Property: + """Get resource property by it's name. + + Args: + property_name (str): property name + + Raises: + ResourceNotFound: Resource has no property with given name + + Returns: + Property: Resource's property object + + """ + for property_obj in self.properties: + if property_obj.name == property_name: + return property_obj + + msg = f"Resource has no property with {property_name} name" + raise ResourceNotFound(msg) + + @property + def resource_inputs_url(self) -> str: + """Resource inputs url. + + Method which returns url which point to resource inputs. + + Returns: + str: Resource inputs url + + """ + return (f"{self._base_create_url()}/resources/" + f"{self.unique_identifier}") + + + def create(self) -> None: + """Create resource. + + Abstract method which should be implemented by subclasses and creates resource in SDC. + + Raises: + NotImplementedError: Method not implemented by subclasses. + + """ + raise NotImplementedError + + @property + def inputs(self) -> Iterator[Input]: + """SDC resource inputs. + + Iterate resource inputs. + + Yields: + Iterator[Input]: Resource input + + """ + url = f"{self.resource_inputs_url}/filteredDataByParams?include=inputs" + for input_data in self.send_message_json(\ + "GET", f"Get {self.name} resource inputs", + url).get("inputs", []): + + yield Input( + unique_id=input_data["uniqueId"], + input_type=input_data["type"], + name=input_data["name"], + sdc_resource=self, + _default_value=input_data.get("defaultValue") + ) + + def get_input(self, input_name: str) -> Input: + """Get input by it's name. + + Args: + input_name (str): Input name + + Raises: + ResourceNotFound: Resource doesn't have input with given name + + Returns: + Input: Found input object + + """ + for input_obj in self.inputs: + if input_obj.name == input_name: + return input_obj + raise ResourceNotFound(f"SDC resource has no {input_name} input") + + def add_deployment_artifact(self, artifact_type: str, artifact_label: str, + artifact_name: str, artifact: str): + """ + Add deployment artifact to resource. + + Add deployment artifact to resource using payload data. + + Args: + artifact_type (str): all SDC artifact types are supported (DCAE_*, HEAT_*, ...) + artifact_name (str): the artifact file name including its extension + artifact (str): artifact file to upload + artifact_label (str): Unique Identifier of the artifact within the VF / Service. + + Raises: + StatusError: Resource has not DRAFT status + + """ + data = open(artifact, 'rb').read() + artifact_string = base64.b64encode(data).decode('utf-8') + if self.status != const.DRAFT: + msg = "Can't add artifact to resource which is not in DRAFT status" + raise StatusError(msg) + self._logger.debug("Add deployment artifact to sdc resource") + my_data = jinja_env().get_template( + "sdc_resource_add_deployment_artifact.json.j2").\ + render(artifact_name=artifact_name, + artifact_label=artifact_label, + artifact_type=artifact_type, + b64_artifact=artifact_string) + my_header = headers_sdc_artifact_upload(base_header=self.headers, data=my_data) + + self.send_message_json("POST", + f"Add deployment artifact for {self.name} sdc resource", + self.add_deployment_artifacts_url, + data=my_data, + headers=my_header) + + @property + def components(self) -> Iterator[Component]: + """Resource components. + + Iterate resource components. + + Yields: + Component: Resource component object + + """ + for component_instance in self.send_message_json(\ + "GET", + f"Get {self.name} resource inputs", + f"{self.resource_inputs_url}/filteredDataByParams?include=componentInstances" + ).get("componentInstances", []): + sdc_resource: "SdcResource" = SdcResource.import_from_sdc(self.send_message_json(\ + "GET", + f"Get {self.name} component's SDC resource metadata", + (f"{self.base_front_url}/sdc1/feProxy/rest/v1/catalog/resources/" + f"{component_instance['actualComponentUid']}/" + "filteredDataByParams?include=metadata"))["metadata"]) + yield Component.create_from_api_response(api_response=component_instance, + sdc_resource=sdc_resource, + parent_sdc_resource=self) + + @property + def category(self) -> Union[ResourceCategory, ServiceCategory]: + """Sdc resource category. + + Depends on the resource type returns ResourceCategory or ServiceCategory. + + Returns: + Uniton[ResourceCategory, ServiceCategory]: resource category + + """ + if self.created(): + if not any([self._category_name, self._subcategory_name]): + self.deep_load() + if all([self._category_name, self._subcategory_name]): + return ResourceCategory.get(name=self._category_name, + subcategory=self._subcategory_name) + return ServiceCategory.get(name=self._category_name) + return self.get_category_for_new_resource() + + def get_category_for_new_resource(self) -> ResourceCategory: + """Get category for resource not created in SDC yet. + + If no category values are provided default category is going to be used. + + Returns: + ResourceCategory: Category of the new resource + + """ + if not all([self._category_name, self._subcategory_name]): + return ResourceCategory.get(name="Generic", subcategory="Abstract") + return ResourceCategory.get(name=self._category_name, subcategory=self._subcategory_name) + + def get_component_properties_url(self, component: "Component") -> str: + """Url to get component's properties. + + This method is here because component can have different url when + it's a component of another SDC resource type, eg. for service and + for VF components have different urls. + + Args: + component (Component): Component object to prepare url for + + Returns: + str: Component's properties url + + """ + return (f"{self.resource_inputs_url}/" + f"componentInstances/{component.unique_id}/properties") + + def get_component_properties_value_set_url(self, component: "Component") -> str: + """Url to set component property value. + + This method is here because component can have different url when + it's a component of another SDC resource type, eg. for service and + for VF components have different urls. + + Args: + component (Component): Component object to prepare url for + + Returns: + str: Component's properties url + + """ + return (f"{self.resource_inputs_url}/" + f"resourceInstance/{component.unique_id}/properties") + + def is_own_property(self, property_to_check: Property) -> bool: + """Check if given property is one of the resource's properties. + + Args: + property_to_check (Property): Property to check + + Returns: + bool: True if resource has given property, False otherwise + + """ + return any(( + prop == property_to_check for prop in self.properties + )) + + def get_component(self, sdc_resource: "SdcResource") -> Component: + """Get resource's component. + + Get component by SdcResource object. + + Args: + sdc_resource (SdcResource): Component's SdcResource + + Raises: + ResourceNotFound: Component with given SdcResource does not exist + + Returns: + Component: Component object + + """ + for component in self.components: + if component.sdc_resource.name == sdc_resource.name: + return component + msg = f"SDC resource {sdc_resource.name} is not a component" + raise ResourceNotFound(msg) + + def get_component_by_name(self, component_name: str) -> Component: + """Get resource's component by it's name. + + Get component by name. + + Args: + component_name (str): Component's name + + Raises: + ResourceNotFound: Component with given name does not exist + + Returns: + Component: Component object + + """ + for component in self.components: + if component.sdc_resource.name == component_name: + return component + msg = f"SDC resource {component_name} is not a component" + raise ResourceNotFound(msg) + + def declare_input_for_own_property(self, property_obj: Property) -> None: + """Declare input for resource's property. + + For each property input can be declared. + + Args: + property_obj (Property): Property to declare input + + """ + self._logger.debug("Declare input for SDC resource property") + self.send_message_json("POST", + f"Declare new input for {property_obj.name} property", + f"{self.resource_inputs_url}/create/inputs", + data=jinja_env().get_template(\ + "sdc_resource_add_input.json.j2").\ + render(\ + sdc_resource=self, + property=property_obj)) + + def declare_nested_input(self, + nested_input: NestedInput) -> None: + """Declare nested input for SDC resource. + + Nested input is an input of one of the components. + + Args: + nested_input (NestedInput): Nested input object + + """ + self._logger.debug("Declare input for SDC resource's component property") + component: Component = self.get_component(nested_input.sdc_resource) + self.send_message_json("POST", + f"Declare new input for {nested_input.input_obj.name} input", + f"{self.resource_inputs_url}/create/inputs", + data=jinja_env().get_template(\ + "sdc_resource_add_nested_input.json.j2").\ + render(\ + sdc_resource=self, + component=component, + input=nested_input.input_obj)) + + def declare_input(self, input_to_declare: Union[Property, NestedInput]) -> None: + """Declare input for given property or nested input object. + + Call SDC FE API to declare input for given property. + + Args: + input_declaration (Union[Property, NestedInput]): Property to declare input + or NestedInput object + + Raises: + ParameterError: if the given property is not SDC resource property + + """ + self._logger.debug("Declare input") + if isinstance(input_to_declare, Property): + if self.is_own_property(input_to_declare): + self.declare_input_for_own_property(input_to_declare) + else: + msg = "Given property is not SDC resource property" + raise ParameterError(msg) + else: + self.declare_nested_input(input_to_declare) + + def add_property(self, property_to_add: Property) -> None: + """Add property to resource. + + Call SDC FE API to add property to resource. + + Args: + property_to_add (Property): Property object to add to resource. + + Raises: + StatusError: Resource has not DRAFT status + + """ + if self.status != const.DRAFT: + msg = "Can't add property to resource which is not in DRAFT status" + raise StatusError(msg) + self._logger.debug("Add property to sdc resource") + self.send_message_json("POST", + f"Declare new property for {self.name} sdc resource", + self.add_property_url, + data=jinja_env().get_template( + "sdc_resource_add_property.json.j2").\ + render( + property=property_to_add + )) + + def set_property_value(self, property_obj: Property, value: Any) -> None: + """Set property value. + + Set given value to resource property + + Args: + property_obj (Property): Property object + value (Any): Property value to set + + Raises: + ParameterError: if the given property is not the resource's property + + """ + if not self.is_own_property(property_obj): + raise ParameterError("Given property is not a resource's property") + self._logger.debug("Set %s property value", property_obj.name) + self.send_message_json("PUT", + f"Set {property_obj.name} value to {value}", + self.add_property_url, + data=jinja_env().get_template( + "sdc_resource_set_property_value.json.j2").\ + render( + sdc_resource=self, + property=property_obj, + value=value + ) + ) + + def set_input_default_value(self, input_obj: Input, default_value: Any) -> None: + """Set input default value. + + Set given value as input default value + + Args: + input_obj (Input): Input object + value (Any): Default value to set + + """ + self._logger.debug("Set %s input default value", input_obj.name) + self.send_message_json("POST", + f"Set {input_obj.name} default value to {default_value}", + self.set_input_default_value_url, + data=jinja_env().get_template( + "sdc_resource_set_input_default_value.json.j2").\ + render( + sdc_resource=self, + input=input_obj, + default_value=default_value + ) + ) + + def checkout(self) -> None: + """Checkout SDC resource.""" + self._logger.debug("Checkout %s SDC resource", self.name) + result = self._action_to_sdc(const.CHECKOUT, "lifecycleState") + if result: + self.load() + + def undo_checkout(self) -> None: + """Undo Checkout SDC resource.""" + self._logger.debug("Undo Checkout %s SDC resource", self.name) + result = self._action_to_sdc(const.UNDOCHECKOUT, "lifecycleState") + if result: + self.load() + + def certify(self) -> None: + """Certify SDC resource.""" + self._logger.debug("Certify %s SDC resource", self.name) + result = self._action_to_sdc(const.CERTIFY, "lifecycleState") + if result: + self.load() + + def add_resource(self, resource: 'SdcResource') -> None: + """ + Add a Resource. + + Args: + resource (SdcResource): the resource to add + + """ + if self.status == const.DRAFT: + url = "{}/{}/{}/resourceInstance".format(self._base_create_url(), + self._sdc_path(), + self.unique_identifier) + + template = jinja_env().get_template( + "add_resource_to_service.json.j2") + data = template.render(resource=resource, + resource_type=resource.origin_type) + result = self.send_message("POST", + f"Add {resource.origin_type} to {self.origin_type}", + url, + data=data) + if result: + self._logger.info("Resource %s %s has been added on %s %s", + resource.origin_type, resource.name, + self.origin_type, self.name) + return result + self._logger.error(("an error occured during adding resource %s %s" + " on %s %s in SDC"), + resource.origin_type, resource.name, + self.origin_type, self.name) + return None + msg = f"Can't add resource to {self.origin_type} which is not in DRAFT status" + raise StatusError(msg) diff --git a/src/onapsdk/sdc/service.py b/src/onapsdk/sdc/service.py new file mode 100644 index 0000000..c866fa4 --- /dev/null +++ b/src/onapsdk/sdc/service.py @@ -0,0 +1,932 @@ +"""Service 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. +import base64 +import pathlib as Path +import time +from dataclasses import dataclass, field +from enum import Enum +from io import BytesIO, TextIOWrapper +from os import makedirs +from typing import Dict, List, Callable, Iterator, Optional, Type, Union, Any, BinaryIO +from zipfile import ZipFile, BadZipFile + +import oyaml as yaml +from requests import Response + +import onapsdk.constants as const +from onapsdk.exceptions import (ParameterError, RequestError, ResourceNotFound, + StatusError, ValidationError) +from onapsdk.sdc.category_management import ServiceCategory +from onapsdk.sdc.properties import NestedInput, Property +from onapsdk.sdc.sdc_resource import SdcResource +from onapsdk.utils.configuration import (components_needing_distribution, + tosca_path) +from onapsdk.utils.headers_creator import headers_sdc_creator, headers_sdc_artifact_upload +from onapsdk.utils.jinja import jinja_env + + +@dataclass +class VfModule: # pylint: disable=too-many-instance-attributes + """VfModule dataclass.""" + + name: str + group_type: str + model_name: str + model_version_id: str + model_invariant_uuid: str + model_version: str + model_customization_id: str + properties: Iterator[Property] + + +@dataclass +class NodeTemplate: # pylint: disable=too-many-instance-attributes + """Node template dataclass. + + Base class for Vnf, Pnf and Network classes. + """ + + name: str + node_template_type: str + model_name: str + model_version_id: str + model_invariant_id: str + model_version: str + model_customization_id: str + model_instance_name: str + component: "Component" + + @property + def properties(self) -> Iterator["Property"]: + """Node template properties. + + Returns: + Iterator[Property]: Node template properties iterator + + """ + return self.component.properties + + +@dataclass +class Vnf(NodeTemplate): + """Vnf dataclass.""" + + vf_modules: List[VfModule] = field(default_factory=list) + + +@dataclass +class Pnf(NodeTemplate): + """Pnf dataclass.""" + + +class Network(NodeTemplate): # pylint: disable=too-few-public-methods + """Network dataclass.""" + + +class ServiceInstantiationType(Enum): + """Service instantiation type enum class. + + Service can be instantiated using `A-la-carte` or `Macro` flow. + It has to be determined during design time. That class stores these + two values to set during initialization. + + """ + + A_LA_CARTE = "A-la-carte" + MACRO = "Macro" + + +class Service(SdcResource): # pylint: disable=too-many-instance-attributes, too-many-public-methods + """ + ONAP Service Object used for SDC operations. + + Attributes: + name (str): the name of the service. Defaults to "ONAP-test-Service". + identifier (str): the unique ID of the service from SDC. + status (str): the status of the service from SDC. + version (str): the version ID of the service from SDC. + uuid (str): the UUID of the Service (which is different from + identifier, don't ask why...) + distribution_status (str): the status of distribution in the different + ONAP parts. + distribution_id (str): the ID of the distribution when service is + distributed. + distributed (bool): True if the service is distributed + unique_identifier (str): Yet Another ID, just to puzzle us... + + """ + + SERVICE_PATH = "services" + + def __init__(self, name: str = None, version: str = None, sdc_values: Dict[str, str] = None, # pylint: disable=too-many-arguments + resources: List[SdcResource] = None, properties: List[Property] = None, + inputs: List[Union[Property, NestedInput]] = None, + instantiation_type: Optional[ServiceInstantiationType] = \ + None, + category: str = None, role: str = "", function: str = "", service_type: str = ""): + """ + Initialize service object. + + Args: + name (str, optional): the name of the service + version (str, optional): the version of the service + sdc_values (Dict[str, str], optional): dictionary of values + returned by SDC + resources (List[SdcResource], optional): list of SDC resources + properties (List[Property], optional): list of properties to add to service. + None by default. + inputs (List[Union[Property, NestedInput]], optional): list of inputs + to declare for service. It can be both Property or NestedInput object. + None by default. + instantiation_type (ServiceInstantiationType, optional): service instantiation + type. ServiceInstantiationType.A_LA_CARTE by default + category (str, optional): service category name + role (str, optional): service role + function (str, optional): service function. Empty by default + service_type (str, optional): service type. Empty by default + + """ + super().__init__(sdc_values=sdc_values, version=version, properties=properties, + inputs=inputs, category=category) + self.name: str = name or "ONAP-test-Service" + self.distribution_status = None + self.category_name: str = category + self.role: str = role + self.function: str = function + self.service_type: str = service_type + if sdc_values: + self.distribution_status = sdc_values['distributionStatus'] + self.category_name = sdc_values["category"] + self.resources = resources or [] + self._instantiation_type: Optional[ServiceInstantiationType] = instantiation_type + self._distribution_id: str = None + self._distributed: bool = False + self._resource_type: str = "services" + self._tosca_model: bytes = None + self._tosca_template: str = None + self._vnfs: list = None + self._pnfs: list = None + self._networks: list = None + self._vf_modules: list = None + + @classmethod + def get_by_unique_uuid(cls, unique_uuid: str) -> "Service": + """Get the service model using unique uuid. + + Returns: + Service: object with provided unique_uuid + + Raises: + ResourceNotFound: No service with given unique_uuid exists + + """ + services: List["Service"] = cls.get_all() + for service in services: + if service.unique_uuid == unique_uuid: + return service + raise ResourceNotFound("Service with given unique uuid doesn't exist") + + def onboard(self) -> None: + """Onboard the Service in SDC. + + Raises: + StatusError: service has an invalid status + ParameterError: no resources, no properties for service + in DRAFT status + + """ + # first Lines are equivalent for all onboard functions but it's more + # readable + if not self.status: + # equivalent step as in onboard-function in sdc_resource + self.create() + time.sleep(self._time_wait) + self.onboard() + elif self.status == const.DRAFT: + if not any([self.resources, self._properties_to_add]): + raise ParameterError("No resources nor properties were given") + self.declare_resources_and_properties() + self.checkin() + time.sleep(self._time_wait) + self.onboard() + elif self.status == const.CHECKED_IN: + self.certify() + time.sleep(self._time_wait) + self.onboard() + elif self.status == const.CERTIFIED: + self.distribute() + self.onboard() + elif self.status == const.DISTRIBUTED: + self._logger.info("Service %s onboarded", self.name) + else: + self._logger.error("Service has invalid status: %s", self.status) + raise StatusError(self.status) + + @property + def distribution_id(self) -> str: + """Return and lazy load the distribution_id.""" + if not self._distribution_id: + self.load_metadata() + return self._distribution_id + + @distribution_id.setter + def distribution_id(self, value: str) -> None: + """Set value for distribution_id.""" + self._distribution_id = value + + @property + def distributed(self) -> bool: + """Return and lazy load the distributed state.""" + if not self._distributed: + self._check_distributed() + return self._distributed + + @property + def tosca_template(self) -> str: + """Service tosca template file. + + Get tosca template from service tosca model bytes. + + Returns: + str: Tosca template file + + """ + if not self._tosca_template and self.tosca_model: + self._unzip_csar_file(BytesIO(self.tosca_model), + self._load_tosca_template) + return self._tosca_template + + @property + def tosca_model(self) -> bytes: + """Service's tosca model file. + + Send request to get service TOSCA model, + + Returns: + bytes: TOSCA model file bytes + + """ + if not self._tosca_model: + url = "{}/services/{}/toscaModel".format(self._base_url(), + self.identifier) + headers = self.headers.copy() + headers["Accept"] = "application/octet-stream" + self._tosca_model = self.send_message( + "GET", + "Download Tosca Model for {}".format(self.name), + url, + headers=headers).content + return self._tosca_model + + def create_node_template(self, + node_template_type: Type[NodeTemplate], + component: "Component") -> NodeTemplate: + """Create a node template type object. + + The base of the all node template types objects (Vnf, Pnf, Network) is the + same. The difference is only for the Vnf which can have vf modules associated with. + Vf modules could have "vf_module_label" property with"base_template_dummy_ignore" + value. These vf modules should be ignored/ + + Args: + node_template_type (Type[NodeTemplate]): Node template class type + component (Component): Component on which base node template object should be created + + Returns: + NodeTemplate: Node template object created from component + + """ + node_template: NodeTemplate = node_template_type( + name=component.name, + node_template_type=component.tosca_component_name, + model_name=component.component_name, + model_version_id=component.sdc_resource.identifier, + model_invariant_id=component.sdc_resource.unique_uuid, + model_version=component.sdc_resource.version, + model_customization_id=component.customization_uuid, + model_instance_name=self.name, + component=component + ) + if node_template_type is Vnf: + if component.group_instances: + for vf_module in component.group_instances: + if not any([property_def["name"] == "vf_module_label"] and \ + property_def["value"] == "base_template_dummy_ignore" for \ + property_def in vf_module["properties"]): + node_template.vf_modules.append(VfModule( + name=vf_module["name"], + group_type=vf_module["type"], + model_name=vf_module["groupName"], + model_version_id=vf_module["groupUUID"], + model_invariant_uuid=vf_module["invariantUUID"], + model_version=vf_module["version"], + model_customization_id=vf_module["customizationUUID"], + properties=( + Property( + name=property_def["name"], + property_type=property_def["type"], + description=property_def["description"], + value=property_def["value"] + ) for property_def in vf_module["properties"] \ + if property_def["value"] and not ( + property_def["name"] == "vf_module_label" and \ + property_def["value"] == "base_template_dummy_ignore" + ) + ) + )) + return node_template + + def __has_component_type(self, origin_type: str) -> bool: + """Check if any of Service's component type is provided origin type. + + In template generation is checked if Service has some types of components, + based on that blocks are added to the request template. It's not + the best option to get all components to check if at least one with + given type exists for conditional statement. + + Args: + origin_type (str): Type to check if any component exists. + + Returns: + bool: True if service has at least one component with given origin type, + False otherwise + + """ + return any((component.origin_type == origin_type for component in self.components)) + + @property + def has_vnfs(self) -> bool: + """Check if service has at least one VF component.""" + return self.__has_component_type("VF") + + @property + def has_pnfs(self) -> bool: + """Check if service has at least one PNF component.""" + return self.__has_component_type("PNF") + + @property + def has_vls(self) -> bool: + """Check if service has at least one VL component.""" + return self.__has_component_type("VL") + + @property + def vnfs(self) -> Iterator[Vnf]: + """Service Vnfs. + + Load VNFs from components generator. + It creates a generator of the vf modules as well, but without + vf modules which has "vf_module_label" property value equal + to "base_template_dummy_ignore". + + Returns: + Iterator[Vnf]: Vnf objects iterator + + """ + for component in self.components: + if component.origin_type == "VF": + yield self.create_node_template(Vnf, component) + + @property + def pnfs(self) -> Iterator[Pnf]: + """Service Pnfs. + + Load PNFS from components generator. + + Returns: + Iterator[Pnf]: Pnf objects generator + + """ + for component in self.components: + if component.origin_type == "PNF": + yield self.create_node_template(Pnf, component) + + @property + def networks(self) -> Iterator[Network]: + """Service networks. + + Load networks from service's components generator. + + Returns: + Iterator[Network]: Network objects generator + + """ + for component in self.components: + if component.origin_type == "VL": + yield self.create_node_template(Network, component) + + @property + def deployment_artifacts_url(self) -> str: + """Deployment artifacts url. + + Returns: + str: SdcResource Deployment artifacts url + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}/filteredDataByParams?include=deploymentArtifacts") + + @property + def add_deployment_artifacts_url(self) -> str: + """Add deployment artifacts url. + + Returns: + str: Url used to add deployment artifacts + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}/artifacts") + + @property + def properties_url(self) -> str: + """Properties url. + + Returns: + str: SdcResource properties url + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}/filteredDataByParams?include=properties") + + @property + def metadata_url(self) -> str: + """Metadata url. + + Returns: + str: Service metadata url + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}/filteredDataByParams?include=metadata") + + @property + def resource_inputs_url(self) -> str: + """Service inputs url. + + Returns: + str: Service inputs url + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}") + + @property + def set_input_default_value_url(self) -> str: + """Url to set input default value. + + Returns: + str: SDC API url used to set input default value + + """ + return (f"{self._base_create_url()}/services/" + f"{self.unique_identifier}/update/inputs") + + @property + def origin_type(self) -> str: + """Service origin type. + + Value needed for composition. It's used for adding SDC resource + as an another SDC resource component. + For Service that value has to be set to "ServiceProxy". + + Returns: + str: Service resource origin type + + """ + return "ServiceProxy" + + @property + def instantiation_type(self) -> ServiceInstantiationType: + """Service instantiation type. + + One of `ServiceInstantiationType` enum value. + + Returns: + ServiceInstantiationType: Service instantiation type + + """ + if not self._instantiation_type: + if not self.created(): + self._instantiation_type = ServiceInstantiationType.A_LA_CARTE + else: + response: str = self.send_message_json("GET", + f"Get service {self.name} metadata", + self.metadata_url)["metadata"]\ + ["instantiationType"] + self._instantiation_type = ServiceInstantiationType(response) + return self._instantiation_type + + def create(self) -> None: + """Create the Service in SDC if not already existing.""" + self._create("service_create.json.j2", + name=self.name, + instantiation_type=self.instantiation_type.value, + category=self.category, + role=self.role, service_type=self.service_type, function=self.function) + + def declare_resources_and_properties(self) -> None: + """Delcare resources and properties. + + It declares also inputs. + + """ + for resource in self.resources: + self.add_resource(resource) + for property_to_add in self._properties_to_add: + self.add_property(property_to_add) + for input_to_add in self._inputs_to_add: + self.declare_input(input_to_add) + + def checkin(self) -> None: + """Checkin Service.""" + self._verify_lcm_to_sdc(const.DRAFT, const.CHECKIN) + + def submit(self) -> None: + """Really submit the SDC Service.""" + self._verify_lcm_to_sdc(const.CHECKED_IN, const.SUBMIT_FOR_TESTING) + + def start_certification(self) -> None: + """Start Certification on Service.""" + headers = headers_sdc_creator(SdcResource.headers) + self._verify_lcm_to_sdc(const.CHECKED_IN, + const.START_CERTIFICATION, + headers=headers) + + def certify(self) -> None: + """Certify Service in SDC.""" + headers = headers_sdc_creator(SdcResource.headers) + self._verify_lcm_to_sdc(const.CHECKED_IN, + const.CERTIFY, + headers=headers) + + def approve(self) -> None: + """Approve Service in SDC.""" + headers = headers_sdc_creator(SdcResource.headers) + self._verify_approve_to_sdc(const.CERTIFIED, + const.APPROVE, + headers=headers) + + def distribute(self) -> None: + """Apptove Service in SDC.""" + headers = headers_sdc_creator(SdcResource.headers) + self._verify_distribute_to_sdc(const.CERTIFIED, + const.DISTRIBUTE, + headers=headers) + + def redistribute(self) -> None: + """Apptove Service in SDC.""" + headers = headers_sdc_creator(SdcResource.headers) + self._verify_distribute_to_sdc(const.DISTRIBUTED, + const.DISTRIBUTE, + headers=headers) + + def get_tosca(self) -> None: + """Get Service tosca files and save it.""" + url = "{}/services/{}/toscaModel".format(self._base_url(), + self.identifier) + headers = self.headers.copy() + headers["Accept"] = "application/octet-stream" + result = self.send_message("GET", + "Download Tosca Model for {}".format( + self.name), + url, + headers=headers) + if result: + self._create_tosca_file(result) + + def _create_tosca_file(self, result: Response) -> None: + """Create Service Tosca files from HTTP response.""" + csar_filename = "service-{}-csar.csar".format(self.name) + makedirs(tosca_path(), exist_ok=True) + with open((tosca_path() + csar_filename), 'wb') as csar_file: + for chunk in result.iter_content(chunk_size=128): + csar_file.write(chunk) + try: + self._unzip_csar_file(tosca_path() + csar_filename, + self._write_csar_file) + except BadZipFile as exc: + self._logger.exception(exc) + + def _check_distributed(self) -> bool: + """Check if service is distributed and update status accordingly.""" + url = "{}/services/distribution/{}".format(self._base_create_url(), + self.distribution_id) + headers = headers_sdc_creator(SdcResource.headers) + + status = {} + for component in components_needing_distribution(): + status[component] = False + + try: + result = self.send_message_json("GET", + "Check distribution for {}".format( + self.name), + url, + headers=headers) + except ResourceNotFound: + msg = f"No distributions found for {self.name} of {self.__class__.__name__}." + self._logger.debug(msg) + else: + status = self._update_components_status(status, result) + + for state in status.values(): + if not state: + self._distributed = False + return + self._distributed = True + + def _update_components_status(self, status: Dict[str, bool], + result: Response) -> Dict[str, bool]: + """Update components distribution status.""" + distrib_list = result['distributionStatusList'] + self._logger.debug("[SDC][Get Distribution] distrib_list = %s", + distrib_list) + for elt in distrib_list: + status = self._parse_components_status(status, elt) + return status + + def _parse_components_status(self, status: Dict[str, bool], + element: Dict[str, Any]) -> Dict[str, bool]: + """Parse components distribution status.""" + for key in status: + if ((key in element['omfComponentID']) + and (const.DOWNLOAD_OK in element['status'])): + status[key] = True + self._logger.info(("[SDC][Get Distribution] Service " + "distributed in %s"), key) + return status + + def load_metadata(self) -> None: + """Load Metada of Service and retrieve informations.""" + url = "{}/services/{}/distribution".format(self._base_create_url(), + self.identifier) + headers = headers_sdc_creator(SdcResource.headers) + result = self.send_message_json("GET", + "Get Metadata for {}".format( + self.name), + url, + headers=headers) + if ('distributionStatusOfServiceList' in result + and len(result['distributionStatusOfServiceList']) > 0): + # API changed and the latest distribution is not added to the end + # of distributions list but inserted as the first one. + dist_status = result['distributionStatusOfServiceList'][0] + self._distribution_id = dist_status['distributionID'] + + @classmethod + def _get_all_url(cls) -> str: + """ + Get URL for all elements in SDC. + + Returns: + str: the url + + """ + return "{}/{}".format(cls._base_url(), cls._sdc_path()) + + def _really_submit(self) -> None: + """Really submit the SDC Service in order to enable it.""" + result = self._action_to_sdc(const.CERTIFY, + action_type="lifecycleState") + if result: + self.load() + + def _specific_copy(self, obj: 'Service') -> None: + """ + Copy specific properties from object. + + Args: + obj (Service): the object to "copy" + + """ + super()._specific_copy(obj) + self.category_name = obj.category_name + self.role = obj.role + + def _verify_distribute_to_sdc(self, desired_status: str, + desired_action: str, **kwargs) -> None: + self._verify_action_to_sdc(desired_status, desired_action, + "distribution", **kwargs) + + def _verify_approve_to_sdc(self, desired_status: str, desired_action: str, + **kwargs) -> None: + self._verify_action_to_sdc(desired_status, desired_action, + "distribution-state", **kwargs) + + def _verify_lcm_to_sdc(self, desired_status: str, desired_action: str, + **kwargs) -> None: + self._verify_action_to_sdc(desired_status, desired_action, + "lifecycleState", **kwargs) + + def _verify_action_to_sdc(self, desired_status: str, desired_action: str, + action_type: str, **kwargs) -> None: + """ + Verify action to SDC. + + Verify that object is in right state before launching the action on + SDC. + + Raises: + StatusError: if current status is not the desired status. + + Args: + desired_status (str): the status the object should be + desired_action (str): the action we want to perform + action_type (str): the type of action ('distribution-state' or + 'lifecycleState') + **kwargs: any specific stuff to give to requests + + """ + self._logger.info("attempting to %s Service %s in SDC", desired_action, + self.name) + if self.status == desired_status and self.created(): + self._action_to_sdc(desired_action, + action_type=action_type, + **kwargs) + self.load() + elif not self.created(): + self._logger.warning("Service %s in SDC is not created", self.name) + elif self.status != desired_status: + msg = (f"Service {self.name} in SDC is in status {self.status} " + f"and it should be in status {desired_status}") + raise StatusError(msg) + + @staticmethod + def _unzip_csar_file(zip_file: Union[str, BytesIO], + function: Callable[[str, + TextIOWrapper], None]) -> None: + """ + Unzip Csar File and perform an action on the file. + + Raises: + ValidationError: CSAR file has no service template + + """ + folder = "Definitions" + prefix = "service-" + suffix = "-template.yml" + with ZipFile(zip_file) as myzip: + service_template = None + for name in myzip.namelist(): + if (name[-13:] == suffix + and name[:20] == f"{folder}/{prefix}"): + service_template = name + + if not service_template: + msg = (f"CSAR file has no service template. " + f"Valid path: {folder}/{prefix}*{suffix}") + raise ValidationError(msg) + + with myzip.open(service_template) as template_file: + function(service_template, template_file) + + @staticmethod + def _write_csar_file(service_template: str, + template_file: TextIOWrapper) -> None: + """Write service temple into a file.""" + with open(tosca_path() + service_template[12:], 'wb') as file: + file.write(template_file.read()) + + # _service_template is not used but function generation is generic + # pylint: disable-unused-argument + def _load_tosca_template(self, _service_template: str, + template_file: TextIOWrapper) -> None: + """Load Tosca template.""" + self._tosca_template = yaml.safe_load(template_file.read()) + + @classmethod + def _sdc_path(cls) -> None: + """Give back the end of SDC path.""" + return cls.SERVICE_PATH + + def get_nf_unique_id(self, nf_name: str) -> str: + """ + Get nf (network function) uniqueID. + + Get nf uniqueID from service nf in sdc. + + Args: + nf_name (str): the nf from which we extract the unique ID + + Returns: + the nf unique ID + + Raises: + ResourceNotFound: Couldn't find NF by name. + + """ + url = f"{self._base_create_url()}/services/{self.unique_identifier}" + request_return = self.send_message_json('GET', + 'Get nf unique ID', + url) + + for instance in filter(lambda x: x["componentName"] == nf_name, + request_return["componentInstances"]): + return instance["uniqueId"] + + raise ResourceNotFound(f"NF '{nf_name}'") + + + def add_artifact_to_vf(self, vnf_name: str, artifact_type: str, + artifact_name: str, artifact: BinaryIO = None): + """ + Add artifact to vf. + + Add artifact to vf using payload data. + + Raises: + RequestError: file upload (POST request) for an artifact fails. + + Args: + vnf_name (str): the vnf which we want to add the artifact + artifact_type (str): all SDC artifact types are supported (DCAE_*, HEAT_*, ...) + artifact_name (str): the artifact file name including its extension + artifact (str): binary data to upload + + """ + missing_identifier = self.get_nf_unique_id(vnf_name) + url = (f"{self._base_create_url()}/services/{self.unique_identifier}/" + f"resourceInstance/{missing_identifier}/artifacts") + template = jinja_env().get_template("add_artifact_to_vf.json.j2") + data = template.render(artifact_name=artifact_name, + artifact_label=f"sdk{Path.PurePosixPath(artifact_name).stem}", + artifact_type=artifact_type, + b64_artifact=base64.b64encode(artifact).decode('utf-8')) + headers = headers_sdc_artifact_upload(base_header=self.headers, + data=data) + try: + self.send_message('POST', + 'Add artifact to vf', + url, + headers=headers, + data=data) + except RequestError as exc: + self._logger.error(("an error occured during file upload for an Artifact" + "to VNF %s"), vnf_name) + raise exc + + def get_component_properties_url(self, component: "Component") -> str: + """Url to get component's properties. + + This method is here because component can have different url when + it's a component of another SDC resource type, eg. for service and + for VF components have different urls. + Also for VL origin type components properties url is different than + for the other types. + + Args: + component (Component): Component object to prepare url for + + Returns: + str: Component's properties url + + """ + if component.origin_type == "VL": + return super().get_component_properties_url(component) + return (f"{self.resource_inputs_url}/" + f"componentInstances/{component.unique_id}/{component.actual_component_uid}/inputs") + + def get_component_properties_value_set_url(self, component: "Component") -> str: + """Url to set component property value. + + This method is here because component can have different url when + it's a component of another SDC resource type, eg. for service and + for VF components have different urls. + Also for VL origin type components properties url is different than + for the other types. + + Args: + component (Component): Component object to prepare url for + + Returns: + str: Component's properties url + + """ + if component.origin_type == "VL": + return super().get_component_properties_value_set_url(component) + return (f"{self.resource_inputs_url}/" + f"resourceInstance/{component.unique_id}/inputs") + + def get_category_for_new_resource(self) -> ServiceCategory: + """Get category for service not created in SDC yet. + + If no category values are provided default category is going to be used. + + Returns: + ServiceCategory: Category of the new service + + """ + if not self._category_name: + return ServiceCategory.get(name="Network Service") + return ServiceCategory.get(name=self._category_name) diff --git a/src/onapsdk/sdc/templates/add_artifact_to_vf.json.j2 b/src/onapsdk/sdc/templates/add_artifact_to_vf.json.j2 new file mode 100644 index 0000000..2b0446c --- /dev/null +++ b/src/onapsdk/sdc/templates/add_artifact_to_vf.json.j2 @@ -0,0 +1,9 @@ +{ + "artifactGroupType": "DEPLOYMENT", + "artifactName": "{{artifact_name}}", + "artifactLabel": "{{artifact_label}}", + "artifactType": "{{artifact_type}}", + "description": "test", + "payloadData": "{{b64_artifact}}", + "heatParameters": [] +} diff --git a/src/onapsdk/sdc/templates/add_resource_to_service.json.j2 b/src/onapsdk/sdc/templates/add_resource_to_service.json.j2 new file mode 100644 index 0000000..d6676e9 --- /dev/null +++ b/src/onapsdk/sdc/templates/add_resource_to_service.json.j2 @@ -0,0 +1,10 @@ +{ + "name": "{{ resource.name }}", + "componentVersion": "{{ resource.version }}", + "posY": {{ posY| default(100) }}, + "posX": {{ posX| default(200) }}, + "uniqueId": "{{ resource.unique_identifier }}", + "originType": "{{ resource_type }}", + "componentUid": "{{ resource.unique_identifier }}", + "icon": "defaulticon" +} diff --git a/src/onapsdk/sdc/templates/component_declare_input.json.j2 b/src/onapsdk/sdc/templates/component_declare_input.json.j2 new file mode 100644 index 0000000..fd0ee03 --- /dev/null +++ b/src/onapsdk/sdc/templates/component_declare_input.json.j2 @@ -0,0 +1,37 @@ +{ + "componentInstanceInputsMap": {}, + "componentInstanceProperties": { + "{{ component.unique_id }}": [ + { + "constraints": null, + "defaultValue": null, + "description": "", + "name": "{{ property.name }}", + "origName": "{{ property.name }}", + "parentUniqueId": null, + "password": false, + "required": true, + "schema": { + "property": {} + }, + "schemaType": null, + "type": "{{ property.property_type }}", + "uniqueId": "{{ property.unique_id }}", + {% if property.value is not none %} + "value":"{{ property.value }}", + {% else %} + "value":null, + {% endif %} + "definition": false, + "getInputValues": null, + "parentPropertyType": null, + "subPropertyInputPath": null, + "getPolicyValues": null, + "inputPath": null, + "metadata": null + } + ] + }, + "groupProperties": {}, + "policyProperties": {} +}
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/pnf_create.json.j2 b/src/onapsdk/sdc/templates/pnf_create.json.j2 new file mode 100644 index 0000000..fe7e60c --- /dev/null +++ b/src/onapsdk/sdc/templates/pnf_create.json.j2 @@ -0,0 +1,29 @@ +{ + "artifacts": {}, + "attributes": [], + "capabilities": {}, + {% include "sdc_resource_category.json.j2" %}, + "componentInstances": [], + "componentInstancesAttributes": {}, + "componentInstancesProperties": {}, + "componentType": "RESOURCE", + "contactId": "cs0008", + {% if vsp is not none %} + "csarUUID": "{{ vsp.csar_uuid }}", + "csarVersion": "1.0", + "vendorName": "{{ vsp.vendor.name }}", + {% else %} + "vendorName": "{{ vendor.name }}", + {% endif %} + "deploymentArtifacts": {}, + "description": "PNF", + "icon": "defaulticon", + "name": "{{ name }}", + "properties": [], + "groups": [], + "requirements": {}, + "resourceType": "PNF", + "tags": ["{{ name }}"], + "toscaArtifacts": {}, + "vendorRelease": "1.0" +} diff --git a/src/onapsdk/sdc/templates/sdc_element_action.json.j2 b/src/onapsdk/sdc/templates/sdc_element_action.json.j2 new file mode 100644 index 0000000..04fd946 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_element_action.json.j2 @@ -0,0 +1,6 @@ +{ +{% if action == const.COMMIT %} + "commitRequest":{"message":"ok"}, +{% endif %} + "action": "{{ action }}" +} diff --git a/src/onapsdk/sdc/templates/sdc_resource_action.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_action.json.j2 new file mode 100644 index 0000000..742d076 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_action.json.j2 @@ -0,0 +1,3 @@ +{ + "userRemarks": "{{ action | lower }}" +} diff --git a/src/onapsdk/sdc/templates/sdc_resource_add_deployment_artifact.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_add_deployment_artifact.json.j2 new file mode 100644 index 0000000..290c6d2 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_add_deployment_artifact.json.j2 @@ -0,0 +1,8 @@ +{ + "artifactGroupType": "DEPLOYMENT", + "artifactName": "{{artifact_name}}", + "artifactLabel": "{{artifact_label}}", + "artifactType": "{{artifact_type}}", + "description": "test", + "payloadData": "{{b64_artifact}}" +} diff --git a/src/onapsdk/sdc/templates/sdc_resource_add_input.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_add_input.json.j2 new file mode 100644 index 0000000..1964f36 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_add_input.json.j2 @@ -0,0 +1,39 @@ +{ + "serviceProperties":{ + "{{ sdc_resource.unique_identifier }}":[ + { + "constraints":null, + "defaultValue":null, + "description":null, + "name":"{{ property.name }}", + "origName":"{{ property.name }}", + "parentUniqueId":"{{ sdc_resource.unique_identifier }}", + "password":false, + "required":false, + "schema":{ + "property":{ + "type":"", + "required":false, + "definition":false, + "description":null, + "password":false + } + }, + "schemaType":"", + "type":"{{ property.property_type }}", + "uniqueId":"{{ sdc_resource.unique_identifier }}.{{ property.name }}", + {% if property.value is not none %} + "value":"{{ property.value }}", + {% else %} + "value":null, + {% endif %} + "definition":false, + "getInputValues":null, + "parentPropertyType":null, + "subPropertyInputPath":null, + "getPolicyValues":null, + "inputPath":null + } + ] + } +}
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/sdc_resource_add_nested_input.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_add_nested_input.json.j2 new file mode 100644 index 0000000..9dc8261 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_add_nested_input.json.j2 @@ -0,0 +1,35 @@ +{ + "componentInstanceInputsMap":{ + "{{ component.unique_id }}":[ + { + {# "defaultValue":null, #} + "name":"{{ input.name }}", + "origName":"{{ input.name }}", + {# "parentUniqueId":"cs0008", #} + "password":false, + "required":false, + "schema":{ + "property":{ + {# "type":"", + "required":false, + "definition":false, + "password":false #} + } + }, + {# "schemaType":"", #} + "type":"{{ input.input_type }}", + "uniqueId":"{{ sdc_resource.unique_identifier }}.{{ input.name }}", + {% if input.default_value is not none %} + "value":"{{ input.default_value }}", + {% endif %} + "definition":false + {# "type":"{{ input.input_type }}", #} + } + ] + }, + "componentInstanceProperties":{ + "{{ component.unique_id }}":[] + }, + "groupProperties":{}, + "policyProperties":{} +}
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/sdc_resource_add_property.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_add_property.json.j2 new file mode 100644 index 0000000..bed49ca --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_add_property.json.j2 @@ -0,0 +1,17 @@ +{ + "{{ property.name }}":{ + "schema":{ + "property":{ + "type":"" + } + }, + "name": "{{ property.name }}", + {% if property.description %} + "description": "{{ property.description }}", + {% endif %} + {% if property.value %} + "value": "{{ property.value }}", + {% endif %} + "type": "{{ property.property_type }}" + } +}
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/sdc_resource_category.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_category.json.j2 new file mode 100644 index 0000000..633aacd --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_category.json.j2 @@ -0,0 +1,13 @@ + "categories": [ + { + "normalizedName": "{{ category.normalized_name }}", + "name": "{{ category.name }}", + "uniqueId": "{{ category.unique_id }}", + "subcategories": {% if category.subcategories %}{{ category.subcategories|tojson }}{% else %}null{% endif %}, + "version": {% if category.version %}"{{ category.version }}"{% else %}null{% endif %}, + "ownerId": {% if category.owner_id %}"{{ category.owner_id }}"{% else %}null{% endif %}, + "empty": {{ category.empty|tojson }}, + "type": {% if category.type %}"{{ category.type }}"{% else %}null{% endif %}, + "icons": {% if category.icons %}{{ category.icons|tojson }}{% else %}null{% endif %} + } + ]
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/sdc_resource_component_set_property_value.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_component_set_property_value.json.j2 new file mode 100644 index 0000000..46bd527 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_component_set_property_value.json.j2 @@ -0,0 +1,13 @@ +[ + { + "name":"{{ property.name }}", + "parentUniqueId":"{{ component.actual_component_uid }}", + "type":"{{ property.property_type }}", + "uniqueId":"{{ component.actual_component_uid }}.{{ property.name }}", + "value":"{{ value }}", + "definition":false, + "toscaPresentation":{ + "ownerId":"{{ component.actual_component_uid }}" + } + } +]
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/sdc_resource_set_input_default_value.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_set_input_default_value.json.j2 new file mode 100644 index 0000000..97c2cfd --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_set_input_default_value.json.j2 @@ -0,0 +1,8 @@ +[ + { + "defaultValue":"{{ default_value }}", + "name":"{{ input.name }}", + "type":"{{ input.input_type }}", + "uniqueId":"{{ input.unique_id }}" + } +]
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/sdc_resource_set_property_value.json.j2 b/src/onapsdk/sdc/templates/sdc_resource_set_property_value.json.j2 new file mode 100644 index 0000000..d0e73f7 --- /dev/null +++ b/src/onapsdk/sdc/templates/sdc_resource_set_property_value.json.j2 @@ -0,0 +1,13 @@ +[ + { + "name":"{{ property.name }}", + "parentUniqueId":"{{ sdc_resource.unique_identifier }}", + "type":"{{ property.property_type }}", + "uniqueId":"{{ sdc_resource.unique_identifier }}.{{ property.name }}", + "value":"{{ value }}", + "definition":false, + "toscaPresentation":{ + "ownerId":"{{ sdc_resource.unique_identifier }}" + } + } +]
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/service_create.json.j2 b/src/onapsdk/sdc/templates/service_create.json.j2 new file mode 100644 index 0000000..f247059 --- /dev/null +++ b/src/onapsdk/sdc/templates/service_create.json.j2 @@ -0,0 +1,29 @@ + +{ + "componentType": "SERVICE", + "properties": [], + "requirements": {}, + "toscaArtifacts": {}, + "tags": ["{{ name }}"], + "artifacts": {}, + "description": "service", + "serviceApiArtifacts": {}, + "capabilities": {}, + "name": "{{ name }}", + "componentInstancesProperties": {}, + "componentInstancesAttributes": {}, + "contactId": "cs0008", + "groups": [], + "projectCode": "123456", + "deploymentArtifacts": {}, + "attributes": [], + "componentInstances": [], + "ecompGeneratedNaming": true, + "instantiationType": "{{ instantiation_type }}", + "environmentContext": "General_Revenue-Bearing", + {% include "sdc_resource_category.json.j2" %}, + "icon": "network_l_1-3", + "serviceFunction": "{{ function }}", + "serviceRole": "{{ role }}", + "serviceType": "{{ service_type }}" +} diff --git a/src/onapsdk/sdc/templates/vendor_create.json.j2 b/src/onapsdk/sdc/templates/vendor_create.json.j2 new file mode 100644 index 0000000..858f736 --- /dev/null +++ b/src/onapsdk/sdc/templates/vendor_create.json.j2 @@ -0,0 +1,5 @@ +{ + "iconRef": "icon", + "vendorName": "{{ name }}", + "description": "vendor" +} diff --git a/src/onapsdk/sdc/templates/vf_create.json.j2 b/src/onapsdk/sdc/templates/vf_create.json.j2 new file mode 100644 index 0000000..6f165e5 --- /dev/null +++ b/src/onapsdk/sdc/templates/vf_create.json.j2 @@ -0,0 +1,27 @@ +{ + "artifacts": {}, + "attributes": [], + "capabilities": {}, + {% include "sdc_resource_category.json.j2" %}, + "componentInstances": [], + "componentInstancesAttributes": {}, + "componentInstancesProperties": {}, + "componentType": "RESOURCE", + "contactId": "cs0008", + {% if category.name != "Allotted Resource" %} + "csarUUID": "{{ vsp.csar_uuid }}", + "csarVersion": "1.0", + {% endif %} + "deploymentArtifacts": {}, + "description": "VF", + "icon": "defaulticon", + "name": "{{ name }}", + "properties": [], + "groups": [], + "requirements": {}, + "resourceType": "VF", + "tags": ["{{ name }}"], + "toscaArtifacts": {}, + "vendorName": "{{ vendor.name }}", + "vendorRelease": "1.0" +} diff --git a/src/onapsdk/sdc/templates/vf_vsp_update.json.j2 b/src/onapsdk/sdc/templates/vf_vsp_update.json.j2 new file mode 100644 index 0000000..f862676 --- /dev/null +++ b/src/onapsdk/sdc/templates/vf_vsp_update.json.j2 @@ -0,0 +1,61 @@ +{ + "resourceType": "{{ resource_data['resourceType'] }}", + "componentType": "{{ resource_data['componentType'] }}", + "tags": {{ resource_data['tags'] | tojson }}, + "icon": "{{ resource_data['icon'] }}", + "uniqueId": "{{ resource_data['uniqueId'] }}", + "uuid": "{{ resource_data['uuid'] }}", + "invariantUUID": "{{ resource_data['invariantUUID'] }}", + "contactId": "{{ resource_data['contactId'] }}", + "categories": {{ resource_data['categories'] | tojson }}, + "creatorUserId": "{{ resource_data['creatorUserId'] }}", + "creationDate": {{ resource_data['creationDate'] }}, + "creatorFullName": "{{ resource_data['creatorFullName'] }}", + "description": "{{ resource_data['description'] }}", + "lastUpdateDate": {{ resource_data['lastUpdateDate'] }}, + "lastUpdaterUserId": "{{ resource_data['lastUpdaterUserId'] }}", + "lastUpdaterFullName": "{{ resource_data['lastUpdaterFullName'] }}", + "lifecycleState": "{{ resource_data['lifecycleState'] }}", + "name": "{{ resource_data['name'] }}", + "version": "{{ resource_data['version'] }}", + "allVersions": {{ resource_data['allVersions'] | tojson }}, + "vendorName": "{{ resource_data['vendorName'] }}", + "vendorRelease": "{{ resource_data['vendorRelease'] }}", + "normalizedName": "{{ resource_data['normalizedName'] }}", + "systemName": "{{ resource_data['systemName'] }}", + "archived": {{ resource_data['archived'] | tojson }}, + "componentMetadata": { + "resourceType": "{{ resource_data['resourceType'] }}", + "componentType": "{{ resource_data['componentType'] }}", + "tags": {{ resource_data['tags'] | tojson }}, + "icon": "{{ resource_data['icon'] }}", + "uniqueId": "{{ resource_data['uniqueId'] }}", + "uuid": "{{ resource_data['uuid'] }}", + "invariantUUID": "{{ resource_data['invariantUUID'] }}", + "contactId": "{{ resource_data['contactId'] }}", + "categories": {{ resource_data['categories'] | tojson }}, + "creatorUserId": "{{ resource_data['creatorUserId'] }}", + "creationDate": {{ resource_data['creationDate'] }}, + "creatorFullName": "{{ resource_data['creatorFullName'] }}", + "description": "{{ resource_data['description'] }}", + "lastUpdateDate": {{ resource_data['lastUpdateDate'] }}, + "lastUpdaterUserId": "{{ resource_data['lastUpdaterUserId'] }}", + "lastUpdaterFullName": "{{ resource_data['lastUpdaterFullName'] }}", + "lifecycleState": "{{ resource_data['lifecycleState'] }}", + "name": "{{ resource_data['name'] }}", + "version": "{{ resource_data['version'] }}", + "allVersions": {{ resource_data['allVersions'] | tojson }}, + "vendorName": "{{ resource_data['vendorName'] }}", + "vendorRelease": "{{ resource_data['vendorRelease'] }}", + "normalizedName": "{{ resource_data['normalizedName'] }}", + "systemName": "{{ resource_data['systemName'] }}", + "csarUUID": "{{ resource_data['csarUUID'] }}", + "csarVersion": "{{ resource_data['csarVersion'] }}", + "derivedFrom": null, + "resourceVendorModelNumber": "{{ resource_data['resourceVendorModelNumber'] }}" + }, + "csarUUID": "{{ csarUUID }}", + "csarVersion": "{{ csarVersion }}", + "derivedFrom": null, + "resourceVendorModelNumber": "{{ resource_data['resourceVendorModelNumber'] }}" +}
\ No newline at end of file diff --git a/src/onapsdk/sdc/templates/vsp_create.json.j2 b/src/onapsdk/sdc/templates/vsp_create.json.j2 new file mode 100644 index 0000000..30fa6b9 --- /dev/null +++ b/src/onapsdk/sdc/templates/vsp_create.json.j2 @@ -0,0 +1,11 @@ +{ + "name": "{{ name }}", + "description": "vendor software product", + "icon": "icon", + "category": "resourceNewCategory.generic", + "subCategory": "resourceNewCategory.generic.abstract", + "vendorName": "{{ vendor.name }}", + "vendorId": "{{ vendor.identifier }}", + "licensingData": {}, + "onboardingMethod": "NetworkPackage" +} diff --git a/src/onapsdk/sdc/vendor.py b/src/onapsdk/sdc/vendor.py new file mode 100644 index 0000000..a82e3b9 --- /dev/null +++ b/src/onapsdk/sdc/vendor.py @@ -0,0 +1,108 @@ +"""Vendor 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 typing import Any +from typing import Dict + +from onapsdk.sdc.sdc_element import SdcElement +import onapsdk.constants as const +from onapsdk.utils.headers_creator import headers_sdc_creator + + +class Vendor(SdcElement): + """ + ONAP Vendor Object used for SDC operations. + + Attributes: + name (str): the name of the vendor. Defaults to "Generic-Vendor". + identifier (str): the unique ID of the vendor from SDC. + status (str): the status of the vendor from SDC. + version (str): the version ID of the vendor from SDC. + + """ + + VENDOR_PATH = "vendor-license-models" + headers = headers_sdc_creator(SdcElement.headers) + + def __init__(self, name: str = None): + """ + Initialize vendor object. + + Args: + name (optional): the name of the vendor + + """ + super().__init__() + self.name: str = name or "Generic-Vendor" + + def onboard(self) -> None: + """Onboard the vendor in SDC.""" + if not self.status: + self.create() + self.onboard() + elif self.status == const.DRAFT: + self.submit() + + def create(self) -> None: + """Create the vendor in SDC if not already existing.""" + self._create("vendor_create.json.j2", name=self.name) + + def submit(self) -> None: + """Submit the SDC vendor in order to enable it.""" + self._logger.info("attempting to certify/sumbit vendor %s in SDC", + self.name) + if self.status != const.CERTIFIED and self.created(): + self._really_submit() + elif self.status == const.CERTIFIED: + self._logger.warning( + "vendor %s in SDC is already submitted/certified", self.name) + elif not self.created(): + self._logger.warning("vendor %s in SDC is not created", self.name) + + def update_informations_from_sdc(self, details: Dict[str, Any]) -> None: + """ + + Update instance with details from SDC. + + Args: + details (Dict[str, Any]): dict from SDC + + """ + self._status = details['status'] + + @classmethod + def import_from_sdc(cls, values: Dict[str, Any]) -> 'Vendor': + """ + Import Vendor from SDC. + + Args: + values (Dict[str, Any]): dict to parse returned from SDC. + + Returns: + a Vsp instance with right values + + """ + vendor = Vendor(values['name']) + vendor.identifier = values['id'] + return vendor + + def _really_submit(self) -> None: + """Really submit the SDC Vf in order to enable it.""" + self._action_to_sdc(const.SUBMIT) + self._status = const.CERTIFIED + + @classmethod + def _sdc_path(cls) -> None: + """Give back the end of SDC path.""" + return cls.VENDOR_PATH diff --git a/src/onapsdk/sdc/vf.py b/src/onapsdk/sdc/vf.py new file mode 100644 index 0000000..7e4e4a7 --- /dev/null +++ b/src/onapsdk/sdc/vf.py @@ -0,0 +1,164 @@ +"""Vf 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 __future__ import annotations +from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union + +from onapsdk.exceptions import ParameterError +from onapsdk.sdc.properties import ComponentProperty, NestedInput, Property +from onapsdk.sdc.sdc_resource import SdcResource +from onapsdk.sdc.vendor import Vendor +from onapsdk.utils.jinja import jinja_env +import onapsdk.constants as const + +if TYPE_CHECKING: + from onapsdk.sdc.vsp import Vsp + + +class Vf(SdcResource): + """ + ONAP Vf Object used for SDC operations. + + Attributes: + name (str): the name of the vendor. Defaults to "Generic-Vendor". + identifier (str): the unique ID of the vendor from SDC. + status (str): the status of the vendor from SDC. + version (str): the version ID of the vendor from SDC. + vsp (Vsp): the Vsp used for VF creation + uuid (str): the UUID of the VF (which is different from identifier, + don't ask why...) + unique_identifier (str): Yet Another ID, just to puzzle us... + + """ + + def __init__(self, name: str = None, version: str = None, sdc_values: Dict[str, str] = None, # pylint: disable=too-many-arguments + vsp: Vsp = None, properties: List[Property] = None, + inputs: Union[Property, NestedInput] = None, + category: str = None, subcategory: str = None, + vendor: Vendor = None): + """ + Initialize vf object. + + Args: + name (optional): the name of the vf + version (str, optional): the version of the vf + vsp (Vsp, optional): VSP object related with the Vf object. + Defaults to None. + vendor (Vendor, optional): Vendor object used if VSP was not provided. + Defaults to None. + + """ + super().__init__(sdc_values=sdc_values, version=version, properties=properties, + inputs=inputs, category=category, subcategory=subcategory) + self.name: str = name or "ONAP-test-VF" + self.vsp: Vsp = vsp + self._vendor: Vendor = vendor + + @property + def vendor(self) -> Optional[Vendor]: + """Vendor related wth Vf. + + If Vf is created vendor is get from it's resource metadata. + Otherwise it's vendor provided by the user or the vendor from vsp. + It's possible that method returns None, but it won't be possible then + to create that Vf resource. + + Returns: + Optional[Vendor]: Vendor object related with Vf + + """ + if self._vendor: + return self._vendor + if self.created(): + resource_data: Dict[str, Any] = self.send_message_json( + "GET", + "Get VF data to update VSP", + self.resource_inputs_url + ) + self._vendor = Vendor(name=resource_data.get("vendorName")) + elif self.vsp: + self._vendor = self.vsp.vendor + return self._vendor + + + def create(self) -> None: + """Create the Vf in SDC if not already existing. + + Raises: + ParameterError: VSP is not provided during VF object initalization + + """ + if not any([self.vsp, self.vendor]): + raise ParameterError("At least vsp or vendor needs to be given") + self._create("vf_create.json.j2", name=self.name, vsp=self.vsp, + category=self.category, vendor=self.vendor) + + def _really_submit(self) -> None: + """Really submit the SDC Vf in order to enable it.""" + self._action_to_sdc(const.CERTIFY, "lifecycleState") + self.load() + + def update_vsp(self, vsp: Vsp) -> None: + """Update Vsp. + + Update VSP UUID and version for Vf object. + + Args: + vsp (Vsp): Object to be used in Vf + + Raises: + ValidationError: Vf object request has invalid structure. + + """ + resource_data: Dict[str, Any] = self.send_message_json( + "GET", + "Get VF data to update VSP", + self.resource_inputs_url + ) + self.send_message_json( + "PUT", + "Update vsp data", + self.resource_inputs_url, + data=jinja_env() + .get_template("vf_vsp_update.json.j2") + .render(resource_data=resource_data, + csarUUID=vsp.csar_uuid, + csarVersion=vsp.human_readable_version) + ) + + def declare_input(self, + input_to_declare: Union[Property, NestedInput, ComponentProperty]) -> None: + """Declare input for given property, nested input or component property object. + + Call SDC FE API to declare input for given property. + + Args: + input_declaration (Union[Property, NestedInput]): Property or ComponentProperty + to declare input or NestedInput object + + Raises: + ParameterError: if the given property is not SDC resource property + + """ + if not isinstance(input_to_declare, ComponentProperty): + super().declare_input(input_to_declare) + else: + self.send_message("POST", + f"Declare new input for {input_to_declare.name} property", + f"{self.resource_inputs_url}/create/inputs", + data=jinja_env().get_template(\ + "component_declare_input.json.j2").\ + render(\ + component=input_to_declare.component, + property=input_to_declare)) diff --git a/src/onapsdk/sdc/vfc.py b/src/onapsdk/sdc/vfc.py new file mode 100644 index 0000000..e2a6f57 --- /dev/null +++ b/src/onapsdk/sdc/vfc.py @@ -0,0 +1,46 @@ +"""VFC 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 typing import Dict + +from onapsdk.exceptions import ResourceNotFound + +from .sdc_resource import SdcResource + + +class Vfc(SdcResource): + """ONAP VFC Object used for SDC operations.""" + + def __init__(self, name: str, version: str = None, sdc_values: Dict[str, str] = None): + """Initialize VFC object. + + Vfc has to exist in SDC. + + Args: + name (str): Vfc name + version (str): Vfc version + sdc_values (Dict[str, str], optional): Sdc values of existing Vfc. Defaults to None. + + Raises: + ResourceNotFound: Vfc doesn't exist in SDC + + """ + super().__init__(name=name, version=version, sdc_values=sdc_values) + if not sdc_values and not self.exists(): + raise ResourceNotFound( + "This Vfc has to exist prior to object initialization.") + + def _really_submit(self) -> None: + """Really submit the SDC in order to enable it.""" + raise NotImplementedError("Vfc doesn't need _really_submit") diff --git a/src/onapsdk/sdc/vl.py b/src/onapsdk/sdc/vl.py new file mode 100644 index 0000000..22872f0 --- /dev/null +++ b/src/onapsdk/sdc/vl.py @@ -0,0 +1,46 @@ +"""Vl 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 typing import Dict + +from onapsdk.exceptions import ResourceNotFound + +from .sdc_resource import SdcResource + + +class Vl(SdcResource): + """ONAP Vl Object used for SDC operations.""" + + def __init__(self, name: str, version: str = None, sdc_values: Dict[str, str] = None): + """Initialize Vl object. + + Vl has to exists in SDC. + + Args: + name (str): Vl name + version (str): Vl version + sdc_values (Dict[str, str], optional): Sdc values of existing Vl. Defaults to None. + + Raises: + ResourceNotFound: Vl doesn't exist in SDC + + """ + super().__init__(name=name, version=version, sdc_values=sdc_values) + if not sdc_values and not self.exists(): + raise ResourceNotFound( + "This Vl has to exist prior to object initialization.") + + def _really_submit(self) -> None: + """Really submit the SDC in order to enable it.""" + raise NotImplementedError("Vl don't need _really_submit") diff --git a/src/onapsdk/sdc/vsp.py b/src/onapsdk/sdc/vsp.py new file mode 100644 index 0000000..a6c4c24 --- /dev/null +++ b/src/onapsdk/sdc/vsp.py @@ -0,0 +1,370 @@ +"""VSP 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. +import json +from typing import Any, Optional +from typing import BinaryIO +from typing import Callable +from typing import Dict + +from onapsdk.exceptions import APIError, ParameterError +from onapsdk.sdc.sdc_element import SdcElement +from onapsdk.sdc.vendor import Vendor +import onapsdk.constants as const +from onapsdk.utils.headers_creator import headers_sdc_creator + +# Hard to do fewer attributes and still mapping SDC VSP object. +class Vsp(SdcElement): # pylint: disable=too-many-instance-attributes + """ + ONAP VSP Object used for SDC operations. + + Attributes: + name (str): the name of the vsp. Defaults to "ONAP-test-VSP". + identifier (str): the unique ID of the VSP from SDC. + status (str): the status of the VSP from SDC. + version (str): the version ID of the VSP from SDC. + csar_uuid (str): the CSAR ID of the VSP from SDC. + vendor (Vendor): The VSP Vendor + + """ + + VSP_PATH = "vendor-software-products" + headers = headers_sdc_creator(SdcElement.headers) + + def __init__(self, name: str = None, package: BinaryIO = None, + vendor: Vendor = None): + """ + Initialize vsp object. + + Args: + name (optional): the name of the vsp + + """ + super().__init__() + self._csar_uuid: str = None + self._vendor: Vendor = vendor or None + self.name: str = name or "ONAP-test-VSP" + self.package = package or None + + @property + def status(self): + """Return and load the status.""" + self.load_status() + return self._status + + def onboard(self) -> None: + """Onboard the VSP in SDC.""" + status: Optional[str] = self.status + if not status: + if not self._vendor: + raise ParameterError("No Vendor provided.") + self.create() + self.onboard() + elif status == const.DRAFT: + if not self.package: + raise ParameterError("No file/package provided.") + self.upload_package(self.package) + self.onboard() + elif status == const.UPLOADED: + self.validate() + self.onboard() + elif status == const.VALIDATED: + self.commit() + self.onboard() + elif status == const.COMMITED: + self.submit() + self.onboard() + elif status == const.CERTIFIED: + self.create_csar() + + def create(self) -> None: + """Create the Vsp in SDC if not already existing.""" + if self.vendor: + self._create("vsp_create.json.j2", + name=self.name, + vendor=self.vendor) + + def upload_package(self, package_to_upload: BinaryIO) -> None: + """ + Upload given zip file into SDC as artifacts for this Vsp. + + Args: + package_to_upload (file): the zip file to upload + + """ + self._action("upload package", + const.DRAFT, + self._upload_action, + package_to_upload=package_to_upload) + + def update_package(self, package_to_upload: BinaryIO) -> None: + """ + Upload given zip file into SDC as artifacts for this Vsp. + + Args: + package_to_upload (file): the zip file to upload + + """ + self._action("update package", + const.COMMITED, + self._upload_action, + package_to_upload=package_to_upload) + + def validate(self) -> None: + """Validate the artifacts uploaded.""" + self._action("validate", const.UPLOADED, self._validate_action) + + def commit(self) -> None: + """Commit the SDC Vsp.""" + self._action("commit", + const.VALIDATED, + self._generic_action, + action=const.COMMIT) + + def submit(self) -> None: + """Submit the SDC Vsp in order to enable it.""" + self._action("certify/sumbit", + const.COMMITED, + self._generic_action, + action=const.SUBMIT) + + def create_csar(self) -> None: + """Create the CSAR package in the SDC Vsp.""" + self._action("create CSAR package", const.CERTIFIED, + self._create_csar_action) + + @property + def vendor(self) -> Vendor: + """Return and lazy load the vendor.""" + if not self._vendor and self.created(): + details = self._get_vsp_details() + if details: + self._vendor = Vendor(name=details['vendorName']) + return self._vendor + + @vendor.setter + def vendor(self, vendor: Vendor) -> None: + """Set value for Vendor.""" + self._vendor = vendor + + @property + def csar_uuid(self) -> str: + """Return and lazy load the CSAR UUID.""" + if self.created() and not self._csar_uuid: + self.create_csar() + return self._csar_uuid + + @csar_uuid.setter + def csar_uuid(self, csar_uuid: str) -> None: + """Set value for csar uuid.""" + self._csar_uuid = csar_uuid + + def _get_item_version_details(self) -> Dict[Any, Any]: + """Get vsp item details.""" + if self.created() and self.version: + url = "{}/items/{}/versions/{}".format(self._base_url(), + self.identifier, + self.version) + return self.send_message_json('GET', 'get item version', url) + return {} + + def _upload_action(self, package_to_upload: BinaryIO): + """Do upload for real.""" + url = "{}/{}/{}/orchestration-template-candidate".format( + self._base_url(), Vsp._sdc_path(), self._version_path()) + headers = self.headers.copy() + headers.pop("Content-Type") + headers["Accept-Encoding"] = "gzip, deflate" + data = {'upload': package_to_upload} + upload_result = self.send_message('POST', + 'upload ZIP for Vsp', + url, + headers=headers, + files=data) + if upload_result: + # TODO https://jira.onap.org/browse/SDC-3505 pylint: disable=W0511 + response_json = json.loads(upload_result.text) + if response_json["status"] != "Success": + self._logger.error( + "an error occured during file upload for Vsp %s", + self.name) + raise APIError(response_json) + # End TODO https://jira.onap.org/browse/SDC-3505 + self._logger.info("Files for Vsp %s have been uploaded", + self.name) + else: + self._logger.error( + "an error occured during file upload for Vsp %s", + self.name) + + def _validate_action(self): + """Do validate for real.""" + url = "{}/{}/{}/orchestration-template-candidate/process".format( + self._base_url(), Vsp._sdc_path(), self._version_path()) + validate_result = self.send_message_json('PUT', + 'Validate artifacts for Vsp', + url) + if validate_result and validate_result['status'] == 'Success': + self._logger.info("Artifacts for Vsp %s have been validated", + self.name) + else: + self._logger.error( + "an error occured during artifacts validation for Vsp %s", + self.name) + + def _generic_action(self, action=None): + """Do a generic action for real.""" + if action: + self._action_to_sdc(action, action_type="lifecycleState") + + def _create_csar_action(self): + """Create CSAR package for real.""" + result = self._action_to_sdc(const.CREATE_PACKAGE, + action_type="lifecycleState") + if result: + self._logger.info("result: %s", result.text) + data = result.json() + self.csar_uuid = data['packageId'] + + def _action(self, action_name: str, right_status: str, + action_function: Callable[['Vsp'], None], **kwargs) -> None: + """ + Generate an action on the instance in order to send it to SDC. + + Args: + action_name (str): The name of the action (for the logs) + right_status (str): The status that the object must be + action_function (function): the function to perform if OK + + """ + self._logger.info("attempting to %s for %s in SDC", action_name, + self.name) + if self.status == right_status: + action_function(**kwargs) + else: + self._logger.warning( + "vsp %s in SDC is not created or not in %s status", self.name, + right_status) + + # VSP: DRAFT --> UPLOADED --> VALIDATED --> COMMITED --> CERTIFIED + def load_status(self) -> None: + """ + Load Vsp status from SDC. + + rules are following: + + * DRAFT: status == DRAFT and networkPackageName not present + + * UPLOADED: status == DRAFT and networkPackageName present and + validationData not present + + * VALIDATED: status == DRAFT and networkPackageName present and + validationData present and state.dirty = true + + * COMMITED: status == DRAFT and networkPackageName present and + validationData present and state.dirty = false + + * CERTIFIED: status == CERTIFIED + + status is found in sdc items + state is found in sdc version from items + networkPackageName and validationData is found in SDC vsp show + + """ + item_details = self._get_item_details() + if (item_details and item_details['status'] == const.CERTIFIED): + self._status = const.CERTIFIED + else: + self._check_status_not_certified() + + def _check_status_not_certified(self) -> None: + """Check a status when it's not certified.""" + vsp_version_details = self._get_item_version_details() + vsp_details = self._get_vsp_details() + if (vsp_version_details and 'state' in vsp_version_details + and not vsp_version_details['state']['dirty'] and vsp_details + and 'validationData' in vsp_details): + self._status = const.COMMITED + else: + self._check_status_not_commited() + + def _check_status_not_commited(self) -> None: + """Check a status when it's not certified or commited.""" + vsp_details = self._get_vsp_details() + if (vsp_details and 'validationData' in vsp_details): + self._status = const.VALIDATED + elif (vsp_details and 'validationData' not in vsp_details + and 'networkPackageName' in vsp_details): + self._status = const.UPLOADED + elif vsp_details: + self._status = const.DRAFT + + def _get_vsp_details(self) -> Dict[Any, Any]: + """Get vsp details.""" + if self.created() and self.version: + url = "{}/vendor-software-products/{}/versions/{}".format( + self._base_url(), self.identifier, self.version) + + return self.send_message_json('GET', 'get vsp version', url) + return {} + + @classmethod + def import_from_sdc(cls, values: Dict[str, Any]) -> 'Vsp': + """ + Import Vsp from SDC. + + Args: + values (Dict[str, Any]): dict to parse returned from SDC. + + Returns: + a Vsp instance with right values + + """ + cls._logger.debug("importing VSP %s from SDC", values['name']) + vsp = Vsp(values['name']) + vsp.identifier = values['id'] + vsp.vendor = Vendor(name=values['vendorName']) + return vsp + + def _really_submit(self) -> None: + """Really submit the SDC Vf in order to enable it. + + Raises: + NotImplementedError + + """ + raise NotImplementedError("VSP don't need _really_submit") + + @classmethod + def _sdc_path(cls) -> None: + """Give back the end of SDC path.""" + return cls.VSP_PATH + + def create_new_version(self) -> None: + """Create new version of VSP. + + Create a new major version of VSP so it would be possible to + update a package or do some changes in VSP. + + """ + self._logger.debug("Create new version of %s VSP", self.name) + self.send_message_json("POST", + "Create new VSP version", + (f"{self._base_url()}/items/{self.identifier}/" + f"versions/{self.version}"), + data=json.dumps({ + "creationMethod": "major", + "description": "New VSP version" + })) + self.load() diff --git a/src/onapsdk/sdnc/__init__.py b/src/onapsdk/sdnc/__init__.py new file mode 100644 index 0000000..8bc40e8 --- /dev/null +++ b/src/onapsdk/sdnc/__init__.py @@ -0,0 +1,15 @@ +"""SDNC package.""" +# 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 .preload import NetworkPreload, VfModulePreload diff --git a/src/onapsdk/sdnc/preload.py b/src/onapsdk/sdnc/preload.py new file mode 100644 index 0000000..8b00e5c --- /dev/null +++ b/src/onapsdk/sdnc/preload.py @@ -0,0 +1,148 @@ +"""SDNC preload 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 typing import Any, Dict, Iterable + +from onapsdk.utils.headers_creator import headers_sdnc_creator +from onapsdk.utils.jinja import jinja_env + +from .sdnc_element import SdncElement + + +class Preload(SdncElement): + """Preload base class.""" + + headers: Dict[str, str] = headers_sdnc_creator(SdncElement.headers) + + +class PreloadInformation(Preload): + """Preload information.""" + + def __init__(self, preload_id: str, preload_type: str, preload_data: Dict[str, Any]) -> None: + """Preload information initialization. + + Args: + preload_id (str): Preload id + preload_type (str): Preload type + preload_data (Dict[str, Any]): Preload data + """ + super().__init__() + self.preload_id: str = preload_id + self.preload_type: str = preload_type + self.preload_data: Dict[str, Any] = preload_data + + def __repr__(self) -> str: # noqa + """Preload information human readble string. + + Returns: + str: Preload information description + + """ + return (f"PreloadInformation(preload_id={self.preload_id}, " + f"preload_type={self.preload_type}, " + f"preload_data={self.preload_data})") + + @classmethod + def get_all(cls) -> Iterable["PreloadInformation"]: + """Get all preload informations. + + Get all uploaded preloads. + + Yields: + PreloadInformation: Preload information object + + """ + for preload_information in \ + cls.send_message_json(\ + "GET",\ + "Get SDNC preload information",\ + f"{cls.base_url}/restconf/operational/GENERIC-RESOURCE-API:preload-information" + ).get('preload-information', {}).get('preload-list', []): + yield PreloadInformation(preload_id=preload_information["preload-id"], + preload_type=preload_information["preload-type"], + preload_data=preload_information["preload-data"]) + + +class NetworkPreload(Preload): + """Class to upload network module preload.""" + + @classmethod + def upload_network_preload(cls, + network: "Network", + network_instance_name: str, + subnets: Iterable["Subnet"] = None) -> None: + """Upload network preload. + + Args: + network: Network object + network_instance_name (str): network instance name + subnets (Iterable[Subnet], optional): Iterable object of Subnet. + Defaults to None. + + """ + cls.send_message_json( + "POST", + "Upload Network preload using GENERIC-RESOURCE-API", + (f"{cls.base_url}/restconf/operations/" + "GENERIC-RESOURCE-API:preload-network-topology-operation"), + data=jinja_env().get_template( + "instantiate_network_ala_carte_upload_preload_gr_api.json.j2"). + render( + network=network, + network_instance_name=network_instance_name, + subnets=subnets if subnets else [] + ) + ) + + +class VfModulePreload(Preload): + """Class to upload vf module preload.""" + + @classmethod + def upload_vf_module_preload(cls, # pylint: disable=too-many-arguments + vnf_instance: "VnfInstance", + vf_module_instance_name: str, + vf_module: "VfModule", + vnf_parameters: Iterable["InstantiationParameter"] = None) -> None: + """Upload vf module preload. + + Args: + vnf_instance: VnfInstance object + vf_module_instance_name (str): VF module instance name + vf_module (VfModule): VF module + vnf_parameters (Iterable[InstantiationParameter], optional): Iterable object + of InstantiationParameter. Defaults to None. + + """ + vnf_para = [] + if vnf_parameters: + for vnf_parameter in vnf_parameters: + vnf_para.append({ + "name": vnf_parameter.name, + "value": vnf_parameter.value + }) + cls.send_message_json( + "POST", + "Upload VF module preload using GENERIC-RESOURCE-API", + (f"{cls.base_url}/restconf/operations/" + "GENERIC-RESOURCE-API:preload-vf-module-topology-operation"), + data=jinja_env().get_template( + "instantiate_vf_module_ala_carte_upload_preload_gr_api.json.j2"). + render( + vnf_instance=vnf_instance, + vf_module_instance_name=vf_module_instance_name, + vf_module=vf_module, + vnf_parameters=vnf_para + ) + ) diff --git a/src/onapsdk/sdnc/sdnc_element.py b/src/onapsdk/sdnc/sdnc_element.py new file mode 100644 index 0000000..84f56d9 --- /dev/null +++ b/src/onapsdk/sdnc/sdnc_element.py @@ -0,0 +1,48 @@ +"""SDNC base 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService +from onapsdk.utils.gui import GuiItem, GuiList + + +class SdncElement(OnapService): + """SDNC base class.""" + + base_url = settings.SDNC_URL + + @classmethod + def get_guis(cls) -> GuiItem: + """Retrieve the status of the SDNC GUIs. + + There are 2 GUIS + - SDNC DG Builder + - SDNC ODL + + Return the list of GUIs + """ + guilist = GuiList([]) + url = settings.SDNC_DG_GUI_SERVICE + response = cls.send_message( + "GET", "Get SDNC GUI DG Status", url) + guilist.add(GuiItem( + url, + response.status_code)) + url = settings.SDNC_ODL_GUI_SERVICE + response = cls.send_message( + "GET", "Get SDNC ODL GUI Status", url) + guilist.add(GuiItem( + url, + response.status_code)) + return guilist diff --git a/src/onapsdk/sdnc/templates/instantiate_network_ala_carte_upload_preload_gr_api.json.j2 b/src/onapsdk/sdnc/templates/instantiate_network_ala_carte_upload_preload_gr_api.json.j2 new file mode 100644 index 0000000..9eead38 --- /dev/null +++ b/src/onapsdk/sdnc/templates/instantiate_network_ala_carte_upload_preload_gr_api.json.j2 @@ -0,0 +1,42 @@ +{ + "input": { + "preload-network-topology-information": { + "network-policy": [], + "route-table-reference": [], + "vpn-bindings": [], + "network-topology-identifier-structure": { + "network-role": "integration_test_net", + "network-technology": "neutron", + "network-name": "{{ network_instance_name }}", + "network-type": "Generic NeutronNet" + }, + "is-external-network": false, + "is-shared-network": false, + "is-provider-network": false, + "physical-network-name": "Not Aplicable", + "subnets": [ + {% for subnet in subnets %} + { + "subnet-name": "{{ subnet.name }}", + "start-address": "{{ subnet.start_address }}", + "cidr-mask": "{{ subnet.cidr_mask }}", + "ip-version": "{{ subnet.ip_version }}", + {% if subnet.dhcp_enabled %} + "dhcp-enabled": "Y", + "dhcp-start-address": "{{ subnet.dhcp_start_address }}", + "dhcp-end-address": "{{ subnet.dhcp_end_address }}", + {% else %} + "dhcp-enabled": "N", + {% endif %} + "gateway-address": "{{ subnet.gateway_address }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + }, + "sdnc-request-header": { + "svc-request-id": "test", + "svc-notification-url": "http:\/\/onap.org:8080\/adapters\/rest\/SDNCNotify", + "svc-action": "reserve" + } + } +} diff --git a/src/onapsdk/sdnc/templates/instantiate_vf_module_ala_carte_upload_preload_gr_api.json.j2 b/src/onapsdk/sdnc/templates/instantiate_vf_module_ala_carte_upload_preload_gr_api.json.j2 new file mode 100644 index 0000000..eaa5936 --- /dev/null +++ b/src/onapsdk/sdnc/templates/instantiate_vf_module_ala_carte_upload_preload_gr_api.json.j2 @@ -0,0 +1,41 @@ +{ + "input":{ + "preload-vf-module-topology-information":{ + "vf-module-topology":{ + "vf-module-topology-identifier":{ + "vf-module-name":"{{vf_module_instance_name}}" + }, + "vf-module-parameters": { + "param": {{ vnf_parameters }} + } + }, + "vnf-topology-identifier-structure":{ + "vnf-name":"{{vnf_instance.vnf_name}}", + "vnf-type":"{{vnf_instance.vnf_type}}" + }, + "vnf-resource-assignments":{ + "availability-zones":{ + "availability-zone":[ + "nova" + ], + "max-count":1 + }, + "vnf-networks":{ + "vnf-network":[] + } + } + }, + "request-information":{ + "request-id":"test", + "order-version":"1", + "notification-url":"onap.org", + "order-number":"1", + "request-action":"PreloadVfModuleRequest" + }, + "sdnc-request-header":{ + "svc-request-id":"test", + "svc-notification-url":"http:\/\/onap.org:8080\/adapters\/rest\/SDNCNotify", + "svc-action":"reserve" + } + } +} diff --git a/src/onapsdk/so/__init__.py b/src/onapsdk/so/__init__.py new file mode 100644 index 0000000..15a483f --- /dev/null +++ b/src/onapsdk/so/__init__.py @@ -0,0 +1,14 @@ +"""ONAP SDK SO package.""" +# 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. diff --git a/src/onapsdk/so/deletion.py b/src/onapsdk/so/deletion.py new file mode 100644 index 0000000..35ff0ee --- /dev/null +++ b/src/onapsdk/so/deletion.py @@ -0,0 +1,167 @@ +"""Deletion 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 abc import ABC + +from onapsdk.onap_service import OnapService +from onapsdk.utils.headers_creator import headers_so_creator +from onapsdk.utils.jinja import jinja_env + +from onapsdk.so.so_element import OrchestrationRequest + + +class DeletionRequest(OrchestrationRequest, ABC): + """Deletion request base class.""" + + @classmethod + def send_request(cls, instance: "AaiResource", a_la_carte: bool = True) -> "Deletion": + """Abstract method to send instance deletion request. + + Raises: + NotImplementedError: Needs to be implemented in inheriting classes + + """ + raise NotImplementedError + + +class VfModuleDeletionRequest(DeletionRequest): # pytest: disable=too-many-ancestors + """VF module deletion class.""" + + @classmethod + def send_request(cls, + instance: "VfModuleInstance", + a_la_carte: bool = True) -> "VfModuleDeletion": + """Send request to SO to delete VNF instance. + + Args: + instance (VfModuleInstance): Vf Module instance to delete + a_la_carte (boolean): deletion mode + + Returns: + VnfDeletionRequest: Deletion request object + + """ + cls._logger.debug("VF module %s deletion request", instance.vf_module_id) + response = cls.send_message_json("DELETE", + (f"Create {instance.vf_module_id} VF module" + "deletion request"), + (f"{cls.base_url}/onap/so/infra/" + f"serviceInstantiation/{cls.api_version}/" + "serviceInstances/" + f"{instance.vnf_instance.service_instance.instance_id}/" + f"vnfs/{instance.vnf_instance.vnf_id}/" + f"vfModules/{instance.vf_module_id}"), + data=jinja_env(). + get_template("deletion_vf_module.json.j2"). + render(vf_module_instance=instance, + a_la_carte=a_la_carte), + headers=headers_so_creator(OnapService.headers)) + return cls(request_id=response["requestReferences"]["requestId"]) + + +class VnfDeletionRequest(DeletionRequest): # pytest: disable=too-many-ancestors + """VNF deletion class.""" + + @classmethod + def send_request(cls, + instance: "VnfInstance", + a_la_carte: bool = True) -> "VnfDeletionRequest": + """Send request to SO to delete VNF instance. + + Args: + instance (VnfInstance): VNF instance to delete + a_la_carte (boolean): deletion mode + + Returns: + VnfDeletionRequest: Deletion request object + + """ + cls._logger.debug("VNF %s deletion request", instance.vnf_id) + response = cls.send_message_json("DELETE", + f"Create {instance.vnf_id} VNF deletion request", + (f"{cls.base_url}/onap/so/infra/" + f"serviceInstantiation/{cls.api_version}/" + "serviceInstances/" + f"{instance.service_instance.instance_id}/" + f"vnfs/{instance.vnf_id}"), + data=jinja_env(). + get_template("deletion_vnf.json.j2"). + render(vnf_instance=instance, + a_la_carte=a_la_carte), + headers=headers_so_creator(OnapService.headers)) + return cls(request_id=response["requestReferences"]["requestId"]) + + +class ServiceDeletionRequest(DeletionRequest): # pytest: disable=too-many-ancestors + """Service deletion request class.""" + + @classmethod + def send_request(cls, + instance: "ServiceInstance", + a_la_carte: bool = True) -> "ServiceDeletionRequest": + """Send request to SO to delete service instance. + + Args: + instance (ServiceInstance): service instance to delete + a_la_carte (boolean): deletion mode + + Returns: + ServiceDeletionRequest: Deletion request object + + """ + cls._logger.debug("Service %s deletion request", instance.instance_id) + response = cls.send_message_json("DELETE", + f"Create {instance.instance_id} Service deletion request", + (f"{cls.base_url}/onap/so/infra/" + f"serviceInstantiation/{cls.api_version}/" + f"serviceInstances/{instance.instance_id}"), + data=jinja_env(). + get_template("deletion_service.json.j2"). + render(service_instance=instance, + a_la_carte=a_la_carte), + headers=headers_so_creator(OnapService.headers)) + return cls(request_id=response["requestReferences"]["requestId"]) + + +class NetworkDeletionRequest(DeletionRequest): # pylint: disable=too-many-ancestors + """Network deletion request class.""" + + @classmethod + def send_request(cls, + instance: "NetworkInstance", + a_la_carte: bool = True) -> "VnfDeletionRequest": + """Send request to SO to delete Network instance. + + Args: + instance (NetworkInstance): Network instance to delete + a_la_carte (boolean): deletion mode + + Returns: + NetworkDeletionRequest: Deletion request object + + """ + cls._logger.debug("Network %s deletion request", instance.network_id) + response = cls.send_message_json("DELETE", + f"Create {instance.network_id} Network deletion request", + (f"{cls.base_url}/onap/so/infra/" + f"serviceInstantiation/{cls.api_version}/" + "serviceInstances/" + f"{instance.service_instance.instance_id}/" + f"networks/{instance.network_id}"), + data=jinja_env(). + get_template("deletion_network.json.j2"). + render(network_instance=instance, + a_la_carte=a_la_carte), + headers=headers_so_creator(OnapService.headers)) + return cls(request_id=response["requestReferences"]["requestId"]) diff --git a/src/onapsdk/so/instantiation.py b/src/onapsdk/so/instantiation.py new file mode 100644 index 0000000..bb0bde2 --- /dev/null +++ b/src/onapsdk/so/instantiation.py @@ -0,0 +1,957 @@ +"""Instantion 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 abc import ABC +from dataclasses import dataclass, field +from typing import Any, Dict, Iterable, List, Optional +from uuid import uuid4 +from dacite import from_dict + +from onapsdk.aai.business.owning_entity import OwningEntity +from onapsdk.exceptions import ( + APIError, InvalidResponse, ParameterError, ResourceNotFound, StatusError +) +from onapsdk.onap_service import OnapService +from onapsdk.sdnc import NetworkPreload, VfModulePreload +from onapsdk.sdc.service import Network, Service as SdcService, Vnf, VfModule +from onapsdk.utils.jinja import jinja_env +from onapsdk.utils.headers_creator import headers_so_creator +from onapsdk.configuration import settings + +from .so_element import OrchestrationRequest + + +@dataclass +class Operation: + """Operation class with data about method and suffix for VnfOperation.""" + + request_method: str + request_suffix: str + + +class VnfOperation(Operation): # pylint: disable=too-few-public-methods + """Class to store possible operations' data for vnfs (request method and suffix).""" + + UPDATE = Operation("PUT", "") + HEALTHCHECK = Operation("POST", "/healthcheck") + + +@dataclass +class SoServiceVfModule: + """Class to store a VfModule instance parameters.""" + + model_name: str + instance_name: str + parameters: Dict[str, Any] = field(default_factory=dict) + processing_priority: Optional[int] = None + + +@dataclass +class SoServiceXnf: + """Class to store a Xnf instance parameters.""" + + model_name: str + instance_name: str + parameters: Dict[str, Any] = field(default_factory=dict) + processing_priority: Optional[int] = None + + @classmethod + def load(cls, data: Dict[str, Any]) -> "SoServiceVnf": + """Create a vnf instance description object from the dict. + + Useful if you keep your instance data in file. + + Returns: + SoServiceVnf: SoServiceVnf object created from the dictionary + + """ + return from_dict(data_class=cls, data=data) + + +@dataclass +class SoServiceVnf(SoServiceXnf): + """Class to store a Vnf instance parameters.""" + + vf_modules: List[SoServiceVfModule] = field(default_factory=list) + + +@dataclass +class SoServicePnf(SoServiceXnf): + """Class to store a Pnf instance parameters.""" + + +@dataclass +class SoService: + """Class to store SO Service parameters used for macro instantiation. + + Contains value list: List of vnfs to instantiate + Contains value: subscription service type + """ + + subscription_service_type: str + vnfs: List[SoServiceVnf] = field(default_factory=list) + pnfs: List[SoServicePnf] = field(default_factory=list) + instance_name: Optional[str] = None + + @classmethod + def load(cls, data: Dict[str, Any]) -> "SoService": + """Create a service instance description object from the dict. + + Useful if you keep your instance data in file. + + Returns: + SoService: SoService object created from the dictionary + + """ + return from_dict(data_class=cls, data=data) + + + +@dataclass +class VnfParameters: + """Class to store vnf parameters used for macro instantiation. + + Contains value lists: List vnf Instantiation parameters and list of + vfModule parameters + """ + + name: str + vnf_parameters: Iterable["InstantiationParameter"] = None + vfmodule_parameters: Iterable["VfmoduleParameters"] = None + +@dataclass +class VfmoduleParameters: + """Class to store vfmodule parameters used for macro instantiation. + + Contains value lists: List of vfModule parameters + """ + + name: str + vfmodule_parameters: Iterable["InstantiationParameter"] = None + + +@dataclass +class InstantiationParameter: + """Class to store instantiation parameters used for preload or macro instantiation. + + Contains two values: name of parameter and it's value + """ + + name: str + value: str + + +@dataclass +class Subnet: # pylint: disable=too-many-instance-attributes + """Class to store subnet parameters used for preload.""" + + name: str + start_address: str + gateway_address: str + role: str = None + cidr_mask: str = "24" + ip_version: str = "4" + dhcp_enabled: bool = False + dhcp_start_address: Optional[str] = None + dhcp_end_address: Optional[str] = None + + def __post_init__(self) -> None: + """Post init subnet method. + + Checks if both dhcp_start_address and dhcp_end_address values are + provided if dhcp is enabled. + + Raises: + ParameterError: Neither dhcp_start_addres + nor dhcp_end_address are provided + + """ + if self.dhcp_enabled and \ + not all([self.dhcp_start_address, + self.dhcp_end_address]): + msg = "DHCP is enabled but neither DHCP " \ + "start nor end adresses are provided." + raise ParameterError(msg) + + +class Instantiation(OrchestrationRequest, ABC): + """Abstract class used for instantiation.""" + + def __init__(self, + name: str, + request_id: str, + instance_id: str) -> None: + """Instantiate object initialization. + + Initializator used by classes inherited from this abstract class. + + Args: + name (str): instantiated object name + request_id (str): request ID + instance_id (str): instance ID + """ + super().__init__(request_id) + self.name: str = name + self.instance_id: str = instance_id + + +class VfModuleInstantiation(Instantiation): # pytest: disable=too-many-ancestors + """VF module instantiation class.""" + + def __init__(self, + name: str, + request_id: str, + instance_id: str, + vf_module: VfModule) -> None: + """Initialize class object. + + Args: + name (str): vf module name + request_id (str): request ID + instance_id (str): instance ID + vnf_instantiation (VnfInstantiation): VNF instantiation class object + vf_module (VfModule): VF module used for instantiation + """ + super().__init__(name, request_id, instance_id) + self.vf_module: VfModule = vf_module + + @classmethod + def instantiate_ala_carte(cls, # pylint: disable=too-many-arguments + vf_module: "VfModule", + vnf_instance: "VnfInstance", + cloud_region: "CloudRegion", + tenant: "Tenant", + vf_module_instance_name: str = None, + vnf_parameters: Iterable["InstantiationParameter"] = None, + use_preload: bool = True) -> "VfModuleInstantiation": + """Instantiate VF module. + + Iterate throught vf modules from service Tosca file and instantiate vf modules. + + Args: + vf_module (VfModule): VfModule to instantiate + vnf_instance (VnfInstance): VnfInstance object + cloud_region (CloudRegion, optional): Cloud region to use in instantiation request. + Defaults to None. + tenant (Tenant, optional): Tenant to use in instnatiation request. + Defaults to None. + vf_module_instance_name_factory (str, optional): Factory to create VF module names. + It's going to be a prefix of name. Index of vf module in Tosca file will be + added to it. + If no value is provided it's going to be + "Python_ONAP_SDK_vf_module_service_instance_{str(uuid4())}". + Defaults to None. + vnf_parameters (Iterable[InstantiationParameter], optional): Parameters which are + going to be used in preload upload for vf modules or passed in "userParams". + Defaults to None. + use_preload (bool, optional): This flag determines whether instantiation parameters + are used as preload or "userParams" content. Defaults to True + + Yields: + Iterator[VfModuleInstantiation]: VfModuleInstantiation class object. + + """ + if vf_module_instance_name is None: + vf_module_instance_name = \ + f"Python_ONAP_SDK_vf_module_instance_{str(uuid4())}" + if use_preload: + VfModulePreload.upload_vf_module_preload( + vnf_instance, + vf_module_instance_name, + vf_module, + vnf_parameters + ) + vnf_parameters = None + sdc_service: SdcService = vnf_instance.service_instance.sdc_service + response: dict = cls.send_message_json( + "POST", + (f"Instantiate {sdc_service.name} " + f"service vf module {vf_module.name}"), + (f"{cls.base_url}/onap/so/infra/serviceInstantiation/{cls.api_version}/" + f"serviceInstances/{vnf_instance.service_instance.instance_id}/vnfs/" + f"{vnf_instance.vnf_id}/vfModules"), + data=jinja_env().get_template("instantiate_vf_module_ala_carte.json.j2"). + render( + vf_module_instance_name=vf_module_instance_name, + vf_module=vf_module, + service=sdc_service, + cloud_region=cloud_region, + tenant=tenant, + vnf_instance=vnf_instance, + vf_module_parameters=vnf_parameters or [] + ), + headers=headers_so_creator(OnapService.headers) + ) + return VfModuleInstantiation( + name=vf_module_instance_name, + request_id=response["requestReferences"].get("requestId"), + instance_id=response["requestReferences"].get("instanceId"), + vf_module=vf_module + ) + + +class NodeTemplateInstantiation(Instantiation, ABC): # pytest: disable=too-many-ancestors + """Base class for service's node_template object instantiation.""" + + def __init__(self, # pylint: disable=too-many-arguments + name: str, + request_id: str, + instance_id: str, + line_of_business: str, + platform: str) -> None: + """Node template object initialization. + + Args: + name (str): Node template name + request_id (str): Node template instantiation request ID + instance_id (str): Node template instance ID + line_of_business (str): LineOfBusiness name + platform (str): Platform name + """ + super().__init__(name, request_id, instance_id) + self.line_of_business = line_of_business + self.platform = platform + + +class VnfInstantiation(NodeTemplateInstantiation): # pylint: disable=too-many-ancestors + """VNF instantiation class.""" + + def __init__(self, # pylint: disable=too-many-arguments + name: str, + request_id: str, + instance_id: str, + line_of_business: str, + platform: str, + vnf: Vnf) -> None: + """Class VnfInstantion object initialization. + + Args: + name (str): VNF name + request_id (str): request ID + instance_id (str): instance ID + service_instantiation ([type]): ServiceInstantiation class object + line_of_business (str): LineOfBusiness name + platform (str): Platform name + vnf (Vnf): Vnf class object + """ + super().__init__(name, request_id, instance_id, line_of_business, platform) + self.vnf = vnf + + @classmethod + def create_from_request_response(cls, request_response: dict) -> "VnfInstantiation": + """Create VNF instantiation object based on request details. + + Raises: + ResourceNotFound: Service related with given object doesn't exist + ResourceNotFound: No ServiceInstantiation related with given VNF instantiation + ResourceNotFound: VNF related with given object doesn't exist + InvalidResponse: Invalid dictionary - couldn't create VnfInstantiation object + + Returns: + VnfInstantiation: VnfInstantiation object + + """ + if request_response.get("request", {}).get("requestScope") == "vnf" and \ + request_response.get("request", {}).get("requestType") == "createInstance": + service: SdcService = None + for related_instance in request_response.get("request", {}).get("requestDetails", {})\ + .get("relatedInstanceList", []): + if related_instance.get("relatedInstance", {}).get("modelInfo", {})\ + .get("modelType") == "service": + service = SdcService(related_instance.get("relatedInstance", {})\ + .get("modelInfo", {}).get("modelName")) + if not service: + raise ResourceNotFound("No related service in Vnf instance details response") + vnf: Vnf = None + for service_vnf in service.vnfs: + if service_vnf.name == request_response.get("request", {})\ + .get("requestDetails", {}).get("modelInfo", {}).get("modelCustomizationName"): + vnf = service_vnf + if not vnf: + raise ResourceNotFound("No vnf in service vnfs list") + return cls( + name=request_response.get("request", {})\ + .get("instanceReferences", {}).get("vnfInstanceName"), + request_id=request_response.get("request", {}).get("requestId"), + instance_id=request_response.get("request", {})\ + .get("instanceReferences", {}).get("vnfInstanceId"), + line_of_business=request_response.get("request", {})\ + .get("requestDetails", {}).get("lineOfBusiness", {}).get("lineOfBusinessName"), + platform=request_response.get("request", {})\ + .get("requestDetails", {}).get("platform", {}).get("platformName"), + vnf=vnf + ) + raise InvalidResponse("Invalid vnf instantions in response dictionary's requestList") + + @classmethod + def get_by_vnf_instance_name(cls, vnf_instance_name: str) -> "VnfInstantiation": + """Get VNF instantiation request by instance name. + + Raises: + InvalidResponse: Vnf instance with given name does not contain + requestList or the requestList does not contain any details. + + Returns: + VnfInstantiation: Vnf instantiation request object + + """ + response: dict = cls.send_message_json( + "GET", + f"Check {vnf_instance_name} service instantiation status", + (f"{cls.base_url}/onap/so/infra/orchestrationRequests/{cls.api_version}?" + f"filter=vnfInstanceName:EQUALS:{vnf_instance_name}"), + headers=headers_so_creator(OnapService.headers) + ) + key = "requestList" + if not response.get(key, []): + raise InvalidResponse(f"{key} of a Vnf instance is missing.") + for details in response[key]: + return cls.create_from_request_response(details) + msg = f"No details available in response dictionary's {key}." + raise InvalidResponse(msg) + + @classmethod + def instantiate_ala_carte(cls, # pylint: disable=too-many-arguments + aai_service_instance: "ServiceInstance", + vnf_object: "Vnf", + line_of_business: str, + platform: str, + cloud_region: "CloudRegion", + tenant: "Tenant", + sdc_service: "SdcService", + vnf_instance_name: str = None, + vnf_parameters: Iterable["InstantiationParameter"] = None + ) -> "VnfInstantiation": + """Instantiate Vnf using a'la carte method. + + Args: + vnf_object (Vnf): Vnf to instantiate + line_of_business_object (LineOfBusiness): LineOfBusiness to use in instantiation request + platform_object (Platform): Platform to use in instantiation request + cloud_region (CloudRegion): Cloud region to use in instantiation request. + tenant (Tenant): Tenant to use in instnatiation request. + vnf_instance_name (str, optional): Vnf instance name. Defaults to None. + vnf_parameters (Iterable[InstantiationParameter], optional): Instantiation parameters + that are sent in the request. Defaults to None + + Returns: + VnfInstantiation: VnfInstantiation object + + """ + if vnf_instance_name is None: + vnf_instance_name = \ + f"Python_ONAP_SDK_vnf_instance_{str(uuid4())}" + response: dict = cls.send_message_json( + "POST", + (f"Instantiate {sdc_service.name} " + f"service vnf {vnf_object.name}"), + (f"{cls.base_url}/onap/so/infra/serviceInstantiation/{cls.api_version}/" + f"serviceInstances/{aai_service_instance.instance_id}/vnfs"), + data=jinja_env().get_template("instantiate_vnf_ala_carte.json.j2"). + render( + instance_name=vnf_instance_name, + vnf=vnf_object, + service=sdc_service, + cloud_region=cloud_region or \ + next(aai_service_instance.service_subscription.cloud_regions), + tenant=tenant or next(aai_service_instance.service_subscription.tenants), + line_of_business=line_of_business, + platform=platform, + service_instance=aai_service_instance, + vnf_parameters=vnf_parameters or [] + ), + headers=headers_so_creator(OnapService.headers) + ) + return VnfInstantiation( + name=vnf_instance_name, + request_id=response["requestReferences"]["requestId"], + instance_id=response["requestReferences"]["instanceId"], + line_of_business=line_of_business, + platform=platform, + vnf=vnf_object + ) + + @classmethod + def instantiate_macro(cls, # pylint: disable=too-many-arguments, too-many-locals + aai_service_instance: "ServiceInstance", + vnf_object: "Vnf", + line_of_business: str, + platform: str, + cloud_region: "CloudRegion", + tenant: "Tenant", + sdc_service: "SdcService", + vnf_instance_name: str = None, + vnf_parameters: Iterable["InstantiationParameter"] = None, + so_vnf: "SoServiceVnf" = None + ) -> "VnfInstantiation": + """Instantiate Vnf using macro method. + + Args: + aai_service_instance (ServiceInstance): Service instance associated with request + vnf_object (Vnf): Vnf to instantiate + line_of_business (LineOfBusiness): LineOfBusiness to use in instantiation request + platform (Platform): Platform to use in instantiation request + cloud_region (CloudRegion): Cloud region to use in instantiation request. + tenant (Tenant): Tenant to use in instantiation request. + vnf_instance_name (str, optional): Vnf instance name. Defaults to None. + vnf_parameters (Iterable[InstantiationParameter], optional): Instantiation parameters + that are sent in the request. Defaults to None + so_vnf (SoServiceVnf): object with vnf instance parameters + + Returns: + VnfInstantiation: VnfInstantiation object + + """ + owning_entity_id = None + project = settings.PROJECT + + for relationship in aai_service_instance.relationships: + if relationship.related_to == "owning-entity": + owning_entity_id = relationship.relationship_data.pop().get("relationship-value") + if relationship.related_to == "project": + project = relationship.relationship_data.pop().get("relationship-value") + + owning_entity = OwningEntity.get_by_owning_entity_id( + owning_entity_id=owning_entity_id) + + if so_vnf: + template_file = "instantiate_vnf_macro_so_vnf.json.j2" + if so_vnf.instance_name: + vnf_instance_name = so_vnf.instance_name + else: + template_file = "instantiate_vnf_macro.json.j2" + if vnf_instance_name is None: + vnf_instance_name = \ + f"Python_ONAP_SDK_vnf_instance_{str(uuid4())}" + + response: dict = cls.send_message_json( + "POST", + (f"Instantiate {sdc_service.name} " + f"service vnf {vnf_object.name}"), + (f"{cls.base_url}/onap/so/infra/serviceInstantiation/{cls.api_version}/" + f"serviceInstances/{aai_service_instance.instance_id}/vnfs"), + data=jinja_env().get_template(template_file).render( + instance_name=vnf_instance_name, + vnf=vnf_object, + service=sdc_service, + cloud_region=cloud_region or \ + next(aai_service_instance.service_subscription.cloud_regions), + tenant=tenant or next(aai_service_instance.service_subscription.tenants), + project=project, + owning_entity=owning_entity, + line_of_business=line_of_business, + platform=platform, + service_instance=aai_service_instance, + vnf_parameters=vnf_parameters or [], + so_vnf=so_vnf + ), + headers=headers_so_creator(OnapService.headers) + ) + + return VnfInstantiation( + name=vnf_instance_name, + request_id=response["requestReferences"]["requestId"], + instance_id=response["requestReferences"]["instanceId"], + line_of_business=line_of_business, + platform=platform, + vnf=vnf_object + ) + + @classmethod + def so_action(cls, # pylint: disable=too-many-arguments, too-many-locals + vnf_instance: "VnfInstance", + operation_type: VnfOperation, + aai_service_instance: "ServiceInstance", + line_of_business: str, + platform: str, + sdc_service: "SdcService", + so_service: "SoService" = None + ) -> "VnfInstantiation": + """Execute SO action (update or healthcheck) for selected vnf with SO macro request. + + Args: + vnf_instance (VnfInstance): vnf instance object + operation_type (VnfOperation): name of the operation to trigger + aai_service_instance (AaiService): Service Instance object from aai + line_of_business (LineOfBusiness): LineOfBusiness name to use + in instantiation request + platform (Platform): Platform name to use in instantiation request + sdc_service (SdcService): Service model information + so_service (SoService, optional): SO values to use in SO request + + Raises: + StatusError: if the provided operation is not supported + + Returns: + VnfInstantiation: VnfInstantiation object + + """ + if operation_type not in (VnfOperation.HEALTHCHECK, VnfOperation.UPDATE): + raise StatusError("Operation not supported!") + + owning_entity_id = None + project = settings.PROJECT + + for relationship in aai_service_instance.relationships: + if relationship.related_to == "owning-entity": + owning_entity_id = relationship.relationship_data.pop().get("relationship-value") + if relationship.related_to == "project": + project = relationship.relationship_data.pop().get("relationship-value") + + owning_entity = OwningEntity.get_by_owning_entity_id( + owning_entity_id=owning_entity_id) + + response: dict = cls.send_message_json( + operation_type.request_method, + (f"So Action {sdc_service.name} " + f" vnf instance {vnf_instance.vnf_id}"), + (f"{cls.base_url}/onap/so/infra/serviceInstantiation/{cls.api_version}/" + f"serviceInstances/{aai_service_instance.instance_id}/vnfs/{vnf_instance.vnf_id}" + f"{operation_type.request_suffix}"), + data=jinja_env().get_template("instantiate_multi_vnf_service_macro.json.j2").render( + sdc_service=sdc_service, + cloud_region=next(aai_service_instance.service_subscription.cloud_regions), + tenant=next(aai_service_instance.service_subscription.tenants), + customer=aai_service_instance.service_subscription.customer, + project=project, + owning_entity=owning_entity, + line_of_business=line_of_business, + platform=platform, + service_instance_name=aai_service_instance.instance_name, + so_service=so_service + ), + headers=headers_so_creator(OnapService.headers) + ) + + return VnfInstantiation( + name=vnf_instance.vnf_name, + request_id=response["requestReferences"]["requestId"], + instance_id=response["requestReferences"]["instanceId"], + line_of_business=line_of_business, + platform=platform, + vnf=vnf_instance + ) + + +class ServiceInstantiation(Instantiation): # pylint: disable=too-many-ancestors + """Service instantiation class.""" + + def __init__(self, # pylint: disable=too-many-arguments + name: str, + request_id: str, + instance_id: str, + sdc_service: "SdcService", + cloud_region: "CloudRegion", + tenant: "Tenant", + customer: "Customer", + owning_entity: OwningEntity, + project: str) -> None: + """Class ServiceInstantiation object initialization. + + Args: + name (str): service instance name + request_id (str): service instantiation request ID + instance_id (str): service instantiation ID + sdc_service (SdcService): SdcService class object + cloud_region (CloudRegion): CloudRegion class object + tenant (Tenant): Tenant class object + customer (Customer): Customer class object + owning_entity (OwningEntity): OwningEntity class object + project (str): Project name + + """ + super().__init__(name, request_id, instance_id) + self.sdc_service = sdc_service + self.cloud_region = cloud_region + self.tenant = tenant + self.customer = customer + self.owning_entity = owning_entity + self.project = project + + @classmethod + def instantiate_ala_carte(cls, # pylint: disable=too-many-arguments + sdc_service: "SdcService", + cloud_region: "CloudRegion", + tenant: "Tenant", + customer: "Customer", + owning_entity: OwningEntity, + project: str, + service_subscription: "ServiceSubscription", + service_instance_name: str = None, + enable_multicloud: bool = False) -> "ServiceInstantiation": + """Instantiate service using SO a'la carte request. + + Args: + sdc_service (SdcService): Service to instantiate + cloud_region (CloudRegion): Cloud region to use in instantiation request + tenant (Tenant): Tenant to use in instantiation request + customer (Customer): Customer to use in instantiation request + owning_entity (OwningEntity): Owning entity to use in instantiation request + project (str): Project name to use in instantiation request + service_subscription (ServiceSubscription): Customer's service subsription. + service_instance_name (str, optional): Service instance name. Defaults to None. + enable_multicloud (bool, optional): Determines if Multicloud should be enabled + for instantiation request. Defaults to False. + + Raises: + StatusError: if a service is not distributed. + + Returns: + ServiceInstantiation: instantiation request object + + """ + if not sdc_service.distributed: + msg = f"Service {sdc_service.name} is not distributed." + raise StatusError(msg) + if service_instance_name is None: + service_instance_name = f"Python_ONAP_SDK_service_instance_{str(uuid4())}" + response: dict = cls.send_message_json( + "POST", + f"Instantiate {sdc_service.name} service a'la carte", + (f"{cls.base_url}/onap/so/infra/" + f"serviceInstantiation/{cls.api_version}/serviceInstances"), + data=jinja_env().get_template("instantiate_service_ala_carte.json.j2"). + render( + sdc_service=sdc_service, + cloud_region=cloud_region, + tenant=tenant, + customer=customer, + owning_entity=owning_entity, + service_instance_name=service_instance_name, + project=project, + enable_multicloud=enable_multicloud, + service_subscription=service_subscription + ), + headers=headers_so_creator(OnapService.headers) + ) + return cls( + name=service_instance_name, + request_id=response["requestReferences"].get("requestId"), + instance_id=response["requestReferences"].get("instanceId"), + sdc_service=sdc_service, + cloud_region=cloud_region, + tenant=tenant, + customer=customer, + owning_entity=owning_entity, + project=project + ) + + # pylint: disable=too-many-arguments, too-many-locals + @classmethod + def instantiate_macro(cls, + sdc_service: "SdcService", + customer: "Customer", + owning_entity: OwningEntity, + project: str, + line_of_business: str, + platform: str, + aai_service: "AaiService" = None, + cloud_region: "CloudRegion" = None, + tenant: "Tenant" = None, + service_instance_name: str = None, + vnf_parameters: Iterable["VnfParameters"] = None, + enable_multicloud: bool = False, + so_service: "SoService" = None, + service_subscription: "ServiceSubscription" = None + ) -> "ServiceInstantiation": + """Instantiate service using SO macro request. + + Args: + sdc_service (SdcService): Service to instantiate + customer (Customer): Customer to use in instantiation request + owning_entity (OwningEntity): Owning entity to use in instantiation request + project (Project): Project name to use in instantiation request + line_of_business_object (LineOfBusiness): LineOfBusiness name to use + in instantiation request + platform_object (Platform): Platform name to use in instantiation request + aai_service (AaiService): Service object from aai sdc + cloud_region (CloudRegion): Cloud region to use in instantiation request + tenant (Tenant): Tenant to use in instantiation request + service_instance_name (str, optional): Service instance name. Defaults to None. + vnf_parameters: (Iterable[VnfParameters]): Parameters which are + going to be used for vnfs instantiation. Defaults to None. + enable_multicloud (bool, optional): Determines if Multicloud should be enabled + for instantiation request. Defaults to False. + so_service (SoService, optional): SO values to use in instantiation request + service_subscription(ServiceSubscription, optional): Customer service subscription + for the instantiated service. Required if so_service is not provided. + + Raises: + StatusError: if a service is not distributed. + + Returns: + ServiceInstantiation: instantiation request object + + """ + template_file = "instantiate_service_macro.json.j2" + if so_service: + template_file = "instantiate_multi_vnf_service_macro.json.j2" + if so_service.instance_name: + service_instance_name = so_service.instance_name + else: + if not service_subscription: + raise ParameterError("If no so_service is provided, " + "service_subscription parameter is required!") + if service_instance_name is None: + service_instance_name = f"Python_ONAP_SDK_service_instance_{str(uuid4())}" + if not sdc_service.distributed: + msg = f"Service {sdc_service.name} is not distributed." + raise StatusError(msg) + + response: dict = cls.send_message_json( + "POST", + f"Instantiate {sdc_service.name} service macro", + (f"{cls.base_url}/onap/so/infra/" + f"serviceInstantiation/{cls.api_version}/serviceInstances"), + data=jinja_env().get_template(template_file). \ + render( + so_service=so_service, + sdc_service=sdc_service, + cloud_region=cloud_region, + tenant=tenant, + customer=customer, + owning_entity=owning_entity, + project=project, + aai_service=aai_service, + line_of_business=line_of_business, + platform=platform, + service_instance_name=service_instance_name, + vnf_parameters=vnf_parameters, + enable_multicloud=enable_multicloud, + service_subscription=service_subscription + ), + headers=headers_so_creator(OnapService.headers) + ) + return cls( + name=service_instance_name, + request_id=response["requestReferences"].get("requestId"), + instance_id=response["requestReferences"].get("instanceId"), + sdc_service=sdc_service, + cloud_region=cloud_region, + tenant=tenant, + customer=customer, + owning_entity=owning_entity, + project=project + ) + + @property + def aai_service_instance(self) -> "ServiceInstance": + """Service instance associated with service instantiation request. + + Raises: + StatusError: if a service is not instantiated - + not in COMPLETE status. + APIError: A&AI resource is not created + + Returns: + ServiceInstance: ServiceInstance + + """ + required_status = self.StatusEnum.COMPLETED + if self.status != required_status: + msg = (f"Service {self.name} is not instantiated - " + f"not in {required_status} status.") + raise StatusError(msg) + try: + service_subscription: "ServiceSubscription" = \ + self.customer.get_service_subscription_by_service_type(self.sdc_service.name) + return service_subscription.get_service_instance_by_name(self.name) + except APIError as exc: + self._logger.error("A&AI resources not created properly") + raise exc + + +class NetworkInstantiation(NodeTemplateInstantiation): # pylint: disable=too-many-ancestors + """Network instantiation class.""" + + def __init__(self, # pylint: disable=too-many-arguments + name: str, + request_id: str, + instance_id: str, + line_of_business: str, + platform: str, + network: Network) -> None: + """Class NetworkInstantiation object initialization. + + Args: + name (str): VNF name + request_id (str): request ID + instance_id (str): instance ID + service_instantiation ([type]): ServiceInstantiation class object + line_of_business (str): LineOfBusiness name + platform (str): Platform name + vnf (Network): Network class object + """ + super().__init__(name, request_id, instance_id, line_of_business, platform) + self.network = network + + @classmethod + def instantiate_ala_carte(cls, # pylint: disable=too-many-arguments + aai_service_instance: "ServiceInstance", + network_object: "Network", + line_of_business: str, + platform: str, + cloud_region: "CloudRegion", + tenant: "Tenant", + network_instance_name: str = None, + subnets: Iterable[Subnet] = None) -> "NetworkInstantiation": + """Instantiate Network using a'la carte method. + + Args: + network_object (Network): Network to instantiate + line_of_business (str): LineOfBusiness name to use in instantiation request + platform (str): Platform name to use in instantiation request + cloud_region (CloudRegion): Cloud region to use in instantiation request. + tenant (Tenant): Tenant to use in instnatiation request. + network_instance_name (str, optional): Network instance name. Defaults to None. + + Returns: + NetworkInstantiation: NetworkInstantiation object + + """ + if network_instance_name is None: + network_instance_name = \ + f"Python_ONAP_SDK_network_instance_{str(uuid4())}" + NetworkPreload.upload_network_preload(network=network_object, + network_instance_name=network_instance_name, + subnets=subnets) + response: dict = cls.send_message_json( + "POST", + (f"Instantiate {aai_service_instance.sdc_service.name} " + f"service network {network_object.name}"), + (f"{cls.base_url}/onap/so/infra/serviceInstantiation/{cls.api_version}/" + f"serviceInstances/{aai_service_instance.instance_id}/networks"), + data=jinja_env().get_template("instantiate_network_ala_carte.json.j2"). + render( + instance_name=network_instance_name, + network=network_object, + service=aai_service_instance.sdc_service, + cloud_region=cloud_region or \ + next(aai_service_instance.service_subscription.cloud_regions), + tenant=tenant or next(aai_service_instance.service_subscription.tenants), + line_of_business=line_of_business, + platform=platform, + service_instance=aai_service_instance, + subnets=subnets + ), + headers=headers_so_creator(OnapService.headers) + ) + return cls( + name=network_instance_name, + request_id=response["requestReferences"]["requestId"], + instance_id=response["requestReferences"]["instanceId"], + line_of_business=line_of_business, + platform=platform, + network=network_object + ) diff --git a/src/onapsdk/so/so_db_adapter.py b/src/onapsdk/so/so_db_adapter.py new file mode 100644 index 0000000..b3694d1 --- /dev/null +++ b/src/onapsdk/so/so_db_adapter.py @@ -0,0 +1,94 @@ +"""Database Adapter 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 abc import ABC +from dataclasses import dataclass +from typing import Dict, Any + +from onapsdk.so.so_element import SoElement +from onapsdk.onap_service import OnapService +from onapsdk.utils.headers_creator import headers_so_creator, headers_so_catelog_db_creator +from onapsdk.utils.jinja import jinja_env + + +@dataclass +class IdentityService: # pylint: disable=too-many-instance-attributes + """Class to store identity service details.""" + + identity_id: str + url: str = "http://1.2.3.4:5000/v2.0" + mso_id: str = "onapsdk_user" + mso_pass: str = "mso_pass_onapsdk" + project_domain_name: str = "NULL" + user_domain_name: str = "NULL" + admin_tenant: str = "service" + member_role: str = "admin" + identity_server_type: str = "KEYSTONE" + identity_authentication_type: str = "USERNAME_PASSWORD" + hibernate_lazy_initializer = {} + server_type_as_string: str = "KEYSTONE" + tenant_metadata: bool = True + + +class SoDbAdapter(SoElement, ABC): + """DB Adapter class.""" + + @classmethod + def add_cloud_site(cls, + cloud_region_id: str, + complex_id: str, + identity_service: IdentityService, + orchestrator: str = "multicloud" + ): + """Add cloud_site data with identity_service to SO db. + + Args: + cloud_region_id (str): The id of cloud region + complex_id (str): The id of complex + identity_service (IdentityService): Identity service related to the cloud region + orchestrator (str, optional): Orchestrator type. Defaults to multicloud. + + Important: + identity_services data will be overwrite, but in the same time + cloud_sites data will not (shouldn't) be overwrite! + SOCatalogDB REST API has some limitations reported: https://jira.onap.org/browse/SO-2727 + + Return: + response object + """ + response = cls.send_message_json( + "POST", + "Create a region in SO db", + f"{cls.base_url}/cloudSite", + data=jinja_env().get_template("add_cloud_site_with_identity_service.json.j2"). + render( + cloud_region_id=cloud_region_id, + complex_id=complex_id, + orchestrator=orchestrator, + identity_service=identity_service + ), + headers=headers_so_creator(OnapService.headers) + ) + return response + @classmethod + def get_service_vnf_info(cls, identifier: str) -> Dict[Any, Any]: + """Get Service VNF and VF details. + + Returns: + The response in a dict format + + """ + url = f"{cls.base_url}/ecomp/mso/catalog/v2/serviceVnfs?serviceModelUuid={identifier}" + headers = headers_so_catelog_db_creator(OnapService.headers) + return cls.send_message_json("GET", "Get Service Details", url, headers=headers) diff --git a/src/onapsdk/so/so_element.py b/src/onapsdk/so/so_element.py new file mode 100644 index 0000000..fca6ba7 --- /dev/null +++ b/src/onapsdk/so/so_element.py @@ -0,0 +1,223 @@ +"""SO Element 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. +import json +from abc import ABC +from dataclasses import dataclass +from enum import Enum +from typing import Dict + +from onapsdk.configuration import settings +from onapsdk.sdc.service import Service +from onapsdk.sdc.vf import Vf +from onapsdk.onap_service import OnapService +from onapsdk.utils.headers_creator import headers_so_creator +from onapsdk.utils.jinja import jinja_env +from onapsdk.utils.mixins import WaitForFinishMixin +from onapsdk.utils.tosca_file_handler import get_modules_list_from_tosca_file +from onapsdk.utils.gui import GuiItem, GuiList + +@dataclass +class SoElement(OnapService): + """Mother Class of all SO elements.""" + + name: str = None + _server: str = "SO" + base_url = settings.SO_URL + api_version = settings.SO_API_VERSION + _status: str = None + + @property + def headers(self): + """Create headers for SO request. + + It is used as a property because x-transactionid header should be unique for each request. + """ + return headers_so_creator(OnapService.headers) + + @classmethod + def get_subscription_service_type(cls, vf_name): + """Retrieve the model info of the VFs.""" + vf_object = Vf(name=vf_name) + return vf_object.name + + @classmethod + def get_service_model_info(cls, service_name): + """Retrieve Service Model info.""" + service = Service(name=service_name) + template_service = jinja_env().get_template("service_instance_model_info.json.j2") + # Get service instance model + parsed = json.loads( + template_service.render( + model_invariant_id=service.unique_uuid, + model_name_version_id=service.identifier, + model_name=service.name, + model_version=service.version, + ) + ) + return json.dumps(parsed, indent=4) + + @classmethod + def get_vnf_model_info(cls, vf_name): + """Retrieve the model info of the VFs.""" + vf_object = Vf(name=vf_name) + template_service = jinja_env().get_template("vnf_model_info.json.j2") + parsed = json.loads( + template_service.render( + vnf_model_invariant_uuid=vf_object.unique_uuid, + vnf_model_customization_id="????", + vnf_model_version_id=vf_object.identifier, + vnf_model_name=vf_object.name, + vnf_model_version=vf_object.version, + vnf_model_instance_name=(vf_object.name + " 0"), + ) + ) + # we need also a vnf instance Name + # Usually it is found like that + # name: toto + # instance name: toto 0 + # it can be retrieved from the toscafrom onapsdk.configuration import settings + return json.dumps(parsed, indent=4) + + @classmethod + def get_vf_model_info(cls, vf_model: str) -> str: + """Retrieve the VF model info From Tosca?.""" + modules: Dict = get_modules_list_from_tosca_file(vf_model) + template_service = jinja_env().get_template("vf_model_info.json.j2") + parsed = json.loads(template_service.render(modules=modules)) + return json.dumps(parsed, indent=4) + + @classmethod + def _base_create_url(cls) -> str: + """ + Give back the base url of SO. + + Returns: + str: the base url + + """ + return "{}/onap/so/infra/serviceInstantiation/{}/serviceInstances".format( + cls.base_url, cls.api_version + ) + + @classmethod + def get_guis(cls) -> GuiItem: + """Retrieve the status of the SO GUIs. + + Only one GUI is referenced for SO: SO monitor + + Return the list of GUIs + """ + gui_url = settings.SO_MONITOR_GUI_SERVICE + so_gui_response = cls.send_message( + "GET", "Get SO GUI Status", gui_url) + guilist = GuiList([]) + guilist.add(GuiItem( + gui_url, + so_gui_response.status_code)) + return guilist + + +class OrchestrationRequest(SoElement, WaitForFinishMixin, ABC): + """Base SO orchestration request class.""" + + WAIT_FOR_SLEEP_TIME = 10 + + def __init__(self, + request_id: str) -> None: + """Instantiate object initialization. + + Initializator used by classes inherited from this abstract class. + + Args: + request_id (str): request ID + """ + super().__init__() + self.request_id: str = request_id + + class StatusEnum(Enum): + """Status enum. + + Store possible statuses for instantiation: + - IN_PROGRESS, + - FAILED, + - COMPLETE. + If instantiation has status which is not covered by these values + UNKNOWN value is used. + + """ + + IN_PROGRESS = "IN_PROGRESS" + FAILED = "FAILED" + COMPLETED = "COMPLETE" + UNKNOWN = "UNKNOWN" + + @property + def status(self) -> "StatusEnum": + """Object instantiation status. + + It's populated by call SO orchestation request endpoint. + + Returns: + StatusEnum: Instantiation status. + + """ + response: dict = self.send_message_json( + "GET", + f"Check {self.request_id} orchestration request status", + (f"{self.base_url}/onap/so/infra/" + f"orchestrationRequests/{self.api_version}/{self.request_id}"), + headers=headers_so_creator(OnapService.headers) + ) + try: + return self.StatusEnum(response["request"]["requestStatus"]["requestState"]) + except (KeyError, ValueError): + self._logger.exception("Invalid status") + return self.StatusEnum.UNKNOWN + + @property + def finished(self) -> bool: + """Store an information if instantion is finished or not. + + Instantiation is finished if it's status is COMPLETED or FAILED. + + Returns: + bool: True if instantiation is finished, False otherwise. + + """ + return self.status in [self.StatusEnum.COMPLETED, self.StatusEnum.FAILED] + + @property + def completed(self) -> bool: + """Store an information if instantion is completed or not. + + Instantiation is completed if it's status is COMPLETED. + + Returns: + bool: True if instantiation is completed, False otherwise. + + """ + return self.finished and self.status == self.StatusEnum.COMPLETED + + @property + def failed(self) -> bool: + """Store an information if instantion is failed or not. + + Instantiation is failed if it's status is FAILED. + + Returns: + bool: True if instantiation is failed, False otherwise. + + """ + return self.finished and self.status == self.StatusEnum.FAILED diff --git a/src/onapsdk/so/templates/add_cloud_site_with_identity_service.json.j2 b/src/onapsdk/so/templates/add_cloud_site_with_identity_service.json.j2 new file mode 100644 index 0000000..31599f5 --- /dev/null +++ b/src/onapsdk/so/templates/add_cloud_site_with_identity_service.json.j2 @@ -0,0 +1,22 @@ +{ + "id": "{{ cloud_region_id }}", + "region_id": "{{ cloud_region_id }}", + "aic_version": "2.5", + "clli": "{{ complex_id }}", + "orchestrator": "{{ orchestrator }}", + "identityService": { + "id": "{{ identity_service.identity_id }}", + "identityServerTypeAsString": "{{ identity_service.server_type_as_string }}", + "hibernateLazyInitializer": {{ identity_service.hibernate_lazy_initializer }}, + "identity_url": "{{ identity_service.url }}", + "mso_id": "{{ identity_service.mso_id }}", + "mso_pass": "{{ identity_service.mso_pass }}", + "project_domain_name": "{{ identity_service.project_domain_name }}", + "user_domain_name": "{{ identity_service.user_domain_name }}", + "admin_tenant": "{{ identity_service.admin_tenant }}", + "member_role": "{{ identity_service.member_role }}", + "tenant_metadata": "{{ identity_service.tenant_metadata }}", + "identity_server_type": "{{ identity_service.identity_server_type }}", + "identity_authentication_type": "{{ identity_service.identity_authentication_type }}" + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/deletion_network.json.j2 b/src/onapsdk/so/templates/deletion_network.json.j2 new file mode 100644 index 0000000..93f0990 --- /dev/null +++ b/src/onapsdk/so/templates/deletion_network.json.j2 @@ -0,0 +1,22 @@ +{ + "requestDetails": { + "requestInfo": { + "source": "VID", + "requestorId": "demo" + }, + "modelInfo": { + "modelType": "network" + }, + "requestParameters": { + "testApi": "GR_API", + "aLaCarte": {{ a_la_carte | tojson }} + }, + {# the code below is needed to be refactored #} + {# https://gitlab.com/Orange-OpenSource/lfn/onap/python-onapsdk/-/issues/133 #} + "cloudConfiguration": { + "cloudOwner": "{{ network_instance.service_instance.service_subscription.cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ network_instance.service_instance.service_subscription.cloud_region.cloud_region_id }}", + "tenantId": "{{ network_instance.service_instance.service_subscription.tenant.tenant_id }}" + } + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/deletion_service.json.j2 b/src/onapsdk/so/templates/deletion_service.json.j2 new file mode 100644 index 0000000..1244e97 --- /dev/null +++ b/src/onapsdk/so/templates/deletion_service.json.j2 @@ -0,0 +1,26 @@ +{ + "requestDetails": { + "requestInfo": { + "source": "VID", + "requestorId": "demo" + }, + "modelInfo": { + "modelType": "service", + "modelName": "{{ service_instance.sdc_service.name }}", + "modelInvariantId": "{{ service_instance.sdc_service.unique_uuid }}", + "modelVersion": "1.0", + "modelVersionId": "{{ service_instance.sdc_service.identifier }}" + }, + "requestParameters": { + "testApi": "GR_API", + "aLaCarte": {{ a_la_carte | tojson }} + }{% if service_instance.sdc_service.has_vnfs %}, + {# the code below is needed to be refactored #} + {# https://gitlab.com/Orange-OpenSource/lfn/onap/python-onapsdk/-/issues/133 #} + "cloudConfiguration": { + "cloudOwner": "{{ service_instance.service_subscription.cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ service_instance.service_subscription.cloud_region.cloud_region_id }}", + "tenantId": "{{ service_instance.service_subscription.tenant.tenant_id }}" + }{% endif %} + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/deletion_vf_module.json.j2 b/src/onapsdk/so/templates/deletion_vf_module.json.j2 new file mode 100644 index 0000000..8f83717 --- /dev/null +++ b/src/onapsdk/so/templates/deletion_vf_module.json.j2 @@ -0,0 +1,27 @@ +{ + "requestDetails": { + "requestInfo": { + "source": "VID", + "requestorId": "demo" + }, + "modelInfo": { + "modelType": "vfModule", + "modelInvariantId": "{{ vf_module_instance.model_invariant_id }}", + "modelVersionId": "{{ vf_module_instance.model_version_id }}", + "modelName": "{{ vf_module_instance.vf_module_name }}", + "modelVersion": "{{ vf_module_instance.resource_version }}", + "modelCustomizationId": "{{ vf_module_instance.model_customization_id }}" + }, + "requestParameters": { + "testApi": "GR_API", + "aLaCarte": {{ a_la_carte | tojson }} + }, + {# the code below is needed to be refactored #} + {# https://gitlab.com/Orange-OpenSource/lfn/onap/python-onapsdk/-/issues/133 #} + "cloudConfiguration": { + "cloudOwner": "{{ vf_module_instance.vnf_instance.service_instance.service_subscription.cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ vf_module_instance.vnf_instance.service_instance.service_subscription.cloud_region.cloud_region_id }}", + "tenantId": "{{ vf_module_instance.vnf_instance.service_instance.service_subscription.tenant.tenant_id }}" + } + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/deletion_vnf.json.j2 b/src/onapsdk/so/templates/deletion_vnf.json.j2 new file mode 100644 index 0000000..fb640ec --- /dev/null +++ b/src/onapsdk/so/templates/deletion_vnf.json.j2 @@ -0,0 +1,28 @@ +{ + "requestDetails": { + "requestInfo": { + "source": "VID", + "requestorId": "demo" + }, + "modelInfo": { + "modelType": "vnf", + "modelName": "{{ vnf_instance.vnf.model_name }}", + "modelInvariantId": "{{ vnf_instance.vnf.model_invariant_id }}", + "modelVersion": "{{ vnf_instance.vnf.model_version }}", + "modelVersionId": "{{ vnf_instance.vnf.model_version_id }}", + "modelCustomizationId": "{{ vnf_instance.vnf.model_customization_id }}", + "modelCustomizationName": "{{ vnf_instance.vnf.name }}" + }, + "requestParameters": { + "testApi": "GR_API", + "aLaCarte": {{ a_la_carte | tojson }} + }, + {# the code below is needed to be refactored #} + {# https://gitlab.com/Orange-OpenSource/lfn/onap/python-onapsdk/-/issues/133 #} + "cloudConfiguration": { + "cloudOwner": "{{ vnf_instance.service_instance.service_subscription.cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ vnf_instance.service_instance.service_subscription.cloud_region.cloud_region_id }}", + "tenantId": "{{ vnf_instance.service_instance.service_subscription.tenant.tenant_id }}" + } + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/instantiate_multi_vnf_service_macro.json.j2 b/src/onapsdk/so/templates/instantiate_multi_vnf_service_macro.json.j2 new file mode 100644 index 0000000..32e1b68 --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_multi_vnf_service_macro.json.j2 @@ -0,0 +1,121 @@ +{% extends "instantiate_service_macro.json.j2" %} + +{% block subscriptionServiceType %} + "subscriptionServiceType": "{{ so_service.subscription_service_type }}", +{% endblock %} + +{% block pnfs %} + {% if so_service.pnfs %} + "pnfs": [ + {% for pnf in so_service.pnfs %} + { + "modelInfo":{ + {% for sdc_pnf in sdc_service.pnfs %} + {% if sdc_pnf.model_name == pnf.model_name %} + "modelCustomizationName":"{{ sdc_pnf.name }}", + "modelCustomizationId":"{{ sdc_pnf.model_customization_id }}", + "modelInvariantId":"{{ sdc_service.unique_uuid }}", + "modelVersionId":"{{ sdc_service.identifier }}", + "modelName":"{{ sdc_service.name }}", + "modelType":"pnf", + "modelVersion":"{{ sdc_pnf.model_version }}" + {% endif %} + {% endfor %} + }, + "platform":{ + "platformName":"{{ platform }}" + }, + "lineOfBusiness":{ + "lineOfBusinessName":"{{ line_of_business }}" + }, + "productFamilyId":"{{ aai_service.service_id }}", + "instanceParams":[], + {% if pnf.processing_priority %} + "processingPriority": "{{ pnf.processing_priority }}", + {% endif %} + "instanceName": "{{ pnf.instance_name }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ]{% if so_service.vnfs %},{% endif %} + {% endif %} +{% endblock %} + +{% block vnfs %} + {% if so_service.vnfs %} + "vnfs": [ + {% for vnf in so_service.vnfs %} + { + "modelInfo": { + {% for sdc_vnf in sdc_service.vnfs %} + {% if sdc_vnf.model_name == vnf.model_name %} + "modelName": "{{ sdc_vnf.model_name }}", + "modelVersionId": "{{ sdc_vnf.model_version_id }}", + "modelInvariantUuid": "{{ sdc_vnf.model_invariant_uuid }}", + "modelVersion": "{{ sdc_vnf.model_version }}", + "modelCustomizationId": "{{ sdc_vnf.model_customization_id }}", + "modelInstanceName": "{{ sdc_vnf.model_name }}" + {% endif %} + {% endfor %} + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "productFamilyId": "1234", + "instanceName": "{{ vnf.instance_name }}", + "instanceParams": [ + { + {% for key, value in vnf.parameters.items() %} + "{{ key }}": "{{ value }}"{% if not loop.last %},{% endif %} + {% endfor %} + } + ], + {% if vnf.processing_priority %} + "processingPriority": "{{ vnf.processing_priority }}", + {% endif %} + "vfModules": [ + {% for vf_module in vnf.vf_modules %} + { + "modelInfo": { + {% for sdc_vnf in sdc_service.vnfs %} + {% if sdc_vnf.model_name == vnf.model_name %} + {% for sdc_vf_module in sdc_vnf.vf_modules %} + {% set mylist = sdc_vf_module.name.split('..') %} + {% set item = mylist|length-2 %} + {% if vf_module.model_name == mylist[item] %} + "modelName": "{{ sdc_vf_module.model_name }}", + "modelVersionId": "{{ sdc_vf_module.model_version_id }}", + "modelInvariantUuid": "{{ sdc_vf_module.model_invariant_uuid }}", + "modelVersion": "{{ sdc_vf_module.model_version }}", + "modelCustomizationId": "{{ sdc_vf_module.model_customization_id }}" + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + }, + "instanceName": "{{ vf_module.instance_name }}", + {% if vf_module.processing_priority %} + "processingPriority": "{{ vf_module.processing_priority }}", + {% endif %} + "instanceParams": [ + { + {% for key, value in vf_module.parameters.items() %} + "{{ key }}": "{{ value }}"{% if not loop.last %},{% endif %} + {% endfor %} + } + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + {% endif %} +{% endblock %} diff --git a/src/onapsdk/so/templates/instantiate_network_ala_carte.json.j2 b/src/onapsdk/so/templates/instantiate_network_ala_carte.json.j2 new file mode 100644 index 0000000..90b3c16 --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_network_ala_carte.json.j2 @@ -0,0 +1,10 @@ +{% extends "instantiate_network_vnf_ala_carte_base.json.j2" %} +{% block model_info %} + "modelType": "network", + "modelInvariantId": "{{ network.model_invariant_id }}", + "modelVersionId": "{{ network.model_version_id }}", + "modelName": "{{ network.model_name }}", + "modelVersion": "{{ network.model_version }}", + "modelCustomizationId": "{{ network.model_customization_id }}", + "modelCustomizationName": "{{ network.name }}" +{% endblock %}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/instantiate_network_vnf_ala_carte_base.json.j2 b/src/onapsdk/so/templates/instantiate_network_vnf_ala_carte_base.json.j2 new file mode 100644 index 0000000..757cdd8 --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_network_vnf_ala_carte_base.json.j2 @@ -0,0 +1,44 @@ +{ + "requestDetails": { + "requestInfo": { + "instanceName": "{{ instance_name }}", + "source": "VID", + "suppressRollback": false, + "requestorId": "test", + "productFamilyId": "{{ service_instance.model_invariant_id }}" + }, + "modelInfo": { + {% block model_info %}{% endblock %} + }, + "requestParameters": { + "userParams": [ + {% block user_params %}{% endblock %} + ], + "aLaCarte": true, + "testApi": "GR_API" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "relatedInstanceList": [{ + "relatedInstance": { + "instanceId": "{{ service_instance.instance_id }}", + "modelInfo": { + "modelType": "service", + "modelName": "{{ service.name }}", + "modelInvariantId": "{{ service.unique_uuid }}", + "modelVersion": "1.0", + "modelVersionId": "{{ service.identifier }}" + } + } + }] + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/instantiate_service_ala_carte.json.j2 b/src/onapsdk/so/templates/instantiate_service_ala_carte.json.j2 new file mode 100644 index 0000000..4954cde --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_service_ala_carte.json.j2 @@ -0,0 +1,45 @@ +{ + "requestDetails": { + "requestInfo": { + "instanceName": "{{ service_instance_name }}", + "source": "VID", + "suppressRollback": false, + "requestorId": "demo" + }, + "modelInfo": { + "modelType": "service", + "modelInvariantId": "{{ sdc_service.unique_uuid }}", + "modelVersionId": "{{ sdc_service.identifier }}", + "modelName": "{{ sdc_service.name }}", + "modelVersion": "1.0" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "requestParameters": { + "userParams": [ + {% if enable_multicloud %} + { + "name":"orchestrator", + "value":"multicloud" + } + {% endif %} + ], + "testApi": "GR_API", + "subscriptionServiceType": "{{ service_subscription.service_type }}", + "aLaCarte": true + }, + "subscriberInfo": { + "globalSubscriberId": "{{ customer.global_customer_id }}" + }, + "project": { + "projectName": "{{ project }}" + }, + "owningEntity": { + "owningEntityId": "{{ owning_entity.owning_entity_id }}", + "owningEntityName": "{{ owning_entity.name }}" + } + } +}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/instantiate_service_macro.json.j2 b/src/onapsdk/so/templates/instantiate_service_macro.json.j2 new file mode 100644 index 0000000..43b92ee --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_service_macro.json.j2 @@ -0,0 +1,173 @@ +{ + "requestDetails": { + "requestInfo": { + "suppressRollback": false, + {% if aai_service %} + "productFamilyId":"{{ aai_service.service_id }}", + {% else %} + "productFamilyId": "1234", + {% endif %} + "requestorId": "demo", + "instanceName": "{{ service_instance_name }}", + "source": "VID" + }, + "modelInfo": { + "modelType": "service", + "modelInvariantId": "{{ sdc_service.unique_uuid }}", + "modelVersionId": "{{ sdc_service.identifier }}", + "modelName": "{{ sdc_service.name }}", + "modelVersion": "1.0" + }, + {% if sdc_service.has_vnfs %} + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + {% endif %} + "subscriberInfo": { + "globalSubscriberId": "{{ customer.global_customer_id }}" + }, + "requestParameters": { + {% block subscriptionServiceType %} + "subscriptionServiceType": "{{ service_subscription.service_type }}", + {% endblock %} + "userParams": [ + { + "Homing_Solution": "none" + }, + {% if enable_multicloud %} + { + "name":"orchestrator", + "value":"multicloud" + }, + {% endif %} + { + "service": { + "instanceParams": [], + "instanceName": "{{ service_instance_name }}", + "resources": { + {% block pnfs %} + {% if sdc_service.pnfs %} + "pnfs":[ + {% for pnf in sdc_service.pnfs %} + { + "modelInfo":{ + "modelCustomizationName":"{{ pnf.name }}", + "modelCustomizationId":"{{ pnf.model_customization_id }}", + "modelInvariantId":"{{ sdc_service.unique_uuid }}", + "modelVersionId":"{{ sdc_service.identifier }}", + "modelName":"{{ sdc_service.name }}", + "modelType":"pnf", + "modelVersion":"1.0" + }, + "platform":{ + "platformName":"{{ platform }}" + }, + "lineOfBusiness":{ + "lineOfBusinessName":"{{ line_of_business }}" + }, + "productFamilyId":"{{ aai_service.service_id }}", + "instanceParams":[], + "instanceName":"{{ service_instance_name }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + {% if sdc_service.vnfs %},{% endif %} + {% endif %} + {% endblock %} + {% block vnfs %} + {% if sdc_service.vnfs %} + "vnfs": [ + {% for vnf in sdc_service.vnfs %} + { + "modelInfo": { + "modelName": "{{ vnf.model_name }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelInvariantUuid": "{{ vnf.model_invariant_id }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelInstanceName": "{{ vnf.model_name }}" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "productFamilyId": "1234", + "instanceName": "{{ vnf.model_name }}", + "instanceParams": [ + { + {% for vnf_parameter in vnf_parameters %} + {% if vnf_parameter.name == vnf.model_name %} + {% for parameter in vnf_parameter.vnf_parameters %} + "{{ parameter.name }}": "{{ parameter.value }}"{% if not loop.last %},{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + } + ], + "vfModules": [ + {% for vf_module in vnf.vf_modules %} + { + "modelInfo": { + "modelName": "{{ vf_module.model_name }}", + "modelVersionId": "{{ vf_module.model_version_id }}", + "modelInvariantUuid": "{{ vf_module.model_invariant_uuid }}", + "modelVersion": "{{ vf_module.model_version }}", + "modelCustomizationId": "{{ vf_module.model_customization_id }}" + }, + "instanceName": "{{ service_instance_name }}_{{ vf_module.name }}", + "instanceParams": [ + { + {% for vnf_parameter in vnf_parameters %} + {% if vnf_parameter.name == vnf.model_name %} + {% set mylist = vf_module.name.split('..') %} + {% set item = mylist|length-2 %} + {% for vf_module_parameter in vnf_parameter.vfmodule_parameters %} + {% if vf_module_parameter.name == mylist[item] %} + {% for parameter in vf_module_parameter.vfmodule_parameters %} + "{{ parameter.name }}": "{{ parameter.value }}"{% if not loop.last %},{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + } + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + {% endif %} + {% endblock %} + }, + "modelInfo": { + "modelVersion": "1.0", + "modelVersionId": "{{ sdc_service.identifier }}", + "modelInvariantId": "{{ sdc_service.unique_uuid }}", + "modelName": "{{ sdc_service.name }}", + "modelType": "service" + } + } + } + ], + "aLaCarte": false + }, + "project": { + "projectName": "{{ project }}" + }, + "owningEntity": { + "owningEntityId": "{{ owning_entity.owning_entity_id }}", + "owningEntityName": "{{ owning_entity.name }}" + } + } +} diff --git a/src/onapsdk/so/templates/instantiate_vf_module_ala_carte.json.j2 b/src/onapsdk/so/templates/instantiate_vf_module_ala_carte.json.j2 new file mode 100644 index 0000000..0738379 --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_vf_module_ala_carte.json.j2 @@ -0,0 +1,66 @@ +{ + "requestDetails": { + "requestInfo": + { + "instanceName": "{{ vf_module_instance_name }}", + "source": "VID", + "suppressRollback": false, + "requestorId": "test" + }, + "modelInfo": { + "modelType": "vfModule", + "modelInvariantId": "{{ vf_module.model_invariant_uuid }}", + "modelVersionId": "{{ vf_module.model_version_id }}", + "modelName": "{{ vf_module.model_name }}", + "modelVersion": "{{ vf_module.model_version }}", + "modelCustomizationId": "{{ vf_module.model_customization_id }}", + "modelCustomizationName": "{{ vf_module.model_name }}" + }, + "requestParameters": { + "userParams": [ + {% for parameter in vf_module_parameters %} + { + "name": "{{ parameter.name }}", + "value": "{{ parameter.value }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ], + "testApi": "GR_API", + "usePreload": true, + "aLaCarte": true + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "relatedInstanceList": [ + { + "relatedInstance": { + "instanceId": "{{ vnf_instance.service_instance.instance_id }}", + "modelInfo": { + "modelType": "service", + "modelName": "{{ service.name }}", + "modelInvariantId": "{{ service.unique_uuid }}", + "modelVersion": "1.0", + "modelVersionId": "{{ service.identifier }}" + } + } + }, + { + "relatedInstance": { + "instanceId": "{{ vnf_instance.vnf_id }}", + "modelInfo": { + "modelType": "vnf", + "modelName": "{{ vnf_instance.vnf.model_name }}", + "modelInvariantId": "{{ vnf_instance.vnf.model_invariant_id }}", + "modelVersion": "{{ vnf_instance.vnf.model_version }}", + "modelVersionId": "{{ vnf_instance.vnf.model_version_id }}", + "modelCustomizationId": "{{ vnf_instance.vnf.model_customization_id }}", + "modelCustomizationName": "{{ vnf_instance.vnf.name }}" + } + } + } + ] + } +} diff --git a/src/onapsdk/so/templates/instantiate_vnf_ala_carte.json.j2 b/src/onapsdk/so/templates/instantiate_vnf_ala_carte.json.j2 new file mode 100644 index 0000000..9fbf989 --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_vnf_ala_carte.json.j2 @@ -0,0 +1,18 @@ +{% extends "instantiate_network_vnf_ala_carte_base.json.j2" %} +{% block model_info %} + "modelType": "vnf", + "modelInvariantId": "{{ vnf.model_invariant_id }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelName": "{{ vnf.model_name }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelCustomizationName": "{{ vnf.name }}" +{% endblock %} +{% block user_params %} +{% for parameter in vnf_parameters %} +{ + "name": "{{ parameter.name }}", + "value": "{{ parameter.value }}" +}{% if not loop.last %},{% endif %} +{% endfor %} +{% endblock %}
\ No newline at end of file diff --git a/src/onapsdk/so/templates/instantiate_vnf_macro.json.j2 b/src/onapsdk/so/templates/instantiate_vnf_macro.json.j2 new file mode 100644 index 0000000..2d7aeee --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_vnf_macro.json.j2 @@ -0,0 +1,153 @@ +{ + "requestDetails": { + "requestInfo": { + "instanceName": "{{ service_instance.instance_name }}", + "source": "VID", + "suppressRollback": false, + "requestorId": "demo", + "productFamilyId": "1234" + }, + "modelInfo": { + "modelType": "vnf", + "modelInvariantId": "{{ vnf.model_invariant_id }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelName": "{{ vnf.model_name }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelInstanceName": "{{ vnf.name }}" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "subscriberInfo": { + "globalSubscriberId": "{{ service_instance.service_subscription.customer.global_customer_id }}" + }, + "requestParameters": { + {% block subscriptionServiceType %} + "subscriptionServiceType": "{{ service.name }}", + {% endblock %} + "userParams": [ + { + "Homing_Solution": "none" + }, + { + "service": { + "instanceParams": [], + "resources": { + {% block vnfs %} + "vnfs": [ + { + "modelInfo": { + "modelName": "{{ vnf.model_name }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelInvariantUuid": "{{ vnf.model_invariant_id }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelInstanceName": "{{ vnf.name }}" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "productFamilyId": "1234", + "instanceName": "{{ instance_name }}", + "instanceParams": [ + { + {% for vnf_parameter in vnf_parameters %} + {% if vnf_parameter.name == vnf.model_name %} + {% for parameter in vnf_parameter.vnf_parameters %} + "{{ parameter.name }}": "{{ parameter.value }}"{% if not loop.last %},{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + } + ], + "vfModules": [ + {% for vf_module in vnf.vf_modules %} + + { + "modelInfo": { + "modelName": "{{ vf_module.model_name }}", + "modelVersionId": "{{ vf_module.model_version_id }}", + "modelInvariantUuid": "{{ vf_module.model_invariant_uuid }}", + "modelVersion": "{{ vf_module.model_version }}", + "modelCustomizationId": "{{ vf_module.model_customization_id }}" + }, + "instanceName": "{{instance_name}}_{{ vf_module.name }}", + "instanceParams": [ + { + {% for vnf_parameter in vnf_parameters %} + {% if vnf_parameter.name == vnf.model_name %} + {% set mylist = vf_module.name.split('..') %} + {% set item = mylist|length-2 %} + {% for vf_module_parameter in vnf_parameter.vfmodule_parameters %} + {% if vf_module_parameter.name == mylist[item] %} + {% for parameter in vf_module_parameter.vfmodule_parameters %} + "{{ parameter.name }}": "{{ parameter.value }}"{% if not loop.last %},{% endif %} + {% endfor %} + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + } + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + } + ] + {% endblock %} + }, + "modelInfo": { + "modelType": "vnf", + "modelInvariantId": "{{ vnf.model_invariant_id }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelName": "{{ vnf.model_name }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelCustomizationName": "{{ vnf.name }}" + } + } + } + ], + "aLaCarte": false + }, + "project": { + "projectName": "{{ project }}" + }, + "owningEntity": { + "owningEntityId": "{{ owning_entity.owning_entity_id }}", + "owningEntityName": "{{ owning_entity.name }}" + }, + "relatedInstanceList": [ + { + "relatedInstance": { + "instanceId": "{{ service_instance.instance_id }}", + "modelInfo": { + "modelType": "service", + "modelInvariantId": "{{ service.unique_uuid }}", + "modelVersionId": "{{ service.identifier }}", + "modelName": "{{ service.name }}", + "modelVersion": "1.0" + } + } + } + ] + }, + "serviceInstanceId": "{{ service_instance.instance_id }}" +} diff --git a/src/onapsdk/so/templates/instantiate_vnf_macro_so_vnf.json.j2 b/src/onapsdk/so/templates/instantiate_vnf_macro_so_vnf.json.j2 new file mode 100644 index 0000000..c7f4356 --- /dev/null +++ b/src/onapsdk/so/templates/instantiate_vnf_macro_so_vnf.json.j2 @@ -0,0 +1,151 @@ +{ + "requestDetails": { + "requestInfo": { + "instanceName": "{{ service_instance.instance_name }}", + "source": "VID", + "suppressRollback": false, + "requestorId": "demo", + "productFamilyId": "1234" + }, + "modelInfo": { + "modelType": "vnf", + "modelInvariantId": "{{ vnf.model_invariant_id }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelName": "{{ vnf.model_name }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelInstanceName": "{{ vnf.name }}" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "subscriberInfo": { + "globalSubscriberId": "{{ service_instance.service_subscription.customer.global_customer_id }}" + }, + "requestParameters": { + {% block subscriptionServiceType %} + "subscriptionServiceType": "{{ service.name }}", + {% endblock %} + "userParams": [ + { + "Homing_Solution": "none" + }, + { + "service": { + "instanceParams": [], + "resources": { + {% block vnfs %} + "vnfs": [ + { + "modelInfo": { + "modelName": "{{ vnf.model_name }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelInvariantUuid": "{{ vnf.model_invariant_id }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelInstanceName": "{{ vnf.name }}" + }, + "cloudConfiguration": { + "tenantId": "{{ tenant.tenant_id }}", + "cloudOwner": "{{ cloud_region.cloud_owner }}", + "lcpCloudRegionId": "{{ cloud_region.cloud_region_id }}" + }, + "platform": { + "platformName": "{{ platform }}" + }, + "lineOfBusiness": { + "lineOfBusinessName": "{{ line_of_business }}" + }, + "productFamilyId": "1234", + "instanceName": "{{ instance_name }}", + "instanceParams": [ + { + {% for key, value in so_vnf.parameters.items() %} + "{{ key }}": "{{ value }}"{% if not loop.last %},{% endif %} + {% endfor %} + } + ], + "vfModules": [ + {% for vf_module in so_vnf.vf_modules %} + { + "modelInfo": { + + {% if vnf.model_name == so_vnf.model_name %} + {% for sdc_vf_module in vnf.vf_modules %} + {% set mylist = sdc_vf_module.name.split('..') %} + {% set item = mylist|length-2 %} + {% if vf_module.model_name == mylist[item] %} + "modelName": "{{ sdc_vf_module.model_name }}", + "modelVersionId": "{{ sdc_vf_module.model_version_id}}", + "modelInvariantUuid": "{{ sdc_vf_module.model_invariant_uuid }}", + "modelVersion": "{{ sdc_vf_module.model_version }}", + "modelCustomizationId": "{{ sdc_vf_module.model_customization_id }}" + {% endif %} + {% endfor %} + {% endif %} + + }, + "instanceName": "{{ vf_module.instance_name }}", + {% if vf_module.processing_priority %} + "processingPriority": "{{ vf_module.processing_priority }}", + {% endif %} + "instanceParams": [ + { + {% for key, value in vf_module.parameters.items() %} + "{{ key }}": "{{ value }}"{% if not loop.last %},{% endif %} + {% endfor %} + } + ] + }{% if not loop.last %},{% endif %} + {% endfor %} + ] + } + ] + {% endblock %} + }, + "modelInfo": { + "modelType": "vnf", + "modelInvariantId": "{{ vnf.model_invariant_id }}", + "modelVersionId": "{{ vnf.model_version_id }}", + "modelName": "{{ vnf.model_name }}", + "modelVersion": "{{ vnf.model_version }}", + "modelCustomizationId": "{{ vnf.model_customization_id }}", + "modelCustomizationName": "{{ vnf.name }}" + } + } + } + ], + "aLaCarte": false + }, + "project": { + "projectName": "{{ project }}" + }, + "owningEntity": { + "owningEntityId": "{{ owning_entity.owning_entity_id }}", + "owningEntityName": "{{ owning_entity.name }}" + }, + "relatedInstanceList": [ + { + "relatedInstance": { + "instanceId": "{{ service_instance.instance_id }}", + "modelInfo": { + "modelType": "service", + "modelInvariantId": "{{ service.unique_uuid }}", + "modelVersionId": "{{ service.identifier }}", + "modelName": "{{ service.name }}", + "modelVersion": "1.0" + } + } + } + ] + }, + "serviceInstanceId": "{{ service_instance.instance_id }}" +} diff --git a/src/onapsdk/so/templates/service_instance_model_info.json.j2 b/src/onapsdk/so/templates/service_instance_model_info.json.j2 new file mode 100644 index 0000000..fc66de4 --- /dev/null +++ b/src/onapsdk/so/templates/service_instance_model_info.json.j2 @@ -0,0 +1,7 @@ +{ + "modelType": "service", + "modelInvariantId": "{{ model_invariant_id }}", + "modelName": "{{ model_name }}", + "modelVersion": "{{ model_version }}", + "modelVersionId": "{{ model_name_version_id }}" +} diff --git a/src/onapsdk/so/templates/vf_model_info.json.j2 b/src/onapsdk/so/templates/vf_model_info.json.j2 new file mode 100644 index 0000000..4b40898 --- /dev/null +++ b/src/onapsdk/so/templates/vf_model_info.json.j2 @@ -0,0 +1,15 @@ +[ +{% for _, module in modules.items() %} + { + "modelInfo": { + "modelName": "{{ module["metadata"]["vfModuleModelName"] }}", + "modelVersion": "{{ module.metadata.vfModuleModelVersion }}", + "modelVersionId": "{{ module.metadata.vfModuleModelUUID }}", + "modelInvariantUuid": "{{ module.metadata.vfModuleInvariantUUID }}", + "modelCustomizationId": "{{ module.metadata.vfModuleModelCustomizationUUID }}" + }, + "instanceName": "{{ module.metadata.vfModuleModelName }}", + "instanceParams": [] + }{% if not loop.last %},{% endif %} +{% endfor %} +]
\ No newline at end of file diff --git a/src/onapsdk/so/templates/vnf_model_info.json.j2 b/src/onapsdk/so/templates/vnf_model_info.json.j2 new file mode 100644 index 0000000..ecc788b --- /dev/null +++ b/src/onapsdk/so/templates/vnf_model_info.json.j2 @@ -0,0 +1,9 @@ +{ + "modelType": "vnf", + "modelName": "{{ vnf_model_name }}", + "modelVersion": "{{ vnf_model_version }}", + "modelVersionId": "{{ vnf_model_version_id }}", + "modelInvariantUuid": "{{ vnf_model_invariant_uuid }}", + "modelCustomizationId": "{{ vnf_model_customization_id }}", + "modelInstanceName": "{{ vnf_model_instance_name }}" +} diff --git a/src/onapsdk/utils/__init__.py b/src/onapsdk/utils/__init__.py new file mode 100644 index 0000000..bd7f9f5 --- /dev/null +++ b/src/onapsdk/utils/__init__.py @@ -0,0 +1,40 @@ +"""ONAP SDK utils package.""" +# 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. +import json +from datetime import datetime + + +def get_zulu_time_isoformat() -> str: + """Get zulu time in accepted by ONAP modules format. + + Returns: + str: Actual Zulu time. + + """ + return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ') + + +def load_json_file(path_to_json_file: str) -> str: + """ + Return json as string from selected file. + + Args: + path_to_json_file: (str) path to file with json + Returns: + File content as string (str) + """ + with open(path_to_json_file) as json_file: + data = json.load(json_file) + return json.dumps(data) diff --git a/src/onapsdk/utils/configuration.py b/src/onapsdk/utils/configuration.py new file mode 100644 index 0000000..89bee5a --- /dev/null +++ b/src/onapsdk/utils/configuration.py @@ -0,0 +1,25 @@ +"""Configuration package.""" +# 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 typing import List + + +def tosca_path() -> str: + """Return tosca file paths.""" + return '/tmp/tosca_files/' + + +def components_needing_distribution() -> List[str]: + """Return the list of components needing distribution.""" + return ["SO", "sdnc", "aai"] diff --git a/src/onapsdk/utils/gui.py b/src/onapsdk/utils/gui.py new file mode 100644 index 0000000..421e966 --- /dev/null +++ b/src/onapsdk/utils/gui.py @@ -0,0 +1,35 @@ +"""Definition of GUI objects.""" +# 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 List + +@dataclass +class GuiItem: + """Class for keeping track of a GUI.""" + + url: str + status: int + +@dataclass +class GuiList: + """Class to list all the GUIs.""" + + guilist: List[GuiItem] + + def add(self, element): + """Add a GUi to GUI list.""" + if not isinstance(element, GuiItem): + raise AttributeError + self.guilist.append(element) diff --git a/src/onapsdk/utils/headers_creator.py b/src/onapsdk/utils/headers_creator.py new file mode 100644 index 0000000..adb0609 --- /dev/null +++ b/src/onapsdk/utils/headers_creator.py @@ -0,0 +1,245 @@ +"""Header creator package.""" +# 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 typing import Dict +from uuid import uuid4 +import base64 +import hashlib + +from onapsdk.configuration import settings + + +def headers_sdc_creator(base_header: Dict[str, str], + user: str = "cs0008", + authorization: str = None): + """ + Create the right headers for SDC creator type. + + Args: + base_header (Dict[str, str]): the base header to use + user (str, optional): the user to use. Default to cs0008 + authorization (str, optional): the basic auth to use. + Default to "classic" one + + Returns: + Dict[str, str]: the needed headers + + """ + return headers_sdc_generic(base_header, user, authorization=authorization) + + +def headers_sdc_tester(base_header: Dict[str, str], + user: str = "jm0007", + authorization: str = None): + """ + Create the right headers for SDC tester type. + + Args: + base_header (Dict[str, str]): the base header to use + user (str, optional): the user to use. Default to jm0007 + authorization (str, optional): the basic auth to use. + Default to "classic" one + + Returns: + Dict[str, str]: the needed headers + + """ + return headers_sdc_generic(base_header, user, authorization=authorization) + + +def headers_sdc_governor(base_header: Dict[str, str], + user: str = "gv0001", + authorization: str = None): + """ + Create the right headers for SDC governor type. + + Args: + base_header (Dict[str, str]): the base header to use + user (str, optional): the user to use. Default to gv0001 + authorization (str, optional): the basic auth to use. + Default to "classic" one + + Returns: + Dict[str, str]: the needed headers + + """ + return headers_sdc_generic(base_header, user, authorization=authorization) + + +def headers_sdc_operator(base_header: Dict[str, str], + user: str = "op0001", + authorization: str = None): + """ + Create the right headers for SDC operator type. + + Args: + base_header (Dict[str, str]): the base header to use + user (str, optional): the user to use. Default to op0001 + authorization (str, optional): the basic auth to use. + Default to "classic" one + + Returns: + Dict[str, str]: the needed headers + + """ + return headers_sdc_generic(base_header, user, authorization=authorization) + + +def headers_sdc_generic(base_header: Dict[str, str], + user: str, + authorization: str = None): + """ + Create the right headers for SDC generic type. + + Args: + base_header (Dict[str, str]): the base header to use + user (str): the user to use. + authorization (str, optional): the basic auth to use. + Default to "classic" one + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["USER_ID"] = user + headers["Authorization"] = authorization or settings.SDC_AUTH + headers["X-ECOMP-InstanceID"] = "onapsdk" + return headers + + +def headers_aai_creator(base_header: Dict[str, str]): + """ + Create the right headers for AAI creator type. + + Args: + base_header (Dict[str, str]): the base header to use + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["x-fromappid"] = "AAI" + headers["x-transactionid"] = "0a3f6713-ba96-4971-a6f8-c2da85a3176e" + headers["authorization"] = settings.AAI_AUTH + return headers + + +def headers_so_creator(base_header: Dict[str, str]): + """ + Create the right headers for SO creator type. + + Args: + base_header (Dict[str, str]): the base header to use + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["x-fromappid"] = "AAI" + headers["x-transactionid"] = str(uuid4()) + headers["authorization"] = settings.SO_AUTH + headers["cache-control"] = "no-cache" + return headers + +def headers_so_catelog_db_creator(base_header: Dict[str, str]): + """ + Create the right headers for SO creator type. + + Args: + base_header (Dict[str, str]): the base header to use + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["x-fromappid"] = "AAI" + headers["x-transactionid"] = str(uuid4()) + headers["authorization"] = settings.SO_CAT_DB_AUTH + headers["cache-control"] = "no-cache" + return headers + +def headers_msb_creator(base_header: Dict[str, str]): + """ + Create the right headers for MSB. + + Args: + base_header (Dict[str, str]): the base header to use + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["cache-control"] = "no-cache" + return headers + + +def headers_sdnc_creator(base_header: Dict[str, str]): + """ + Create the right headers for SDNC. + + Args: + base_header (Dict[str, str]): the base header to use + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["authorization"] = settings.SDNC_AUTH + headers["x-transactionid"] = str(uuid4()) + headers["x-fromappid"] = "API client" + return headers + + +def headers_sdc_artifact_upload(base_header: Dict[str, str], data: str): + """ + Create the right headers for sdc artifact upload. + + Args: + base_header (Dict[str, str]): the base header to use + data (str): payload data used to create an md5 content header + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["Accept"] = "application/json, text/plain, */*" + headers["Accept-Encoding"] = "gzip, deflate, br" + headers["Content-Type"] = "application/json; charset=UTF-8" + md5_content = hashlib.md5(data.encode('UTF-8')).hexdigest() + content = base64.b64encode(md5_content.encode('ascii')).decode('UTF-8') + headers["Content-MD5"] = content + return headers + +def headers_clamp_creator(base_header: Dict[str, str]): + """ + Create the right headers for CLAMP generic type. + + base_header (Dict[str, str]): the base header to use + data (str): payload data used to create an md5 content header + + Returns: + Dict[str, str]: the needed headers + + """ + headers = base_header.copy() + headers["Authorization"] = settings.CLAMP_AUTH + headers["X-ECOMP-InstanceID"] = "onapsdk" + return headers diff --git a/src/onapsdk/utils/jinja.py b/src/onapsdk/utils/jinja.py new file mode 100644 index 0000000..fe59eae --- /dev/null +++ b/src/onapsdk/utils/jinja.py @@ -0,0 +1,50 @@ +"""Jinja 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 jinja2 import Environment, PackageLoader, select_autoescape, ChoiceLoader + + +def jinja_env() -> Environment: + """Create Jinja environment. + + jinja_env allow to fetch simply jinja templates where they are. + by default jinja engine will look for templates in `templates` directory of + the package. So to load a template, you just have to do: + + Example: + >>> template = jinja_env().get_template('vendor_create.json.j2') + >>> data = template.render(name="vendor") + + See also: + SdcElement.create() for real use + + Returns: + Environment: the Jinja environment to use + + """ + return Environment(autoescape=select_autoescape(['html', 'htm', 'xml']), + loader=ChoiceLoader([ + PackageLoader("onapsdk.aai"), + PackageLoader("onapsdk.cds"), + PackageLoader("onapsdk.clamp"), + PackageLoader("onapsdk.msb"), + PackageLoader("onapsdk.nbi"), + PackageLoader("onapsdk.sdc"), + PackageLoader("onapsdk.sdnc"), + PackageLoader("onapsdk.sdnc"), + PackageLoader("onapsdk.so"), + PackageLoader("onapsdk.ves"), + PackageLoader("onapsdk.vid") + ])) diff --git a/src/onapsdk/utils/mixins.py b/src/onapsdk/utils/mixins.py new file mode 100644 index 0000000..7a64a15 --- /dev/null +++ b/src/onapsdk/utils/mixins.py @@ -0,0 +1,99 @@ +"""Mixins 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 abc import ABC, abstractmethod +from ctypes import c_bool +from multiprocessing import Process, Value +from time import sleep + + +class WaitForFinishMixin(ABC): + """Wait for finish mixin. + + Mixin with wait_for_finish method and two properties: + - completed, + - finished. + + Can be used to wait for result of asynchronous tasks. + + """ + + WAIT_FOR_SLEEP_TIME = 10 + + @property + @abstractmethod + def completed(self) -> bool: + """Store an information if object task is completed or not. + + Returns: + bool: True if object task is completed, False otherwise. + + """ + + @property + @abstractmethod + def finished(self) -> bool: + """Store an information if object task is finished or not. + + Returns: + bool: True if object task is finished, False otherwise. + + """ + + def _wait_for_finish(self, return_value: Value) -> bool: + """Wait until object task is finished. + + Method called in another process. + + Args: + return_value(Value): value shared with main process to pass there + if object task was completed or not + + """ + while not self.finished: + sleep(self.WAIT_FOR_SLEEP_TIME) + self._logger.info(f"{self.__class__.__name__} task finished") + return_value.value = self.completed + + def wait_for_finish(self, timeout: float = None) -> bool: + """Wait until object task is finished. + + It uses time.sleep with WAIT_FOR_SLEEP_TIME value as a parameter to + wait unitl request is finished (object's finished property is + equal to True). + + It runs another process to control time of the function. If process timed out + TimeoutError is going to be raised. + + Args: + timeout(float, optional): positive number, wait at most timeout seconds + + Raises: + TimeoutError: Raised when function timed out + + Returns: + bool: True if object's task is successfully completed, False otherwise + + """ + self._logger.debug(f"Wait until {self.__class__.__name__} task is not finished") + return_value: Value = Value(c_bool) + wait_for_process: Process = Process(target=self._wait_for_finish, args=(return_value,)) + try: + wait_for_process.start() + wait_for_process.join(timeout) + return return_value.value + finally: + if wait_for_process.is_alive(): + wait_for_process.terminate() + raise TimeoutError diff --git a/src/onapsdk/utils/tosca_file_handler.py b/src/onapsdk/utils/tosca_file_handler.py new file mode 100644 index 0000000..921b868 --- /dev/null +++ b/src/onapsdk/utils/tosca_file_handler.py @@ -0,0 +1,106 @@ +"""Utils class.""" +# 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. +import json +import string +import random +from typing import Dict, List + +from onapsdk.exceptions import ValidationError + +def get_parameter_from_yaml(parameter: str, config_file: str): + """Get the value of a given parameter in file.yaml. + + Parameter must be given in string format with dots + Example: general.openstack.image_name + + Args: + parameter (str): + config_file (str): configuration yaml file formtatted as string + + Raises: + ParameterError: parameter not defined + + Returns: + the value of the parameter + + """ + value = json.loads(config_file) + + # Workaround for the .. within the params in the yaml file + ugly_param = parameter.replace("..", "##") + for element in ugly_param.split("."): + value = value.get(element.replace("##", "..")) + if value is None: + msg = f"{element} in the {parameter} is not in YAML config file." + raise ValidationError(msg) + + return value + +def get_vf_list_from_tosca_file(model: str) -> List: + """Get the list of Vfs of a VNF based on the tosca file. + + Args: + model (str): the model retrieved from the tosca file at Vnf + instantiation + + Returns: + list: a list of Vfs + + """ + newlist = [] + node_list = get_parameter_from_yaml( + "topology_template.node_templates", model) + + for node in node_list: + value = get_parameter_from_yaml( + "topology_template.node_templates." + node + ".type", + model) + if "org.openecomp.resource.vf" in value: + print(node, value) + if node not in newlist: + search_value = str(node).split(" ")[0] + newlist.append(search_value) + return newlist + +def get_modules_list_from_tosca_file(model: str) -> Dict: + """Get the list of modules from tosca file. + + Modules are stored on topology_template.groups TOSCA file section. + + Args: + model (str): the model retrieved from the tosca file at Vnf + instantiation. + + Returns: + dict: a list of modules + + """ + return get_parameter_from_yaml( + "topology_template.groups", model + ) + +def random_string_generator(size=6, + chars=string.ascii_uppercase + string.digits) -> str: + """Get a random String for VNF. + + Args: + size (int): the number of alphanumerical chars for CI + chars (str): alphanumerical characters (ASCII uppercase and digits) + + Returns: + str: a sequence of random characters + + """ + return ''.join(random.choice(chars) for _ in range(size)) diff --git a/src/onapsdk/version.py b/src/onapsdk/version.py new file mode 100644 index 0000000..9cf4f97 --- /dev/null +++ b/src/onapsdk/version.py @@ -0,0 +1,16 @@ +"""Version 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. + +__version__ = "10.2.0" diff --git a/src/onapsdk/ves/__init__.py b/src/onapsdk/ves/__init__.py new file mode 100644 index 0000000..186f0e7 --- /dev/null +++ b/src/onapsdk/ves/__init__.py @@ -0,0 +1,14 @@ +"""ONAP SDK VES package.""" +# 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. diff --git a/src/onapsdk/ves/templates/ves7_batch_with_stndDefined_valid.json.j2 b/src/onapsdk/ves/templates/ves7_batch_with_stndDefined_valid.json.j2 new file mode 100644 index 0000000..662e55e --- /dev/null +++ b/src/onapsdk/ves/templates/ves7_batch_with_stndDefined_valid.json.j2 @@ -0,0 +1,109 @@ +{ + "eventList": [ + { + "commonEventHeader": { + "version": "4.1", + "vesEventListenerVersion": "7.2", + "domain": "stndDefined", + "eventId": "stndDefined-gNB_Nokia000001", + "eventName": "stndDefined-gNB-Nokia-PowerLost", + "stndDefinedNamespace": "3GPP-FaultSupervision", + "startEpochMicrosec": 1413378172000000, + "lastEpochMicrosec": 1413378172000000, + "reportingEntityName": "ibcx0001vm002oam001", + "sourceName": "scfx0001vm002cap001", + "sequence": 1, + "priority": "High" + }, + "stndDefinedFields": { + "schemaReference": "https://forge.3gpp.org/rep/sa5/MnS/blob/SA88-Rel16/OpenAPI/faultMnS.yaml#components/schemas/NotifyNewAlarm", + "data": { + "href": 1, + "uri": "1", + "notificationId": 1, + "notificationType": "notifyNewAlarm", + "eventTime": "xyz", + "systemDN": "xyz", + "probableCause": 1, + "perceivedSeverity": "INDETERMINATE", + "rootCauseIndicator": false, + "specificProblem": "xyz", + "correlatedNotifications": [], + "backedUpStatus": true, + "backUpObject": "xyz", + "trendIndication": "MORE_SEVERE", + "thresholdInfo": { + "observedMeasurement": "new", + "observedValue": 123 + }, + "stateChangeDefinition": { + }, + "monitoredAttributes": { + "newAtt": "new" + }, + "proposedRepairActions": "xyz", + "additionalText": "xyz", + "additionalInformation": { + "addInfo": "new" + }, + "alarmId": "1", + "alarmType": "COMMUNICATIONS_ALARM" + }, + "stndDefinedFieldsVersion": "1.0" + } + }, + { + "commonEventHeader": { + "version": "4.1", + "vesEventListenerVersion": "7.2", + "domain": "stndDefined", + "eventId": "stndDefined-gNB_Nokia000001", + "eventName": "stndDefined-gNB-Nokia-PowerLost", + "stndDefinedNamespace": "3GPP-FaultSupervision", + "startEpochMicrosec": 1413378172000000, + "lastEpochMicrosec": 1413378172000000, + "reportingEntityName": "ibcx0001vm002oam001", + "sourceName": "scfx0001vm002cap001", + "sequence": 1, + "priority": "High" + }, + "stndDefinedFields": { + "schemaReference": "https://forge.3gpp.org/rep/sa5/MnS/blob/SA88-Rel16/OpenAPI/faultMnS.yaml#components/schemas/NotifyNewAlarm", + "data": { + "href": 1, + "uri": "1", + "notificationId": 1, + "notificationType": "notifyNewAlarm", + "eventTime": "xyz", + "systemDN": "xyz", + "probableCause": 1, + "perceivedSeverity": "INDETERMINATE", + "rootCauseIndicator": false, + "specificProblem": "xyz", + "correlatedNotifications": [], + "backedUpStatus": true, + "backUpObject": "xyz", + "trendIndication": "MORE_SEVERE", + "thresholdInfo": { + "observedMeasurement": "new", + "observedValue": 123 + }, + "stateChangeDefinition": { + }, + "monitoredAttributes": { + "newAtt": "new" + }, + "proposedRepairActions": "xyz", + "additionalText": "xyz", + "additionalInformation": { + "addInfo": "new" + }, + "alarmId": "1", + "alarmType": "COMMUNICATIONS_ALARM" + }, + "stndDefinedFieldsVersion": "1.0" + } + } + ] +} + diff --git a/src/onapsdk/ves/templates/ves_stnd_event.json.j2 b/src/onapsdk/ves/templates/ves_stnd_event.json.j2 new file mode 100644 index 0000000..fd1ce98 --- /dev/null +++ b/src/onapsdk/ves/templates/ves_stnd_event.json.j2 @@ -0,0 +1,54 @@ +{ + "event": { + "commonEventHeader": { + "version": "4.1", + "vesEventListenerVersion": "7.2", + "domain": "stndDefined", + "eventId": "stndDefined-gNB_Nokia000001", + "eventName": "stndDefined-gNB-Nokia-PowerLost", + "stndDefinedNamespace": "3GPP-FaultSupervision", + "startEpochMicrosec": 1413378172000000, + "lastEpochMicrosec": 1413378172000000, + "reportingEntityName": "ibcx0001vm002oam001", + "sourceName": "scfx0001vm002cap001", + "sequence": 1, + "priority": "High" + }, + "stndDefinedFields": { + "schemaReference": "https://forge.3gpp.org/rep/sa5/MnS/blob/SA88-Rel16/OpenAPI/faultMnS.yaml#components/schemas/NotifyNewAlarm", + "data": { + "href": 1, + "uri": "1", + "notificationId": 1, + "notificationType": "notifyNewAlarm", + "eventTime": "xyz", + "systemDN": "xyz", + "probableCause": 1, + "perceivedSeverity": "INDETERMINATE", + "rootCauseIndicator": false, + "specificProblem": "xyz", + "correlatedNotifications": [], + "backedUpStatus": true, + "backUpObject": "xyz", + "trendIndication": "MORE_SEVERE", + "thresholdInfo": { + "observedMeasurement": "new", + "observedValue": 123 + }, + "stateChangeDefinition": { + }, + "monitoredAttributes": { + "newAtt": "new" + }, + "proposedRepairActions": "xyz", + "additionalText": "xyz", + "additionalInformation": { + "addInfo": "new" + }, + "alarmId": "1", + "alarmType": "COMMUNICATIONS_ALARM" + }, + "stndDefinedFieldsVersion": "1.0" + } + } +} diff --git a/src/onapsdk/ves/templates/ves_stnd_valid_event.json.j2 b/src/onapsdk/ves/templates/ves_stnd_valid_event.json.j2 new file mode 100644 index 0000000..08d335b --- /dev/null +++ b/src/onapsdk/ves/templates/ves_stnd_valid_event.json.j2 @@ -0,0 +1,54 @@ +{ + "event": { + "commonEventHeader": { + "version": "4.1", + "vesEventListenerVersion": "7.2", + "domain": "stndDefined", + "eventId": "12", + "eventName": "someEventName", + "stndDefinedNamespace": "{{ header.namespace }}", + "startEpochMicrosec": 1413378172000000, + "lastEpochMicrosec": 1413378172000000, + "reportingEntityName": "ibcx0001vm002oam001", + "sourceName": "scfx0001vm002cap001", + "sequence": 1, + "priority": "High" + }, + "stndDefinedFields": { + "schemaReference": "{{ schema_reference }}", + "data": { + "href": 1, + "uri": "1", + "notificationId": 1, + "notificationType": "notifyNewAlarm", + "eventTime": "xyz", + "systemDN": "xyz", + "probableCause": 1, + "perceivedSeverity": "INDETERMINATE", + "rootCauseIndicator": false, + "specificProblem": "xyz", + "correlatedNotifications": [], + "backedUpStatus": true, + "backUpObject": "xyz", + "trendIndication": "MORE_SEVERE", + "thresholdInfo": { + "observedMeasurement": "new", + "observedValue": 123 + }, + "stateChangeDefinition": { + }, + "monitoredAttributes": { + "newAtt": "new" + }, + "proposedRepairActions": "xyz", + "additionalText": "xyz", + "additionalInformation": { + "addInfo": "new" + }, + "alarmId": "1", + "alarmType": "COMMUNICATIONS_ALARM" + }, + "stndDefinedFieldsVersion": "1.0" + } + } +} diff --git a/src/onapsdk/ves/ves.py b/src/onapsdk/ves/ves.py new file mode 100644 index 0000000..1f3f592 --- /dev/null +++ b/src/onapsdk/ves/ves.py @@ -0,0 +1,84 @@ +"""Base VES event sender.""" +# Copyright 2022 Orange, Deutsche Telekom AG, Nokia +# +# 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 typing import Dict, Union + +import json +import requests + +from onapsdk.ves.ves_service import VesService + +ACTION = "Send event to Ves" +POST_HTTP_METHOD = "POST" + + +class Ves(VesService): + """Ves library provides functions for sending events to VES.""" + + event_endpoint_url: str = "{}/eventListener/{}" + event_batch_endpoint_url: str = "{}/eventListener/{}/eventBatch" + + @classmethod + def send_event(cls, + version: str, + json_event: str, + basic_auth: Dict[str, str]) -> Union[requests.Response, None]: + """ + Send an event stored in a file to VES. + + Args: + version: (str) version of VES data format + json_event: (str) event to send + basic_auth: Dict[str, str], for example:{ 'username': 'bob', 'password': 'secret' } + Returns: + (requests.Response) HTTP response status + + """ + return Ves.__send_event_message(cls.event_endpoint_url.format(VesService._url, version), + json_event, basic_auth) + + @classmethod + def send_batch_event(cls, + version: str, + json_event: str, + basic_auth: Dict[str, str]) -> Union[requests.Response, None]: + """ + Send a batch event stored in a file to VES. + + Args: + version: (str) version of VES data format + json_event: (str) event to send + basic_auth: Dict[str, str], for example:{ 'username': 'bob', 'password': 'secret' } + Returns: + (requests.Response) HTTP response status + + """ + return Ves.__send_event_message(cls.event_batch_endpoint_url. + format(VesService._url, version), + json_event, basic_auth) + + @classmethod + def __send_event_message(cls, + base_url: str, + json_event: str, + basic_auth: Dict[str, str] + ) -> Union[requests.Response, None]: + cls._logger.debug("Event to send %s", json_event) + return cls.send_message( + POST_HTTP_METHOD, + ACTION, + f"{base_url}", + basic_auth=basic_auth, + json=json.loads(json_event) + ) diff --git a/src/onapsdk/ves/ves_service.py b/src/onapsdk/ves/ves_service.py new file mode 100644 index 0000000..c4bc8ed --- /dev/null +++ b/src/onapsdk/ves/ves_service.py @@ -0,0 +1,27 @@ +"""Base VES module.""" +# Copyright 2022 Orange, Deutsche Telekom AG, Nokia +# +# 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 onapsdk.configuration import settings +from onapsdk.onap_service import OnapService + + +class VesService(OnapService): + """Base VES class. + + Stores url to VES API (edit if you want to use other) and authentication tuple + (username, password). + """ + + _url: str = settings.VES_URL diff --git a/src/onapsdk/vid/__init__.py b/src/onapsdk/vid/__init__.py new file mode 100644 index 0000000..fd58581 --- /dev/null +++ b/src/onapsdk/vid/__init__.py @@ -0,0 +1,16 @@ +"""VID package.""" +# 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 .vid import LineOfBusiness, OwningEntity, Platform, Project, Vid diff --git a/src/onapsdk/vid/templates/vid_declare_resource.json.j2 b/src/onapsdk/vid/templates/vid_declare_resource.json.j2 new file mode 100644 index 0000000..b14e6f4 --- /dev/null +++ b/src/onapsdk/vid/templates/vid_declare_resource.json.j2 @@ -0,0 +1,3 @@ +{ + "options": ["{{ name }}"] +}
\ No newline at end of file diff --git a/src/onapsdk/vid/vid.py b/src/onapsdk/vid/vid.py new file mode 100644 index 0000000..31cb92e --- /dev/null +++ b/src/onapsdk/vid/vid.py @@ -0,0 +1,134 @@ +"""VID 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 abc import ABC +from warnings import warn + +from onapsdk.configuration import settings +from onapsdk.onap_service import OnapService +from onapsdk.utils.jinja import jinja_env + + +class Vid(OnapService, ABC): + """VID base class.""" + + base_url = settings.VID_URL + api_version = settings.VID_API_VERSION + + def __init__(self, name: str) -> None: + """VID resource object initialization. + + Args: + name (str): Resource name + """ + warn("VID is deprecated and shouldn't be used! " + "It's not a part of the ONAP release since Istanbul.") + super().__init__() + self.name: str = name + + @classmethod + def get_create_url(cls) -> str: + """Resource url. + + Used to create resources + + Returns: + str: Url used for resource creation + + """ + raise NotImplementedError + + @classmethod + def create(cls, name: str) -> "Vid": + """Create VID resource. + + Returns: + Vid: Created VID resource + + """ + warn("VID is deprecated and shouldn't be used! " + "It's not a part of the ONAP release since Istanbul.") + cls.send_message( + "POST", + f"Declare VID resource with {name} name", + cls.get_create_url(), + data=jinja_env().get_template("vid_declare_resource.json.j2").render( + name=name + ) + ) + return cls(name) + + +class OwningEntity(Vid): + """VID owning entity class.""" + + @classmethod + def get_create_url(cls) -> str: + """Owning entity creation url. + + Returns: + str: Url used for ownint entity creation + + """ + warn("VID is deprecated and shouldn't be used! " + "It's not a part of the ONAP release since Istanbul.") + return f"{cls.base_url}{cls.api_version}/maintenance/category_parameter/owningEntity" + + +class Project(Vid): + """VID project class.""" + + @classmethod + def get_create_url(cls) -> str: + """Project creation url. + + Returns: + str: Url used for project creation + + """ + warn("VID is deprecated and shouldn't be used! " + "It's not a part of the ONAP release since Istanbul.") + return f"{cls.base_url}{cls.api_version}/maintenance/category_parameter/project" + + +class LineOfBusiness(Vid): + """VID line of business class.""" + + @classmethod + def get_create_url(cls) -> str: + """Line of business creation url. + + Returns: + str: Url used for line of business creation + + """ + warn("VID is deprecated and shouldn't be used! " + "It's not a part of the ONAP release since Istanbul.") + return f"{cls.base_url}{cls.api_version}/maintenance/category_parameter/lineOfBusiness" + + +class Platform(Vid): + """VID platform class.""" + + @classmethod + def get_create_url(cls) -> str: + """Platform creation url. + + Returns: + str: Url used for platform creation + + """ + warn("VID is deprecated and shouldn't be used! " + "It's not a part of the ONAP release since Istanbul.") + return f"{cls.base_url}{cls.api_version}/maintenance/category_parameter/platform" |