from juju.client import client
from juju import tag
+from n2vc.definitions import Offer, RelationEndpoint
from n2vc.juju_watcher import JujuModelWatcher
from n2vc.provisioner import AsyncSSHProvisioner
from n2vc.n2vc_conn import N2VCConnector
"""
controller = None
try:
- controller = Controller(loop=self.loop)
+ controller = Controller()
await asyncio.wait_for(
controller.connect(
endpoint=self.vca_connection.data.endpoints,
)
if controller:
await self.disconnect_controller(controller)
- raise JujuControllerFailedConnecting(e)
+
+ raise JujuControllerFailedConnecting(
+ f"Error connecting to Juju controller: {e}"
+ )
async def disconnect(self):
"""Disconnect"""
controller = await self.get_controller()
model = await self.get_model(controller, model_name)
try:
- await model.deploy(uri)
+ await model.deploy(uri, trust=True)
if wait:
await JujuModelWatcher.wait_for_model(model, timeout=timeout)
self.log.debug("All units active in model {}".format(model_name))
application = self._get_application(model, application_name)
if application is not None:
-
# Checks if the given machine id in the model,
# otherwise function raises an error
_machine, _series = self._get_machine_info(model, machine_id)
:param: application_name: Application name
:param: model_name: Model name
:param: machine_id Machine id
- :param: db_dict: Dictionary with data of the DB to write the updates
:param: total_timeout: Timeout for the entity to be active
:return: None
try:
if application_name not in model.applications:
-
if machine_id is not None:
machine, series = self._get_machine_info(model, machine_id)
raise JujuApplicationExists(
"Application {} exists".format(application_name)
)
+ except juju.errors.JujuError as e:
+ if "already exists" in e.message:
+ raise JujuApplicationExists(
+ "Application {} exists".format(application_name)
+ )
+ else:
+ raise e
finally:
await self.disconnect_model(model)
await self.disconnect_controller(controller)
return application
+ async def upgrade_charm(
+ self,
+ application_name: str,
+ path: str,
+ model_name: str,
+ total_timeout: float = None,
+ **kwargs,
+ ):
+ """Upgrade Charm
+
+ :param: application_name: Application name
+ :param: model_name: Model name
+ :param: path: Local path to the charm
+ :param: total_timeout: Timeout for the entity to be active
+
+ :return: (str, str): (output and status)
+ """
+
+ self.log.debug(
+ "Upgrading charm {} in model {} from path {}".format(
+ application_name, model_name, path
+ )
+ )
+
+ await self.resolve_application(
+ model_name=model_name, application_name=application_name
+ )
+
+ # Get controller
+ controller = await self.get_controller()
+
+ # Get model
+ model = await self.get_model(controller, model_name)
+
+ try:
+ # Get application
+ application = self._get_application(
+ model,
+ application_name=application_name,
+ )
+ if application is None:
+ raise JujuApplicationNotFound(
+ "Cannot find application {} to upgrade".format(application_name)
+ )
+
+ await application.refresh(path=path)
+
+ self.log.debug(
+ "Wait until charm upgrade is completed for application {} (model={})".format(
+ application_name, model_name
+ )
+ )
+
+ await JujuModelWatcher.ensure_units_idle(
+ model=model, application=application
+ )
+
+ if application.status == "error":
+ error_message = "Unknown"
+ for unit in application.units:
+ if (
+ unit.workload_status == "error"
+ and unit.workload_status_message != ""
+ ):
+ error_message = unit.workload_status_message
+
+ message = "Application {} failed update in {}: {}".format(
+ application_name, model_name, error_message
+ )
+ self.log.error(message)
+ raise JujuError(message=message)
+
+ self.log.debug(
+ "Application {} is ready in model {}".format(
+ application_name, model_name
+ )
+ )
+
+ finally:
+ await self.disconnect_model(model)
+ await self.disconnect_controller(controller)
+
+ return application
+
+ async def resolve_application(self, model_name: str, application_name: str):
+ controller = await self.get_controller()
+ model = await self.get_model(controller, model_name)
+
+ try:
+ application = self._get_application(
+ model,
+ application_name=application_name,
+ )
+ if application is None:
+ raise JujuApplicationNotFound(
+ "Cannot find application {} to resolve".format(application_name)
+ )
+
+ while application.status == "error":
+ for unit in application.units:
+ if unit.workload_status == "error":
+ self.log.debug(
+ "Model {}, Application {}, Unit {} in error state, resolving".format(
+ model_name, application_name, unit.entity_id
+ )
+ )
+ try:
+ await unit.resolved(retry=False)
+ except Exception:
+ pass
+
+ await asyncio.sleep(1)
+
+ finally:
+ await self.disconnect_model(model)
+ await self.disconnect_controller(controller)
+
+ async def resolve(self, model_name: str):
+ controller = await self.get_controller()
+ model = await self.get_model(controller, model_name)
+ all_units_active = False
+ try:
+ while not all_units_active:
+ all_units_active = True
+ for application_name, application in model.applications.items():
+ if application.status == "error":
+ for unit in application.units:
+ if unit.workload_status == "error":
+ self.log.debug(
+ "Model {}, Application {}, Unit {} in error state, resolving".format(
+ model_name, application_name, unit.entity_id
+ )
+ )
+ try:
+ await unit.resolved(retry=False)
+ all_units_active = False
+ except Exception:
+ pass
+
+ if not all_units_active:
+ await asyncio.sleep(5)
+ finally:
+ await self.disconnect_model(model)
+ await self.disconnect_controller(controller)
+
async def scale_application(
self,
model_name: str,
await self.disconnect_model(model)
await self.disconnect_controller(controller)
+ async def offer(self, endpoint: RelationEndpoint) -> Offer:
+ """
+ Create an offer from a RelationEndpoint
+
+ :param: endpoint: Relation endpoint
+
+ :return: Offer object
+ """
+ model_name = endpoint.model_name
+ offer_name = f"{endpoint.application_name}-{endpoint.endpoint_name}"
+ controller = await self.get_controller()
+ model = None
+ try:
+ model = await self.get_model(controller, model_name)
+ await model.create_offer(endpoint.endpoint, offer_name=offer_name)
+ offer_list = await self._list_offers(model_name, offer_name=offer_name)
+ if offer_list:
+ return Offer(offer_list[0].offer_url)
+ else:
+ raise Exception("offer was not created")
+ except juju.errors.JujuError as e:
+ if "application offer already exists" not in e.message:
+ raise e
+ finally:
+ if model:
+ self.disconnect_model(model)
+ self.disconnect_controller(controller)
+
async def consume(
self,
- offer_url: str,
model_name: str,
- ):
+ offer: Offer,
+ provider_libjuju: "Libjuju",
+ ) -> str:
"""
- Adds a remote offer to the model. Relations can be created later using "juju relate".
+ Consumes a remote offer in the model. Relations can be created later using "juju relate".
- :param: offer_url: Offer Url
- :param: model_name: Model name
+ :param: model_name: Model name
+ :param: offer: Offer object to consume
+ :param: provider_libjuju: Libjuju object of the provider endpoint
:raises ParseError if there's a problem parsing the offer_url
:raises JujuError if remote offer includes and endpoint
:raises JujuAPIError if the operation is not successful
+
+ :returns: Saas name. It is the application name in the model that reference the remote application.
"""
+ saas_name = f'{offer.name}-{offer.model_name.replace("-", "")}'
+ if offer.vca_id:
+ saas_name = f"{saas_name}-{offer.vca_id}"
controller = await self.get_controller()
- model = await controller.get_model(model_name)
-
+ model = None
+ provider_controller = None
try:
- await model.consume(offer_url)
+ model = await controller.get_model(model_name)
+ provider_controller = await provider_libjuju.get_controller()
+ await model.consume(
+ offer.url, application_alias=saas_name, controller=provider_controller
+ )
+ return saas_name
finally:
- await self.disconnect_model(model)
+ if model:
+ await self.disconnect_model(model)
+ if provider_controller:
+ await provider_libjuju.disconnect_controller(provider_controller)
await self.disconnect_controller(controller)
async def destroy_model(self, model_name: str, total_timeout: float = 1800):
model = None
try:
if not await self.model_exists(model_name, controller=controller):
+ self.log.warn(f"Model {model_name} doesn't exist")
return
- self.log.debug("Destroying model {}".format(model_name))
-
+ self.log.debug(f"Getting model {model_name} to be destroyed")
model = await self.get_model(controller, model_name)
+ self.log.debug(f"Destroying manual machines in model {model_name}")
# Destroy machines that are manually provisioned
# and still are in pending state
await self._destroy_pending_machines(model, only_manual=True)
await self.disconnect_model(model)
- await self._destroy_model(
- model_name,
- controller,
+ await asyncio.wait_for(
+ self._destroy_model(model_name, controller),
timeout=total_timeout,
)
+ except Exception as e:
+ if not await self.model_exists(model_name, controller=controller):
+ self.log.warn(
+ f"Failed deleting model {model_name}: model doesn't exist"
+ )
+ return
+ self.log.warn(f"Failed deleting model {model_name}: {e}")
+ raise e
finally:
if model:
await self.disconnect_model(model)
await self.disconnect_controller(controller)
async def _destroy_model(
- self, model_name: str, controller: Controller, timeout: float = 1800
+ self,
+ model_name: str,
+ controller: Controller,
):
"""
Destroy model from controller
:param: controller: Controller object
:param: timeout: Timeout in seconds
"""
+ self.log.debug(f"Destroying model {model_name}")
- async def _destroy_model_loop(model_name: str, controller: Controller):
- while await self.model_exists(model_name, controller=controller):
+ async def _destroy_model_gracefully(model_name: str, controller: Controller):
+ self.log.info(f"Gracefully deleting model {model_name}")
+ resolved = False
+ while model_name in await controller.list_models():
+ if not resolved:
+ await self.resolve(model_name)
+ resolved = True
+ await controller.destroy_model(model_name, destroy_storage=True)
+
+ await asyncio.sleep(5)
+ self.log.info(f"Model {model_name} deleted gracefully")
+
+ async def _destroy_model_forcefully(model_name: str, controller: Controller):
+ self.log.info(f"Forcefully deleting model {model_name}")
+ while model_name in await controller.list_models():
await controller.destroy_model(
- model_name, destroy_storage=True, force=True, max_wait=0
+ model_name, destroy_storage=True, force=True, max_wait=60
)
await asyncio.sleep(5)
+ self.log.info(f"Model {model_name} deleted forcefully")
try:
- await asyncio.wait_for(
- _destroy_model_loop(model_name, controller), timeout=timeout
- )
- except asyncio.TimeoutError:
- raise Exception(
- "Timeout waiting for model {} to be destroyed".format(model_name)
- )
+ try:
+ await asyncio.wait_for(
+ _destroy_model_gracefully(model_name, controller), timeout=120
+ )
+ except asyncio.TimeoutError:
+ await _destroy_model_forcefully(model_name, controller)
+ except juju.errors.JujuError as e:
+ if any("has been removed" in error for error in e.errors):
+ return
+ if any("model not found" in error for error in e.errors):
+ return
+ raise e
async def destroy_application(
self, model_name: str, application_name: str, total_timeout: float
finally:
await self.disconnect_controller(controller)
- async def list_offers(self, model_name: str) -> QueryApplicationOffersResults:
- """List models with certain names
+ async def _list_offers(
+ self, model_name: str, offer_name: str = None
+ ) -> QueryApplicationOffersResults:
+ """
+ List offers within a model
:param: model_name: Model name
+ :param: offer_name: Offer name to filter.
- :return: Returns list of offers
+ :return: Returns application offers results in the model
"""
controller = await self.get_controller()
try:
- return await controller.list_offers(model_name)
+ offers = (await controller.list_offers(model_name)).results
+ if offer_name:
+ matching_offer = []
+ for offer in offers:
+ if offer.offer_name == offer_name:
+ matching_offer.append(offer)
+ break
+ offers = matching_offer
+ return offers
finally:
await self.disconnect_controller(controller)