This patch changes the behaviour of native charm deployments.
it won't deploy another application for the same vnf or
vdu charm at initial deployment or scaling process.
It scales the application.
Change-Id: I3fc52a5ddb0cb7cb16937bc12cf343f7d869c9ee
Signed-off-by: aktas <emin.aktas@ulakhaberlesme.com.tr>
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"]