Deploy/destroy example
[osm/N2VC.git] / juju / model.py
1 import logging
2
3 from .client import client
4 from .client import watcher
5 from .delta import get_entity_delta
6
7 log = logging.getLogger(__name__)
8
9
10 class ModelObserver(object):
11 def __call__(self, delta, old, new, model):
12 if old is None and new is not None:
13 type_ = 'add'
14 else:
15 type_ = delta.type
16 handler_name = 'on_{}_{}'.format(delta.entity, type_)
17 method = getattr(self, handler_name, self.on_change)
18 log.debug(
19 'Model changed: %s %s %s',
20 delta.entity, delta.type, delta.get_id())
21 method(delta, old, new, model)
22
23 def on_change(self, delta, old, new, model):
24 pass
25
26
27 class ModelEntity(object):
28 """An object in the Model tree"""
29
30 def __init__(self, data, model):
31 """Initialize a new entity
32
33 :param data: dict of data from a watcher delta
34 :param model: The model instance in whose object tree this
35 entity resides
36
37 """
38 self.data = data
39 self.model = model
40 self.connection = model.connection
41
42 def __getattr__(self, name):
43 return self.data[name]
44
45
46 class Model(object):
47 def __init__(self, connection):
48 """Instantiate a new connected Model.
49
50 :param connection: `juju.client.connection.Connection` instance
51
52 """
53 self.connection = connection
54 self.observers = set()
55 self.state = dict()
56 self._watching = False
57
58 @property
59 def applications(self):
60 return self.state.get('application', {})
61
62 @property
63 def units(self):
64 return self.state.get('unit', {})
65
66 def stop_watching(self):
67 self.connection.cancel()
68 self._watching = False
69
70 def add_observer(self, callable_):
71 """Register an "on-model-change" callback
72
73 Once a watch is started (Model.watch() is called), ``callable_``
74 will be called each time the model changes. callable_ should
75 accept the following positional arguments:
76
77 delta - An instance of :class:`juju.delta.EntityDelta`
78 containing the raw delta data recv'd from the Juju
79 websocket.
80
81 old_obj - If the delta modifies an existing object in the model,
82 old_obj will be a copy of that object, as it was before the
83 delta was applied. Will be None if the delta creates a new
84 entity in the model.
85
86 new_obj - A copy of the new or updated object, after the delta
87 is applied. Will be None if the delta removes an entity
88 from the model.
89
90 model - The :class:`Model` itself.
91
92 """
93 self.observers.add(callable_)
94
95 async def watch(self):
96 """Start an asynchronous watch against this model.
97
98 See :meth:`add_observer` to register an onchange callback.
99
100 """
101 self._watching = True
102 allwatcher = watcher.AllWatcher()
103 allwatcher.connect(await self.connection.clone())
104 while self._watching:
105 results = await allwatcher.Next()
106 for delta in results.deltas:
107 delta = get_entity_delta(delta)
108 old_obj, new_obj = self._apply_delta(delta)
109 self._notify_observers(delta, old_obj, new_obj)
110
111 def _apply_delta(self, delta):
112 """Apply delta to our model state and return the a copy of the
113 affected object as it was before and after the update, e.g.:
114
115 old_obj, new_obj = self._apply_delta(delta)
116
117 old_obj may be None if the delta is for the creation of a new object,
118 e.g. a new application or unit is deployed.
119
120 new_obj may be None if no object was created or updated, or if an
121 object was deleted as a result of the delta being applied.
122
123 """
124 old_obj, new_obj = None, None
125
126 if (delta.entity in self.state and
127 delta.get_id() in self.state[delta.entity]):
128 old_obj = self.state[delta.entity][delta.get_id()]
129 if delta.type == 'remove':
130 del self.state[delta.entity][delta.get_id()]
131 return old_obj, new_obj
132
133 new_obj = self.state.setdefault(delta.entity, {})[delta.get_id()] = (
134 self._create_model_entity(delta))
135
136 return old_obj, new_obj
137
138 def _create_model_entity(self, delta):
139 """Return an object instance representing the entity created or
140 updated by ``delta``
141
142 """
143 entity_class = delta.get_entity_class()
144 return entity_class(delta.data, self)
145
146 def _notify_observers(self, delta, old_obj, new_obj):
147 """Call observing callbacks, notifying them of a change in model state
148
149 :param delta: The raw change from the watcher
150 (:class:`juju.client.overrides.Delta`)
151 :param old_obj: The object in the model that this delta updates.
152 May be None.
153 :param new_obj: The object in the model that is created or updated
154 by applying this delta.
155
156 """
157 for o in self.observers:
158 o(delta, old_obj, new_obj, self)
159
160 def add_machine(
161 self, spec=None, constraints=None, disks=None, series=None,
162 count=1):
163 """Start a new, empty machine and optionally a container, or add a
164 container to a machine.
165
166 :param str spec: Machine specification
167 Examples::
168
169 (None) - starts a new machine
170 'lxc' - starts a new machine with on lxc container
171 'lxc:4' - starts a new lxc container on machine 4
172 'ssh:user@10.10.0.3' - manually provisions a machine with ssh
173 'zone=us-east-1a' - starts a machine in zone us-east-1s on AWS
174 'maas2.name' - acquire machine maas2.name on MAAS
175 :param constraints: Machine constraints
176 :type constraints: :class:`juju.Constraints`
177 :param list disks: List of disk :class:`constraints <juju.Constraints>`
178 :param str series: Series
179 :param int count: Number of machines to deploy
180
181 Supported container types are: lxc, lxd, kvm
182
183 When deploying a container to an existing machine, constraints cannot
184 be used.
185
186 """
187 pass
188 add_machines = add_machine
189
190 def add_relation(self, relation1, relation2):
191 """Add a relation between two services.
192
193 :param str relation1: '<service>[:<relation_name>]'
194 :param str relation2: '<service>[:<relation_name>]'
195
196 """
197 pass
198
199 def add_space(self, name, *cidrs):
200 """Add a new network space.
201
202 Adds a new space with the given name and associates the given
203 (optional) list of existing subnet CIDRs with it.
204
205 :param str name: Name of the space
206 :param \*cidrs: Optional list of existing subnet CIDRs
207
208 """
209 pass
210
211 def add_ssh_key(self, key):
212 """Add a public SSH key to this model.
213
214 :param str key: The public ssh key
215
216 """
217 pass
218 add_ssh_keys = add_ssh_key
219
220 def add_subnet(self, cidr_or_id, space, *zones):
221 """Add an existing subnet to this model.
222
223 :param str cidr_or_id: CIDR or provider ID of the existing subnet
224 :param str space: Network space with which to associate
225 :param str \*zones: Zone(s) in which the subnet resides
226
227 """
228 pass
229
230 def get_backups(self):
231 """Retrieve metadata for backups in this model.
232
233 """
234 pass
235
236 def block(self, *commands):
237 """Add a new block to this model.
238
239 :param str \*commands: The commands to block. Valid values are
240 'all-changes', 'destroy-model', 'remove-object'
241
242 """
243 pass
244
245 def get_blocks(self):
246 """List blocks for this model.
247
248 """
249 pass
250
251 def get_cached_images(self, arch=None, kind=None, series=None):
252 """Return a list of cached OS images.
253
254 :param str arch: Filter by image architecture
255 :param str kind: Filter by image kind, e.g. 'lxd'
256 :param str series: Filter by image series, e.g. 'xenial'
257
258 """
259 pass
260
261 def create_backup(self, note=None, no_download=False):
262 """Create a backup of this model.
263
264 :param str note: A note to store with the backup
265 :param bool no_download: Do not download the backup archive
266 :return str: Path to downloaded archive
267
268 """
269 pass
270
271 def create_storage_pool(self, name, provider_type, **pool_config):
272 """Create or define a storage pool.
273
274 :param str name: Name to give the storage pool
275 :param str provider_type: Pool provider type
276 :param \*\*pool_config: key/value pool configuration pairs
277
278 """
279 pass
280
281 def debug_log(
282 self, no_tail=False, exclude_module=None, include_module=None,
283 include=None, level=None, limit=0, lines=10, replay=False,
284 exclude=None):
285 """Get log messages for this model.
286
287 :param bool no_tail: Stop after returning existing log messages
288 :param list exclude_module: Do not show log messages for these logging
289 modules
290 :param list include_module: Only show log messages for these logging
291 modules
292 :param list include: Only show log messages for these entities
293 :param str level: Log level to show, valid options are 'TRACE',
294 'DEBUG', 'INFO', 'WARNING', 'ERROR,
295 :param int limit: Return this many of the most recent (possibly
296 filtered) lines are shown
297 :param int lines: Yield this many of the most recent lines, and keep
298 yielding
299 :param bool replay: Yield the entire log, and keep yielding
300 :param list exclude: Do not show log messages for these entities
301
302 """
303 pass
304
305 async def deploy(
306 self, entity_url, service_name=None, bind=None, budget=None,
307 channel=None, config=None, constraints=None, force=False,
308 num_units=1, plan=None, resources=None, series=None, storage=None,
309 to=None):
310 """Deploy a new service or bundle.
311
312 :param str entity_url: Charm or bundle url
313 :param str service_name: Name to give the service
314 :param dict bind: <charm endpoint>:<network space> pairs
315 :param dict budget: <budget name>:<limit> pairs
316 :param str channel: Charm store channel from which to retrieve
317 the charm or bundle, e.g. 'development'
318 :param dict config: Charm configuration dictionary
319 :param constraints: Service constraints
320 :type constraints: :class:`juju.Constraints`
321 :param bool force: Allow charm to be deployed to a machine running
322 an unsupported series
323 :param int num_units: Number of units to deploy
324 :param str plan: Plan under which to deploy charm
325 :param dict resources: <resource name>:<file path> pairs
326 :param str series: Series on which to deploy
327 :param dict storage: Storage constraints TODO how do these look?
328 :param str to: Placement directive, e.g.::
329
330 '23' - machine 23
331 'lxc:7' - new lxc container on machine 7
332 '24/lxc/3' - lxc container 3 or machine 24
333
334 If None, a new machine is provisioned.
335
336
337 TODO::
338
339 - entity_url must have a revision; look up latest automatically if
340 not provided by caller
341 - service_name is required; fill this in automatically if not
342 provided by caller
343 - series is required; how do we pick a default?
344
345 """
346 if constraints:
347 constraints = client.Value(**constraints)
348
349 if to:
350 placement = [
351 client.Placement(**p) for p in to
352 ]
353 else:
354 placement = []
355
356 if storage:
357 storage = {
358 k: client.Constraints(**v)
359 for k, v in storage.items()
360 }
361
362 app_facade = client.ApplicationFacade()
363 client_facade = client.ClientFacade()
364 app_facade.connect(self.connection)
365 client_facade.connect(self.connection)
366
367 log.debug(
368 'Deploying %s', entity_url)
369
370 await client_facade.AddCharm(channel, entity_url)
371 app = client.ApplicationDeploy(
372 application=service_name,
373 channel=channel,
374 charm_url=entity_url,
375 config=config,
376 constraints=constraints,
377 endpoint_bindings=bind,
378 num_units=num_units,
379 placement=placement,
380 resources=resources,
381 series=series,
382 storage=storage,
383 )
384
385 return await app_facade.Deploy([app])
386
387 def destroy(self):
388 """Terminate all machines and resources for this model.
389
390 """
391 pass
392
393 def get_backup(self, archive_id):
394 """Download a backup archive file.
395
396 :param str archive_id: The id of the archive to download
397 :return str: Path to the archive file
398
399 """
400 pass
401
402 def enable_ha(
403 self, num_controllers=0, constraints=None, series=None, to=None):
404 """Ensure sufficient controllers exist to provide redundancy.
405
406 :param int num_controllers: Number of controllers to make available
407 :param constraints: Constraints to apply to the controller machines
408 :type constraints: :class:`juju.Constraints`
409 :param str series: Series of the controller machines
410 :param list to: Placement directives for controller machines, e.g.::
411
412 '23' - machine 23
413 'lxc:7' - new lxc container on machine 7
414 '24/lxc/3' - lxc container 3 or machine 24
415
416 If None, a new machine is provisioned.
417
418 """
419 pass
420
421 def get_config(self):
422 """Return the configuration settings for this model.
423
424 """
425 pass
426
427 def get_constraints(self):
428 """Return the machine constraints for this model.
429
430 """
431 pass
432
433 def grant(self, username, acl='read'):
434 """Grant a user access to this model.
435
436 :param str username: Username
437 :param str acl: Access control ('read' or 'write')
438
439 """
440 pass
441
442 def import_ssh_key(self, identity):
443 """Add a public SSH key from a trusted indentity source to this model.
444
445 :param str identity: User identity in the form <lp|gh>:<username>
446
447 """
448 pass
449 import_ssh_keys = import_ssh_key
450
451 def get_machines(self, machine, utc=False):
452 """Return list of machines in this model.
453
454 :param str machine: Machine id, e.g. '0'
455 :param bool utc: Display time as UTC in RFC3339 format
456
457 """
458 pass
459
460 def get_shares(self):
461 """Return list of all users with access to this model.
462
463 """
464 pass
465
466 def get_spaces(self):
467 """Return list of all known spaces, including associated subnets.
468
469 """
470 pass
471
472 def get_ssh_key(self):
473 """Return known SSH keys for this model.
474
475 """
476 pass
477 get_ssh_keys = get_ssh_key
478
479 def get_storage(self, filesystem=False, volume=False):
480 """Return details of storage instances.
481
482 :param bool filesystem: Include filesystem storage
483 :param bool volume: Include volume storage
484
485 """
486 pass
487
488 def get_storage_pools(self, names=None, providers=None):
489 """Return list of storage pools.
490
491 :param list names: Only include pools with these names
492 :param list providers: Only include pools for these providers
493
494 """
495 pass
496
497 def get_subnets(self, space=None, zone=None):
498 """Return list of known subnets.
499
500 :param str space: Only include subnets in this space
501 :param str zone: Only include subnets in this zone
502
503 """
504 pass
505
506 def remove_blocks(self):
507 """Remove all blocks from this model.
508
509 """
510 pass
511
512 def remove_backup(self, backup_id):
513 """Delete a backup.
514
515 :param str backup_id: The id of the backup to remove
516
517 """
518 pass
519
520 def remove_cached_images(self, arch=None, kind=None, series=None):
521 """Remove cached OS images.
522
523 :param str arch: Architecture of the images to remove
524 :param str kind: Image kind to remove, e.g. 'lxd'
525 :param str series: Image series to remove, e.g. 'xenial'
526
527 """
528 pass
529
530 def remove_machine(self, *machine_ids):
531 """Remove a machine from this model.
532
533 :param str \*machine_ids: Ids of the machines to remove
534
535 """
536 pass
537 remove_machines = remove_machine
538
539 def remove_ssh_key(self, *keys):
540 """Remove a public SSH key(s) from this model.
541
542 :param str \*keys: Keys to remove
543
544 """
545 pass
546 remove_ssh_keys = remove_ssh_key
547
548 def restore_backup(
549 self, bootstrap=False, constraints=None, archive=None,
550 backup_id=None, upload_tools=False):
551 """Restore a backup archive to a new controller.
552
553 :param bool bootstrap: Bootstrap a new state machine
554 :param constraints: Model constraints
555 :type constraints: :class:`juju.Constraints`
556 :param str archive: Path to backup archive to restore
557 :param str backup_id: Id of backup to restore
558 :param bool upload_tools: Upload tools if bootstrapping a new machine
559
560 """
561 pass
562
563 def retry_provisioning(self):
564 """Retry provisioning for failed machines.
565
566 """
567 pass
568
569 def revoke(self, username, acl='read'):
570 """Revoke a user's access to this model.
571
572 :param str username: Username to revoke
573 :param str acl: Access control ('read' or 'write')
574
575 """
576 pass
577
578 def run(self, command, timeout=None):
579 """Run command on all machines in this model.
580
581 :param str command: The command to run
582 :param int timeout: Time to wait before command is considered failed
583
584 """
585 pass
586
587 def set_config(self, **config):
588 """Set configuration keys on this model.
589
590 :param \*\*config: Config key/values
591
592 """
593 pass
594
595 def set_constraints(self, constraints):
596 """Set machine constraints on this model.
597
598 :param :class:`juju.Constraints` constraints: Machine constraints
599
600 """
601 pass
602
603 def get_action_output(self, action_uuid, wait=-1):
604 """Get the results of an action by ID.
605
606 :param str action_uuid: Id of the action
607 :param int wait: Time in seconds to wait for action to complete
608
609 """
610 pass
611
612 def get_action_status(self, uuid_or_prefix=None, name=None):
613 """Get the status of all actions, filtered by ID, ID prefix, or action name.
614
615 :param str uuid_or_prefix: Filter by action uuid or prefix
616 :param str name: Filter by action name
617
618 """
619 pass
620
621 def get_budget(self, budget_name):
622 """Get budget usage info.
623
624 :param str budget_name: Name of budget
625
626 """
627 pass
628
629 def get_status(self, filter_=None, utc=False):
630 """Return the status of the model.
631
632 :param str filter_: Service or unit name or wildcard ('*')
633 :param bool utc: Display time as UTC in RFC3339 format
634
635 """
636 pass
637 status = get_status
638
639 def sync_tools(
640 self, all_=False, destination=None, dry_run=False, public=False,
641 source=None, stream=None, version=None):
642 """Copy Juju tools into this model.
643
644 :param bool all_: Copy all versions, not just the latest
645 :param str destination: Path to local destination directory
646 :param bool dry_run: Don't do the actual copy
647 :param bool public: Tools are for a public cloud, so generate mirrors
648 information
649 :param str source: Path to local source directory
650 :param str stream: Simplestreams stream for which to sync metadata
651 :param str version: Copy a specific major.minor version
652
653 """
654 pass
655
656 def unblock(self, *commands):
657 """Unblock an operation that would alter this model.
658
659 :param str \*commands: The commands to unblock. Valid values are
660 'all-changes', 'destroy-model', 'remove-object'
661
662 """
663 pass
664
665 def unset_config(self, *keys):
666 """Unset configuration on this model.
667
668 :param str \*keys: The keys to unset
669
670 """
671 pass
672
673 def upgrade_gui(self):
674 """Upgrade the Juju GUI for this model.
675
676 """
677 pass
678
679 def upgrade_juju(
680 self, dry_run=False, reset_previous_upgrade=False,
681 upload_tools=False, version=None):
682 """Upgrade Juju on all machines in a model.
683
684 :param bool dry_run: Don't do the actual upgrade
685 :param bool reset_previous_upgrade: Clear the previous (incomplete)
686 upgrade status
687 :param bool upload_tools: Upload local version of tools
688 :param str version: Upgrade to a specific version
689
690 """
691 pass
692
693 def upload_backup(self, archive_path):
694 """Store a backup archive remotely in Juju.
695
696 :param str archive_path: Path to local archive
697
698 """
699 pass