X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=juju%2Fmodel.py;h=e2d3d5296e39df51fd55c6c490d8223687aeed29;hb=b64327bb58139f1e1f770b16492968423f011eab;hp=b218fba0c71da46e830ceb473b7a4e3508b0d66f;hpb=132f5ea91d63a915b2d892fc5a54429a8dc92bba;p=osm%2FN2VC.git diff --git a/juju/model.py b/juju/model.py index b218fba..e2d3d52 100644 --- a/juju/model.py +++ b/juju/model.py @@ -1,10 +1,12 @@ import asyncio import collections import logging +import os import re import weakref from concurrent.futures import CancelledError from functools import partial +from pathlib import Path import yaml from theblues import charmstore @@ -345,6 +347,7 @@ class Model(object): self.connection = None self.observers = weakref.WeakValueDictionary() self.state = ModelState(self) + self.info = None self._watcher_task = None self._watch_shutdown = asyncio.Event(loop=loop) self._watch_received = asyncio.Event(loop=loop) @@ -357,25 +360,31 @@ class Model(object): """ self.connection = await connection.Connection.connect(*args, **kw) - self._watch() - await self._watch_received.wait() + await self._after_connect() async def connect_current(self): """Connect to the current Juju model. """ self.connection = await connection.Connection.connect_current() - self._watch() - await self._watch_received.wait() + await self._after_connect() + + async def connect_model(self, model_name): + """Connect to a specific Juju model by name. - async def connect_model(self, arg): - """Connect to a specific Juju model. - :param arg: : + :param model_name: Format [controller:][user/]model + + """ + self.connection = await connection.Connection.connect_model(model_name) + await self._after_connect() + + async def _after_connect(self): + """Run initialization steps after connecting to websocket. """ - self.connection = await connection.Connection.connect_model(arg) self._watch() await self._watch_received.wait() + await self.get_info() async def disconnect(self): """Shut down the watcher task and close websockets. @@ -449,13 +458,34 @@ class Model(object): """ return self.state.units + async def get_info(self): + """Return a client.ModelInfo object for this Model. + + Retrieves latest info for this Model from the api server. The + return value is cached on the Model.info attribute so that the + valued may be accessed again without another api call, if + desired. + + This method is called automatically when the Model is connected, + resulting in Model.info being initialized without requiring an + explicit call to this method. + + """ + facade = client.ClientFacade() + facade.connect(self.connection) + + self.info = await facade.ModelInfo() + log.debug('Got ModelInfo: %s', vars(self.info)) + + return self.info + def add_observer( self, callable_, entity_type=None, action=None, entity_id=None, predicate=None): """Register an "on-model-change" callback Once the model is connected, ``callable_`` - will be called each time the model changes. callable_ should + will be called each time the model changes. ``callable_`` should be Awaitable and accept the following positional arguments: delta - An instance of :class:`juju.delta.EntityDelta` @@ -474,14 +504,15 @@ class Model(object): model - The :class:`Model` itself. Events for which ``callable_`` is called can be specified by passing - entity_type, action, and/or id_ filter criteria, e.g.: + entity_type, action, and/or entitiy_id filter criteria, e.g.:: add_observer( - myfunc, entity_type='application', action='add', id_='ubuntu') + myfunc, + entity_type='application', action='add', entity_id='ubuntu') For more complex filtering conditions, pass a predicate function. It will be called with a delta as its only argument. If the predicate - function returns True, the callable_ will be called. + function returns True, the ``callable_`` will be called. """ observer = _Observer( @@ -778,14 +809,14 @@ class Model(object): pass async def deploy( - self, entity_url, service_name=None, bind=None, budget=None, + self, entity_url, application_name=None, bind=None, budget=None, channel=None, config=None, constraints=None, force=False, num_units=1, plan=None, resources=None, series=None, storage=None, to=None): """Deploy a new service or bundle. :param str entity_url: Charm or bundle url - :param str service_name: Name to give the service + :param str application_name: Name to give the service :param dict bind: : pairs :param dict budget: : pairs :param str channel: Charm store channel from which to retrieve @@ -811,7 +842,7 @@ class Model(object): TODO:: - - service_name is required; fill this in automatically if not + - application_name is required; fill this in automatically if not provided by caller - series is required; how do we pick a default? @@ -829,14 +860,21 @@ class Model(object): for k, v in storage.items() } - entity_id = await self.charmstore.entityId(entity_url) + is_local = not entity_url.startswith('cs:') and \ + os.path.isdir(entity_url) + entity_id = await self.charmstore.entityId(entity_url) \ + if not is_local else entity_url app_facade = client.ApplicationFacade() client_facade = client.ClientFacade() app_facade.connect(self.connection) client_facade.connect(self.connection) - if 'bundle/' in entity_id: + is_bundle = ((is_local and + (Path(entity_id) / 'bundle.yaml').exists()) or + (not is_local and 'bundle/' in entity_id)) + + if is_bundle: handler = BundleHandler(self) await handler.fetch_plan(entity_id) await handler.execute_plan() @@ -858,7 +896,7 @@ class Model(object): await client_facade.AddCharm(channel, entity_id) app = client.ApplicationDeploy( - application=service_name, + application=application_name, channel=channel, charm_url=entity_id, config=config, @@ -872,7 +910,7 @@ class Model(object): ) await app_facade.Deploy([app]) - return await self._wait_for_new('application', service_name) + return await self._wait_for_new('application', application_name) def destroy(self): """Terminate all machines and resources for this model. @@ -1233,7 +1271,7 @@ class Model(object): raise Exception(error.message) for metric in entity_metrics.metrics: - metrics[metric.unit].append(metric.to_json()) + metrics[metric.unit].append(vars(metric)) return metrics @@ -1260,9 +1298,13 @@ class BundleHandler(object): self.ann_facade.connect(model.connection) async def fetch_plan(self, entity_id): - bundle_yaml = await self.charmstore.files(entity_id, - filename='bundle.yaml', - read_file=True) + is_local = not entity_id.startswith('cs:') and os.path.isdir(entity_id) + if is_local: + bundle_yaml = (Path(entity_id) / "bundle.yaml").read_text() + else: + bundle_yaml = await self.charmstore.files(entity_id, + filename='bundle.yaml', + read_file=True) self.bundle = yaml.safe_load(bundle_yaml) self.plan = await self.client_facade.GetBundleChanges(bundle_yaml) @@ -1302,16 +1344,20 @@ class BundleHandler(object): Keys include: series: string specifying the machine OS series. + 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 instance ""lxc" or kvm"). It is not specified for top level machines. + parent_id: string holding a placeholder pointing to another machine change or to a unit change. This value is only specified in the case this machine is a container, in which case also ContainerType is set. + """ params = params or {}