blob: 5a229529df435cc146b68bcd583e3f25cf5743fb [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 Israelc3e6c2e2018-03-01 09:31:50 -050050# Quiet the debug logging
51logging.getLogger('websockets.protocol').setLevel(logging.INFO)
52logging.getLogger('juju.client.connection').setLevel(logging.WARN)
53logging.getLogger('juju.model').setLevel(logging.WARN)
54logging.getLogger('juju.machine').setLevel(logging.WARN)
55
Adam Israelb5214512018-05-03 10:00:04 -040056
Adam Israelc3e6c2e2018-03-01 09:31:50 -050057class VCAMonitor(ModelObserver):
58 """Monitor state changes within the Juju Model."""
Adam Israelc3e6c2e2018-03-01 09:31:50 -050059 log = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -050060
Adam Israel28a43c02018-04-23 16:04:54 -040061 def __init__(self, ns_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050062 self.log = logging.getLogger(__name__)
63
64 self.ns_name = ns_name
Adam Israeld420a8b2019-04-09 16:07:53 -040065 self.applications = {}
Adam Israel28a43c02018-04-23 16:04:54 -040066
67 def AddApplication(self, application_name, callback, *callback_args):
68 if application_name not in self.applications:
69 self.applications[application_name] = {
70 'callback': callback,
71 'callback_args': callback_args
72 }
73
74 def RemoveApplication(self, application_name):
75 if application_name in self.applications:
76 del self.applications[application_name]
Adam Israelc3e6c2e2018-03-01 09:31:50 -050077
78 async def on_change(self, delta, old, new, model):
79 """React to changes in the Juju model."""
80
81 if delta.entity == "unit":
Adam Israel28a43c02018-04-23 16:04:54 -040082 # Ignore change events from other applications
83 if delta.data['application'] not in self.applications.keys():
84 return
85
Adam Israelc3e6c2e2018-03-01 09:31:50 -050086 try:
Adam Israel28a43c02018-04-23 16:04:54 -040087
88 application_name = delta.data['application']
89
90 callback = self.applications[application_name]['callback']
Adam Israel5e08a0e2018-09-06 19:22:47 -040091 callback_args = \
92 self.applications[application_name]['callback_args']
Adam Israel28a43c02018-04-23 16:04:54 -040093
Adam Israelc3e6c2e2018-03-01 09:31:50 -050094 if old and new:
Adam Israelfc511ed2018-09-21 14:20:55 +020095 # Fire off a callback with the application state
96 if callback:
97 callback(
98 self.ns_name,
99 delta.data['application'],
100 new.workload_status,
101 new.workload_status_message,
102 *callback_args)
Adam Israel28a43c02018-04-23 16:04:54 -0400103
104 if old and not new:
105 # This is a charm being removed
106 if callback:
107 callback(
108 self.ns_name,
109 delta.data['application'],
110 "removed",
Adam Israel9562f432018-05-09 13:55:28 -0400111 "",
Adam Israel28a43c02018-04-23 16:04:54 -0400112 *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500113 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400114 self.log.debug("[1] notify_callback exception: {}".format(e))
115
Adam Israel88a49632018-04-10 13:04:57 -0600116 elif delta.entity == "action":
117 # TODO: Decide how we want to notify the user of actions
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500118
Adam Israel88a49632018-04-10 13:04:57 -0600119 # uuid = delta.data['id'] # The Action's unique id
120 # msg = delta.data['message'] # The output of the action
121 #
122 # if delta.data['status'] == "pending":
123 # # The action is queued
124 # pass
125 # elif delta.data['status'] == "completed""
126 # # The action was successful
127 # pass
128 # elif delta.data['status'] == "failed":
129 # # The action failed.
130 # pass
131
132 pass
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500133
134########
135# TODO
136#
137# Create unique models per network service
138# Document all public functions
139
Adam Israelb5214512018-05-03 10:00:04 -0400140
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500141class N2VC:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500142 def __init__(self,
143 log=None,
144 server='127.0.0.1',
145 port=17070,
146 user='admin',
147 secret=None,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400148 artifacts=None,
149 loop=None,
Adam Israelb2a07f52019-04-25 17:17:05 -0400150 juju_public_key=None,
151 ca_cert=None,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500152 ):
153 """Initialize N2VC
Adam Israelb2a07f52019-04-25 17:17:05 -0400154 :param log obj: The logging object to log to
155 :param server str: The IP Address or Hostname of the Juju controller
156 :param port int: The port of the Juju Controller
157 :param user str: The Juju username to authenticate with
158 :param secret str: The Juju password to authenticate with
159 :param artifacts str: The directory where charms required by a vnfd are
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500160 stored.
Adam Israelb2a07f52019-04-25 17:17:05 -0400161 :param loop obj: The loop to use.
162 :param juju_public_key str: The contents of the Juju public SSH key
163 :param ca_cert str: The CA certificate to use to authenticate
164
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500165
166 :Example:
Adam Israelb2a07f52019-04-25 17:17:05 -0400167 client = n2vc.vnf.N2VC(
168 log=log,
169 server='10.1.1.28',
170 port=17070,
171 user='admin',
172 secret='admin',
173 artifacts='/app/storage/myvnf/charms',
174 loop=loop,
175 juju_public_key='<contents of the juju public key>',
176 ca_cert='<contents of CA certificate>',
177 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500178 """
179
Adam Israel5e08a0e2018-09-06 19:22:47 -0400180 # Initialize instance-level variables
181 self.api = None
182 self.log = None
183 self.controller = None
184 self.connecting = False
185 self.authenticated = False
186
Adam Israelfc511ed2018-09-21 14:20:55 +0200187 # For debugging
188 self.refcount = {
189 'controller': 0,
190 'model': 0,
191 }
192
Adam Israel5e08a0e2018-09-06 19:22:47 -0400193 self.models = {}
Adam Israel5e08a0e2018-09-06 19:22:47 -0400194
195 # Model Observers
196 self.monitors = {}
197
198 # VCA config
199 self.hostname = ""
200 self.port = 17070
201 self.username = ""
202 self.secret = ""
203
Adam Israelb2a07f52019-04-25 17:17:05 -0400204 self.juju_public_key = juju_public_key
205 if juju_public_key:
206 self._create_juju_public_key(juju_public_key)
207
208 self.ca_cert = ca_cert
209
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500210 if log:
211 self.log = log
212 else:
213 self.log = logging.getLogger(__name__)
214
215 # Quiet websocket traffic
216 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
217 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
218 logging.getLogger('model').setLevel(logging.WARN)
219 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
220
221 self.log.debug('JujuApi: instantiated')
222
223 self.server = server
224 self.port = port
225
226 self.secret = secret
227 if user.startswith('user-'):
228 self.user = user
229 else:
230 self.user = 'user-{}'.format(user)
231
232 self.endpoint = '%s:%d' % (server, int(port))
233
234 self.artifacts = artifacts
235
Adam Israel5e08a0e2018-09-06 19:22:47 -0400236 self.loop = loop or asyncio.get_event_loop()
237
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500238 def __del__(self):
239 """Close any open connections."""
240 yield self.logout()
241
Adam Israelb2a07f52019-04-25 17:17:05 -0400242 def _create_juju_public_key(self, public_key):
243 """Recreate the Juju public key on disk.
244
245 Certain libjuju commands expect to be run from the same machine as Juju
246 is bootstrapped to. This method will write the public key to disk in
247 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
248 """
Adam Israele3a05f82019-04-26 13:12:47 -0400249 # Make sure that we have a public key before writing to disk
Adam Israelb2a07f52019-04-25 17:17:05 -0400250 if public_key is None or len(public_key) == 0:
Adam Israele3a05f82019-04-26 13:12:47 -0400251 if 'OSM_VCA_PUBKEY' in os.environ:
252 public_key = os.getenv('OSM_VCA_PUBKEY', '')
253 if len(public_key == 0):
254 return
255 else:
256 return
257
Adam Israelb2a07f52019-04-25 17:17:05 -0400258 path = "{}/.local/share/juju/ssh".format(
259 os.path.expanduser('~'),
260 )
261 if not os.path.exists(path):
262 os.makedirs(path)
263
264 with open('{}/juju_id_rsa.pub'.format(path), 'w') as f:
265 f.write(public_key)
266
Adam Israel5e08a0e2018-09-06 19:22:47 -0400267 def notify_callback(self, model_name, application_name, status, message,
268 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500269 try:
270 if callback:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400271 callback(
272 model_name,
273 application_name,
274 status, message,
275 *callback_args,
276 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500277 except Exception as e:
278 self.log.error("[0] notify_callback exception {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600279 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500280 return True
281
282 # Public methods
Adam Israel85a4b212018-11-29 20:30:24 -0500283 async def Relate(self, model_name, vnfd):
Adam Israel136186e2018-09-14 12:01:12 -0400284 """Create a relation between the charm-enabled VDUs in a VNF.
285
286 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
287
288 vdu:
289 ...
Adam Israel47998e02019-06-03 11:21:27 -0400290 vca-relations:
291 relation:
292 - provides: dataVM:db
293 requires: mgmtVM:app
Adam Israel136186e2018-09-14 12:01:12 -0400294
295 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.
296
297 :param str ns_name: The name of the network service.
298 :param dict vnfd: The parsed yaml VNF descriptor.
299 """
300
301 # Currently, the call to Relate() is made automatically after the
302 # deployment of each charm; if the relation depends on a charm that
303 # hasn't been deployed yet, the call will fail silently. This will
304 # prevent an API breakage, with the intent of making this an explicitly
305 # required call in a more object-oriented refactor of the N2VC API.
306
307 configs = []
308 vnf_config = vnfd.get("vnf-configuration")
309 if vnf_config:
310 juju = vnf_config['juju']
311 if juju:
312 configs.append(vnf_config)
313
314 for vdu in vnfd['vdu']:
315 vdu_config = vdu.get('vdu-configuration')
316 if vdu_config:
317 juju = vdu_config['juju']
318 if juju:
319 configs.append(vdu_config)
320
321 def _get_application_name(name):
322 """Get the application name that's mapped to a vnf/vdu."""
323 vnf_member_index = 0
324 vnf_name = vnfd['name']
325
326 for vdu in vnfd.get('vdu'):
327 # Compare the named portion of the relation to the vdu's id
328 if vdu['id'] == name:
329 application_name = self.FormatApplicationName(
Adam Israel85a4b212018-11-29 20:30:24 -0500330 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400331 vnf_name,
332 str(vnf_member_index),
333 )
334 return application_name
335 else:
336 vnf_member_index += 1
337
338 return None
339
340 # Loop through relations
341 for cfg in configs:
342 if 'juju' in cfg:
Adam Israelc92163f2019-05-27 08:39:19 -0400343 juju = cfg['juju']
endika34cc6602019-06-05 14:48:44 +0200344 if 'vca-relations' in juju and 'relation' in juju['vca-relations']:
Adam Israel47998e02019-06-03 11:21:27 -0400345 for rel in juju['vca-relations']['relation']:
Adam Israel136186e2018-09-14 12:01:12 -0400346 try:
347
348 # get the application name for the provides
349 (name, endpoint) = rel['provides'].split(':')
350 application_name = _get_application_name(name)
351
352 provides = "{}:{}".format(
353 application_name,
354 endpoint
355 )
356
357 # get the application name for thr requires
358 (name, endpoint) = rel['requires'].split(':')
359 application_name = _get_application_name(name)
360
361 requires = "{}:{}".format(
362 application_name,
363 endpoint
364 )
365 self.log.debug("Relation: {} <-> {}".format(
366 provides,
367 requires
368 ))
369 await self.add_relation(
Adam Israel85a4b212018-11-29 20:30:24 -0500370 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400371 provides,
372 requires,
373 )
374 except Exception as e:
375 self.log.debug("Exception: {}".format(e))
376
377 return
378
Adam Israel5e08a0e2018-09-06 19:22:47 -0400379 async def DeployCharms(self, model_name, application_name, vnfd,
380 charm_path, params={}, machine_spec={},
381 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500382 """Deploy one or more charms associated with a VNF.
383
384 Deploy the charm(s) referenced in a VNF Descriptor.
385
Adam Israel85a4b212018-11-29 20:30:24 -0500386 :param str model_name: The name or unique id of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500387 :param str application_name: The name of the application
388 :param dict vnfd: The name of the application
389 :param str charm_path: The path to the Juju charm
390 :param dict params: A dictionary of runtime parameters
391 Examples::
392 {
Adam Israel88a49632018-04-10 13:04:57 -0600393 'rw_mgmt_ip': '1.2.3.4',
394 # Pass the initial-config-primitives section of the vnf or vdu
395 'initial-config-primitives': {...}
tierno1afb30a2018-12-21 13:42:43 +0000396 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
397 inside < >. rw_mgmt_ip will be included here also
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500398 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400399 :param dict machine_spec: A dictionary describing the machine to
400 install to
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500401 Examples::
402 {
403 'hostname': '1.2.3.4',
404 'username': 'ubuntu',
405 }
406 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400407 :param tuple callback_args: A list of arguments to be passed to the
408 callback
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500409 """
410
411 ########################################################
412 # Verify the path to the charm exists and is readable. #
413 ########################################################
414 if not os.path.exists(charm_path):
415 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400416 self.notify_callback(
417 model_name,
418 application_name,
419 "failed",
420 callback,
421 *callback_args,
422 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500423 raise JujuCharmNotFound("No artifacts configured.")
424
425 ################################
426 # Login to the Juju controller #
427 ################################
428 if not self.authenticated:
429 self.log.debug("Authenticating with Juju")
430 await self.login()
431
432 ##########################################
433 # Get the model for this network service #
434 ##########################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500435 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500436
437 ########################################
438 # Verify the application doesn't exist #
439 ########################################
440 app = await self.get_application(model, application_name)
441 if app:
Adam Israel42d88e62018-07-16 14:18:41 -0400442 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500443
Adam Israel28a43c02018-04-23 16:04:54 -0400444 ################################################################
445 # Register this application with the model-level event monitor #
446 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500447 if callback:
Adam Israel04eee1f2019-04-29 14:59:45 -0400448 self.log.debug("JujuApi: Registering callback for {}".format(
Adam Israel28a43c02018-04-23 16:04:54 -0400449 application_name,
Adam Israel04eee1f2019-04-29 14:59:45 -0400450 ))
451 await self.Subscribe(model_name, application_name, callback, *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500452
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500453 ########################################################
454 # Check for specific machine placement (native charms) #
455 ########################################################
456 to = ""
457 if machine_spec.keys():
Adam Israel5963cb42018-09-14 11:26:13 -0400458 if all(k in machine_spec for k in ['host', 'user']):
459 # Enlist an existing machine as a Juju unit
460 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
Adam Israelce31bc32019-05-22 16:30:25 -0400461 machine_spec['username'],
462 machine_spec['hostname'],
Adam Israel5963cb42018-09-14 11:26:13 -0400463 self.GetPrivateKeyPath(),
464 ))
Adam Israelfa329072018-09-14 11:26:13 -0400465 to = machine.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500466
467 #######################################
468 # Get the initial charm configuration #
469 #######################################
470
471 rw_mgmt_ip = None
472 if 'rw_mgmt_ip' in params:
473 rw_mgmt_ip = params['rw_mgmt_ip']
474
Adam Israel5afe0542018-08-08 12:54:55 -0400475 if 'initial-config-primitive' not in params:
476 params['initial-config-primitive'] = {}
477
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500478 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600479 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500480 {'<rw_mgmt_ip>': rw_mgmt_ip}
481 )
482
Adam Israel85a4b212018-11-29 20:30:24 -0500483 self.log.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
484 model_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500485 application_name,
486 charm_path,
487 to=to,
488 ))
489
490 ########################################################
491 # Deploy the charm and apply the initial configuration #
492 ########################################################
493 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600494 # We expect charm_path to be either the path to the charm on disk
495 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500496 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600497 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500498 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600499 # Proxy charms should use the current LTS. This will need to be
500 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500501 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600502 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500503 config=initial_config,
Adam Israelfa329072018-09-14 11:26:13 -0400504 # Where to deploy the charm to.
505 to=to,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500506 )
Adam Israel38bf1642019-05-31 09:59:52 -0400507 #############################
508 # Map the vdu id<->app name #
509 #############################
510 try:
511 await self.Relate(model_name, vnfd)
512 except KeyError as ex:
513 # We don't currently support relations between NS and VNF/VDU charms
514 self.log.warn("[N2VC] Relations not supported: {}".format(ex))
515 except Exception as ex:
516 # This may happen if not all of the charms needed by the relation
517 # are ready. We can safely ignore this, because Relate will be
518 # retried when the endpoint of the relation is deployed.
519 self.log.warn("[N2VC] Relations not ready")
Adam Israel136186e2018-09-14 12:01:12 -0400520
Adam Israel88a49632018-04-10 13:04:57 -0600521 # #######################################
522 # # Execute initial config primitive(s) #
523 # #######################################
Adam Israelcf253202018-10-31 16:29:09 -0700524 uuids = await self.ExecuteInitialPrimitives(
Adam Israel5e08a0e2018-09-06 19:22:47 -0400525 model_name,
526 application_name,
527 params,
528 )
Adam Israelcf253202018-10-31 16:29:09 -0700529 return uuids
Adam Israel5e08a0e2018-09-06 19:22:47 -0400530
531 # primitives = {}
532 #
533 # # Build a sequential list of the primitives to execute
534 # for primitive in params['initial-config-primitive']:
535 # try:
536 # if primitive['name'] == 'config':
537 # # This is applied when the Application is deployed
538 # pass
539 # else:
540 # seq = primitive['seq']
541 #
542 # params = {}
543 # if 'parameter' in primitive:
544 # params = primitive['parameter']
545 #
546 # primitives[seq] = {
547 # 'name': primitive['name'],
548 # 'parameters': self._map_primitive_parameters(
549 # params,
550 # {'<rw_mgmt_ip>': rw_mgmt_ip}
551 # ),
552 # }
553 #
554 # for primitive in sorted(primitives):
555 # await self.ExecutePrimitive(
556 # model_name,
557 # application_name,
558 # primitives[primitive]['name'],
559 # callback,
560 # callback_args,
561 # **primitives[primitive]['parameters'],
562 # )
563 # except N2VCPrimitiveExecutionFailed as e:
564 # self.log.debug(
565 # "[N2VC] Exception executing primitive: {}".format(e)
566 # )
567 # raise
568
569 async def GetPrimitiveStatus(self, model_name, uuid):
570 """Get the status of an executed Primitive.
571
572 The status of an executed Primitive will be one of three values:
573 - completed
574 - failed
575 - running
576 """
577 status = None
578 try:
579 if not self.authenticated:
580 await self.login()
581
Adam Israel5e08a0e2018-09-06 19:22:47 -0400582 model = await self.get_model(model_name)
583
584 results = await model.get_action_status(uuid)
585
586 if uuid in results:
587 status = results[uuid]
588
589 except Exception as e:
590 self.log.debug(
591 "Caught exception while getting primitive status: {}".format(e)
592 )
593 raise N2VCPrimitiveExecutionFailed(e)
594
595 return status
596
597 async def GetPrimitiveOutput(self, model_name, uuid):
598 """Get the output of an executed Primitive.
599
600 Note: this only returns output for a successfully executed primitive.
601 """
602 results = None
603 try:
604 if not self.authenticated:
605 await self.login()
606
Adam Israel5e08a0e2018-09-06 19:22:47 -0400607 model = await self.get_model(model_name)
608 results = await model.get_action_output(uuid, 60)
609 except Exception as e:
610 self.log.debug(
611 "Caught exception while getting primitive status: {}".format(e)
612 )
613 raise N2VCPrimitiveExecutionFailed(e)
614
615 return results
616
Adam Israelfa329072018-09-14 11:26:13 -0400617 # async def ProvisionMachine(self, model_name, hostname, username):
618 # """Provision machine for usage with Juju.
619 #
620 # Provisions a previously instantiated machine for use with Juju.
621 # """
622 # try:
623 # if not self.authenticated:
624 # await self.login()
625 #
626 # # FIXME: This is hard-coded until model-per-ns is added
627 # model_name = 'default'
628 #
629 # model = await self.get_model(model_name)
630 # model.add_machine(spec={})
631 #
632 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
633 # "ubuntu",
634 # host['address'],
635 # private_key_path,
636 # ))
637 # return machine.id
638 #
639 # except Exception as e:
640 # self.log.debug(
641 # "Caught exception while getting primitive status: {}".format(e)
642 # )
643 # raise N2VCPrimitiveExecutionFailed(e)
644
645 def GetPrivateKeyPath(self):
646 homedir = os.environ['HOME']
647 sshdir = "{}/.ssh".format(homedir)
648 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
649 return private_key_path
650
651 async def GetPublicKey(self):
652 """Get the N2VC SSH public key.abs
653
654 Returns the SSH public key, to be injected into virtual machines to
655 be managed by the VCA.
656
657 The first time this is run, a ssh keypair will be created. The public
658 key is injected into a VM so that we can provision the machine with
659 Juju, after which Juju will communicate with the VM directly via the
660 juju agent.
661 """
662 public_key = ""
663
664 # Find the path to where we expect our key to live.
665 homedir = os.environ['HOME']
666 sshdir = "{}/.ssh".format(homedir)
667 if not os.path.exists(sshdir):
668 os.mkdir(sshdir)
669
670 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
671 public_key_path = "{}.pub".format(private_key_path)
672
673 # If we don't have a key generated, generate it.
674 if not os.path.exists(private_key_path):
675 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
676 "rsa",
677 "4096",
678 private_key_path
679 )
680 subprocess.check_output(shlex.split(cmd))
681
682 # Read the public key
683 with open(public_key_path, "r") as f:
684 public_key = f.readline()
685
686 return public_key
687
Adam Israel5e08a0e2018-09-06 19:22:47 -0400688 async def ExecuteInitialPrimitives(self, model_name, application_name,
689 params, callback=None, *callback_args):
690 """Execute multiple primitives.
691
692 Execute multiple primitives as declared in initial-config-primitive.
693 This is useful in cases where the primitives initially failed -- for
694 example, if the charm is a proxy but the proxy hasn't been configured
695 yet.
696 """
697 uuids = []
Adam Israel88a49632018-04-10 13:04:57 -0600698 primitives = {}
699
700 # Build a sequential list of the primitives to execute
701 for primitive in params['initial-config-primitive']:
702 try:
703 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600704 pass
705 else:
Adam Israel88a49632018-04-10 13:04:57 -0600706 seq = primitive['seq']
707
tierno1afb30a2018-12-21 13:42:43 +0000708 params_ = {}
Adam Israel42d88e62018-07-16 14:18:41 -0400709 if 'parameter' in primitive:
tierno1afb30a2018-12-21 13:42:43 +0000710 params_ = primitive['parameter']
711
712 user_values = params.get("user_values", {})
713 if 'rw_mgmt_ip' not in user_values:
714 user_values['rw_mgmt_ip'] = None
715 # just for backward compatibility, because it will be provided always by modern version of LCM
Adam Israel42d88e62018-07-16 14:18:41 -0400716
Adam Israel88a49632018-04-10 13:04:57 -0600717 primitives[seq] = {
718 'name': primitive['name'],
719 'parameters': self._map_primitive_parameters(
tierno1afb30a2018-12-21 13:42:43 +0000720 params_,
721 user_values
Adam Israel88a49632018-04-10 13:04:57 -0600722 ),
723 }
724
725 for primitive in sorted(primitives):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400726 uuids.append(
727 await self.ExecutePrimitive(
728 model_name,
729 application_name,
730 primitives[primitive]['name'],
731 callback,
732 callback_args,
733 **primitives[primitive]['parameters'],
734 )
Adam Israel88a49632018-04-10 13:04:57 -0600735 )
736 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400737 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600738 "[N2VC] Exception executing primitive: {}".format(e)
739 )
740 raise
Adam Israel5e08a0e2018-09-06 19:22:47 -0400741 return uuids
Adam Israel88a49632018-04-10 13:04:57 -0600742
Adam Israel5e08a0e2018-09-06 19:22:47 -0400743 async def ExecutePrimitive(self, model_name, application_name, primitive,
744 callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400745 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600746
Adam Israelc9df96f2018-05-03 14:49:56 -0400747 Execute a primitive defined in the VNF descriptor.
748
Adam Israel85a4b212018-11-29 20:30:24 -0500749 :param str model_name: The name or unique id of the network service.
Adam Israelc9df96f2018-05-03 14:49:56 -0400750 :param str application_name: The name of the application
751 :param str primitive: The name of the primitive to execute.
752 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400753 :param tuple callback_args: A list of arguments to be passed to the
754 callback function.
755 :param dict params: A dictionary of key=value pairs representing the
756 primitive's parameters
Adam Israelc9df96f2018-05-03 14:49:56 -0400757 Examples::
758 {
759 'rw_mgmt_ip': '1.2.3.4',
760 # Pass the initial-config-primitives section of the vnf or vdu
761 'initial-config-primitives': {...}
762 }
Adam Israel6817f612018-04-13 08:41:43 -0600763 """
tierno1afb30a2018-12-21 13:42:43 +0000764 self.log.debug("Executing primitive={} params={}".format(primitive, params))
Adam Israel6817f612018-04-13 08:41:43 -0600765 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500766 try:
767 if not self.authenticated:
768 await self.login()
769
Adam Israel5e08a0e2018-09-06 19:22:47 -0400770 model = await self.get_model(model_name)
Adam Israelb5214512018-05-03 10:00:04 -0400771
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500772 if primitive == 'config':
773 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400774 await self.set_config(
775 model,
776 application_name,
777 params['params'],
778 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500779 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500780 app = await self.get_application(model, application_name)
781 if app:
782 # Run against the first (and probably only) unit in the app
783 unit = app.units[0]
784 if unit:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500785 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600786 uuid = action.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500787 except Exception as e:
Adam Israelb0943662018-08-02 15:32:00 -0400788 self.log.debug(
789 "Caught exception while executing primitive: {}".format(e)
790 )
Adam Israel7d871fb2018-07-17 12:17:06 -0400791 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600792 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500793
Adam Israel5e08a0e2018-09-06 19:22:47 -0400794 async def RemoveCharms(self, model_name, application_name, callback=None,
795 *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400796 """Remove a charm from the VCA.
797
798 Remove a charm referenced in a VNF Descriptor.
799
800 :param str model_name: The name of the network service.
801 :param str application_name: The name of the application
802 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400803 :param tuple callback_args: A list of arguments to be passed to the
804 callback function.
Adam Israelc9df96f2018-05-03 14:49:56 -0400805 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500806 try:
807 if not self.authenticated:
808 await self.login()
809
810 model = await self.get_model(model_name)
811 app = await self.get_application(model, application_name)
812 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400813 # Remove this application from event monitoring
Adam Israel04eee1f2019-04-29 14:59:45 -0400814 await self.Unsubscribe(model_name, application_name)
Adam Israel28a43c02018-04-23 16:04:54 -0400815
816 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400817 self.log.debug(
818 "Removing the application {}".format(application_name)
819 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500820 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400821
Adam Israel85a4b212018-11-29 20:30:24 -0500822 await self.disconnect_model(self.monitors[model_name])
823
Adam Israel5e08a0e2018-09-06 19:22:47 -0400824 self.notify_callback(
825 model_name,
826 application_name,
827 "removed",
Adam Israelc4f393e2019-03-19 16:33:30 -0400828 "Removing charm {}".format(application_name),
Adam Israel5e08a0e2018-09-06 19:22:47 -0400829 callback,
830 *callback_args,
831 )
Adam Israel28a43c02018-04-23 16:04:54 -0400832
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500833 except Exception as e:
834 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600835 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500836 raise e
837
Adam Israel6d84dbd2019-03-08 18:33:35 -0500838 async def CreateNetworkService(self, ns_uuid):
839 """Create a new Juju model for the Network Service.
840
841 Creates a new Model in the Juju Controller.
842
843 :param str ns_uuid: A unique id representing an instaance of a
844 Network Service.
845
846 :returns: True if the model was created. Raises JujuError on failure.
847 """
848 if not self.authenticated:
849 await self.login()
850
851 models = await self.controller.list_models()
852 if ns_uuid not in models:
853 try:
854 self.models[ns_uuid] = await self.controller.add_model(
855 ns_uuid
856 )
857 except JujuError as e:
858 if "already exists" not in e.message:
859 raise e
Adam Israel7bf2f4d2019-03-15 15:28:47 -0400860
861 # Create an observer for this model
862 await self.create_model_monitor(ns_uuid)
863
Adam Israel6d84dbd2019-03-08 18:33:35 -0500864 return True
865
866 async def DestroyNetworkService(self, ns_uuid):
867 """Destroy a Network Service.
868
869 Destroy the Network Service and any deployed charms.
870
871 :param ns_uuid The unique id of the Network Service
872
873 :returns: True if the model was created. Raises JujuError on failure.
874 """
875
876 # Do not delete the default model. The default model was used by all
877 # Network Services, prior to the implementation of a model per NS.
Adam Israelc4f393e2019-03-19 16:33:30 -0400878 if ns_uuid.lower() == "default":
Adam Israel6d84dbd2019-03-08 18:33:35 -0500879 return False
880
881 if not self.authenticated:
882 self.log.debug("Authenticating with Juju")
883 await self.login()
884
885 # Disconnect from the Model
886 if ns_uuid in self.models:
887 await self.disconnect_model(self.models[ns_uuid])
888
889 try:
890 await self.controller.destroy_models(ns_uuid)
Adam Israelc4f393e2019-03-19 16:33:30 -0400891 except JujuError:
Adam Israel6d84dbd2019-03-08 18:33:35 -0500892 raise NetworkServiceDoesNotExist(
893 "The Network Service '{}' does not exist".format(ns_uuid)
894 )
895
896 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500897
Adam Israelb5214512018-05-03 10:00:04 -0400898 async def GetMetrics(self, model_name, application_name):
899 """Get the metrics collected by the VCA.
900
Adam Israel85a4b212018-11-29 20:30:24 -0500901 :param model_name The name or unique id of the network service
Adam Israelb5214512018-05-03 10:00:04 -0400902 :param application_name The name of the application
903 """
904 metrics = {}
905 model = await self.get_model(model_name)
906 app = await self.get_application(model, application_name)
907 if app:
908 metrics = await app.get_metrics()
909
910 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500911
Adam Israelfa329072018-09-14 11:26:13 -0400912 async def HasApplication(self, model_name, application_name):
913 model = await self.get_model(model_name)
914 app = await self.get_application(model, application_name)
915 if app:
916 return True
917 return False
918
Adam Israel04eee1f2019-04-29 14:59:45 -0400919 async def Subscribe(self, ns_name, application_name, callback, *callback_args):
920 """Subscribe to callbacks for an application.
921
922 :param ns_name str: The name of the Network Service
923 :param application_name str: The name of the application
924 :param callback obj: The callback method
925 :param callback_args list: The list of arguments to append to calls to
926 the callback method
927 """
928 self.monitors[ns_name].AddApplication(
929 application_name,
930 callback,
931 *callback_args
932 )
933
934 async def Unsubscribe(self, ns_name, application_name):
935 """Unsubscribe to callbacks for an application.
936
937 Unsubscribes the caller from notifications from a deployed application.
938
939 :param ns_name str: The name of the Network Service
940 :param application_name str: The name of the application
941 """
942 self.monitors[ns_name].RemoveApplication(
943 application_name,
944 )
945
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500946 # Non-public methods
Adam Israel136186e2018-09-14 12:01:12 -0400947 async def add_relation(self, model_name, relation1, relation2):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500948 """
949 Add a relation between two application endpoints.
950
Adam Israel85a4b212018-11-29 20:30:24 -0500951 :param str model_name: The name or unique id of the network service
952 :param str relation1: '<application>[:<relation_name>]'
953 :param str relation2: '<application>[:<relation_name>]'
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500954 """
Adam Israel136186e2018-09-14 12:01:12 -0400955
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500956 if not self.authenticated:
957 await self.login()
958
Adam Israel136186e2018-09-14 12:01:12 -0400959 m = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500960 try:
Adam Israel136186e2018-09-14 12:01:12 -0400961 await m.add_relation(relation1, relation2)
962 except JujuAPIError as e:
963 # If one of the applications in the relationship doesn't exist,
964 # or the relation has already been added, let the operation fail
965 # silently.
966 if 'not found' in e.message:
967 return
968 if 'already exists' in e.message:
969 return
970
971 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500972
Adam Israelb5214512018-05-03 10:00:04 -0400973 # async def apply_config(self, config, application):
974 # """Apply a configuration to the application."""
975 # print("JujuApi: Applying configuration to {}.".format(
976 # application
977 # ))
978 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500979
980 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600981 """Transform the yang config primitive to dict.
982
983 Expected result:
984
985 config = {
986 'config':
987 }
988 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500989 config = {}
990 for primitive in config_primitive:
991 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600992 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500993 for parameter in primitive['parameter']:
994 param = str(parameter['name'])
995 if parameter['value'] == "<rw_mgmt_ip>":
996 config[param] = str(values[parameter['value']])
997 else:
998 config[param] = str(parameter['value'])
999
1000 return config
1001
tierno1afb30a2018-12-21 13:42:43 +00001002 def _map_primitive_parameters(self, parameters, user_values):
Adam Israel88a49632018-04-10 13:04:57 -06001003 params = {}
1004 for parameter in parameters:
1005 param = str(parameter['name'])
tierno1afb30a2018-12-21 13:42:43 +00001006 value = parameter.get('value')
1007
1008 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
1009 # Must exist at user_values except if there is a default value
1010 if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
1011 if parameter['value'][1:-1] in user_values:
1012 value = user_values[parameter['value'][1:-1]]
1013 elif 'default-value' in parameter:
1014 value = parameter['default-value']
1015 else:
1016 raise KeyError("parameter {}='{}' not supplied ".format(param, value))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001017
Adam Israelbf793522018-11-20 13:54:13 -05001018 # If there's no value, use the default-value (if set)
tierno1afb30a2018-12-21 13:42:43 +00001019 if value is None and 'default-value' in parameter:
Adam Israelbf793522018-11-20 13:54:13 -05001020 value = parameter['default-value']
1021
Adam Israel5e08a0e2018-09-06 19:22:47 -04001022 # Typecast parameter value, if present
tierno1afb30a2018-12-21 13:42:43 +00001023 paramtype = "string"
1024 try:
1025 if 'data-type' in parameter:
1026 paramtype = str(parameter['data-type']).lower()
Adam Israel5e08a0e2018-09-06 19:22:47 -04001027
tierno1afb30a2018-12-21 13:42:43 +00001028 if paramtype == "integer":
1029 value = int(value)
1030 elif paramtype == "boolean":
1031 value = bool(value)
1032 else:
1033 value = str(value)
Adam Israel5e08a0e2018-09-06 19:22:47 -04001034 else:
tierno1afb30a2018-12-21 13:42:43 +00001035 # If there's no data-type, assume the value is a string
1036 value = str(value)
1037 except ValueError:
1038 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param, value, paramtype))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001039
tierno1afb30a2018-12-21 13:42:43 +00001040 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -06001041 return params
1042
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001043 def _get_config_from_yang(self, config_primitive, values):
1044 """Transform the yang config primitive to dict."""
1045 config = {}
1046 for primitive in config_primitive.values():
1047 if primitive['name'] == 'config':
1048 for parameter in primitive['parameter'].values():
1049 param = str(parameter['name'])
1050 if parameter['value'] == "<rw_mgmt_ip>":
1051 config[param] = str(values[parameter['value']])
1052 else:
1053 config[param] = str(parameter['value'])
1054
1055 return config
1056
1057 def FormatApplicationName(self, *args):
1058 """
1059 Generate a Juju-compatible Application name
1060
1061 :param args tuple: Positional arguments to be used to construct the
1062 application name.
1063
1064 Limitations::
1065 - Only accepts characters a-z and non-consequitive dashes (-)
1066 - Application name should not exceed 50 characters
1067
1068 Examples::
1069
1070 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
1071 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001072 appname = ""
1073 for c in "-".join(list(args)):
1074 if c.isdigit():
1075 c = chr(97 + int(c))
1076 elif not c.isalpha():
1077 c = "-"
1078 appname += c
Adam Israel6d84dbd2019-03-08 18:33:35 -05001079 return re.sub('-+', '-', appname.lower())
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001080
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001081 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1082 # """Format the name of the application
1083 #
1084 # Limitations:
1085 # - Only accepts characters a-z and non-consequitive dashes (-)
1086 # - Application name should not exceed 50 characters
1087 # """
1088 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1089 # new_name = ''
1090 # for c in name:
1091 # if c.isdigit():
1092 # c = chr(97 + int(c))
1093 # elif not c.isalpha():
1094 # c = "-"
1095 # new_name += c
1096 # return re.sub('\-+', '-', new_name.lower())
1097
1098 def format_model_name(self, name):
1099 """Format the name of model.
1100
1101 Model names may only contain lowercase letters, digits and hyphens
1102 """
1103
1104 return name.replace('_', '-').lower()
1105
1106 async def get_application(self, model, application):
1107 """Get the deployed application."""
1108 if not self.authenticated:
1109 await self.login()
1110
1111 app = None
1112 if application and model:
1113 if model.applications:
1114 if application in model.applications:
1115 app = model.applications[application]
1116
1117 return app
1118
Adam Israel85a4b212018-11-29 20:30:24 -05001119 async def get_model(self, model_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001120 """Get a model from the Juju Controller.
1121
1122 Note: Model objects returned must call disconnected() before it goes
1123 out of scope."""
1124 if not self.authenticated:
1125 await self.login()
1126
1127 if model_name not in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001128 # Get the models in the controller
1129 models = await self.controller.list_models()
1130
1131 if model_name not in models:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001132 try:
1133 self.models[model_name] = await self.controller.add_model(
1134 model_name
1135 )
1136 except JujuError as e:
1137 if "already exists" not in e.message:
1138 raise e
Adam Israel85a4b212018-11-29 20:30:24 -05001139 else:
1140 self.models[model_name] = await self.controller.get_model(
1141 model_name
1142 )
1143
Adam Israelfc511ed2018-09-21 14:20:55 +02001144 self.refcount['model'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001145
Adam Israel28a43c02018-04-23 16:04:54 -04001146 # Create an observer for this model
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001147 await self.create_model_monitor(model_name)
1148
1149 return self.models[model_name]
1150
1151 async def create_model_monitor(self, model_name):
1152 """Create a monitor for the model, if none exists."""
1153 if not self.authenticated:
1154 await self.login()
1155
1156 if model_name not in self.monitors:
Adam Israel28a43c02018-04-23 16:04:54 -04001157 self.monitors[model_name] = VCAMonitor(model_name)
1158 self.models[model_name].add_observer(self.monitors[model_name])
1159
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001160 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001161
1162 async def login(self):
1163 """Login to the Juju controller."""
1164
1165 if self.authenticated:
1166 return
1167
1168 self.connecting = True
1169
1170 self.log.debug("JujuApi: Logging into controller")
1171
Adam Israel5e08a0e2018-09-06 19:22:47 -04001172 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001173
1174 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001175 self.log.debug(
1176 "Connecting to controller... ws://{}:{} as {}/{}".format(
1177 self.endpoint,
1178 self.port,
1179 self.user,
1180 self.secret,
1181 )
1182 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001183 await self.controller.connect(
1184 endpoint=self.endpoint,
1185 username=self.user,
1186 password=self.secret,
Adam Israelb2a07f52019-04-25 17:17:05 -04001187 cacert=self.ca_cert,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001188 )
Adam Israelfc511ed2018-09-21 14:20:55 +02001189 self.refcount['controller'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001190 else:
1191 # current_controller no longer exists
1192 # self.log.debug("Connecting to current controller...")
1193 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -06001194 # await self.controller.connect(
1195 # endpoint=self.endpoint,
1196 # username=self.user,
1197 # cacert=cacert,
1198 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001199 self.log.fatal("VCA credentials not configured.")
1200
1201 self.authenticated = True
1202 self.log.debug("JujuApi: Logged into controller")
1203
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001204 async def logout(self):
1205 """Logout of the Juju controller."""
1206 if not self.authenticated:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001207 return False
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001208
1209 try:
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001210 for model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001211 await self.disconnect_model(model)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001212
1213 if self.controller:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001214 self.log.debug("Disconnecting controller {}".format(
1215 self.controller
1216 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001217 await self.controller.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +02001218 self.refcount['controller'] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -04001219 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001220
1221 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +02001222
1223 self.log.debug(self.refcount)
1224
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001225 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001226 self.log.fatal(
1227 "Fatal error logging out of Juju Controller: {}".format(e)
1228 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001229 raise e
Adam Israel6d84dbd2019-03-08 18:33:35 -05001230 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001231
Adam Israel85a4b212018-11-29 20:30:24 -05001232 async def disconnect_model(self, model):
1233 self.log.debug("Disconnecting model {}".format(model))
1234 if model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001235 print("Disconnecting model")
1236 await self.models[model].disconnect()
1237 self.refcount['model'] -= 1
1238 self.models[model] = None
1239
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001240 # async def remove_application(self, name):
1241 # """Remove the application."""
1242 # if not self.authenticated:
1243 # await self.login()
1244 #
1245 # app = await self.get_application(name)
1246 # if app:
1247 # self.log.debug("JujuApi: Destroying application {}".format(
1248 # name,
1249 # ))
1250 #
1251 # await app.destroy()
1252
1253 async def remove_relation(self, a, b):
1254 """
1255 Remove a relation between two application endpoints
1256
1257 :param a An application endpoint
1258 :param b An application endpoint
1259 """
1260 if not self.authenticated:
1261 await self.login()
1262
1263 m = await self.get_model()
1264 try:
1265 m.remove_relation(a, b)
1266 finally:
1267 await m.disconnect()
1268
Adam Israel85a4b212018-11-29 20:30:24 -05001269 async def resolve_error(self, model_name, application=None):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001270 """Resolve units in error state."""
1271 if not self.authenticated:
1272 await self.login()
1273
Adam Israel85a4b212018-11-29 20:30:24 -05001274 model = await self.get_model(model_name)
1275
1276 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001277 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001278 self.log.debug(
1279 "JujuApi: Resolving errors for application {}".format(
1280 application,
1281 )
1282 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001283
1284 for unit in app.units:
1285 app.resolved(retry=True)
1286
Adam Israel85a4b212018-11-29 20:30:24 -05001287 async def run_action(self, model_name, application, action_name, **params):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001288 """Execute an action and return an Action object."""
1289 if not self.authenticated:
1290 await self.login()
1291 result = {
1292 'status': '',
1293 'action': {
1294 'tag': None,
1295 'results': None,
1296 }
1297 }
Adam Israel85a4b212018-11-29 20:30:24 -05001298
1299 model = await self.get_model(model_name)
1300
1301 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001302 if app:
1303 # We currently only have one unit per application
1304 # so use the first unit available.
1305 unit = app.units[0]
1306
Adam Israel5e08a0e2018-09-06 19:22:47 -04001307 self.log.debug(
1308 "JujuApi: Running Action {} against Application {}".format(
1309 action_name,
1310 application,
1311 )
1312 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001313
1314 action = await unit.run_action(action_name, **params)
1315
1316 # Wait for the action to complete
1317 await action.wait()
1318
1319 result['status'] = action.status
1320 result['action']['tag'] = action.data['id']
1321 result['action']['results'] = action.results
1322
1323 return result
1324
Adam Israelb5214512018-05-03 10:00:04 -04001325 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001326 """Apply a configuration to the application."""
1327 if not self.authenticated:
1328 await self.login()
1329
Adam Israelb5214512018-05-03 10:00:04 -04001330 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001331 if app:
1332 self.log.debug("JujuApi: Setting config for Application {}".format(
1333 application,
1334 ))
1335 await app.set_config(config)
1336
1337 # Verify the config is set
1338 newconf = await app.get_config()
1339 for key in config:
1340 if config[key] != newconf[key]['value']:
1341 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1342
Adam Israelb5214512018-05-03 10:00:04 -04001343 # async def set_parameter(self, parameter, value, application=None):
1344 # """Set a config parameter for a service."""
1345 # if not self.authenticated:
1346 # await self.login()
1347 #
1348 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1349 # parameter,
1350 # value,
1351 # application,
1352 # ))
1353 # return await self.apply_config(
1354 # {parameter: value},
1355 # application=application,
1356 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001357
Adam Israel5e08a0e2018-09-06 19:22:47 -04001358 async def wait_for_application(self, model_name, application_name,
1359 timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001360 """Wait for an application to become active."""
1361 if not self.authenticated:
1362 await self.login()
1363
Adam Israel5e08a0e2018-09-06 19:22:47 -04001364 model = await self.get_model(model_name)
1365
1366 app = await self.get_application(model, application_name)
1367 self.log.debug("Application: {}".format(app))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001368 if app:
1369 self.log.debug(
1370 "JujuApi: Waiting {} seconds for Application {}".format(
1371 timeout,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001372 application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001373 )
1374 )
1375
Adam Israel5e08a0e2018-09-06 19:22:47 -04001376 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001377 lambda: all(
Adam Israel5e08a0e2018-09-06 19:22:47 -04001378 unit.agent_status == 'idle' and unit.workload_status in
1379 ['active', 'unknown'] for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001380 ),
1381 timeout=timeout
1382 )