From: Tim Van Steenburgh Date: Tue, 29 Nov 2016 15:09:30 +0000 (-0500) Subject: Merge pull request #23 from petevg/bug/instance-placement-bug X-Git-Tag: 0.1.0~29 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=8ffeaa57dd88940c1b21028fbee3fc17dae72d5e;hp=0056d3f87102e50a1c35c05bdfa25124c387440f;p=osm%2FN2VC.git Merge pull request #23 from petevg/bug/instance-placement-bug Added Placement parser. --- diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b7e82d2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +dist: trusty +sudo: required +language: python +python: + - "3.5" +before_install: + - sudo add-apt-repository ppa:ubuntu-lxc/lxd-stable -y + - sudo add-apt-repository ppa:juju/daily -y + - sudo apt-get update -q + - sudo apt-get install lxd juju -y + - sudo usermod -a -G lxd $USER + - sudo lxd init --auto +install: pip install tox-travis +before_script: + - sudo -E sudo -u $USER -E bash -c "juju bootstrap localhost test" +script: tox +after_script: + - sudo -E sudo -u $USER -E bash -c "juju destroy-controller --destroy-all-models -y test" diff --git a/examples/leadership.py b/examples/leadership.py new file mode 100644 index 0000000..c52de60 --- /dev/null +++ b/examples/leadership.py @@ -0,0 +1,26 @@ +""" +This example: + +1. Connects to the current model. +2. Prints out leadership status for all deployed units in the model. +3. Cleanly disconnects. + +""" +import asyncio + +from juju.model import Model + +async def report_leadership(): + model = Model() + await model.connect_current() + + print("Leadership: ") + for app in model.applications.values(): + for unit in app.units: + print("{}: {}".format( + unit.name, await unit.is_leader_from_status())) + + await model.disconnect() + +loop = asyncio.get_event_loop() +loop.run_until_complete(report_leadership()) diff --git a/juju/action.py b/juju/action.py index 941fa94..7a136a7 100644 --- a/juju/action.py +++ b/juju/action.py @@ -2,4 +2,9 @@ from . import model class Action(model.ModelEntity): - pass + @property + def status(self): + return self.data['status'] + + async def wait(self): + return await self.model.wait_for_action(self.id) diff --git a/juju/application.py b/juju/application.py index df309b3..83c2275 100644 --- a/juju/application.py +++ b/juju/application.py @@ -50,6 +50,10 @@ class Application(model.ModelEntity): """ return self.data['status']['message'] + @property + def tag(self): + return 'application-%s' % self.name + async def add_relation(self, local_relation, remote_relation): """Add a relation to another application. @@ -338,3 +342,6 @@ class Application(model.ModelEntity): """ pass + + async def get_metrics(self): + return await self.model.get_metrics(self.tag) diff --git a/juju/controller.py b/juju/controller.py index ca871a1..f0ef3b9 100644 --- a/juju/controller.py +++ b/juju/controller.py @@ -63,6 +63,32 @@ class Controller(object): return model + async def destroy_models(self, *args): + + """Destroy a model to this controller. + + :param str : of the Model + param accepts string of only OR `model-` + + + """ + model_facade = client.ModelManagerFacade() + model_facade.connect(self.connection) + + #Generate list of args, pre-pend 'model-' + prependarg = list(args) + for index, item in enumerate(prependarg): + if not item.startswith('model-'): + prependarg[index]="model-%s" % item + + #Create list of objects to pass to DestroyModels() + arglist = [] + for arg in prependarg: + arglist.append(client.Entity(arg)) + log.debug('Destroying Model %s', arg) + + await model_facade.DestroyModels(arglist) + def add_user(self, username, display_name=None, acl=None, models=None): """Add a user to this controller. diff --git a/juju/model.py b/juju/model.py index eb9db7e..b218fba 100644 --- a/juju/model.py +++ b/juju/model.py @@ -577,15 +577,24 @@ class Model(object): entity_id = await q.get() return self.state._live_entity_map(entity_type)[entity_id] - async def _wait_for_new(self, entity_type, entity_id, predicate=None): + async def _wait_for_new(self, entity_type, entity_id=None, predicate=None): """Wait for a new object to appear in the Model and return it. Waits for an object of type ``entity_type`` with id ``entity_id``. + If ``entity_id`` is ``None``, it will wait for the first new entity + of the correct type. This coroutine blocks until the new object appears in the model. """ - return await self._wait(entity_type, entity_id, 'add', predicate) + # if the entity is already in the model, just return it + if entity_id in self.state._live_entity_map(entity_type): + return self.state._live_entity_map(entity_type)[entity_id] + # if we know the entity_id, we can trigger on any action that puts + # the enitty into the model; otherwise, we have to watch for the + # next "add" action on that entity_type + action = 'add' if entity_id is None else None + return await self._wait(entity_type, entity_id, action, predicate) async def wait_for_action(self, action_id): """Given an action, wait for it to complete.""" @@ -1198,6 +1207,36 @@ class Model(object): def charmstore(self): return self._charmstore + async def get_metrics(self, *tags): + """Retrieve metrics. + + :param str \*tags: Tags of entities from which to retrieve metrics. + No tags retrieves the metrics of all units in the model. + """ + log.debug("Retrieving metrics for %s", + ', '.join(tags) if tags else "all units") + + metrics_facade = client.MetricsDebugFacade() + metrics_facade.connect(self.connection) + + entities = [client.Entity(tag) for tag in tags] + metrics_result = await metrics_facade.GetMetrics(entities) + + metrics = collections.defaultdict(list) + + for entity_metrics in metrics_result.results: + error = entity_metrics.error + if error: + if "is not a valid tag" in error: + raise ValueError(error.message) + else: + raise Exception(error.message) + + for metric in entity_metrics.metrics: + metrics[metric.unit].append(metric.to_json()) + + return metrics + class BundleHandler(object): """ @@ -1266,7 +1305,7 @@ class BundleHandler(object): constraints: string holding machine constraints, if any. We'll parse this into the json friendly dict that the juju api expects. - Container_type: string holding the type of the container (for + container_type: string holding the type of the container (for instance ""lxc" or kvm"). It is not specified for top level machines. parent_id: string holding a placeholder pointing to another @@ -1359,6 +1398,8 @@ class BundleHandler(object): # do the do log.info('Deploying %s', charm) await self.app_facade.Deploy([app]) + # ensure the app is in the model for future operations + await self.model._wait_for_new('application', application) return application async def addUnit(self, application, to): diff --git a/juju/unit.py b/juju/unit.py index 3dbc1e9..8511a47 100644 --- a/juju/unit.py +++ b/juju/unit.py @@ -1,4 +1,3 @@ -import asyncio import logging from datetime import datetime @@ -57,6 +56,10 @@ class Unit(model.ModelEntity): """ return self.data['workload-status']['message'] + @property + def tag(self): + return 'unit-%s' % self.name.replace('/', '-') + def add_storage(self, name, constraints=None): """Add unit storage dynamically. @@ -128,14 +131,39 @@ class Unit(model.ModelEntity): ) return await self.model.wait_for_action(res.results[0].action.tag) - def run_action(self, action_name, **params): - """Run action on this unit. + async def run_action(self, action_name, **params): + """Run an action on this unit. :param str action_name: Name of action to run :param \*\*params: Action parameters - - """ - pass + :returns: An `juju.action.Action` instance. + + Note that this only enqueues the action. You will need to call + ``action.wait()`` on the resulting `Action` instance if you wish + to block until the action is complete. + """ + action_facade = client.ActionFacade() + action_facade.connect(self.connection) + + log.debug('Starting action `%s` on %s', action_name, self.name) + + res = await action_facade.Enqueue([client.Action( + name=action_name, + parameters=params, + receiver=self.tag, + )]) + action = res.results[0].action + error = res.results[0].error + if error and error.code == 'not found': + raise ValueError('Action `%s` not found on %s' % (action_name, + self.name)) + elif error: + raise Exception('Unknown action error: %s' % error.serialize()) + action_id = action.tag[len('action-'):] + log.debug('Action started as %s', action_id) + # we mustn't use wait_for_action because that blocks until the + # action is complete, rather than just being in the model + return await self.model._wait_for_new('action', action_id) def scp( self, source_path, user=None, destination_path=None, proxy=False, @@ -178,3 +206,39 @@ class Unit(model.ModelEntity): """ pass + + async def is_leader_from_status(self): + """ + Check to see if this unit is the leader. Returns True if so, and + False if it is not, or if leadership does not make sense + (e.g., there is no leader in this application.) + + This method is a kluge that calls FullStatus in the + ClientFacade to get its information. Once + https://bugs.launchpad.net/juju/+bug/1643691 is resolved, we + should add a simple .is_leader property, and deprecate this + method. + + """ + app = self.name.split("/")[0] + + c = client.ClientFacade() + c.connect(self.model.connection) + + status = await c.FullStatus(None) + + try: + return status.applications[app]['units'][self.name].get( + 'leader', False) + except KeyError: + # FullStatus may be more up-to-date than the model + # referenced by this class. If this unit has been + # destroyed between the time the class was created and the + # time that we call this method, we'll get a KeyError. In + # that case, we simply return False, as a destroyed unit + # is not a leader. + return False + + async def get_metrics(self): + metrics = await self.model.get_metrics(self.tag) + return metrics[self.name]