import logging
from n2vc.config import EnvironConfig
+from n2vc.definitions import RelationEndpoint
from n2vc.exceptions import (
N2VCBadArgumentsException,
N2VCException,
from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml
from n2vc.libjuju import Libjuju
from n2vc.store import MotorStore
+from n2vc.utils import get_ee_id_components, generate_random_alfanum_string
from n2vc.vca.connection import get_connection
from retrying_async import retry
db_uri = EnvironConfig(prefixes=["OSMLCM_", "OSMMON_"]).get("database_uri")
self._store = MotorStore(db_uri)
self.loading_libjuju = asyncio.Lock(loop=self.loop)
-
+ self.delete_namespace_locks = {}
self.log.info("N2VC juju connector initialized")
async def get_status(
artifact_path = artifact_path.replace("//", "/")
# check charm path
- if not self.fs.file_exists(artifact_path, mode="dir"):
+ if not self.fs.file_exists(artifact_path):
msg = "artifact path does not exist: {}".format(artifact_path)
raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
artifact_path = artifact_path.replace("//", "/")
# check charm path
- if not self.fs.file_exists(artifact_path, mode="dir"):
+ if not self.fs.file_exists(artifact_path):
msg = "artifact path does not exist: {}".format(artifact_path)
raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
async def add_relation(
self,
- ee_id_1: str,
- ee_id_2: str,
- endpoint_1: str,
- endpoint_2: str,
- vca_id: str = None,
+ provider: RelationEndpoint,
+ requirer: RelationEndpoint,
):
"""
Add relation between two charmed endpoints
- :param: ee_id_1: The id of the first execution environment
- :param: ee_id_2: The id of the second execution environment
- :param: endpoint_1: The endpoint in the first execution environment
- :param: endpoint_2: The endpoint in the second execution environment
- :param: vca_id: VCA ID
+ :param: provider: Provider relation endpoint
+ :param: requirer: Requirer relation endpoint
"""
- self.log.debug(
- "adding new relation between {} and {}, endpoints: {}, {}".format(
- ee_id_1, ee_id_2, endpoint_1, endpoint_2
- )
+ self.log.debug(f"adding new relation between {provider} and {requirer}")
+ cross_model_relation = (
+ provider.model_name != requirer.model_name
+ or requirer.vca_id != requirer.vca_id
)
- libjuju = await self._get_libjuju(vca_id)
-
- # check arguments
- if not ee_id_1:
- message = "EE 1 is mandatory"
- self.log.error(message)
- raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_1"])
- if not ee_id_2:
- message = "EE 2 is mandatory"
- self.log.error(message)
- raise N2VCBadArgumentsException(message=message, bad_args=["ee_id_2"])
- if not endpoint_1:
- message = "endpoint 1 is mandatory"
- self.log.error(message)
- raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_1"])
- if not endpoint_2:
- message = "endpoint 2 is mandatory"
- self.log.error(message)
- raise N2VCBadArgumentsException(message=message, bad_args=["endpoint_2"])
-
- # get the model, the applications and the machines from the ee_id's
- model_1, app_1, _machine_1 = self._get_ee_id_components(ee_id_1)
- model_2, app_2, _machine_2 = self._get_ee_id_components(ee_id_2)
-
- # model must be the same
- if model_1 != model_2:
- message = "EE models are not the same: {} vs {}".format(ee_id_1, ee_id_2)
- self.log.error(message)
- raise N2VCBadArgumentsException(
- message=message, bad_args=["ee_id_1", "ee_id_2"]
- )
-
- # add juju relations between two applications
try:
- await libjuju.add_relation(
- model_name=model_1,
- endpoint_1="{}:{}".format(app_1, endpoint_1),
- endpoint_2="{}:{}".format(app_2, endpoint_2),
- )
+ if cross_model_relation:
+ # Cross-model relation
+ provider_libjuju = await self._get_libjuju(provider.vca_id)
+ requirer_libjuju = await self._get_libjuju(requirer.vca_id)
+ offer = await provider_libjuju.offer(provider)
+ if offer:
+ saas_name = await requirer_libjuju.consume(
+ requirer.model_name, offer, provider_libjuju
+ )
+ await requirer_libjuju.add_relation(
+ requirer.model_name,
+ requirer.endpoint,
+ saas_name,
+ )
+ else:
+ # Standard relation
+ vca_id = provider.vca_id
+ model = provider.model_name
+ libjuju = await self._get_libjuju(vca_id)
+ # add juju relations between two applications
+ await libjuju.add_relation(
+ model_name=model,
+ endpoint_1=provider.endpoint,
+ endpoint_2=requirer.endpoint,
+ )
except Exception as e:
- message = "Error adding relation between {} and {}: {}".format(
- ee_id_1, ee_id_2, e
- )
+ message = f"Error adding relation between {provider} and {requirer}: {e}"
self.log.error(message)
raise N2VCException(message=message)
:param: vca_id: VCA ID
"""
self.log.info("Deleting namespace={}".format(namespace))
- libjuju = await self._get_libjuju(vca_id)
+ will_not_delete = False
+ if namespace not in self.delete_namespace_locks:
+ self.delete_namespace_locks[namespace] = asyncio.Lock(loop=self.loop)
+ delete_lock = self.delete_namespace_locks[namespace]
- # check arguments
- if namespace is None:
- raise N2VCBadArgumentsException(
- message="namespace is mandatory", bad_args=["namespace"]
- )
+ while delete_lock.locked():
+ will_not_delete = True
+ await asyncio.sleep(0.1)
- _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
- namespace=namespace
- )
- if ns_id is not None:
- try:
- models = await libjuju.list_models(contains=ns_id)
- for model in models:
- await libjuju.destroy_model(
- model_name=model, total_timeout=total_timeout
+ if will_not_delete:
+ self.log.info("Namespace {} deleted by another worker.".format(namespace))
+ return
+
+ try:
+ async with delete_lock:
+ libjuju = await self._get_libjuju(vca_id)
+
+ # check arguments
+ if namespace is None:
+ raise N2VCBadArgumentsException(
+ message="namespace is mandatory", bad_args=["namespace"]
)
- except Exception as e:
- raise N2VCException(
- message="Error deleting namespace {} : {}".format(namespace, e)
- )
- else:
- raise N2VCBadArgumentsException(
- message="only ns_id is permitted to delete yet", bad_args=["namespace"]
- )
+ (
+ _nsi_id,
+ ns_id,
+ _vnf_id,
+ _vdu_id,
+ _vdu_count,
+ ) = self._get_namespace_components(namespace=namespace)
+ if ns_id is not None:
+ try:
+ models = await libjuju.list_models(contains=ns_id)
+ for model in models:
+ await libjuju.destroy_model(
+ model_name=model, total_timeout=total_timeout
+ )
+ except Exception as e:
+ self.log.error(f"Error deleting namespace {namespace} : {e}")
+ raise N2VCException(
+ message="Error deleting namespace {} : {}".format(
+ namespace, e
+ )
+ )
+ else:
+ raise N2VCBadArgumentsException(
+ message="only ns_id is permitted to delete yet",
+ bad_args=["namespace"],
+ )
+ except Exception as e:
+ self.log.error(f"Error deleting namespace {namespace} : {e}")
+ raise e
+ finally:
+ self.delete_namespace_locks.pop(namespace)
self.log.info("Namespace {} deleted".format(namespace))
async def delete_execution_environment(
machine_id=machine_id,
progress_timeout=progress_timeout,
total_timeout=total_timeout,
- **params_dict
+ **params_dict,
)
if status == "completed":
return output
primitive_name=primitive_name,
)
+ async def upgrade_charm(
+ self,
+ ee_id: str = None,
+ path: str = None,
+ charm_id: str = None,
+ charm_type: str = None,
+ timeout: float = None,
+ ) -> str:
+ """This method upgrade charms in VNFs
+
+ Args:
+ ee_id: Execution environment id
+ path: Local path to the charm
+ charm_id: charm-id
+ charm_type: Charm type can be lxc-proxy-charm, native-charm or k8s-proxy-charm
+ timeout: (Float) Timeout for the ns update operation
+
+ Returns:
+ The output of the update operation if status equals to "completed"
+
+ """
+ self.log.info("Upgrading charm: {} on ee: {}".format(path, ee_id))
+ libjuju = await self._get_libjuju(charm_id)
+
+ # check arguments
+ if ee_id is None or len(ee_id) == 0:
+ raise N2VCBadArgumentsException(
+ message="ee_id is mandatory", bad_args=["ee_id"]
+ )
+ try:
+ (
+ model_name,
+ application_name,
+ machine_id,
+ ) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
+
+ except Exception:
+ raise N2VCBadArgumentsException(
+ message="ee_id={} is not a valid execution environment id".format(
+ ee_id
+ ),
+ bad_args=["ee_id"],
+ )
+
+ try:
+
+ await libjuju.upgrade_charm(
+ application_name=application_name,
+ path=path,
+ model_name=model_name,
+ total_timeout=timeout,
+ )
+
+ return f"Charm upgraded with application name {application_name}"
+
+ except Exception as e:
+ self.log.error("Error upgrading charm {}: {}".format(path, e))
+
+ raise N2VCException(
+ message="Error upgrading charm {} in ee={} : {}".format(path, ee_id, e)
+ )
+
async def disconnect(self, vca_id: str = None):
"""
Disconnect from VCA
:return: model_name, application_name, machine_id
"""
- if ee_id is None:
- return None, None, None
-
- # split components of id
- parts = ee_id.split(".")
- model_name = parts[0]
- application_name = parts[1]
- machine_id = parts[2]
- return model_name, application_name, machine_id
+ return get_ee_id_components(ee_id)
def _get_application_name(self, namespace: str) -> str:
"""
Build application name from namespace
:param namespace:
- :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>
+ :return: app-vnf-<vnf id>-vdu-<vdu-id>-cnt-<vdu-count>-<random_value>
"""
# TODO: Enforce the Juju 50-character application limit
else:
vdu_count = "-cnt-" + vdu_count
- application_name = "app-{}{}{}".format(vnf_id, vdu_id, vdu_count)
+ # Generate a random suffix with 5 characters (the default size used by K8s)
+ random_suffix = generate_random_alfanum_string(size=5)
+
+ application_name = "app-{}{}{}-{}".format(
+ vnf_id, vdu_id, vdu_count, random_suffix
+ )
return N2VCJujuConnector._format_app_name(application_name)