blob: 42633a1cb000b97d70656d875b5b43dd2145f14d [file] [log] [blame]
Adam Israeldcdf82b2017-08-15 15:26:43 -04001Models
2======
3A Juju controller provides websocket endpoints for each of its
4models. In order to do anything useful with a model, the juju lib must
5connect to one of these endpoints. There are several ways to do this.
6
Adam Israelb0943662018-08-02 15:32:00 -04007For api docs, see py:class:`juju.model.Model`.
Adam Israeldcdf82b2017-08-15 15:26:43 -04008
9
10Connecting to the Current Model
11-------------------------------
12Connect 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 Israeldcdf82b2017-08-15 15:26:43 -040017 model = Model()
Adam Israelb0943662018-08-02 15:32:00 -040018 await model.connect()
Adam Israeldcdf82b2017-08-15 15:26:43 -040019
20
21Connecting to a Named Model
22---------------------------
23Connect 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'.
25This only works if you have the Juju CLI client installed.
26
27.. code:: python
28
Adam Israelb0943662018-08-02 15:32:00 -040029 model = Model()
30 await model.connect('juju-2.0.1:admin/libjuju')
Adam Israeldcdf82b2017-08-15 15:26:43 -040031
Adam Israelb0943662018-08-02 15:32:00 -040032
33Connecting with Authentication
34------------------------------
35You can control what user you are connecting with by specifying either a
36username/password pair, or a macaroon or bakery client that can provide
37a macaroon.
38
39
40.. code:: python
Adam Israeldcdf82b2017-08-15 15:26:43 -040041
42 model = Model()
Adam Israelb0943662018-08-02 15:32:00 -040043 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 Israeldcdf82b2017-08-15 15:26:43 -040068
69
Adam Israelb0943662018-08-02 15:32:00 -040070Connecting with an Explicit Endpoint
71------------------------------------
Adam Israeldcdf82b2017-08-15 15:26:43 -040072The most flexible, but also most verbose, way to connect is using the API
Adam Israelb0943662018-08-02 15:32:00 -040073endpoint url, model UUID, and credentials directly. This method does NOT
74require the Juju CLI client to be installed.
Adam Israeldcdf82b2017-08-15 15:26:43 -040075
76.. code:: python
77
78 from juju.model import Model
79
80 model = Model()
Adam Israeldcdf82b2017-08-15 15:26:43 -040081 await model.connect(
Adam Israelb0943662018-08-02 15:32:00 -040082 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 Israeldcdf82b2017-08-15 15:26:43 -040089 )
90
91
92Creating and Destroying a Model
93-------------------------------
94Example of creating a new model and then destroying it. See
Adam Israelb0943662018-08-02 15:32:00 -040095py:method:`juju.controller.Controller.add_model` and
96py:method:`juju.controller.Controller.destroy_model` for more info.
Adam Israeldcdf82b2017-08-15 15:26:43 -040097
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
118Adding Machines and Containers
119------------------------------
120To add a machine or container, connect to a model and then call its
Adam Israelb0943662018-08-02 15:32:00 -0400121py:method:`~juju.model.Model.add_machine` method. A
122py:class:`~juju.machine.Machine` instance is returned. The machine id
Adam Israeldcdf82b2017-08-15 15:26:43 -0400123can 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
175Reacting to Changes in a Model
176------------------------------
177To watch for and respond to changes in a model, register an observer with the
178model. The easiest way to do this is by creating a
Adam Israelb0943662018-08-02 15:32:00 -0400179py:class:`juju.model.ModelObserver` subclass.
Adam Israeldcdf82b2017-08-15 15:26:43 -0400180
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
237Every change in the model will result in a call to the `on_change()`
238method of your observer(s).
239
240To target your code more precisely, define method names that correspond
241to 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 Israelb0943662018-08-02 15:32:00 -0400270Any py:class:`juju.model.ModelEntity` object can be observed directly by
Adam Israeldcdf82b2017-08-15 15:26:43 -0400271registering 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)