From 5fef7503b13f145adc7cd4ee31c2d684e09a6a85 Mon Sep 17 00:00:00 2001 From: Tim Van Steenburgh Date: Tue, 21 Feb 2017 13:57:30 -0500 Subject: [PATCH] Implement Model.add_machine() Also fixes a bug that was causing the 'to' parameter to Model.deploy() to not be handled correctly. Add docs and examples for adding machines and containers and deploying charms to them. Fixes #51. --- docs/api/juju.rst | 16 ++++++++ docs/narrative/model.rst | 44 +++++++++++++++++++++ examples/add_machine.py | 68 +++++++++++++++++++++++++++++++++ juju/model.py | 82 ++++++++++++++++++++++++++++------------ juju/placement.py | 6 +-- 5 files changed, 189 insertions(+), 27 deletions(-) create mode 100755 examples/add_machine.py diff --git a/docs/api/juju.rst b/docs/api/juju.rst index 37cf094..bb3de5f 100644 --- a/docs/api/juju.rst +++ b/docs/api/juju.rst @@ -91,6 +91,14 @@ juju.juju module :undoc-members: :show-inheritance: +juju.loop module +---------------- + +.. automodule:: juju.loop + :members: + :undoc-members: + :show-inheritance: + juju.machine module ------------------- @@ -139,6 +147,14 @@ juju.unit module :undoc-members: :show-inheritance: +juju.utils module +----------------- + +.. automodule:: juju.utils + :members: + :undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/docs/narrative/model.rst b/docs/narrative/model.rst index 79df011..74f7e84 100644 --- a/docs/narrative/model.rst +++ b/docs/narrative/model.rst @@ -131,6 +131,50 @@ Example of creating a new model and then destroying it. See model = None +Adding Machines and Containers +------------------------------ +To add a machine or container, connect to a model and then call its +:meth:`~juju.model.Model.add_machine` method. A +:class:`~juju.machine.Machine` instance is returned. The machine id +can be used to deploy a charm to a specific machine or container. + +.. code:: python + + from juju.model import Model + + model = Model() + await model.connect_current() + + # add a new default machine + machine1 = await model.add_machine() + + # add a machine with constraints, disks, and series specified + machine2 = await model.add_machine( + constraints={ + 'mem': 256 * MB, + }, + disks=[{ + 'pool': 'rootfs', + 'size': 10 * GB, + 'count': 1, + }], + series='xenial', + ) + + # add a lxd container to machine2 + machine3 = await model.add_machine( + 'lxd:{}'.format(machine2.id)) + + # deploy charm to the lxd container + application = await model.deploy( + 'ubuntu-10', + application_name='ubuntu', + series='xenial', + channel='stable', + to=machine3.id + ) + + Reacting to Changes in a Model ------------------------------ To watch for and respond to changes in a model, register an observer with the diff --git a/examples/add_machine.py b/examples/add_machine.py new file mode 100755 index 0000000..34f7869 --- /dev/null +++ b/examples/add_machine.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3.5 + +""" +This example: + +1. Connects to the current model +2. Creates two machines and a lxd container +3. Deploys charm to the lxd container + +""" +import logging + +from juju import loop +from juju.model import Model + +MB = 1024 * 1024 +GB = MB * 1024 + + +async def main(): + model = Model() + await model.connect_current() + + try: + # add a new default machine + machine1 = await model.add_machine() + # add a machine with constraints, disks, and series + machine2 = await model.add_machine( + constraints={ + 'mem': 256 * MB, + }, + disks=[{ + 'pool': 'rootfs', + 'size': 10 * GB, + 'count': 1, + }], + series='xenial', + ) + # add a lxd container to machine2 + machine3 = await model.add_machine( + 'lxd:{}'.format(machine2.id)) + + # deploy charm to the lxd container + application = await model.deploy( + 'ubuntu-10', + application_name='ubuntu', + series='xenial', + channel='stable', + to=machine3.id + ) + + await model.block_until( + lambda: all(unit.workload_status == 'active' + for unit in application.units)) + + await application.remove() + await machine3.destroy() + await machine2.destroy() + await machine1.destroy() + finally: + await model.disconnect() + + +logging.basicConfig(level=logging.DEBUG) +ws_logger = logging.getLogger('websockets.protocol') +ws_logger.setLevel(logging.INFO) + +loop.run(main()) diff --git a/juju/model.py b/juju/model.py index 653a20c..ef6bc85 100644 --- a/juju/model.py +++ b/juju/model.py @@ -23,6 +23,7 @@ from .delta import get_entity_delta from .delta import get_entity_class from .exceptions import DeadEntityException from .errors import JujuError, JujuAPIError +from .placement import parse as parse_placement log = logging.getLogger(__name__) @@ -719,9 +720,8 @@ class Model(object): return await self._wait('action', action_id, 'change', predicate) - def add_machine( - self, spec=None, constraints=None, disks=None, series=None, - count=1): + async def add_machine( + self, spec=None, constraints=None, disks=None, series=None): """Start a new, empty machine and optionally a container, or add a container to a machine. @@ -729,25 +729,64 @@ class Model(object): Examples:: (None) - starts a new machine - 'lxc' - starts a new machine with on lxc container - 'lxc:4' - starts a new lxc container on machine 4 + 'lxd' - starts a new machine with one lxd container + 'lxd:4' - starts a new lxd container on machine 4 'ssh:user@10.10.0.3' - manually provisions a machine with ssh 'zone=us-east-1a' - starts a machine in zone us-east-1s on AWS 'maas2.name' - acquire machine maas2.name on MAAS - :param constraints: Machine constraints - :type constraints: :class:`juju.Constraints` - :param list disks: List of disk :class:`constraints ` - :param str series: Series - :param int count: Number of machines to deploy - Supported container types are: lxc, lxd, kvm + :param dict constraints: Machine constraints + Example:: + + constraints={ + 'mem': 256 * MB, + } + + :param list disks: List of disk constraint dictionaries + Example:: + + disks=[{ + 'pool': 'rootfs', + 'size': 10 * GB, + 'count': 1, + }] + + :param str series: Series, e.g. 'xenial' + + Supported container types are: lxd, kvm When deploying a container to an existing machine, constraints cannot be used. """ - pass - add_machines = add_machine + params = client.AddMachineParams() + params.jobs = ['JobHostUnits'] + + if spec: + placement = parse_placement(spec) + if placement: + params.placement = placement[0] + + if constraints: + params.constraints = client.Value.from_json(constraints) + + if disks: + params.disks = [ + client.Constraints.from_json(o) for o in disks] + + if series: + params.series = series + + # Submit the request. + client_facade = client.ClientFacade() + client_facade.connect(self.connection) + results = await client_facade.AddMachines([params]) + error = results.machines[0].error + if error: + raise ValueError("Error adding machine: %s", error.message) + machine_id = results.machines[0].machine + log.debug('Added new machine %s', machine_id) + return await self._wait_for_new('machine', machine_id) async def add_relation(self, relation1, relation2): """Add a relation between two applications. @@ -910,14 +949,11 @@ class Model(object): :param dict resources: : pairs :param str series: Series on which to deploy :param dict storage: Storage constraints TODO how do these look? - :param to: Placement directive. Use placement.parse to generate it. - For example: - - from juju import placement + :param to: Placement directive as a string. For example: - placement.parse('23') - place on machine 23 - placement.parse('lxc:7') - place in new lxc container on machine 7 - placement.parse('24/lxc/3') - place in container 3 on machine 24 + '23' - place on machine 23 + 'lxd:7' - place in new lxd container on machine 7 + '24/lxd/3' - place in container 3 on machine 24 If None, a new machine is provisioned. @@ -930,9 +966,7 @@ class Model(object): """ if to: - placement = [ - client.Placement(**p) for p in to - ] + placement = parse_placement(to) else: placement = [] @@ -1000,11 +1034,11 @@ class Model(object): constraints=parse_constraints(constraints), endpoint_bindings=bind, num_units=num_units, - placement=placement, resources=resources, series=series, storage=storage, ) + app.placement = placement result = await app_facade.Deploy([app]) errors = [r.error.message for r in result.results if r.error] diff --git a/juju/placement.py b/juju/placement.py index 561bc40..5ee9ba6 100644 --- a/juju/placement.py +++ b/juju/placement.py @@ -41,10 +41,10 @@ def parse(directive): return [client.Placement(scope=MACHINE_SCOPE, directive=directive)] if "/" in directive: - machine, container, container_num = directive.split("/") + # e.g. "0/lxd/0" + # https://github.com/juju/juju/blob/master/instance/placement_test.go#L29 return [ - client.Placement(scope=MACHINE_SCOPE, directive=machine), - client.Placement(scope=container, directive=container_num) + client.Placement(scope=MACHINE_SCOPE, directive=directive), ] # Planner has probably given us a container type. Leave it up to -- 2.25.1