Squashed 'modules/libjuju/' content from commit c50c361
git-subtree-dir: modules/libjuju
git-subtree-split: c50c361a8b9a3bbf1a33f5659e492b481f065cd2
diff --git a/docs/narrative/application.rst b/docs/narrative/application.rst
new file mode 100644
index 0000000..1565e5f
--- /dev/null
+++ b/docs/narrative/application.rst
@@ -0,0 +1,136 @@
+Applications
+============
+For api docs, see :class:`juju.application.Application`.
+
+
+Deploying
+---------
+To deploy a new application, connect a model and then call its
+:meth:`~juju.model.Model.deploy` method. An
+:class:`~juju.application.Application` instance is returned.
+
+.. code:: python
+
+ from juju.model import Model
+
+ model = Model()
+ await model.connect_current()
+
+ mysql_app = await model.deploy(
+ # If a revision number is not included in the charm url,
+ # the latest revision from the Charm Store will be used.
+ 'cs:mysql-55',
+ application_name='mysql',
+ series='trusty',
+ channel='stable',
+ config={
+ 'tuning-level': 'safest',
+ },
+ constraints={
+ 'mem': 256 * MB,
+ },
+ )
+
+
+Deploying a Local Charm
+-----------------------
+To deploy a local charm, pass the charm directory path to
+`Model.deploy()`.
+
+.. code:: python
+
+ from juju.model import Model
+
+ model = Model()
+ await model.connect_current()
+
+ # Deploy a local charm using a path to the charm directory
+ await model.deploy(
+ '/home/tvansteenburgh/src/charms/ubuntu',
+ application_name='ubuntu',
+ series='trusty',
+ )
+
+
+Adding Units
+------------
+To add units to a deployed application, use the
+:meth:`juju.application.Application.add_units` method. A list of the newly
+added units (:class:`~juju.unit.Unit` objects) is returned.
+
+.. code:: python
+
+ ubuntu_app = await model.deploy(
+ 'ubuntu',
+ application_name='ubuntu',
+ series='trusty',
+ channel='stable',
+ )
+
+ unit_a, unit_b = await ubuntu_app.add_units(count=2)
+
+
+Updating Config and Constraints
+-------------------------------
+Example showing how to update configuration and constraints on a deployed
+application. The `mysql_app` object is an instance of
+:class:`juju.application.Application`.
+
+.. code:: python
+
+ MB = 1024 * 1024
+
+ # Update and check app config
+ await mysql_app.set_config({'tuning-level': 'fast'})
+ config = await mysql_app.get_config()
+
+ assert(config['tuning-level']['value'] == 'fast')
+
+ # update and check app constraints
+ await mysql_app.set_constraints({'mem': 512 * MB})
+ constraints = await mysql_app.get_constraints()
+
+ assert(constraints['mem'] == 512 * MB)
+
+
+Adding and Removing Relations
+-----------------------------
+The :meth:`juju.application.Application.add_relation` method returns a
+:class:`juju.relation.Relation` instance.
+
+.. code:: python
+
+ from juju.model import Model
+
+ model = Model()
+ await model.connect_current()
+
+ # Deploy mysql-master application
+ mysql_master = await model.deploy(
+ 'cs:mysql-55',
+ application_name='mysql-master',
+ series='trusty',
+ channel='stable',
+ )
+
+ # Deploy mysql-slave application
+ mysql_slave = await model.deploy(
+ 'cs:mysql-55',
+ application_name='mysql-slave',
+ series='trusty',
+ channel='stable',
+ )
+
+ # Add the master-slave relation
+ relation = await mysql_master.add_relation(
+ # Name of the relation on the local (mysql-master) side
+ 'master',
+ # Name of the app:relation on the remote side
+ 'mysql-slave:slave',
+ )
+
+ # Remove the relation
+ await mysql_master.remove_relation(
+ 'master',
+ 'mysql-slave:slave',
+ )
diff --git a/docs/narrative/controller.rst b/docs/narrative/controller.rst
new file mode 100644
index 0000000..2da0e7b
--- /dev/null
+++ b/docs/narrative/controller.rst
@@ -0,0 +1,100 @@
+Controllers
+===========
+A Juju controller provides websocket endpoints for itself and each of its
+models. In order to do anything useful, the juju lib must connect to one of
+these endpoints.
+
+Connecting to the controller endpoint is useful if you want to programmatically
+create a new model. If the model you want to use already exists, you can
+connect directly to it (see :doc:`model`).
+
+For api docs, see :class:`juju.controller.Controller`.
+
+
+Connecting to the Current Controller
+------------------------------------
+Connect to the currently active Juju controller (the one returned by
+`juju switch`). This only works if you have the Juju CLI client installed.
+
+.. code:: python
+
+ from juju.controller import Controller
+
+ controller = Controller()
+ await controller.connect_current()
+
+
+Connecting to a Named Controller
+--------------------------------
+Connect to a controller by name.
+
+.. code:: python
+
+ from juju.controller import Controller
+
+ controller = Controller()
+ await controller.connect_controller('mycontroller')
+
+
+Connecting with Username/Password Authentication
+------------------------------------------------
+The most flexible, but also most verbose, way to connect is using the API
+endpoint url and credentials directly. This method does NOT require the Juju
+CLI client to be installed.
+
+.. code:: python
+
+ from juju.controller import Controller
+
+ controller = Controller()
+
+ controller_endpoint = '10.0.4.171:17070'
+ username = 'admin'
+ password = 'f53f08cfc32a2e257fe5393271d89d62'
+
+ # Left out for brevity, but if you have a cert string you should pass it in.
+ # You can copy the cert from the output of The `juju show-controller`
+ # command.
+ cacert = None
+
+ await controller.connect(
+ controller_endpoint,
+ username,
+ password,
+ cacert,
+ )
+
+
+Connecting with Macaroon Authentication
+---------------------------------------
+To connect to a shared controller, you'll need
+to use macaroon authentication. The simplest example is shown below, and uses
+already-discharged macaroons from the local filesystem. This will work if you
+have the Juju CLI installed.
+
+.. note::
+
+ The library does not yet contain support for fetching and discharging
+ macaroons. Until it does, if you want to use macaroon auth, you'll need
+ to supply already-discharged macaroons yourself.
+
+.. code:: python
+
+ from juju.client.connection import get_macaroons()
+ from juju.controller import Controller
+
+ controller = Controller()
+
+ controller_endpoint = '10.0.4.171:17070'
+ username = None
+ password = None
+ cacert = None
+ macaroons = get_macaroons()
+
+ await controller.connect(
+ controller_endpoint,
+ username,
+ password,
+ cacert,
+ macaroons,
+ )
diff --git a/docs/narrative/index.rst b/docs/narrative/index.rst
new file mode 100644
index 0000000..eb77e4c
--- /dev/null
+++ b/docs/narrative/index.rst
@@ -0,0 +1,11 @@
+Narrative Docs
+==============
+
+.. toctree::
+ :glob:
+ :maxdepth: 2
+
+ controller
+ model
+ application
+ unit
diff --git a/docs/narrative/model.rst b/docs/narrative/model.rst
new file mode 100644
index 0000000..57dbc81
--- /dev/null
+++ b/docs/narrative/model.rst
@@ -0,0 +1,306 @@
+Models
+======
+A Juju controller provides websocket endpoints for each of its
+models. In order to do anything useful with a model, the juju lib must
+connect to one of these endpoints. There are several ways to do this.
+
+For api docs, see :class:`juju.model.Model`.
+
+
+Connecting to the Current Model
+-------------------------------
+Connect to the currently active Juju model (the one returned by
+`juju switch`). This only works if you have the Juju CLI client installed.
+
+.. code:: python
+
+ from juju.model import Model
+
+ model = Model()
+ await model.connect_current()
+
+
+Connecting to a Named Model
+---------------------------
+Connect to a model by name, using the same format as that returned from the
+`juju switch` command. The accepted format is '[controller:][user/]model'.
+This only works if you have the Juju CLI client installed.
+
+.. code:: python
+
+ # $ juju switch
+ # juju-2.0.1:admin/libjuju
+
+ from juju.model import Model
+
+ model = Model()
+ await model.connect_model('juju-2.0.1:admin/libjuju')
+
+
+Connecting with Username/Password Authentication
+------------------------------------------------
+The most flexible, but also most verbose, way to connect is using the API
+endpoint url and credentials directly. This method does NOT require the Juju
+CLI client to be installed.
+
+.. code:: python
+
+ from juju.model import Model
+
+ model = Model()
+
+ controller_endpoint = '10.0.4.171:17070'
+ model_uuid = 'e8399ac7-078c-4817-8e5e-32316d55b083'
+ username = 'admin'
+ password = 'f53f08cfc32a2e257fe5393271d89d62'
+
+ # Left out for brevity, but if you have a cert string you should pass it in.
+ # You can copy the cert from the output of The `juju show-controller`
+ # command.
+ cacert = None
+
+ await model.connect(
+ controller_endpoint,
+ model_uuid,
+ username,
+ password,
+ cacert,
+ )
+
+
+Connecting with Macaroon Authentication
+---------------------------------------
+To connect to a shared model, or a model an a shared controller, you'll need
+to use macaroon authentication. The simplest example is shown below, and uses
+already-discharged macaroons from the local filesystem. This will work if you
+have the Juju CLI installed.
+
+.. note::
+
+ The library does not yet contain support for fetching and discharging
+ macaroons. Until it does, if you want to use macaroon auth, you'll need
+ to supply already-discharged macaroons yourself.
+
+.. code:: python
+
+ from juju.client.connection import get_macaroons()
+ from juju.model import Model
+
+ model = Model()
+
+ controller_endpoint = '10.0.4.171:17070'
+ model_uuid = 'e8399ac7-078c-4817-8e5e-32316d55b083'
+ username = None
+ password = None
+ cacert = None
+ macaroons = get_macaroons()
+
+ await model.connect(
+ controller_endpoint,
+ model_uuid,
+ username,
+ password,
+ cacert,
+ macaroons,
+ )
+
+
+Creating and Destroying a Model
+-------------------------------
+Example of creating a new model and then destroying it. See
+:meth:`juju.controller.Controller.add_model` and
+:meth:`juju.controller.Controller.destroy_model` for more info.
+
+.. code:: python
+
+ from juju.controller import Controller
+
+ controller = Controller()
+ await controller.connect_current()
+
+ # Create our new model
+ model = await controller.add_model(
+ 'mymodel', # name of your new model
+ )
+
+ # Do stuff with our model...
+
+ # Destroy the model
+ await model.disconnect()
+ await controller.destroy_model(model.info.uuid)
+ 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
+
+ MB = 1
+ GB = 1024
+
+
+ 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
+ )
+
+ # remove application
+ await application.remove()
+
+ # destroy machines - note that machine3 must be destroyed before machine2
+ # since it's a container on machine2
+ await machine3.destroy(force=True)
+ await machine2.destroy(force=True)
+ await machine1.destroy(force=True)
+
+
+Reacting to Changes in a Model
+------------------------------
+To watch for and respond to changes in a model, register an observer with the
+model. The easiest way to do this is by creating a
+:class:`juju.model.ModelObserver` subclass.
+
+.. code:: python
+
+ from juju.model import Model, ModelObserver
+
+ class MyModelObserver(ModelObserver):
+ async def on_change(self, delta, old, new, model):
+ # The raw change data (dict) from the websocket.
+ print(delta.data)
+
+ # The entity type (str) affected by this change.
+ # One of ('action', 'application', 'annotation', 'machine',
+ # 'unit', 'relation')
+ print(delta.entity)
+
+ # The type (str) of change.
+ # One of ('add', 'change', 'remove')
+ print(delta.type)
+
+ # The 'old' and 'new' parameters are juju.model.ModelEntity
+ # instances which represent an entity in the model both before
+ # this change was applied (old) and after (new).
+
+ # If an entity is being added to the model, the 'old' param
+ # will be None.
+ if delta.type == 'add':
+ assert(old is None)
+
+ # If an entity is being removed from the model, the 'new' param
+ # will be None.
+ if delta.type == 'remove':
+ assert(new is None)
+
+ # The 'old' and 'new' parameters, when not None, will be instances
+ # of a juju.model.ModelEntity subclass. The type of the subclass
+ # depends on the value of 'delta.entity', for example:
+ #
+ # delta.entity type
+ # ------------ ----
+ # 'action' -> juju.action.Action
+ # 'application' -> juju.application.Application
+ # 'annotation' -> juju.annotation.Annotation
+ # 'machine' -> juju.machine.Machine
+ # 'unit' -> juju.unit.Unit
+ # 'relation' -> juju.relation.Relation
+
+ # Finally, the 'model' parameter is a reference to the
+ # juju.model.Model instance to which this observer is attached.
+ print(id(model))
+
+
+ model = Model()
+ await model.connect_current()
+
+ model.add_observer(MyModelObserver())
+
+
+Every change in the model will result in a call to the `on_change()`
+method of your observer(s).
+
+To target your code more precisely, define method names that correspond
+to the entity and type of change that you wish to handle.
+
+.. code:: python
+
+ from juju.model import Model, ModelObserver
+
+ class MyModelObserver(ModelObserver):
+ async def on_application_change(self, delta, old, new, model):
+ # Both 'old' and 'new' params will be instances of
+ # juju.application.Application
+ pass
+
+ async def on_unit_remove(self, delta, old, new, model):
+ # Since a unit is being removed, the 'new' param will always
+ # be None in this handler. The 'old' param will be an instance
+ # of juju.unit.Unit - the state of the unit before it was removed.
+ pass
+
+ async def on_machine_add(self, delta, old, new, model):
+ # Since a machine is being added, the 'old' param will always be
+ # None in this handler. The 'new' param will be an instance of
+ # juju.machine.Machine.
+ pass
+
+ async def on_change(self, delta, old, new, model):
+ # The catch-all handler - will be called whenever a more
+ # specific handler method is not defined.
+
+
+Any :class:`juju.model.ModelEntity` object can be observed directly by
+registering callbacks on the object itself.
+
+.. code:: python
+
+ import logging
+
+ async def on_app_change(delta, old, new, model):
+ logging.debug('App changed: %r', new)
+
+ async def on_app_remove(delta, old, new, model):
+ logging.debug('App removed: %r', old)
+
+ ubuntu_app = await model.deploy(
+ 'ubuntu',
+ application_name='ubuntu',
+ series='trusty',
+ channel='stable',
+ )
+ ubuntu_app.on_change(on_app_change)
+ ubuntu_app.on_remove(on_app_remove)
diff --git a/docs/narrative/unit.rst b/docs/narrative/unit.rst
new file mode 100644
index 0000000..5d6b48d
--- /dev/null
+++ b/docs/narrative/unit.rst
@@ -0,0 +1,65 @@
+Units
+=====
+For api docs, see :class:`juju.unit.Unit`.
+
+
+Running Commands
+----------------
+Run arbitrary commands on a unit with the
+:meth:`juju.unit.Unit.run` method. This method blocks
+the current coroutine until a result is available, and
+returns a :class:`juju.action.Action` instance.
+
+
+.. code:: python
+
+ from juju.model import Model
+
+ model = Model()
+ await model.connect_current()
+
+ app = await model.deploy(
+ 'ubuntu',
+ application_name='ubuntu',
+ series='trusty',
+ channel='stable',
+ )
+
+ for unit in app.units:
+ action = await unit.run('unit-get public-address')
+ print(action.results)
+
+ action = await unit.run('uname -a')
+ print(action.results)
+
+
+Running Actions
+---------------
+Run actions on a unit with the
+:meth:`juju.unit.Unit.run_action` method. This method
+returns a :class:`juju.action.Action` instance immediately. To block until
+the action completes, use the :meth:`juju.action.Action.wait` method, as
+in the example below.
+
+
+.. code:: python
+
+ from juju.model import Model
+
+ model = Model()
+ await model.connect_current()
+
+ app = await model.deploy(
+ 'git',
+ application_name='git',
+ series='trusty',
+ channel='stable',
+ )
+
+ for unit in app.units:
+ # run the 'add-repo' action, passing a 'repo' param
+ action = await unit.run_action('add-repo', repo='myrepo')
+ # wait for the action to complete
+ action = await action.wait()
+
+ print(action.results)