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)