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


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.
