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