blob: c8ee2ef64d7f3cf5cc833223f63a24d3a184ba81 [file] [log] [blame]
Adam Israel5e08a0e2018-09-06 19:22:47 -04001import asyncio
Adam Israelc3e6c2e2018-03-01 09:31:50 -05002import logging
3import os
4import os.path
5import re
Adam Israelfa329072018-09-14 11:26:13 -04006import shlex
Adam Israelc3e6c2e2018-03-01 09:31:50 -05007import ssl
Adam Israelfa329072018-09-14 11:26:13 -04008import subprocess
Adam Israelc3e6c2e2018-03-01 09:31:50 -05009import sys
Adam Israel5e08a0e2018-09-06 19:22:47 -040010# import time
Adam Israelc3e6c2e2018-03-01 09:31:50 -050011
12# FIXME: this should load the juju inside or modules without having to
13# explicitly install it. Check why it's not working.
14# Load our subtree of the juju library
15path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
16path = os.path.join(path, "modules/libjuju/")
17if path not in sys.path:
18 sys.path.insert(1, path)
19
20from juju.controller import Controller
Adam Israel5e08a0e2018-09-06 19:22:47 -040021from juju.model import ModelObserver
Adam Israel6d84dbd2019-03-08 18:33:35 -050022from juju.errors import JujuAPIError, JujuError
Adam Israelc3e6c2e2018-03-01 09:31:50 -050023
24# We might need this to connect to the websocket securely, but test and verify.
25try:
26 ssl._create_default_https_context = ssl._create_unverified_context
27except AttributeError:
28 # Legacy Python doesn't verify by default (see pep-0476)
29 # https://www.python.org/dev/peps/pep-0476/
30 pass
31
32
33# Custom exceptions
34class JujuCharmNotFound(Exception):
35 """The Charm can't be found or is not readable."""
36
37
38class JujuApplicationExists(Exception):
39 """The Application already exists."""
40
Adam Israelb5214512018-05-03 10:00:04 -040041
Adam Israel88a49632018-04-10 13:04:57 -060042class N2VCPrimitiveExecutionFailed(Exception):
43 """Something failed while attempting to execute a primitive."""
44
Adam Israelc3e6c2e2018-03-01 09:31:50 -050045
Adam Israel6d84dbd2019-03-08 18:33:35 -050046class NetworkServiceDoesNotExist(Exception):
47 """The Network Service being acted against does not exist."""
48
49
Adam Israelc4616c82019-06-24 11:44:47 -040050class PrimitiveDoesNotExist(Exception):
51 """The Primitive being executed does not exist."""
52
Adam Israelc3e6c2e2018-03-01 09:31:50 -050053# Quiet the debug logging
54logging.getLogger('websockets.protocol').setLevel(logging.INFO)
55logging.getLogger('juju.client.connection').setLevel(logging.WARN)
56logging.getLogger('juju.model').setLevel(logging.WARN)
57logging.getLogger('juju.machine').setLevel(logging.WARN)
58
Adam Israelb5214512018-05-03 10:00:04 -040059
Adam Israelc3e6c2e2018-03-01 09:31:50 -050060class VCAMonitor(ModelObserver):
61 """Monitor state changes within the Juju Model."""
Adam Israelc3e6c2e2018-03-01 09:31:50 -050062 log = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -050063
Adam Israel28a43c02018-04-23 16:04:54 -040064 def __init__(self, ns_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050065 self.log = logging.getLogger(__name__)
66
67 self.ns_name = ns_name
Adam Israeld420a8b2019-04-09 16:07:53 -040068 self.applications = {}
Adam Israel28a43c02018-04-23 16:04:54 -040069
70 def AddApplication(self, application_name, callback, *callback_args):
71 if application_name not in self.applications:
72 self.applications[application_name] = {
73 'callback': callback,
74 'callback_args': callback_args
75 }
76
77 def RemoveApplication(self, application_name):
78 if application_name in self.applications:
79 del self.applications[application_name]
Adam Israelc3e6c2e2018-03-01 09:31:50 -050080
81 async def on_change(self, delta, old, new, model):
82 """React to changes in the Juju model."""
83
84 if delta.entity == "unit":
Adam Israel28a43c02018-04-23 16:04:54 -040085 # Ignore change events from other applications
86 if delta.data['application'] not in self.applications.keys():
87 return
88
Adam Israelc3e6c2e2018-03-01 09:31:50 -050089 try:
Adam Israel28a43c02018-04-23 16:04:54 -040090
91 application_name = delta.data['application']
92
93 callback = self.applications[application_name]['callback']
Adam Israel5e08a0e2018-09-06 19:22:47 -040094 callback_args = \
95 self.applications[application_name]['callback_args']
Adam Israel28a43c02018-04-23 16:04:54 -040096
Adam Israelc3e6c2e2018-03-01 09:31:50 -050097 if old and new:
Adam Israelfc511ed2018-09-21 14:20:55 +020098 # Fire off a callback with the application state
99 if callback:
100 callback(
101 self.ns_name,
102 delta.data['application'],
103 new.workload_status,
104 new.workload_status_message,
105 *callback_args)
Adam Israel28a43c02018-04-23 16:04:54 -0400106
107 if old and not new:
108 # This is a charm being removed
109 if callback:
110 callback(
111 self.ns_name,
112 delta.data['application'],
113 "removed",
Adam Israel9562f432018-05-09 13:55:28 -0400114 "",
Adam Israel28a43c02018-04-23 16:04:54 -0400115 *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500116 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400117 self.log.debug("[1] notify_callback exception: {}".format(e))
118
Adam Israel88a49632018-04-10 13:04:57 -0600119 elif delta.entity == "action":
120 # TODO: Decide how we want to notify the user of actions
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500121
Adam Israel88a49632018-04-10 13:04:57 -0600122 # uuid = delta.data['id'] # The Action's unique id
123 # msg = delta.data['message'] # The output of the action
124 #
125 # if delta.data['status'] == "pending":
126 # # The action is queued
127 # pass
128 # elif delta.data['status'] == "completed""
129 # # The action was successful
130 # pass
131 # elif delta.data['status'] == "failed":
132 # # The action failed.
133 # pass
134
135 pass
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500136
137########
138# TODO
139#
140# Create unique models per network service
141# Document all public functions
142
Adam Israelb5214512018-05-03 10:00:04 -0400143
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500144class N2VC:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500145 def __init__(self,
146 log=None,
147 server='127.0.0.1',
148 port=17070,
149 user='admin',
150 secret=None,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400151 artifacts=None,
152 loop=None,
Adam Israelb2a07f52019-04-25 17:17:05 -0400153 juju_public_key=None,
154 ca_cert=None,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500155 ):
156 """Initialize N2VC
Adam Israelb2a07f52019-04-25 17:17:05 -0400157 :param log obj: The logging object to log to
158 :param server str: The IP Address or Hostname of the Juju controller
159 :param port int: The port of the Juju Controller
160 :param user str: The Juju username to authenticate with
161 :param secret str: The Juju password to authenticate with
162 :param artifacts str: The directory where charms required by a vnfd are
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500163 stored.
Adam Israelb2a07f52019-04-25 17:17:05 -0400164 :param loop obj: The loop to use.
165 :param juju_public_key str: The contents of the Juju public SSH key
166 :param ca_cert str: The CA certificate to use to authenticate
167
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500168
169 :Example:
Adam Israelb2a07f52019-04-25 17:17:05 -0400170 client = n2vc.vnf.N2VC(
171 log=log,
172 server='10.1.1.28',
173 port=17070,
174 user='admin',
175 secret='admin',
176 artifacts='/app/storage/myvnf/charms',
177 loop=loop,
178 juju_public_key='<contents of the juju public key>',
179 ca_cert='<contents of CA certificate>',
180 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500181 """
182
Adam Israel5e08a0e2018-09-06 19:22:47 -0400183 # Initialize instance-level variables
184 self.api = None
185 self.log = None
186 self.controller = None
187 self.connecting = False
188 self.authenticated = False
189
Adam Israelfc511ed2018-09-21 14:20:55 +0200190 # For debugging
191 self.refcount = {
192 'controller': 0,
193 'model': 0,
194 }
195
Adam Israel5e08a0e2018-09-06 19:22:47 -0400196 self.models = {}
Adam Israel5e08a0e2018-09-06 19:22:47 -0400197
198 # Model Observers
199 self.monitors = {}
200
201 # VCA config
202 self.hostname = ""
203 self.port = 17070
204 self.username = ""
205 self.secret = ""
206
Adam Israelb2a07f52019-04-25 17:17:05 -0400207 self.juju_public_key = juju_public_key
208 if juju_public_key:
209 self._create_juju_public_key(juju_public_key)
210
211 self.ca_cert = ca_cert
212
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500213 if log:
214 self.log = log
215 else:
216 self.log = logging.getLogger(__name__)
217
218 # Quiet websocket traffic
219 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
220 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
221 logging.getLogger('model').setLevel(logging.WARN)
222 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
223
224 self.log.debug('JujuApi: instantiated')
225
226 self.server = server
227 self.port = port
228
229 self.secret = secret
230 if user.startswith('user-'):
231 self.user = user
232 else:
233 self.user = 'user-{}'.format(user)
234
235 self.endpoint = '%s:%d' % (server, int(port))
236
237 self.artifacts = artifacts
238
Adam Israel5e08a0e2018-09-06 19:22:47 -0400239 self.loop = loop or asyncio.get_event_loop()
240
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500241 def __del__(self):
242 """Close any open connections."""
243 yield self.logout()
244
Adam Israelb2a07f52019-04-25 17:17:05 -0400245 def _create_juju_public_key(self, public_key):
246 """Recreate the Juju public key on disk.
247
248 Certain libjuju commands expect to be run from the same machine as Juju
249 is bootstrapped to. This method will write the public key to disk in
250 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
251 """
Adam Israele3a05f82019-04-26 13:12:47 -0400252 # Make sure that we have a public key before writing to disk
Adam Israelb2a07f52019-04-25 17:17:05 -0400253 if public_key is None or len(public_key) == 0:
Adam Israele3a05f82019-04-26 13:12:47 -0400254 if 'OSM_VCA_PUBKEY' in os.environ:
255 public_key = os.getenv('OSM_VCA_PUBKEY', '')
256 if len(public_key == 0):
257 return
258 else:
259 return
260
Adam Israelb2a07f52019-04-25 17:17:05 -0400261 path = "{}/.local/share/juju/ssh".format(
262 os.path.expanduser('~'),
263 )
264 if not os.path.exists(path):
265 os.makedirs(path)
266
267 with open('{}/juju_id_rsa.pub'.format(path), 'w') as f:
268 f.write(public_key)
269
Adam Israel5e08a0e2018-09-06 19:22:47 -0400270 def notify_callback(self, model_name, application_name, status, message,
271 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500272 try:
273 if callback:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400274 callback(
275 model_name,
276 application_name,
277 status, message,
278 *callback_args,
279 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500280 except Exception as e:
281 self.log.error("[0] notify_callback exception {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600282 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500283 return True
284
285 # Public methods
Adam Israel85a4b212018-11-29 20:30:24 -0500286 async def Relate(self, model_name, vnfd):
Adam Israel136186e2018-09-14 12:01:12 -0400287 """Create a relation between the charm-enabled VDUs in a VNF.
288
289 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
290
291 vdu:
292 ...
Adam Israelbc0daf82019-06-12 17:08:36 -0400293 vca-relationships:
Adam Israel47998e02019-06-03 11:21:27 -0400294 relation:
295 - provides: dataVM:db
296 requires: mgmtVM:app
Adam Israel136186e2018-09-14 12:01:12 -0400297
298 This tells N2VC that the charm referred to by the dataVM vdu offers a relation named 'db', and the mgmtVM vdu has an 'app' endpoint that should be connected to a database.
299
300 :param str ns_name: The name of the network service.
301 :param dict vnfd: The parsed yaml VNF descriptor.
302 """
303
304 # Currently, the call to Relate() is made automatically after the
305 # deployment of each charm; if the relation depends on a charm that
306 # hasn't been deployed yet, the call will fail silently. This will
307 # prevent an API breakage, with the intent of making this an explicitly
308 # required call in a more object-oriented refactor of the N2VC API.
309
310 configs = []
311 vnf_config = vnfd.get("vnf-configuration")
312 if vnf_config:
313 juju = vnf_config['juju']
314 if juju:
315 configs.append(vnf_config)
316
317 for vdu in vnfd['vdu']:
318 vdu_config = vdu.get('vdu-configuration')
319 if vdu_config:
320 juju = vdu_config['juju']
321 if juju:
322 configs.append(vdu_config)
323
324 def _get_application_name(name):
325 """Get the application name that's mapped to a vnf/vdu."""
326 vnf_member_index = 0
327 vnf_name = vnfd['name']
328
329 for vdu in vnfd.get('vdu'):
330 # Compare the named portion of the relation to the vdu's id
331 if vdu['id'] == name:
332 application_name = self.FormatApplicationName(
Adam Israel85a4b212018-11-29 20:30:24 -0500333 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400334 vnf_name,
335 str(vnf_member_index),
336 )
337 return application_name
338 else:
339 vnf_member_index += 1
340
341 return None
342
343 # Loop through relations
344 for cfg in configs:
345 if 'juju' in cfg:
Adam Israelc92163f2019-05-27 08:39:19 -0400346 juju = cfg['juju']
Adam Israelbc0daf82019-06-12 17:08:36 -0400347 if 'vca-relationships' in juju and 'relation' in juju['vca-relationships']:
348 for rel in juju['vca-relationships']['relation']:
Adam Israel136186e2018-09-14 12:01:12 -0400349 try:
350
351 # get the application name for the provides
352 (name, endpoint) = rel['provides'].split(':')
353 application_name = _get_application_name(name)
354
355 provides = "{}:{}".format(
356 application_name,
357 endpoint
358 )
359
360 # get the application name for thr requires
361 (name, endpoint) = rel['requires'].split(':')
362 application_name = _get_application_name(name)
363
364 requires = "{}:{}".format(
365 application_name,
366 endpoint
367 )
368 self.log.debug("Relation: {} <-> {}".format(
369 provides,
370 requires
371 ))
372 await self.add_relation(
Adam Israel85a4b212018-11-29 20:30:24 -0500373 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400374 provides,
375 requires,
376 )
377 except Exception as e:
378 self.log.debug("Exception: {}".format(e))
379
380 return
381
Adam Israel5e08a0e2018-09-06 19:22:47 -0400382 async def DeployCharms(self, model_name, application_name, vnfd,
383 charm_path, params={}, machine_spec={},
384 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500385 """Deploy one or more charms associated with a VNF.
386
387 Deploy the charm(s) referenced in a VNF Descriptor.
388
Adam Israel85a4b212018-11-29 20:30:24 -0500389 :param str model_name: The name or unique id of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500390 :param str application_name: The name of the application
391 :param dict vnfd: The name of the application
392 :param str charm_path: The path to the Juju charm
393 :param dict params: A dictionary of runtime parameters
394 Examples::
395 {
Adam Israel88a49632018-04-10 13:04:57 -0600396 'rw_mgmt_ip': '1.2.3.4',
397 # Pass the initial-config-primitives section of the vnf or vdu
398 'initial-config-primitives': {...}
tierno1afb30a2018-12-21 13:42:43 +0000399 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
400 inside < >. rw_mgmt_ip will be included here also
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500401 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400402 :param dict machine_spec: A dictionary describing the machine to
403 install to
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500404 Examples::
405 {
406 'hostname': '1.2.3.4',
407 'username': 'ubuntu',
408 }
409 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400410 :param tuple callback_args: A list of arguments to be passed to the
411 callback
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500412 """
413
414 ########################################################
415 # Verify the path to the charm exists and is readable. #
416 ########################################################
417 if not os.path.exists(charm_path):
418 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400419 self.notify_callback(
420 model_name,
421 application_name,
422 "failed",
423 callback,
424 *callback_args,
425 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500426 raise JujuCharmNotFound("No artifacts configured.")
427
428 ################################
429 # Login to the Juju controller #
430 ################################
431 if not self.authenticated:
432 self.log.debug("Authenticating with Juju")
433 await self.login()
434
435 ##########################################
436 # Get the model for this network service #
437 ##########################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500438 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500439
440 ########################################
441 # Verify the application doesn't exist #
442 ########################################
443 app = await self.get_application(model, application_name)
444 if app:
Adam Israel42d88e62018-07-16 14:18:41 -0400445 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500446
Adam Israel28a43c02018-04-23 16:04:54 -0400447 ################################################################
448 # Register this application with the model-level event monitor #
449 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500450 if callback:
Adam Israel04eee1f2019-04-29 14:59:45 -0400451 self.log.debug("JujuApi: Registering callback for {}".format(
Adam Israel28a43c02018-04-23 16:04:54 -0400452 application_name,
Adam Israel04eee1f2019-04-29 14:59:45 -0400453 ))
454 await self.Subscribe(model_name, application_name, callback, *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500455
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500456 ########################################################
457 # Check for specific machine placement (native charms) #
458 ########################################################
459 to = ""
460 if machine_spec.keys():
Adam Israel5963cb42018-09-14 11:26:13 -0400461 if all(k in machine_spec for k in ['host', 'user']):
462 # Enlist an existing machine as a Juju unit
463 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
Adam Israelce31bc32019-05-22 16:30:25 -0400464 machine_spec['username'],
465 machine_spec['hostname'],
Adam Israel5963cb42018-09-14 11:26:13 -0400466 self.GetPrivateKeyPath(),
467 ))
Adam Israelfa329072018-09-14 11:26:13 -0400468 to = machine.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500469
470 #######################################
471 # Get the initial charm configuration #
472 #######################################
473
474 rw_mgmt_ip = None
475 if 'rw_mgmt_ip' in params:
476 rw_mgmt_ip = params['rw_mgmt_ip']
477
Adam Israel5afe0542018-08-08 12:54:55 -0400478 if 'initial-config-primitive' not in params:
479 params['initial-config-primitive'] = {}
480
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500481 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600482 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500483 {'<rw_mgmt_ip>': rw_mgmt_ip}
484 )
485
Adam Israel85a4b212018-11-29 20:30:24 -0500486 self.log.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
487 model_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500488 application_name,
489 charm_path,
490 to=to,
491 ))
492
493 ########################################################
494 # Deploy the charm and apply the initial configuration #
495 ########################################################
496 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600497 # We expect charm_path to be either the path to the charm on disk
498 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500499 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600500 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500501 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600502 # Proxy charms should use the current LTS. This will need to be
503 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500504 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600505 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500506 config=initial_config,
Adam Israelfa329072018-09-14 11:26:13 -0400507 # Where to deploy the charm to.
508 to=to,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500509 )
Adam Israel38bf1642019-05-31 09:59:52 -0400510 #############################
511 # Map the vdu id<->app name #
512 #############################
513 try:
514 await self.Relate(model_name, vnfd)
515 except KeyError as ex:
516 # We don't currently support relations between NS and VNF/VDU charms
517 self.log.warn("[N2VC] Relations not supported: {}".format(ex))
518 except Exception as ex:
519 # This may happen if not all of the charms needed by the relation
520 # are ready. We can safely ignore this, because Relate will be
521 # retried when the endpoint of the relation is deployed.
522 self.log.warn("[N2VC] Relations not ready")
Adam Israel136186e2018-09-14 12:01:12 -0400523
Adam Israel88a49632018-04-10 13:04:57 -0600524 # #######################################
525 # # Execute initial config primitive(s) #
526 # #######################################
Adam Israelcf253202018-10-31 16:29:09 -0700527 uuids = await self.ExecuteInitialPrimitives(
Adam Israel5e08a0e2018-09-06 19:22:47 -0400528 model_name,
529 application_name,
530 params,
531 )
Adam Israelcf253202018-10-31 16:29:09 -0700532 return uuids
Adam Israel5e08a0e2018-09-06 19:22:47 -0400533
534 # primitives = {}
535 #
536 # # Build a sequential list of the primitives to execute
537 # for primitive in params['initial-config-primitive']:
538 # try:
539 # if primitive['name'] == 'config':
540 # # This is applied when the Application is deployed
541 # pass
542 # else:
543 # seq = primitive['seq']
544 #
545 # params = {}
546 # if 'parameter' in primitive:
547 # params = primitive['parameter']
548 #
549 # primitives[seq] = {
550 # 'name': primitive['name'],
551 # 'parameters': self._map_primitive_parameters(
552 # params,
553 # {'<rw_mgmt_ip>': rw_mgmt_ip}
554 # ),
555 # }
556 #
557 # for primitive in sorted(primitives):
558 # await self.ExecutePrimitive(
559 # model_name,
560 # application_name,
561 # primitives[primitive]['name'],
562 # callback,
563 # callback_args,
564 # **primitives[primitive]['parameters'],
565 # )
566 # except N2VCPrimitiveExecutionFailed as e:
567 # self.log.debug(
568 # "[N2VC] Exception executing primitive: {}".format(e)
569 # )
570 # raise
571
572 async def GetPrimitiveStatus(self, model_name, uuid):
573 """Get the status of an executed Primitive.
574
575 The status of an executed Primitive will be one of three values:
576 - completed
577 - failed
578 - running
579 """
580 status = None
581 try:
582 if not self.authenticated:
583 await self.login()
584
Adam Israel5e08a0e2018-09-06 19:22:47 -0400585 model = await self.get_model(model_name)
586
587 results = await model.get_action_status(uuid)
588
589 if uuid in results:
590 status = results[uuid]
591
592 except Exception as e:
593 self.log.debug(
594 "Caught exception while getting primitive status: {}".format(e)
595 )
596 raise N2VCPrimitiveExecutionFailed(e)
597
598 return status
599
600 async def GetPrimitiveOutput(self, model_name, uuid):
601 """Get the output of an executed Primitive.
602
603 Note: this only returns output for a successfully executed primitive.
604 """
605 results = None
606 try:
607 if not self.authenticated:
608 await self.login()
609
Adam Israel5e08a0e2018-09-06 19:22:47 -0400610 model = await self.get_model(model_name)
611 results = await model.get_action_output(uuid, 60)
612 except Exception as e:
613 self.log.debug(
614 "Caught exception while getting primitive status: {}".format(e)
615 )
616 raise N2VCPrimitiveExecutionFailed(e)
617
618 return results
619
Adam Israelfa329072018-09-14 11:26:13 -0400620 # async def ProvisionMachine(self, model_name, hostname, username):
621 # """Provision machine for usage with Juju.
622 #
623 # Provisions a previously instantiated machine for use with Juju.
624 # """
625 # try:
626 # if not self.authenticated:
627 # await self.login()
628 #
629 # # FIXME: This is hard-coded until model-per-ns is added
630 # model_name = 'default'
631 #
632 # model = await self.get_model(model_name)
633 # model.add_machine(spec={})
634 #
635 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
636 # "ubuntu",
637 # host['address'],
638 # private_key_path,
639 # ))
640 # return machine.id
641 #
642 # except Exception as e:
643 # self.log.debug(
644 # "Caught exception while getting primitive status: {}".format(e)
645 # )
646 # raise N2VCPrimitiveExecutionFailed(e)
647
648 def GetPrivateKeyPath(self):
649 homedir = os.environ['HOME']
650 sshdir = "{}/.ssh".format(homedir)
651 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
652 return private_key_path
653
654 async def GetPublicKey(self):
655 """Get the N2VC SSH public key.abs
656
657 Returns the SSH public key, to be injected into virtual machines to
658 be managed by the VCA.
659
660 The first time this is run, a ssh keypair will be created. The public
661 key is injected into a VM so that we can provision the machine with
662 Juju, after which Juju will communicate with the VM directly via the
663 juju agent.
664 """
665 public_key = ""
666
667 # Find the path to where we expect our key to live.
668 homedir = os.environ['HOME']
669 sshdir = "{}/.ssh".format(homedir)
670 if not os.path.exists(sshdir):
671 os.mkdir(sshdir)
672
673 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
674 public_key_path = "{}.pub".format(private_key_path)
675
676 # If we don't have a key generated, generate it.
677 if not os.path.exists(private_key_path):
678 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
679 "rsa",
680 "4096",
681 private_key_path
682 )
683 subprocess.check_output(shlex.split(cmd))
684
685 # Read the public key
686 with open(public_key_path, "r") as f:
687 public_key = f.readline()
688
689 return public_key
690
Adam Israel5e08a0e2018-09-06 19:22:47 -0400691 async def ExecuteInitialPrimitives(self, model_name, application_name,
692 params, callback=None, *callback_args):
693 """Execute multiple primitives.
694
695 Execute multiple primitives as declared in initial-config-primitive.
696 This is useful in cases where the primitives initially failed -- for
697 example, if the charm is a proxy but the proxy hasn't been configured
698 yet.
699 """
700 uuids = []
Adam Israel88a49632018-04-10 13:04:57 -0600701 primitives = {}
702
703 # Build a sequential list of the primitives to execute
704 for primitive in params['initial-config-primitive']:
705 try:
706 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600707 pass
708 else:
Adam Israel88a49632018-04-10 13:04:57 -0600709 seq = primitive['seq']
710
tierno1afb30a2018-12-21 13:42:43 +0000711 params_ = {}
Adam Israel42d88e62018-07-16 14:18:41 -0400712 if 'parameter' in primitive:
tierno1afb30a2018-12-21 13:42:43 +0000713 params_ = primitive['parameter']
714
715 user_values = params.get("user_values", {})
716 if 'rw_mgmt_ip' not in user_values:
717 user_values['rw_mgmt_ip'] = None
718 # just for backward compatibility, because it will be provided always by modern version of LCM
Adam Israel42d88e62018-07-16 14:18:41 -0400719
Adam Israel88a49632018-04-10 13:04:57 -0600720 primitives[seq] = {
721 'name': primitive['name'],
722 'parameters': self._map_primitive_parameters(
tierno1afb30a2018-12-21 13:42:43 +0000723 params_,
724 user_values
Adam Israel88a49632018-04-10 13:04:57 -0600725 ),
726 }
727
728 for primitive in sorted(primitives):
Adam Israelc4616c82019-06-24 11:44:47 -0400729 try:
730 # self.log.debug("Queuing action {}".format(primitives[primitive]['name']))
731 uuids.append(
732 await self.ExecutePrimitive(
733 model_name,
734 application_name,
735 primitives[primitive]['name'],
736 callback,
737 callback_args,
738 **primitives[primitive]['parameters'],
739 )
Adam Israel5e08a0e2018-09-06 19:22:47 -0400740 )
Adam Israelc4616c82019-06-24 11:44:47 -0400741 except PrimitiveDoesNotExist as e:
742 self.log.debug("Ignoring exception PrimitiveDoesNotExist: {}".format(e))
743 pass
744 except Exception as e:
745 self.log.debug("XXXXXXXXXXXXXXXXXXXXXXXXX Unexpected exception: {}".format(e))
746 raise e
747
Adam Israel88a49632018-04-10 13:04:57 -0600748 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400749 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600750 "[N2VC] Exception executing primitive: {}".format(e)
751 )
752 raise
Adam Israel5e08a0e2018-09-06 19:22:47 -0400753 return uuids
Adam Israel88a49632018-04-10 13:04:57 -0600754
Adam Israel5e08a0e2018-09-06 19:22:47 -0400755 async def ExecutePrimitive(self, model_name, application_name, primitive,
756 callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400757 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600758
Adam Israelc9df96f2018-05-03 14:49:56 -0400759 Execute a primitive defined in the VNF descriptor.
760
Adam Israel85a4b212018-11-29 20:30:24 -0500761 :param str model_name: The name or unique id of the network service.
Adam Israelc9df96f2018-05-03 14:49:56 -0400762 :param str application_name: The name of the application
763 :param str primitive: The name of the primitive to execute.
764 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400765 :param tuple callback_args: A list of arguments to be passed to the
766 callback function.
767 :param dict params: A dictionary of key=value pairs representing the
768 primitive's parameters
Adam Israelc9df96f2018-05-03 14:49:56 -0400769 Examples::
770 {
771 'rw_mgmt_ip': '1.2.3.4',
772 # Pass the initial-config-primitives section of the vnf or vdu
773 'initial-config-primitives': {...}
774 }
Adam Israel6817f612018-04-13 08:41:43 -0600775 """
tierno1afb30a2018-12-21 13:42:43 +0000776 self.log.debug("Executing primitive={} params={}".format(primitive, params))
Adam Israel6817f612018-04-13 08:41:43 -0600777 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500778 try:
779 if not self.authenticated:
780 await self.login()
781
Adam Israel5e08a0e2018-09-06 19:22:47 -0400782 model = await self.get_model(model_name)
Adam Israelb5214512018-05-03 10:00:04 -0400783
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500784 if primitive == 'config':
785 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400786 await self.set_config(
787 model,
788 application_name,
789 params['params'],
790 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500791 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500792 app = await self.get_application(model, application_name)
793 if app:
Adam Israelc4616c82019-06-24 11:44:47 -0400794 # Does this primitive exist?
795 actions = await app.get_actions()
796
797 if primitive not in actions.keys():
798 raise PrimitiveDoesNotExist("Primitive {} does not exist".format(primitive))
799
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500800 # Run against the first (and probably only) unit in the app
801 unit = app.units[0]
802 if unit:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500803 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600804 uuid = action.id
Adam Israelc4616c82019-06-24 11:44:47 -0400805 except PrimitiveDoesNotExist as e:
806 # Catch and raise this exception if it's thrown from the inner block
807 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500808 except Exception as e:
Adam Israelc4616c82019-06-24 11:44:47 -0400809 # An unexpected exception was caught
Adam Israelb0943662018-08-02 15:32:00 -0400810 self.log.debug(
811 "Caught exception while executing primitive: {}".format(e)
812 )
Adam Israel7d871fb2018-07-17 12:17:06 -0400813 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600814 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500815
Adam Israel5e08a0e2018-09-06 19:22:47 -0400816 async def RemoveCharms(self, model_name, application_name, callback=None,
817 *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400818 """Remove a charm from the VCA.
819
820 Remove a charm referenced in a VNF Descriptor.
821
822 :param str model_name: The name of the network service.
823 :param str application_name: The name of the application
824 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400825 :param tuple callback_args: A list of arguments to be passed to the
826 callback function.
Adam Israelc9df96f2018-05-03 14:49:56 -0400827 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500828 try:
829 if not self.authenticated:
830 await self.login()
831
832 model = await self.get_model(model_name)
833 app = await self.get_application(model, application_name)
834 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400835 # Remove this application from event monitoring
Adam Israel04eee1f2019-04-29 14:59:45 -0400836 await self.Unsubscribe(model_name, application_name)
Adam Israel28a43c02018-04-23 16:04:54 -0400837
838 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400839 self.log.debug(
840 "Removing the application {}".format(application_name)
841 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500842 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400843
Adam Israel85a4b212018-11-29 20:30:24 -0500844 await self.disconnect_model(self.monitors[model_name])
845
Adam Israel5e08a0e2018-09-06 19:22:47 -0400846 self.notify_callback(
847 model_name,
848 application_name,
849 "removed",
Adam Israelc4f393e2019-03-19 16:33:30 -0400850 "Removing charm {}".format(application_name),
Adam Israel5e08a0e2018-09-06 19:22:47 -0400851 callback,
852 *callback_args,
853 )
Adam Israel28a43c02018-04-23 16:04:54 -0400854
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500855 except Exception as e:
856 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600857 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500858 raise e
859
Adam Israel6d84dbd2019-03-08 18:33:35 -0500860 async def CreateNetworkService(self, ns_uuid):
861 """Create a new Juju model for the Network Service.
862
863 Creates a new Model in the Juju Controller.
864
865 :param str ns_uuid: A unique id representing an instaance of a
866 Network Service.
867
868 :returns: True if the model was created. Raises JujuError on failure.
869 """
870 if not self.authenticated:
871 await self.login()
872
873 models = await self.controller.list_models()
874 if ns_uuid not in models:
875 try:
876 self.models[ns_uuid] = await self.controller.add_model(
877 ns_uuid
878 )
879 except JujuError as e:
880 if "already exists" not in e.message:
881 raise e
Adam Israel7bf2f4d2019-03-15 15:28:47 -0400882
883 # Create an observer for this model
884 await self.create_model_monitor(ns_uuid)
885
Adam Israel6d84dbd2019-03-08 18:33:35 -0500886 return True
887
888 async def DestroyNetworkService(self, ns_uuid):
889 """Destroy a Network Service.
890
891 Destroy the Network Service and any deployed charms.
892
893 :param ns_uuid The unique id of the Network Service
894
895 :returns: True if the model was created. Raises JujuError on failure.
896 """
897
898 # Do not delete the default model. The default model was used by all
899 # Network Services, prior to the implementation of a model per NS.
Adam Israelc4f393e2019-03-19 16:33:30 -0400900 if ns_uuid.lower() == "default":
Adam Israel6d84dbd2019-03-08 18:33:35 -0500901 return False
902
903 if not self.authenticated:
904 self.log.debug("Authenticating with Juju")
905 await self.login()
906
907 # Disconnect from the Model
908 if ns_uuid in self.models:
909 await self.disconnect_model(self.models[ns_uuid])
910
911 try:
912 await self.controller.destroy_models(ns_uuid)
Adam Israelc4f393e2019-03-19 16:33:30 -0400913 except JujuError:
Adam Israel6d84dbd2019-03-08 18:33:35 -0500914 raise NetworkServiceDoesNotExist(
915 "The Network Service '{}' does not exist".format(ns_uuid)
916 )
917
918 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500919
Adam Israelb5214512018-05-03 10:00:04 -0400920 async def GetMetrics(self, model_name, application_name):
921 """Get the metrics collected by the VCA.
922
Adam Israel85a4b212018-11-29 20:30:24 -0500923 :param model_name The name or unique id of the network service
Adam Israelb5214512018-05-03 10:00:04 -0400924 :param application_name The name of the application
925 """
926 metrics = {}
927 model = await self.get_model(model_name)
928 app = await self.get_application(model, application_name)
929 if app:
930 metrics = await app.get_metrics()
931
932 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500933
Adam Israelfa329072018-09-14 11:26:13 -0400934 async def HasApplication(self, model_name, application_name):
935 model = await self.get_model(model_name)
936 app = await self.get_application(model, application_name)
937 if app:
938 return True
939 return False
940
Adam Israel04eee1f2019-04-29 14:59:45 -0400941 async def Subscribe(self, ns_name, application_name, callback, *callback_args):
942 """Subscribe to callbacks for an application.
943
944 :param ns_name str: The name of the Network Service
945 :param application_name str: The name of the application
946 :param callback obj: The callback method
947 :param callback_args list: The list of arguments to append to calls to
948 the callback method
949 """
950 self.monitors[ns_name].AddApplication(
951 application_name,
952 callback,
953 *callback_args
954 )
955
956 async def Unsubscribe(self, ns_name, application_name):
957 """Unsubscribe to callbacks for an application.
958
959 Unsubscribes the caller from notifications from a deployed application.
960
961 :param ns_name str: The name of the Network Service
962 :param application_name str: The name of the application
963 """
964 self.monitors[ns_name].RemoveApplication(
965 application_name,
966 )
967
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500968 # Non-public methods
Adam Israel136186e2018-09-14 12:01:12 -0400969 async def add_relation(self, model_name, relation1, relation2):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500970 """
971 Add a relation between two application endpoints.
972
Adam Israel85a4b212018-11-29 20:30:24 -0500973 :param str model_name: The name or unique id of the network service
974 :param str relation1: '<application>[:<relation_name>]'
975 :param str relation2: '<application>[:<relation_name>]'
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500976 """
Adam Israel136186e2018-09-14 12:01:12 -0400977
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500978 if not self.authenticated:
979 await self.login()
980
Adam Israel136186e2018-09-14 12:01:12 -0400981 m = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500982 try:
Adam Israel136186e2018-09-14 12:01:12 -0400983 await m.add_relation(relation1, relation2)
984 except JujuAPIError as e:
985 # If one of the applications in the relationship doesn't exist,
986 # or the relation has already been added, let the operation fail
987 # silently.
988 if 'not found' in e.message:
989 return
990 if 'already exists' in e.message:
991 return
992
993 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500994
Adam Israelb5214512018-05-03 10:00:04 -0400995 # async def apply_config(self, config, application):
996 # """Apply a configuration to the application."""
997 # print("JujuApi: Applying configuration to {}.".format(
998 # application
999 # ))
1000 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001001
1002 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -06001003 """Transform the yang config primitive to dict.
1004
1005 Expected result:
1006
1007 config = {
1008 'config':
1009 }
1010 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001011 config = {}
1012 for primitive in config_primitive:
1013 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -06001014 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001015 for parameter in primitive['parameter']:
1016 param = str(parameter['name'])
1017 if parameter['value'] == "<rw_mgmt_ip>":
1018 config[param] = str(values[parameter['value']])
1019 else:
1020 config[param] = str(parameter['value'])
1021
1022 return config
1023
tierno1afb30a2018-12-21 13:42:43 +00001024 def _map_primitive_parameters(self, parameters, user_values):
Adam Israel88a49632018-04-10 13:04:57 -06001025 params = {}
1026 for parameter in parameters:
1027 param = str(parameter['name'])
tierno1afb30a2018-12-21 13:42:43 +00001028 value = parameter.get('value')
1029
1030 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
1031 # Must exist at user_values except if there is a default value
1032 if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
1033 if parameter['value'][1:-1] in user_values:
1034 value = user_values[parameter['value'][1:-1]]
1035 elif 'default-value' in parameter:
1036 value = parameter['default-value']
1037 else:
1038 raise KeyError("parameter {}='{}' not supplied ".format(param, value))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001039
Adam Israelbf793522018-11-20 13:54:13 -05001040 # If there's no value, use the default-value (if set)
tierno1afb30a2018-12-21 13:42:43 +00001041 if value is None and 'default-value' in parameter:
Adam Israelbf793522018-11-20 13:54:13 -05001042 value = parameter['default-value']
1043
Adam Israel5e08a0e2018-09-06 19:22:47 -04001044 # Typecast parameter value, if present
tierno1afb30a2018-12-21 13:42:43 +00001045 paramtype = "string"
1046 try:
1047 if 'data-type' in parameter:
1048 paramtype = str(parameter['data-type']).lower()
Adam Israel5e08a0e2018-09-06 19:22:47 -04001049
tierno1afb30a2018-12-21 13:42:43 +00001050 if paramtype == "integer":
1051 value = int(value)
1052 elif paramtype == "boolean":
1053 value = bool(value)
1054 else:
1055 value = str(value)
Adam Israel5e08a0e2018-09-06 19:22:47 -04001056 else:
tierno1afb30a2018-12-21 13:42:43 +00001057 # If there's no data-type, assume the value is a string
1058 value = str(value)
1059 except ValueError:
1060 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param, value, paramtype))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001061
tierno1afb30a2018-12-21 13:42:43 +00001062 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -06001063 return params
1064
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001065 def _get_config_from_yang(self, config_primitive, values):
1066 """Transform the yang config primitive to dict."""
1067 config = {}
1068 for primitive in config_primitive.values():
1069 if primitive['name'] == 'config':
1070 for parameter in primitive['parameter'].values():
1071 param = str(parameter['name'])
1072 if parameter['value'] == "<rw_mgmt_ip>":
1073 config[param] = str(values[parameter['value']])
1074 else:
1075 config[param] = str(parameter['value'])
1076
1077 return config
1078
1079 def FormatApplicationName(self, *args):
1080 """
1081 Generate a Juju-compatible Application name
1082
1083 :param args tuple: Positional arguments to be used to construct the
1084 application name.
1085
1086 Limitations::
1087 - Only accepts characters a-z and non-consequitive dashes (-)
1088 - Application name should not exceed 50 characters
1089
1090 Examples::
1091
1092 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
1093 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001094 appname = ""
1095 for c in "-".join(list(args)):
1096 if c.isdigit():
1097 c = chr(97 + int(c))
1098 elif not c.isalpha():
1099 c = "-"
1100 appname += c
Adam Israel6d84dbd2019-03-08 18:33:35 -05001101 return re.sub('-+', '-', appname.lower())
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001102
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001103 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1104 # """Format the name of the application
1105 #
1106 # Limitations:
1107 # - Only accepts characters a-z and non-consequitive dashes (-)
1108 # - Application name should not exceed 50 characters
1109 # """
1110 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1111 # new_name = ''
1112 # for c in name:
1113 # if c.isdigit():
1114 # c = chr(97 + int(c))
1115 # elif not c.isalpha():
1116 # c = "-"
1117 # new_name += c
1118 # return re.sub('\-+', '-', new_name.lower())
1119
1120 def format_model_name(self, name):
1121 """Format the name of model.
1122
1123 Model names may only contain lowercase letters, digits and hyphens
1124 """
1125
1126 return name.replace('_', '-').lower()
1127
1128 async def get_application(self, model, application):
1129 """Get the deployed application."""
1130 if not self.authenticated:
1131 await self.login()
1132
1133 app = None
1134 if application and model:
1135 if model.applications:
1136 if application in model.applications:
1137 app = model.applications[application]
1138
1139 return app
1140
Adam Israel85a4b212018-11-29 20:30:24 -05001141 async def get_model(self, model_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001142 """Get a model from the Juju Controller.
1143
1144 Note: Model objects returned must call disconnected() before it goes
1145 out of scope."""
1146 if not self.authenticated:
1147 await self.login()
1148
1149 if model_name not in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001150 # Get the models in the controller
1151 models = await self.controller.list_models()
1152
1153 if model_name not in models:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001154 try:
1155 self.models[model_name] = await self.controller.add_model(
1156 model_name
1157 )
1158 except JujuError as e:
1159 if "already exists" not in e.message:
1160 raise e
Adam Israel85a4b212018-11-29 20:30:24 -05001161 else:
1162 self.models[model_name] = await self.controller.get_model(
1163 model_name
1164 )
1165
Adam Israelfc511ed2018-09-21 14:20:55 +02001166 self.refcount['model'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001167
Adam Israel28a43c02018-04-23 16:04:54 -04001168 # Create an observer for this model
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001169 await self.create_model_monitor(model_name)
1170
1171 return self.models[model_name]
1172
1173 async def create_model_monitor(self, model_name):
1174 """Create a monitor for the model, if none exists."""
1175 if not self.authenticated:
1176 await self.login()
1177
1178 if model_name not in self.monitors:
Adam Israel28a43c02018-04-23 16:04:54 -04001179 self.monitors[model_name] = VCAMonitor(model_name)
1180 self.models[model_name].add_observer(self.monitors[model_name])
1181
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001182 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001183
1184 async def login(self):
1185 """Login to the Juju controller."""
1186
1187 if self.authenticated:
1188 return
1189
1190 self.connecting = True
1191
1192 self.log.debug("JujuApi: Logging into controller")
1193
Adam Israel5e08a0e2018-09-06 19:22:47 -04001194 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001195
1196 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001197 self.log.debug(
1198 "Connecting to controller... ws://{}:{} as {}/{}".format(
1199 self.endpoint,
1200 self.port,
1201 self.user,
1202 self.secret,
1203 )
1204 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001205 await self.controller.connect(
1206 endpoint=self.endpoint,
1207 username=self.user,
1208 password=self.secret,
Adam Israelb2a07f52019-04-25 17:17:05 -04001209 cacert=self.ca_cert,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001210 )
Adam Israelfc511ed2018-09-21 14:20:55 +02001211 self.refcount['controller'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001212 else:
1213 # current_controller no longer exists
1214 # self.log.debug("Connecting to current controller...")
1215 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -06001216 # await self.controller.connect(
1217 # endpoint=self.endpoint,
1218 # username=self.user,
1219 # cacert=cacert,
1220 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001221 self.log.fatal("VCA credentials not configured.")
1222
1223 self.authenticated = True
1224 self.log.debug("JujuApi: Logged into controller")
1225
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001226 async def logout(self):
1227 """Logout of the Juju controller."""
1228 if not self.authenticated:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001229 return False
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001230
1231 try:
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001232 for model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001233 await self.disconnect_model(model)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001234
1235 if self.controller:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001236 self.log.debug("Disconnecting controller {}".format(
1237 self.controller
1238 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001239 await self.controller.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +02001240 self.refcount['controller'] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -04001241 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001242
1243 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +02001244
1245 self.log.debug(self.refcount)
1246
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001247 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001248 self.log.fatal(
1249 "Fatal error logging out of Juju Controller: {}".format(e)
1250 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001251 raise e
Adam Israel6d84dbd2019-03-08 18:33:35 -05001252 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001253
Adam Israel85a4b212018-11-29 20:30:24 -05001254 async def disconnect_model(self, model):
1255 self.log.debug("Disconnecting model {}".format(model))
1256 if model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001257 print("Disconnecting model")
1258 await self.models[model].disconnect()
1259 self.refcount['model'] -= 1
1260 self.models[model] = None
1261
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001262 # async def remove_application(self, name):
1263 # """Remove the application."""
1264 # if not self.authenticated:
1265 # await self.login()
1266 #
1267 # app = await self.get_application(name)
1268 # if app:
1269 # self.log.debug("JujuApi: Destroying application {}".format(
1270 # name,
1271 # ))
1272 #
1273 # await app.destroy()
1274
1275 async def remove_relation(self, a, b):
1276 """
1277 Remove a relation between two application endpoints
1278
1279 :param a An application endpoint
1280 :param b An application endpoint
1281 """
1282 if not self.authenticated:
1283 await self.login()
1284
1285 m = await self.get_model()
1286 try:
1287 m.remove_relation(a, b)
1288 finally:
1289 await m.disconnect()
1290
Adam Israel85a4b212018-11-29 20:30:24 -05001291 async def resolve_error(self, model_name, application=None):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001292 """Resolve units in error state."""
1293 if not self.authenticated:
1294 await self.login()
1295
Adam Israel85a4b212018-11-29 20:30:24 -05001296 model = await self.get_model(model_name)
1297
1298 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001299 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001300 self.log.debug(
1301 "JujuApi: Resolving errors for application {}".format(
1302 application,
1303 )
1304 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001305
1306 for unit in app.units:
1307 app.resolved(retry=True)
1308
Adam Israel85a4b212018-11-29 20:30:24 -05001309 async def run_action(self, model_name, application, action_name, **params):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001310 """Execute an action and return an Action object."""
1311 if not self.authenticated:
1312 await self.login()
1313 result = {
1314 'status': '',
1315 'action': {
1316 'tag': None,
1317 'results': None,
1318 }
1319 }
Adam Israel85a4b212018-11-29 20:30:24 -05001320
1321 model = await self.get_model(model_name)
1322
1323 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001324 if app:
1325 # We currently only have one unit per application
1326 # so use the first unit available.
1327 unit = app.units[0]
1328
Adam Israel5e08a0e2018-09-06 19:22:47 -04001329 self.log.debug(
1330 "JujuApi: Running Action {} against Application {}".format(
1331 action_name,
1332 application,
1333 )
1334 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001335
1336 action = await unit.run_action(action_name, **params)
1337
1338 # Wait for the action to complete
1339 await action.wait()
1340
1341 result['status'] = action.status
1342 result['action']['tag'] = action.data['id']
1343 result['action']['results'] = action.results
1344
1345 return result
1346
Adam Israelb5214512018-05-03 10:00:04 -04001347 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001348 """Apply a configuration to the application."""
1349 if not self.authenticated:
1350 await self.login()
1351
Adam Israelb5214512018-05-03 10:00:04 -04001352 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001353 if app:
1354 self.log.debug("JujuApi: Setting config for Application {}".format(
1355 application,
1356 ))
1357 await app.set_config(config)
1358
1359 # Verify the config is set
1360 newconf = await app.get_config()
1361 for key in config:
1362 if config[key] != newconf[key]['value']:
1363 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1364
Adam Israelb5214512018-05-03 10:00:04 -04001365 # async def set_parameter(self, parameter, value, application=None):
1366 # """Set a config parameter for a service."""
1367 # if not self.authenticated:
1368 # await self.login()
1369 #
1370 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1371 # parameter,
1372 # value,
1373 # application,
1374 # ))
1375 # return await self.apply_config(
1376 # {parameter: value},
1377 # application=application,
1378 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001379
Adam Israel5e08a0e2018-09-06 19:22:47 -04001380 async def wait_for_application(self, model_name, application_name,
1381 timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001382 """Wait for an application to become active."""
1383 if not self.authenticated:
1384 await self.login()
1385
Adam Israel5e08a0e2018-09-06 19:22:47 -04001386 model = await self.get_model(model_name)
1387
1388 app = await self.get_application(model, application_name)
1389 self.log.debug("Application: {}".format(app))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001390 if app:
1391 self.log.debug(
1392 "JujuApi: Waiting {} seconds for Application {}".format(
1393 timeout,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001394 application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001395 )
1396 )
1397
Adam Israel5e08a0e2018-09-06 19:22:47 -04001398 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001399 lambda: all(
Adam Israel5e08a0e2018-09-06 19:22:47 -04001400 unit.agent_status == 'idle' and unit.workload_status in
1401 ['active', 'unknown'] for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001402 ),
1403 timeout=timeout
1404 )