| Adam Israel | dcdf82b | 2017-08-15 15:26:43 -0400 | [diff] [blame^] | 1 | Models |
| 2 | ====== |
| 3 | A Juju controller provides websocket endpoints for each of its |
| 4 | models. In order to do anything useful with a model, the juju lib must |
| 5 | connect to one of these endpoints. There are several ways to do this. |
| 6 | |
| 7 | For api docs, see :class:`juju.model.Model`. |
| 8 | |
| 9 | |
| 10 | Connecting to the Current Model |
| 11 | ------------------------------- |
| 12 | Connect to the currently active Juju model (the one returned by |
| 13 | `juju switch`). This only works if you have the Juju CLI client installed. |
| 14 | |
| 15 | .. code:: python |
| 16 | |
| 17 | from juju.model import Model |
| 18 | |
| 19 | model = Model() |
| 20 | await model.connect_current() |
| 21 | |
| 22 | |
| 23 | Connecting to a Named Model |
| 24 | --------------------------- |
| 25 | Connect to a model by name, using the same format as that returned from the |
| 26 | `juju switch` command. The accepted format is '[controller:][user/]model'. |
| 27 | This only works if you have the Juju CLI client installed. |
| 28 | |
| 29 | .. code:: python |
| 30 | |
| 31 | # $ juju switch |
| 32 | # juju-2.0.1:admin/libjuju |
| 33 | |
| 34 | from juju.model import Model |
| 35 | |
| 36 | model = Model() |
| 37 | await model.connect_model('juju-2.0.1:admin/libjuju') |
| 38 | |
| 39 | |
| 40 | Connecting with Username/Password Authentication |
| 41 | ------------------------------------------------ |
| 42 | The most flexible, but also most verbose, way to connect is using the API |
| 43 | endpoint url and credentials directly. This method does NOT require the Juju |
| 44 | CLI client to be installed. |
| 45 | |
| 46 | .. code:: python |
| 47 | |
| 48 | from juju.model import Model |
| 49 | |
| 50 | model = Model() |
| 51 | |
| 52 | controller_endpoint = '10.0.4.171:17070' |
| 53 | model_uuid = 'e8399ac7-078c-4817-8e5e-32316d55b083' |
| 54 | username = 'admin' |
| 55 | password = 'f53f08cfc32a2e257fe5393271d89d62' |
| 56 | |
| 57 | # Left out for brevity, but if you have a cert string you should pass it in. |
| 58 | # You can copy the cert from the output of The `juju show-controller` |
| 59 | # command. |
| 60 | cacert = None |
| 61 | |
| 62 | await model.connect( |
| 63 | controller_endpoint, |
| 64 | model_uuid, |
| 65 | username, |
| 66 | password, |
| 67 | cacert, |
| 68 | ) |
| 69 | |
| 70 | |
| 71 | Connecting with Macaroon Authentication |
| 72 | --------------------------------------- |
| 73 | To connect to a shared model, or a model an a shared controller, you'll need |
| 74 | to use macaroon authentication. The simplest example is shown below, and uses |
| 75 | already-discharged macaroons from the local filesystem. This will work if you |
| 76 | have the Juju CLI installed. |
| 77 | |
| 78 | .. note:: |
| 79 | |
| 80 | The library does not yet contain support for fetching and discharging |
| 81 | macaroons. Until it does, if you want to use macaroon auth, you'll need |
| 82 | to supply already-discharged macaroons yourself. |
| 83 | |
| 84 | .. code:: python |
| 85 | |
| 86 | from juju.client.connection import get_macaroons() |
| 87 | from juju.model import Model |
| 88 | |
| 89 | model = Model() |
| 90 | |
| 91 | controller_endpoint = '10.0.4.171:17070' |
| 92 | model_uuid = 'e8399ac7-078c-4817-8e5e-32316d55b083' |
| 93 | username = None |
| 94 | password = None |
| 95 | cacert = None |
| 96 | macaroons = get_macaroons() |
| 97 | |
| 98 | await model.connect( |
| 99 | controller_endpoint, |
| 100 | model_uuid, |
| 101 | username, |
| 102 | password, |
| 103 | cacert, |
| 104 | macaroons, |
| 105 | ) |
| 106 | |
| 107 | |
| 108 | Creating and Destroying a Model |
| 109 | ------------------------------- |
| 110 | Example of creating a new model and then destroying it. See |
| 111 | :meth:`juju.controller.Controller.add_model` and |
| 112 | :meth:`juju.controller.Controller.destroy_model` for more info. |
| 113 | |
| 114 | .. code:: python |
| 115 | |
| 116 | from juju.controller import Controller |
| 117 | |
| 118 | controller = Controller() |
| 119 | await controller.connect_current() |
| 120 | |
| 121 | # Create our new model |
| 122 | model = await controller.add_model( |
| 123 | 'mymodel', # name of your new model |
| 124 | ) |
| 125 | |
| 126 | # Do stuff with our model... |
| 127 | |
| 128 | # Destroy the model |
| 129 | await model.disconnect() |
| 130 | await controller.destroy_model(model.info.uuid) |
| 131 | model = None |
| 132 | |
| 133 | |
| 134 | Adding Machines and Containers |
| 135 | ------------------------------ |
| 136 | To add a machine or container, connect to a model and then call its |
| 137 | :meth:`~juju.model.Model.add_machine` method. A |
| 138 | :class:`~juju.machine.Machine` instance is returned. The machine id |
| 139 | can be used to deploy a charm to a specific machine or container. |
| 140 | |
| 141 | .. code:: python |
| 142 | |
| 143 | from juju.model import Model |
| 144 | |
| 145 | MB = 1 |
| 146 | GB = 1024 |
| 147 | |
| 148 | |
| 149 | model = Model() |
| 150 | await model.connect_current() |
| 151 | |
| 152 | # add a new default machine |
| 153 | machine1 = await model.add_machine() |
| 154 | |
| 155 | # add a machine with constraints, disks, and series specified |
| 156 | machine2 = await model.add_machine( |
| 157 | constraints={ |
| 158 | 'mem': 256 * MB, |
| 159 | }, |
| 160 | disks=[{ |
| 161 | 'pool': 'rootfs', |
| 162 | 'size': 10 * GB, |
| 163 | 'count': 1, |
| 164 | }], |
| 165 | series='xenial', |
| 166 | ) |
| 167 | |
| 168 | # add a lxd container to machine2 |
| 169 | machine3 = await model.add_machine( |
| 170 | 'lxd:{}'.format(machine2.id)) |
| 171 | |
| 172 | # deploy charm to the lxd container |
| 173 | application = await model.deploy( |
| 174 | 'ubuntu-10', |
| 175 | application_name='ubuntu', |
| 176 | series='xenial', |
| 177 | channel='stable', |
| 178 | to=machine3.id |
| 179 | ) |
| 180 | |
| 181 | # remove application |
| 182 | await application.remove() |
| 183 | |
| 184 | # destroy machines - note that machine3 must be destroyed before machine2 |
| 185 | # since it's a container on machine2 |
| 186 | await machine3.destroy(force=True) |
| 187 | await machine2.destroy(force=True) |
| 188 | await machine1.destroy(force=True) |
| 189 | |
| 190 | |
| 191 | Reacting to Changes in a Model |
| 192 | ------------------------------ |
| 193 | To watch for and respond to changes in a model, register an observer with the |
| 194 | model. The easiest way to do this is by creating a |
| 195 | :class:`juju.model.ModelObserver` subclass. |
| 196 | |
| 197 | .. code:: python |
| 198 | |
| 199 | from juju.model import Model, ModelObserver |
| 200 | |
| 201 | class MyModelObserver(ModelObserver): |
| 202 | async def on_change(self, delta, old, new, model): |
| 203 | # The raw change data (dict) from the websocket. |
| 204 | print(delta.data) |
| 205 | |
| 206 | # The entity type (str) affected by this change. |
| 207 | # One of ('action', 'application', 'annotation', 'machine', |
| 208 | # 'unit', 'relation') |
| 209 | print(delta.entity) |
| 210 | |
| 211 | # The type (str) of change. |
| 212 | # One of ('add', 'change', 'remove') |
| 213 | print(delta.type) |
| 214 | |
| 215 | # The 'old' and 'new' parameters are juju.model.ModelEntity |
| 216 | # instances which represent an entity in the model both before |
| 217 | # this change was applied (old) and after (new). |
| 218 | |
| 219 | # If an entity is being added to the model, the 'old' param |
| 220 | # will be None. |
| 221 | if delta.type == 'add': |
| 222 | assert(old is None) |
| 223 | |
| 224 | # If an entity is being removed from the model, the 'new' param |
| 225 | # will be None. |
| 226 | if delta.type == 'remove': |
| 227 | assert(new is None) |
| 228 | |
| 229 | # The 'old' and 'new' parameters, when not None, will be instances |
| 230 | # of a juju.model.ModelEntity subclass. The type of the subclass |
| 231 | # depends on the value of 'delta.entity', for example: |
| 232 | # |
| 233 | # delta.entity type |
| 234 | # ------------ ---- |
| 235 | # 'action' -> juju.action.Action |
| 236 | # 'application' -> juju.application.Application |
| 237 | # 'annotation' -> juju.annotation.Annotation |
| 238 | # 'machine' -> juju.machine.Machine |
| 239 | # 'unit' -> juju.unit.Unit |
| 240 | # 'relation' -> juju.relation.Relation |
| 241 | |
| 242 | # Finally, the 'model' parameter is a reference to the |
| 243 | # juju.model.Model instance to which this observer is attached. |
| 244 | print(id(model)) |
| 245 | |
| 246 | |
| 247 | model = Model() |
| 248 | await model.connect_current() |
| 249 | |
| 250 | model.add_observer(MyModelObserver()) |
| 251 | |
| 252 | |
| 253 | Every change in the model will result in a call to the `on_change()` |
| 254 | method of your observer(s). |
| 255 | |
| 256 | To target your code more precisely, define method names that correspond |
| 257 | to the entity and type of change that you wish to handle. |
| 258 | |
| 259 | .. code:: python |
| 260 | |
| 261 | from juju.model import Model, ModelObserver |
| 262 | |
| 263 | class MyModelObserver(ModelObserver): |
| 264 | async def on_application_change(self, delta, old, new, model): |
| 265 | # Both 'old' and 'new' params will be instances of |
| 266 | # juju.application.Application |
| 267 | pass |
| 268 | |
| 269 | async def on_unit_remove(self, delta, old, new, model): |
| 270 | # Since a unit is being removed, the 'new' param will always |
| 271 | # be None in this handler. The 'old' param will be an instance |
| 272 | # of juju.unit.Unit - the state of the unit before it was removed. |
| 273 | pass |
| 274 | |
| 275 | async def on_machine_add(self, delta, old, new, model): |
| 276 | # Since a machine is being added, the 'old' param will always be |
| 277 | # None in this handler. The 'new' param will be an instance of |
| 278 | # juju.machine.Machine. |
| 279 | pass |
| 280 | |
| 281 | async def on_change(self, delta, old, new, model): |
| 282 | # The catch-all handler - will be called whenever a more |
| 283 | # specific handler method is not defined. |
| 284 | |
| 285 | |
| 286 | Any :class:`juju.model.ModelEntity` object can be observed directly by |
| 287 | registering callbacks on the object itself. |
| 288 | |
| 289 | .. code:: python |
| 290 | |
| 291 | import logging |
| 292 | |
| 293 | async def on_app_change(delta, old, new, model): |
| 294 | logging.debug('App changed: %r', new) |
| 295 | |
| 296 | async def on_app_remove(delta, old, new, model): |
| 297 | logging.debug('App removed: %r', old) |
| 298 | |
| 299 | ubuntu_app = await model.deploy( |
| 300 | 'ubuntu', |
| 301 | application_name='ubuntu', |
| 302 | series='trusty', |
| 303 | channel='stable', |
| 304 | ) |
| 305 | ubuntu_app.on_change(on_app_change) |
| 306 | ubuntu_app.on_remove(on_app_remove) |