pyvenv.cfg
share/
venv/
+.idea
return "<{}> Not found: {}".format(type(self), super().__str__())
+class N2VCApplicationExists(N2VCException):
+ """
+ Application Exists
+ """
+
+ def __init__(self, message: str = ""):
+ N2VCException.__init__(self, message=message)
+
+ def __str__(self):
+ return "<{}> Application Exists: {}".format(type(self), super().__str__())
+
+
class JujuError(N2VCException):
"""
Juju Error
await self.disconnect_model(model)
await self.disconnect_controller(controller)
+ async def add_unit(
+ self,
+ application_name: str,
+ model_name: str,
+ machine_id: str,
+ db_dict: dict = None,
+ progress_timeout: float = None,
+ total_timeout: float = None,
+ ):
+ """Add unit
+
+ :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: progress_timeout: Maximum time between two updates in the model
+ :param: total_timeout: Timeout for the entity to be active
+
+ :return: None
+ """
+
+ model = None
+ controller = await self.get_controller()
+ try:
+ model = await self.get_model(controller, 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)
+
+ self.log.debug(
+ "Adding unit (machine {}) to application {} in model ~{}".format(
+ machine_id, application_name, model_name
+ )
+ )
+
+ await application.add_unit(to=machine_id)
+
+ await JujuModelWatcher.wait_for(
+ model=model,
+ entity=application,
+ progress_timeout=progress_timeout,
+ total_timeout=total_timeout,
+ db_dict=db_dict,
+ n2vc=self.n2vc,
+ vca_id=self.vca_connection._vca_id,
+ )
+ self.log.debug(
+ "Unit is added to application {} in model {}".format(
+ application_name, model_name
+ )
+ )
+ else:
+ raise JujuApplicationNotFound(
+ "Application {} not exists".format(application_name)
+ )
+ finally:
+ if model:
+ await self.disconnect_model(model)
+ await self.disconnect_controller(controller)
+
+ async def destroy_unit(
+ self,
+ application_name: str,
+ model_name: str,
+ machine_id: str,
+ total_timeout: float = None,
+ ):
+ """Destroy unit
+
+ :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
+ """
+
+ model = None
+ controller = await self.get_controller()
+ try:
+ model = await self.get_model(controller, model_name)
+ application = self._get_application(model, application_name)
+
+ if application is None:
+ raise JujuApplicationNotFound(
+ "Application not found: {} (model={})".format(
+ application_name, model_name
+ )
+ )
+
+ unit = self._get_unit(application, machine_id)
+ if not unit:
+ raise JujuError(
+ "A unit with machine id {} not in available units".format(
+ machine_id
+ )
+ )
+
+ unit_name = unit.name
+
+ self.log.debug(
+ "Destroying unit {} from application {} in model {}".format(
+ unit_name, application_name, model_name
+ )
+ )
+ await application.destroy_unit(unit_name)
+
+ self.log.debug(
+ "Waiting for unit {} to be destroyed in application {} (model={})...".format(
+ unit_name, application_name, model_name
+ )
+ )
+
+ # TODO: Add functionality in the Juju watcher to replace this kind of blocks
+ if total_timeout is None:
+ total_timeout = 3600
+ end = time.time() + total_timeout
+ while time.time() < end:
+ if not self._get_unit(application, machine_id):
+ self.log.debug(
+ "The unit {} was destroyed in application {} (model={}) ".format(
+ unit_name, application_name, model_name
+ )
+ )
+ return
+ await asyncio.sleep(5)
+ self.log.debug(
+ "Unit {} is destroyed from application {} in model {}".format(
+ unit_name, application_name, model_name
+ )
+ )
+ finally:
+ if model:
+ await self.disconnect_model(model)
+ await self.disconnect_controller(controller)
+
async def deploy_charm(
self,
application_name: str,
model = await self.get_model(controller, model_name)
try:
- application = None
if application_name not in model.applications:
if machine_id is not None:
- if machine_id not in model.machines:
- msg = "Machine {} not found in model".format(machine_id)
- self.log.error(msg=msg)
- raise JujuMachineNotFound(msg)
- machine = model.machines[machine_id]
- series = machine.series
+ machine, series = self._get_machine_info(model, machine_id)
application = await model.deploy(
entity_url=path,
if model.applications and application_name in model.applications:
return model.applications[application_name]
+ def _get_unit(self, application: Application, machine_id: str) -> Unit:
+ """Get unit
+
+ :param: application: Application object
+ :param: machine_id: Machine id
+
+ :return: Unit
+ """
+ unit = None
+ for u in application.units:
+ if u.machine_id == machine_id:
+ unit = u
+ break
+ return unit
+
+ def _get_machine_info(
+ self,
+ model,
+ machine_id: str,
+ ) -> (str, str):
+ """Get machine info
+
+ :param: model: Model object
+ :param: machine_id: Machine id
+
+ :return: (str, str): (machine, series)
+ """
+ if machine_id not in model.machines:
+ msg = "Machine {} not found in model".format(machine_id)
+ self.log.error(msg=msg)
+ raise JujuMachineNotFound(msg)
+ machine = model.machines[machine_id]
+ return machine, machine.series
+
async def execute_action(
self,
application_name: str,
model_name: str,
action_name: str,
db_dict: dict = None,
+ machine_id: str = None,
progress_timeout: float = None,
total_timeout: float = None,
**kwargs,
:param: model_name: Model name
:param: action_name: Name of the action
:param: db_dict: Dictionary with data of the DB to write the updates
+ :param: machine_id Machine id
:param: progress_timeout: Maximum time between two updates in the model
:param: total_timeout: Timeout for the entity to be active
)
if application is None:
raise JujuApplicationNotFound("Cannot execute action")
-
- # Get leader unit
# Racing condition:
# Ocassionally, self._get_leader_unit() will return None
# because the leader elected hook has not been triggered yet.
# Therefore, we are doing some retries. If it happens again,
# re-open bug 1236
- unit = await self._get_leader_unit(application)
+ if machine_id is None:
+ unit = await self._get_leader_unit(application)
+ self.log.debug(
+ "Action {} is being executed on the leader unit {}".format(
+ action_name, unit.name
+ )
+ )
+ else:
+ unit = self._get_unit(application, machine_id)
+ if not unit:
+ raise JujuError(
+ "A unit with machine id {} not in available units".format(
+ machine_id
+ )
+ )
+ self.log.debug(
+ "Action {} is being executed on {} unit".format(
+ action_name, unit.name
+ )
+ )
actions = await application.get_actions()
return (await facade.Credential(params)).results
finally:
await self.disconnect_controller(controller)
+
+ async def check_application_exists(self, model_name, application_name) -> bool:
+ """Check application exists
+
+ :param: model_name: Model Name
+ :param: application_name: Application Name
+
+ :return: Boolean
+ """
+
+ model = None
+ controller = await self.get_controller()
+ try:
+ model = await self.get_model(controller, model_name)
+ self.log.debug(
+ "Checking if application {} exists in model {}".format(
+ application_name, model_name
+ )
+ )
+ return self._get_application(model, application_name) is not None
+ finally:
+ if model:
+ await self.disconnect_model(model)
+ await self.disconnect_controller(controller)
N2VCException,
N2VCConnectionException,
N2VCExecutionException,
+ N2VCApplicationExists,
+ JujuApplicationExists,
# N2VCNotFound,
MethodNotImplemented,
)
from n2vc.libjuju import Libjuju
from n2vc.store import MotorStore
from n2vc.vca.connection import get_connection
+from retrying_async import retry
class N2VCJujuConnector(N2VCConnector):
return ee_id
+ # In case of native_charm is being deployed, if JujuApplicationExists error happens
+ # it will try to add_unit
+ @retry(attempts=3, delay=5, retry_exceptions=(N2VCApplicationExists,))
async def install_configuration_sw(
self,
ee_id: str,
config: dict = None,
num_units: int = 1,
vca_id: str = None,
+ scaling_out: bool = False,
+ vca_type: str = None,
):
"""
Install the software inside the execution environment identified by ee_id
:param: config: Dictionary with deployment config information.
:param: num_units: Number of units to deploy of a particular charm.
:param: vca_id: VCA ID
+ :param: scaling_out: Boolean to indicate if it is a scaling out operation
+ :param: vca_type: VCA type
"""
self.log.info(
full_path = self.fs.path + "/" + artifact_path
try:
- await libjuju.deploy_charm(
- model_name=model_name,
- application_name=application_name,
- path=full_path,
- machine_id=machine_id,
- db_dict=db_dict,
- progress_timeout=progress_timeout,
- total_timeout=total_timeout,
- config=config,
- num_units=num_units,
+ if vca_type == "native_charm" and await libjuju.check_application_exists(
+ model_name, application_name
+ ):
+ await libjuju.add_unit(
+ application_name=application_name,
+ model_name=model_name,
+ machine_id=machine_id,
+ db_dict=db_dict,
+ progress_timeout=progress_timeout,
+ total_timeout=total_timeout,
+ )
+ else:
+ await libjuju.deploy_charm(
+ model_name=model_name,
+ application_name=application_name,
+ path=full_path,
+ machine_id=machine_id,
+ db_dict=db_dict,
+ progress_timeout=progress_timeout,
+ total_timeout=total_timeout,
+ config=config,
+ num_units=num_units,
+ )
+ except JujuApplicationExists as e:
+ raise N2VCApplicationExists(
+ message="Error deploying charm into ee={} : {}".format(ee_id, e.message)
)
except Exception as e:
raise N2VCException(
- message="Error desploying charm into ee={} : {}".format(ee_id, e)
+ message="Error deploying charm into ee={} : {}".format(ee_id, e)
)
self.log.info("Configuration sw installed")
db_dict: dict = None,
total_timeout: float = None,
scaling_in: bool = False,
+ vca_type: str = None,
vca_id: str = None,
):
"""
e.g. {collection: "nsrs", filter:
{_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
:param: total_timeout: Total timeout
- :param: scaling_in: Boolean to indicate if is it a scaling in operation
+ :param: scaling_in: Boolean to indicate if it is a scaling in operation
+ :param: vca_type: VCA type
:param: vca_id: VCA ID
"""
self.log.info("Deleting execution environment ee_id={}".format(ee_id))
message="ee_id is mandatory", bad_args=["ee_id"]
)
- model_name, application_name, _machine_id = self._get_ee_id_components(
+ model_name, application_name, machine_id = self._get_ee_id_components(
ee_id=ee_id
)
try:
if not scaling_in:
# destroy the model
- # TODO: should this be removed?
await libjuju.destroy_model(
model_name=model_name,
total_timeout=total_timeout,
)
+ elif vca_type == "native_charm" and scaling_in:
+ # destroy the unit in the application
+ await libjuju.destroy_unit(
+ application_name=application_name,
+ model_name=model_name,
+ machine_id=machine_id,
+ db_dict=db_dict,
+ total_timeout=total_timeout,
+ )
else:
# destroy the application
await libjuju.destroy_application(
progress_timeout: float = None,
total_timeout: float = None,
vca_id: str = None,
+ vca_type: str = None,
) -> str:
"""
Execute a primitive in the execution environment
:param: progress_timeout: Progress timeout
:param: total_timeout: Total timeout
:param: vca_id: VCA ID
+ :param: vca_type: VCA type
:returns str: primitive result, if ok. It raises exceptions in case of fail
"""
(
model_name,
application_name,
- _machine_id,
+ machine_id,
) = N2VCJujuConnector._get_ee_id_components(ee_id=ee_id)
+ # To run action on the leader unit in libjuju.execute_action function,
+ # machine_id must be set to None if vca_type is not native_charm
+ if vca_type != "native_charm":
+ machine_id = None
except Exception:
raise N2VCBadArgumentsException(
message="ee_id={} is not a valid execution environment id".format(
application_name=application_name,
action_name=primitive_name,
db_dict=db_dict,
+ machine_id=machine_id,
progress_timeout=progress_timeout,
total_timeout=total_timeout,
**params_dict
import kubernetes
from juju.errors import JujuAPIError
import logging
-from .utils import FakeMachine, FakeApplication
+from .utils import (
+ FakeApplication,
+ FakeMachine,
+ FakeManualMachine,
+ FakeUnit,
+)
from n2vc.libjuju import Libjuju
from n2vc.exceptions import (
JujuControllerFailedConnecting,
mock_disconnect_controller.assert_called()
mock_disconnect_model.assert_called()
- def test_succesful_exec(
+ def test_successful_exec(
self,
mock_get_action_status,
mock_get_action_output,
def setUp(self):
super(GetUnitNumberTest, self).setUp()
- def test_succesful_get_unit_number(
+ def test_successful_get_unit_number(
self,
mock_get_applications,
):
model = juju.model.Model()
result = self.libjuju._get_application_count(model, "app")
self.assertEqual(result, None)
+
+
+@asynctest.mock.patch("juju.model.Model.machines", new_callable=asynctest.PropertyMock)
+class GetMachineInfoTest(LibjujuTestCase):
+ def setUp(self):
+ super(GetMachineInfoTest, self).setUp()
+
+ def test_successful(
+ self,
+ mock_machines,
+ ):
+ machine_id = "existing_machine"
+ model = juju.model.Model()
+ mock_machines.return_value = {"existing_machine": FakeManualMachine()}
+ machine, series = self.libjuju._get_machine_info(
+ machine_id=machine_id,
+ model=model,
+ )
+ self.assertIsNotNone(machine, series)
+
+ def test_exception(
+ self,
+ mock_machines,
+ ):
+ machine_id = "not_existing_machine"
+ machine = series = None
+ model = juju.model.Model()
+ mock_machines.return_value = {"existing_machine": FakeManualMachine()}
+ with self.assertRaises(JujuMachineNotFound):
+ machine, series = self.libjuju._get_machine_info(
+ machine_id=machine_id,
+ model=model,
+ )
+ self.assertIsNone(machine, series)
+
+
+class GetUnitTest(LibjujuTestCase):
+ def setUp(self):
+ super(GetUnitTest, self).setUp()
+
+ def test_successful(self):
+ result = self.libjuju._get_unit(FakeApplication(), "existing_machine_id")
+ self.assertIsInstance(result, FakeUnit)
+
+ def test_return_none(self):
+ result = self.libjuju._get_unit(FakeApplication(), "not_existing_machine_id")
+ self.assertIsNone(result)
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju._get_application")
+class CheckApplicationExists(LibjujuTestCase):
+ def setUp(self):
+ super(CheckApplicationExists, self).setUp()
+
+ def test_successful(
+ self,
+ mock_get_application,
+ mock_disconnect_controller,
+ mock_disconnect_model,
+ mock_get_model,
+ mock_get_controller,
+ ):
+ mock_get_model.return_value = juju.model.Model()
+ mock_get_application.return_value = FakeApplication()
+ result = self.loop.run_until_complete(
+ self.libjuju.check_application_exists(
+ "model",
+ "app",
+ )
+ )
+ self.assertEqual(result, True)
+
+ mock_get_application.assert_called_once()
+ mock_get_controller.assert_called_once()
+ mock_get_model.assert_called_once()
+ mock_disconnect_controller.assert_called_once()
+ mock_disconnect_model.assert_called_once()
+
+ def test_no_application(
+ self,
+ mock_get_application,
+ mock_disconnect_controller,
+ mock_disconnect_model,
+ mock_get_model,
+ mock_get_controller,
+ ):
+ mock_get_model.return_value = juju.model.Model()
+ mock_get_application.return_value = None
+ result = self.loop.run_until_complete(
+ self.libjuju.check_application_exists(
+ "model",
+ "app",
+ )
+ )
+ self.assertEqual(result, False)
+
+ mock_get_application.assert_called_once()
+ mock_get_controller.assert_called_once()
+ mock_get_model.assert_called_once()
+ mock_disconnect_controller.assert_called_once()
+ mock_disconnect_model.assert_called_once()
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju._get_application")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju._get_machine_info")
+class AddUnitTest(LibjujuTestCase):
+ def setUp(self):
+ super(AddUnitTest, self).setUp()
+
+ @asynctest.mock.patch("n2vc.juju_watcher.JujuModelWatcher.wait_for")
+ @asynctest.mock.patch("asyncio.sleep")
+ def test_successful(
+ self,
+ mock_sleep,
+ mock_wait_for,
+ mock_get_machine_info,
+ mock_get_application,
+ mock_disconnect_controller,
+ mock_disconnect_model,
+ mock_get_model,
+ mock_get_controller,
+ ):
+ mock_get_model.return_value = juju.model.Model()
+ mock_get_application.return_value = FakeApplication()
+ mock_get_machine_info.return_value = FakeMachine(), "series"
+ self.loop.run_until_complete(
+ self.libjuju.add_unit(
+ "existing_app",
+ "model",
+ "machine",
+ )
+ )
+
+ mock_wait_for.assert_called_once()
+ mock_get_application.assert_called_once()
+ mock_get_controller.assert_called_once()
+ mock_get_model.assert_called_once()
+ mock_disconnect_controller.assert_called_once()
+ mock_disconnect_model.assert_called_once()
+
+ def test_no_app(
+ self,
+ mock_get_machine_info,
+ mock_get_application,
+ mock_disconnect_controller,
+ mock_disconnect_model,
+ mock_get_model,
+ mock_get_controller,
+ ):
+ mock_get_model.return_value = juju.model.Model()
+ mock_get_application.return_value = None
+ with self.assertRaises(JujuApplicationNotFound):
+ self.loop.run_until_complete(
+ self.libjuju.add_unit(
+ "existing_app",
+ "model",
+ "machine",
+ )
+ )
+
+ mock_get_application.assert_called_once()
+ mock_get_controller.assert_called_once()
+ mock_get_model.assert_called_once()
+ mock_disconnect_controller.assert_called_once()
+ mock_disconnect_model.assert_called_once()
+
+ def test_no_machine(
+ self,
+ mock_get_machine_info,
+ mock_get_application,
+ mock_disconnect_controller,
+ mock_disconnect_model,
+ mock_get_model,
+ mock_get_controller,
+ ):
+ mock_get_model.return_value = juju.model.Model()
+ mock_get_application.return_value = FakeApplication()
+ mock_get_machine_info.side_effect = JujuMachineNotFound()
+ with self.assertRaises(JujuMachineNotFound):
+ self.loop.run_until_complete(
+ self.libjuju.add_unit(
+ "existing_app",
+ "model",
+ "machine",
+ )
+ )
+
+ mock_get_application.assert_called_once()
+ mock_get_controller.assert_called_once()
+ mock_get_model.assert_called_once()
+ mock_disconnect_controller.assert_called_once()
+ mock_disconnect_model.assert_called_once()
+
+
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_model")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju._get_application")
+@asynctest.mock.patch("n2vc.libjuju.Libjuju._get_unit")
+class DestroyUnitTest(LibjujuTestCase):
+ def setUp(self):
+ super(DestroyUnitTest, self).setUp()
+
+ @asynctest.mock.patch("asyncio.sleep")
+ def test_successful(
+ self,
+ mock_sleep,
+ mock_get_unit,
+ mock_get_application,
+ mock_disconnect_controller,
+ mock_disconnect_model,
+ mock_get_model,
+ mock_get_controller,
+ ):
+ mock_get_model.return_value = juju.model.Model()
+ mock_get_application.return_value = FakeApplication()
+
+ self.loop.run_until_complete(
+ self.libjuju.destroy_unit("app", "model", "machine", 0)
+ )
+
+ mock_get_unit.assert_called()
+ mock_get_application.assert_called_once()
+ mock_get_controller.assert_called_once()
+ mock_get_model.assert_called_once()
+ mock_disconnect_controller.assert_called_once()
+ mock_disconnect_model.assert_called_once()
+
+ def test_no_app(
+ self,
+ mock_get_unit,
+ mock_get_application,
+ mock_disconnect_controller,
+ mock_disconnect_model,
+ mock_get_model,
+ mock_get_controller,
+ ):
+ mock_get_model.return_value = juju.model.Model()
+ mock_get_application.return_value = None
+
+ with self.assertRaises(JujuApplicationNotFound):
+ self.loop.run_until_complete(
+ self.libjuju.destroy_unit("app", "model", "machine")
+ )
+
+ mock_get_application.assert_called_once()
+ mock_get_controller.assert_called_once()
+ mock_get_model.assert_called_once()
+ mock_disconnect_controller.assert_called_once()
+ mock_disconnect_model.assert_called_once()
+
+ def test_no_unit(
+ self,
+ mock_get_unit,
+ mock_get_application,
+ mock_disconnect_controller,
+ mock_disconnect_model,
+ mock_get_model,
+ mock_get_controller,
+ ):
+ mock_get_model.return_value = juju.model.Model()
+ mock_get_application.return_value = FakeApplication()
+ mock_get_unit.return_value = None
+
+ with self.assertRaises(JujuError):
+ self.loop.run_until_complete(
+ self.libjuju.destroy_unit("app", "model", "machine")
+ )
+
+ mock_get_unit.assert_called_once()
+ mock_get_application.assert_called_once()
+ mock_get_controller.assert_called_once()
+ mock_get_model.assert_called_once()
+ mock_disconnect_controller.assert_called_once()
+ mock_disconnect_model.assert_called_once()
model_name = "FAKE MODEL"
entity_type = "machine"
safe_data = {"instance-id": "manual:myid"}
+ series = "FAKE SERIES"
async def destroy(self, force):
pass
async def run_action(self, action_name, **kwargs):
return FakeAction()
+ @property
+ def machine_id(self):
+ return "existing_machine_id"
+
+ name = "existing_unit"
+
class FakeApplication(AsyncMock):
async def set_config(self, config):
async def add_unit(self, to):
pass
+ async def destroy_unit(self, unit_name):
+ pass
+
async def get_actions(self):
return ["existing_action"]