Implement Model.add_machine()
authorTim Van Steenburgh <tvansteenburgh@gmail.com>
Tue, 21 Feb 2017 18:57:30 +0000 (13:57 -0500)
committerTim Van Steenburgh <tvansteenburgh@gmail.com>
Tue, 21 Feb 2017 18:57:30 +0000 (13:57 -0500)
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
docs/narrative/model.rst
examples/add_machine.py [new file with mode: 0755]
juju/model.py
juju/placement.py

index 37cf094..bb3de5f 100644 (file)
@@ -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
 ---------------
index 79df011..74f7e84 100644 (file)
@@ -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 (executable)
index 0000000..34f7869
--- /dev/null
@@ -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())
index 653a20c..ef6bc85 100644 (file)
@@ -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 <juju.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: <resource name>:<file path> 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]
index 561bc40..5ee9ba6 100644 (file)
@@ -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