:undoc-members:
:show-inheritance:
+juju.loop module
+----------------
+
+.. automodule:: juju.loop
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
juju.machine module
-------------------
:undoc-members:
:show-inheritance:
+juju.utils module
+-----------------
+
+.. automodule:: juju.utils
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
Module contents
---------------
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
--- /dev/null
+#!/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 = 1
+GB = 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(force=True)
+ await machine2.destroy(force=True)
+ await machine1.destroy(force=True)
+ finally:
+ await model.disconnect()
+
+
+logging.basicConfig(level=logging.DEBUG)
+ws_logger = logging.getLogger('websockets.protocol')
+ws_logger.setLevel(logging.INFO)
+
+loop.run(main())
MEM = re.compile('^[1-9][0-9]*[MGTP]$')
# Multiplication factors to get Megabytes
+# https://github.com/juju/juju/blob/master/constraints/constraints.go#L666
FACTORS = {
"M": 1,
"G": 1024,
SNAKE1 = re.compile(r'(.)([A-Z][a-z]+)')
SNAKE2 = re.compile('([a-z0-9])([A-Z])')
+
def parse(constraints):
"""
Constraints must be expressed as a string containing only spaces
async def destroy(self, force=False):
"""Remove this machine from the model.
+ Blocks until the machine is actually removed.
+
"""
facade = client.ClientFacade()
facade.connect(self.connection)
log.debug(
'Destroying machine %s', self.id)
- return await facade.DestroyMachines(force, [self.id])
+ await facade.DestroyMachines(force, [self.id])
+ return await self.model._wait(
+ 'machine', self.id, 'remove')
remove = destroy
def run(self, command, timeout=None):
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__)
:param entity_type: The entity's type.
:param entity_id: The entity's id.
- :param action: the type of action (e.g., 'add' or 'change')
+ :param action: the type of action (e.g., 'add', 'change', or 'remove')
:param predicate: optional callable that must take as an
argument a delta, and must return a boolean, indicating
whether the delta contains the specific action we're looking
self.add_observer(callback, entity_type, action, entity_id, predicate)
entity_id = await q.get()
- return self.state._live_entity_map(entity_type)[entity_id]
+ # object might not be in the entity_map if we were waiting for a
+ # 'remove' action
+ return self.state._live_entity_map(entity_type).get(entity_id)
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.
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.
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.
: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.
"""
if to:
- placement = [
- client.Placement(**p) for p in to
- ]
+ placement = parse_placement(to)
else:
placement = []
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]
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
+import uuid
import subprocess
import pytest
+from juju.controller import Controller
+
def is_bootstrapped():
result = subprocess.run(['juju', 'switch'], stdout=subprocess.PIPE)
bootstrapped = pytest.mark.skipif(
not is_bootstrapped(),
reason='bootstrapped Juju environment required')
+
+
+class CleanModel():
+ def __init__(self):
+ self.controller = None
+ self.model = None
+
+ async def __aenter__(self):
+ self.controller = Controller()
+ await self.controller.connect_current()
+
+ model_name = 'model-{}'.format(uuid.uuid4())
+ self.model = await self.controller.add_model(model_name)
+
+ return self.model
+
+ async def __aexit__(self, exc_type, exc, tb):
+ await self.model.disconnect()
+ await self.controller.destroy_model(self.model.info.uuid)
+ await self.controller.disconnect()
-import asyncio
-import unittest
+import pytest
-from juju.client.connection import Connection
from juju.client import client
-from ..base import bootstrapped
+from .. import base
-@bootstrapped
-class UserManagerTest(unittest.TestCase):
- def test_user_info(self):
- loop = asyncio.get_event_loop()
- conn = loop.run_until_complete(
- Connection.connect_current())
- conn = loop.run_until_complete(
- conn.controller())
+@base.bootstrapped
+@pytest.mark.asyncio
+async def test_user_info(event_loop):
+ async with base.CleanModel() as model:
+ controller_conn = await model.connection.controller()
um = client.UserManagerFacade()
- um.connect(conn)
- result = loop.run_until_complete(
- um.UserInfo([client.Entity('user-admin')], True))
+ um.connect(controller_conn)
+ result = await um.UserInfo(
+ [client.Entity('user-admin')], True)
+ await controller_conn.close()
- self.assertIsInstance(result, client.UserInfoResults)
+ assert isinstance(result, client.UserInfoResults)
for r in result.results:
- self.assertIsInstance(r, client.UserInfoResult)
+ assert isinstance(r, client.UserInfoResult)
-import asyncio
-import unittest
+import pytest
from juju.client.connection import Connection
-from ..base import bootstrapped
+from .. import base
-@bootstrapped
-class FunctionalConnectionTest(unittest.TestCase):
- def test_connect_current(self):
- loop = asyncio.get_event_loop()
- conn = loop.run_until_complete(
- Connection.connect_current())
+@base.bootstrapped
+@pytest.mark.asyncio
+async def test_connect_current(event_loop):
+ async with base.CleanModel():
+ conn = await Connection.connect_current()
- self.assertIsInstance(conn, Connection)
+ assert isinstance(conn, Connection)
+ await conn.close()
--- /dev/null
+import pytest
+
+from .. import base
+
+MB = 1
+GB = 1024
+
+
+@base.bootstrapped
+@pytest.mark.asyncio
+async def test_add_machine(event_loop):
+ from juju.machine import Machine
+
+ async with base.CleanModel() as model:
+ # 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))
+
+ for m in (machine1, machine2, machine3):
+ assert isinstance(m, Machine)
+
+ assert len(model.machines) == 3
+
+ await machine3.destroy(force=True)
+ await machine2.destroy(force=True)
+ res = await machine1.destroy(force=True)
+
+ assert res is None
+ assert len(model.machines) == 0
+++ /dev/null
-import unittest
-import juju.loop
-
-
-class TestLoop(unittest.TestCase):
- def test_run(self):
- async def _test():
- return 'success'
- self.assertEqual(juju.loop.run(_test()), 'success')
-
- def test_run_interrupt(self):
- async def _test():
- juju.loop.run._sigint = True
- self.assertRaises(KeyboardInterrupt, juju.loop.run, _test())
-
- def test_run_exception(self):
- async def _test():
- raise ValueError()
- self.assertRaises(ValueError, juju.loop.run, _test())
--- /dev/null
+import asyncio
+import unittest
+import juju.loop
+
+
+class TestLoop(unittest.TestCase):
+ def setUp(self):
+ # new event loop for each test
+ policy = asyncio.get_event_loop_policy()
+ self.loop = policy.new_event_loop()
+ policy.set_event_loop(self.loop)
+
+ def tearDown(self):
+ self.loop.close()
+
+ def test_run(self):
+ assert asyncio.get_event_loop() == self.loop
+ async def _test():
+ return 'success'
+ self.assertEqual(juju.loop.run(_test()), 'success')
+
+ def test_run_interrupt(self):
+ async def _test():
+ juju.loop.run._sigint = True
+ self.assertRaises(KeyboardInterrupt, juju.loop.run, _test())
+
+ def test_run_exception(self):
+ async def _test():
+ raise ValueError()
+ self.assertRaises(ValueError, juju.loop.run, _test())
usedevelop=True
passenv =
HOME
-commands = py.test -rsx
+commands = py.test -ra -s -x -n auto
deps =
pytest
+ pytest-asyncio
+ pytest-xdist
mock