blob: 31e48773671c2428954c446576706d5de00dcb15 [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 Israel136186e2018-09-14 12:01:12 -040022from juju.errors import JujuAPIError
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
46# Quiet the debug logging
47logging.getLogger('websockets.protocol').setLevel(logging.INFO)
48logging.getLogger('juju.client.connection').setLevel(logging.WARN)
49logging.getLogger('juju.model').setLevel(logging.WARN)
50logging.getLogger('juju.machine').setLevel(logging.WARN)
51
Adam Israelb5214512018-05-03 10:00:04 -040052
Adam Israelc3e6c2e2018-03-01 09:31:50 -050053class VCAMonitor(ModelObserver):
54 """Monitor state changes within the Juju Model."""
Adam Israelc3e6c2e2018-03-01 09:31:50 -050055 log = None
56 ns_name = None
Adam Israel28a43c02018-04-23 16:04:54 -040057 applications = {}
Adam Israelc3e6c2e2018-03-01 09:31:50 -050058
Adam Israel28a43c02018-04-23 16:04:54 -040059 def __init__(self, ns_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050060 self.log = logging.getLogger(__name__)
61
62 self.ns_name = ns_name
Adam Israel28a43c02018-04-23 16:04:54 -040063
64 def AddApplication(self, application_name, callback, *callback_args):
65 if application_name not in self.applications:
66 self.applications[application_name] = {
67 'callback': callback,
68 'callback_args': callback_args
69 }
70
71 def RemoveApplication(self, application_name):
72 if application_name in self.applications:
73 del self.applications[application_name]
Adam Israelc3e6c2e2018-03-01 09:31:50 -050074
75 async def on_change(self, delta, old, new, model):
76 """React to changes in the Juju model."""
77
78 if delta.entity == "unit":
Adam Israel28a43c02018-04-23 16:04:54 -040079 # Ignore change events from other applications
80 if delta.data['application'] not in self.applications.keys():
81 return
82
Adam Israelc3e6c2e2018-03-01 09:31:50 -050083 try:
Adam Israel28a43c02018-04-23 16:04:54 -040084
85 application_name = delta.data['application']
86
87 callback = self.applications[application_name]['callback']
Adam Israel5e08a0e2018-09-06 19:22:47 -040088 callback_args = \
89 self.applications[application_name]['callback_args']
Adam Israel28a43c02018-04-23 16:04:54 -040090
Adam Israelc3e6c2e2018-03-01 09:31:50 -050091 if old and new:
Adam Israelfc511ed2018-09-21 14:20:55 +020092 # Fire off a callback with the application state
93 if callback:
94 callback(
95 self.ns_name,
96 delta.data['application'],
97 new.workload_status,
98 new.workload_status_message,
99 *callback_args)
Adam Israel28a43c02018-04-23 16:04:54 -0400100
101 if old and not new:
102 # This is a charm being removed
103 if callback:
104 callback(
105 self.ns_name,
106 delta.data['application'],
107 "removed",
Adam Israel9562f432018-05-09 13:55:28 -0400108 "",
Adam Israel28a43c02018-04-23 16:04:54 -0400109 *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500110 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400111 self.log.debug("[1] notify_callback exception: {}".format(e))
112
Adam Israel88a49632018-04-10 13:04:57 -0600113 elif delta.entity == "action":
114 # TODO: Decide how we want to notify the user of actions
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500115
Adam Israel88a49632018-04-10 13:04:57 -0600116 # uuid = delta.data['id'] # The Action's unique id
117 # msg = delta.data['message'] # The output of the action
118 #
119 # if delta.data['status'] == "pending":
120 # # The action is queued
121 # pass
122 # elif delta.data['status'] == "completed""
123 # # The action was successful
124 # pass
125 # elif delta.data['status'] == "failed":
126 # # The action failed.
127 # pass
128
129 pass
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500130
131########
132# TODO
133#
134# Create unique models per network service
135# Document all public functions
136
Adam Israelb5214512018-05-03 10:00:04 -0400137
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500138class N2VC:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500139 def __init__(self,
140 log=None,
141 server='127.0.0.1',
142 port=17070,
143 user='admin',
144 secret=None,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400145 artifacts=None,
146 loop=None,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500147 ):
148 """Initialize N2VC
149
150 :param vcaconfig dict A dictionary containing the VCA configuration
151
152 :param artifacts str The directory where charms required by a vnfd are
153 stored.
154
155 :Example:
156 n2vc = N2VC(vcaconfig={
157 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
158 'user': 'admin',
159 'ip-address': '10.44.127.137',
160 'port': 17070,
161 'artifacts': '/path/to/charms'
162 })
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500163 """
164
Adam Israel5e08a0e2018-09-06 19:22:47 -0400165 # Initialize instance-level variables
166 self.api = None
167 self.log = None
168 self.controller = None
169 self.connecting = False
170 self.authenticated = False
171
Adam Israelfc511ed2018-09-21 14:20:55 +0200172 # For debugging
173 self.refcount = {
174 'controller': 0,
175 'model': 0,
176 }
177
Adam Israel5e08a0e2018-09-06 19:22:47 -0400178 self.models = {}
179 self.default_model = None
180
181 # Model Observers
182 self.monitors = {}
183
184 # VCA config
185 self.hostname = ""
186 self.port = 17070
187 self.username = ""
188 self.secret = ""
189
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500190 if log:
191 self.log = log
192 else:
193 self.log = logging.getLogger(__name__)
194
195 # Quiet websocket traffic
196 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
197 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
198 logging.getLogger('model').setLevel(logging.WARN)
199 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
200
201 self.log.debug('JujuApi: instantiated')
202
203 self.server = server
204 self.port = port
205
206 self.secret = secret
207 if user.startswith('user-'):
208 self.user = user
209 else:
210 self.user = 'user-{}'.format(user)
211
212 self.endpoint = '%s:%d' % (server, int(port))
213
214 self.artifacts = artifacts
215
Adam Israel5e08a0e2018-09-06 19:22:47 -0400216 self.loop = loop or asyncio.get_event_loop()
217
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500218 def __del__(self):
219 """Close any open connections."""
220 yield self.logout()
221
Adam Israel5e08a0e2018-09-06 19:22:47 -0400222 def notify_callback(self, model_name, application_name, status, message,
223 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500224 try:
225 if callback:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400226 callback(
227 model_name,
228 application_name,
229 status, message,
230 *callback_args,
231 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500232 except Exception as e:
233 self.log.error("[0] notify_callback exception {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600234 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500235 return True
236
237 # Public methods
238 async def CreateNetworkService(self, nsd):
239 """Create a new model to encapsulate this network service.
240
241 Create a new model in the Juju controller to encapsulate the
242 charms associated with a network service.
243
244 You can pass either the nsd record or the id of the network
245 service, but this method will fail without one of them.
246 """
247 if not self.authenticated:
248 await self.login()
249
250 # Ideally, we will create a unique model per network service.
251 # This change will require all components, i.e., LCM and SO, to use
252 # N2VC for 100% compatibility. If we adopt unique models for the LCM,
253 # services deployed via LCM would't be manageable via SO and vice versa
254
255 return self.default_model
256
Adam Israel136186e2018-09-14 12:01:12 -0400257 async def Relate(self, ns_name, vnfd):
258 """Create a relation between the charm-enabled VDUs in a VNF.
259
260 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
261
262 vdu:
263 ...
264 relation:
265 - provides: dataVM:db
266 requires: mgmtVM:app
267
268 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.
269
270 :param str ns_name: The name of the network service.
271 :param dict vnfd: The parsed yaml VNF descriptor.
272 """
273
274 # Currently, the call to Relate() is made automatically after the
275 # deployment of each charm; if the relation depends on a charm that
276 # hasn't been deployed yet, the call will fail silently. This will
277 # prevent an API breakage, with the intent of making this an explicitly
278 # required call in a more object-oriented refactor of the N2VC API.
279
280 configs = []
281 vnf_config = vnfd.get("vnf-configuration")
282 if vnf_config:
283 juju = vnf_config['juju']
284 if juju:
285 configs.append(vnf_config)
286
287 for vdu in vnfd['vdu']:
288 vdu_config = vdu.get('vdu-configuration')
289 if vdu_config:
290 juju = vdu_config['juju']
291 if juju:
292 configs.append(vdu_config)
293
294 def _get_application_name(name):
295 """Get the application name that's mapped to a vnf/vdu."""
296 vnf_member_index = 0
297 vnf_name = vnfd['name']
298
299 for vdu in vnfd.get('vdu'):
300 # Compare the named portion of the relation to the vdu's id
301 if vdu['id'] == name:
302 application_name = self.FormatApplicationName(
303 ns_name,
304 vnf_name,
305 str(vnf_member_index),
306 )
307 return application_name
308 else:
309 vnf_member_index += 1
310
311 return None
312
313 # Loop through relations
314 for cfg in configs:
315 if 'juju' in cfg:
316 if 'relation' in juju:
317 for rel in juju['relation']:
318 try:
319
320 # get the application name for the provides
321 (name, endpoint) = rel['provides'].split(':')
322 application_name = _get_application_name(name)
323
324 provides = "{}:{}".format(
325 application_name,
326 endpoint
327 )
328
329 # get the application name for thr requires
330 (name, endpoint) = rel['requires'].split(':')
331 application_name = _get_application_name(name)
332
333 requires = "{}:{}".format(
334 application_name,
335 endpoint
336 )
337 self.log.debug("Relation: {} <-> {}".format(
338 provides,
339 requires
340 ))
341 await self.add_relation(
342 ns_name,
343 provides,
344 requires,
345 )
346 except Exception as e:
347 self.log.debug("Exception: {}".format(e))
348
349 return
350
Adam Israel5e08a0e2018-09-06 19:22:47 -0400351 async def DeployCharms(self, model_name, application_name, vnfd,
352 charm_path, params={}, machine_spec={},
353 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500354 """Deploy one or more charms associated with a VNF.
355
356 Deploy the charm(s) referenced in a VNF Descriptor.
357
Adam Israelc9df96f2018-05-03 14:49:56 -0400358 :param str model_name: The name of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500359 :param str application_name: The name of the application
360 :param dict vnfd: The name of the application
361 :param str charm_path: The path to the Juju charm
362 :param dict params: A dictionary of runtime parameters
363 Examples::
364 {
Adam Israel88a49632018-04-10 13:04:57 -0600365 'rw_mgmt_ip': '1.2.3.4',
366 # Pass the initial-config-primitives section of the vnf or vdu
367 'initial-config-primitives': {...}
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500368 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400369 :param dict machine_spec: A dictionary describing the machine to
370 install to
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500371 Examples::
372 {
373 'hostname': '1.2.3.4',
374 'username': 'ubuntu',
375 }
376 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400377 :param tuple callback_args: A list of arguments to be passed to the
378 callback
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500379 """
380
381 ########################################################
382 # Verify the path to the charm exists and is readable. #
383 ########################################################
384 if not os.path.exists(charm_path):
385 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400386 self.notify_callback(
387 model_name,
388 application_name,
389 "failed",
390 callback,
391 *callback_args,
392 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500393 raise JujuCharmNotFound("No artifacts configured.")
394
395 ################################
396 # Login to the Juju controller #
397 ################################
398 if not self.authenticated:
399 self.log.debug("Authenticating with Juju")
400 await self.login()
401
402 ##########################################
403 # Get the model for this network service #
404 ##########################################
405 # TODO: In a point release, we will use a model per deployed network
406 # service. In the meantime, we will always use the 'default' model.
407 model_name = 'default'
408 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500409
410 ########################################
411 # Verify the application doesn't exist #
412 ########################################
413 app = await self.get_application(model, application_name)
414 if app:
Adam Israel42d88e62018-07-16 14:18:41 -0400415 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500416
Adam Israel28a43c02018-04-23 16:04:54 -0400417 ################################################################
418 # Register this application with the model-level event monitor #
419 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500420 if callback:
Adam Israel28a43c02018-04-23 16:04:54 -0400421 self.monitors[model_name].AddApplication(
422 application_name,
423 callback,
424 *callback_args
425 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500426
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500427 ########################################################
428 # Check for specific machine placement (native charms) #
429 ########################################################
430 to = ""
431 if machine_spec.keys():
Adam Israelfa329072018-09-14 11:26:13 -0400432 if all(k in machine_spec for k in ['host', 'user']):
433 # Enlist an existing machine as a Juju unit
434 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
435 machine_spec['user'],
436 machine_spec['host'],
437 self.GetPrivateKeyPath(),
438 ))
439 to = machine.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500440
441 #######################################
442 # Get the initial charm configuration #
443 #######################################
444
445 rw_mgmt_ip = None
446 if 'rw_mgmt_ip' in params:
447 rw_mgmt_ip = params['rw_mgmt_ip']
448
Adam Israel5afe0542018-08-08 12:54:55 -0400449 if 'initial-config-primitive' not in params:
450 params['initial-config-primitive'] = {}
451
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500452 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600453 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500454 {'<rw_mgmt_ip>': rw_mgmt_ip}
455 )
456
Adam Israel88a49632018-04-10 13:04:57 -0600457 self.log.debug("JujuApi: Deploying charm ({}) from {}".format(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500458 application_name,
459 charm_path,
460 to=to,
461 ))
462
463 ########################################################
464 # Deploy the charm and apply the initial configuration #
465 ########################################################
466 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600467 # We expect charm_path to be either the path to the charm on disk
468 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500469 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600470 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500471 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600472 # Proxy charms should use the current LTS. This will need to be
473 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500474 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600475 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500476 config=initial_config,
Adam Israelfa329072018-09-14 11:26:13 -0400477 # Where to deploy the charm to.
478 to=to,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500479 )
480
Adam Israel136186e2018-09-14 12:01:12 -0400481 # Map the vdu id<->app name,
482 #
483 await self.Relate(model_name, vnfd)
484
Adam Israel88a49632018-04-10 13:04:57 -0600485 # #######################################
486 # # Execute initial config primitive(s) #
487 # #######################################
Adam Israel5e08a0e2018-09-06 19:22:47 -0400488 await self.ExecuteInitialPrimitives(
489 model_name,
490 application_name,
491 params,
492 )
493
494 # primitives = {}
495 #
496 # # Build a sequential list of the primitives to execute
497 # for primitive in params['initial-config-primitive']:
498 # try:
499 # if primitive['name'] == 'config':
500 # # This is applied when the Application is deployed
501 # pass
502 # else:
503 # seq = primitive['seq']
504 #
505 # params = {}
506 # if 'parameter' in primitive:
507 # params = primitive['parameter']
508 #
509 # primitives[seq] = {
510 # 'name': primitive['name'],
511 # 'parameters': self._map_primitive_parameters(
512 # params,
513 # {'<rw_mgmt_ip>': rw_mgmt_ip}
514 # ),
515 # }
516 #
517 # for primitive in sorted(primitives):
518 # await self.ExecutePrimitive(
519 # model_name,
520 # application_name,
521 # primitives[primitive]['name'],
522 # callback,
523 # callback_args,
524 # **primitives[primitive]['parameters'],
525 # )
526 # except N2VCPrimitiveExecutionFailed as e:
527 # self.log.debug(
528 # "[N2VC] Exception executing primitive: {}".format(e)
529 # )
530 # raise
531
532 async def GetPrimitiveStatus(self, model_name, uuid):
533 """Get the status of an executed Primitive.
534
535 The status of an executed Primitive will be one of three values:
536 - completed
537 - failed
538 - running
539 """
540 status = None
541 try:
542 if not self.authenticated:
543 await self.login()
544
545 # FIXME: This is hard-coded until model-per-ns is added
546 model_name = 'default'
547
548 model = await self.get_model(model_name)
549
550 results = await model.get_action_status(uuid)
551
552 if uuid in results:
553 status = results[uuid]
554
555 except Exception as e:
556 self.log.debug(
557 "Caught exception while getting primitive status: {}".format(e)
558 )
559 raise N2VCPrimitiveExecutionFailed(e)
560
561 return status
562
563 async def GetPrimitiveOutput(self, model_name, uuid):
564 """Get the output of an executed Primitive.
565
566 Note: this only returns output for a successfully executed primitive.
567 """
568 results = None
569 try:
570 if not self.authenticated:
571 await self.login()
572
573 # FIXME: This is hard-coded until model-per-ns is added
574 model_name = 'default'
575
576 model = await self.get_model(model_name)
577 results = await model.get_action_output(uuid, 60)
578 except Exception as e:
579 self.log.debug(
580 "Caught exception while getting primitive status: {}".format(e)
581 )
582 raise N2VCPrimitiveExecutionFailed(e)
583
584 return results
585
Adam Israelfa329072018-09-14 11:26:13 -0400586 # async def ProvisionMachine(self, model_name, hostname, username):
587 # """Provision machine for usage with Juju.
588 #
589 # Provisions a previously instantiated machine for use with Juju.
590 # """
591 # try:
592 # if not self.authenticated:
593 # await self.login()
594 #
595 # # FIXME: This is hard-coded until model-per-ns is added
596 # model_name = 'default'
597 #
598 # model = await self.get_model(model_name)
599 # model.add_machine(spec={})
600 #
601 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
602 # "ubuntu",
603 # host['address'],
604 # private_key_path,
605 # ))
606 # return machine.id
607 #
608 # except Exception as e:
609 # self.log.debug(
610 # "Caught exception while getting primitive status: {}".format(e)
611 # )
612 # raise N2VCPrimitiveExecutionFailed(e)
613
614 def GetPrivateKeyPath(self):
615 homedir = os.environ['HOME']
616 sshdir = "{}/.ssh".format(homedir)
617 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
618 return private_key_path
619
620 async def GetPublicKey(self):
621 """Get the N2VC SSH public key.abs
622
623 Returns the SSH public key, to be injected into virtual machines to
624 be managed by the VCA.
625
626 The first time this is run, a ssh keypair will be created. The public
627 key is injected into a VM so that we can provision the machine with
628 Juju, after which Juju will communicate with the VM directly via the
629 juju agent.
630 """
631 public_key = ""
632
633 # Find the path to where we expect our key to live.
634 homedir = os.environ['HOME']
635 sshdir = "{}/.ssh".format(homedir)
636 if not os.path.exists(sshdir):
637 os.mkdir(sshdir)
638
639 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
640 public_key_path = "{}.pub".format(private_key_path)
641
642 # If we don't have a key generated, generate it.
643 if not os.path.exists(private_key_path):
644 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
645 "rsa",
646 "4096",
647 private_key_path
648 )
649 subprocess.check_output(shlex.split(cmd))
650
651 # Read the public key
652 with open(public_key_path, "r") as f:
653 public_key = f.readline()
654
655 return public_key
656
Adam Israel5e08a0e2018-09-06 19:22:47 -0400657 async def ExecuteInitialPrimitives(self, model_name, application_name,
658 params, callback=None, *callback_args):
659 """Execute multiple primitives.
660
661 Execute multiple primitives as declared in initial-config-primitive.
662 This is useful in cases where the primitives initially failed -- for
663 example, if the charm is a proxy but the proxy hasn't been configured
664 yet.
665 """
666 uuids = []
Adam Israel88a49632018-04-10 13:04:57 -0600667 primitives = {}
668
669 # Build a sequential list of the primitives to execute
670 for primitive in params['initial-config-primitive']:
671 try:
672 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600673 pass
674 else:
Adam Israel88a49632018-04-10 13:04:57 -0600675 seq = primitive['seq']
676
Adam Israel42d88e62018-07-16 14:18:41 -0400677 params = {}
678 if 'parameter' in primitive:
679 params = primitive['parameter']
680
Adam Israel88a49632018-04-10 13:04:57 -0600681 primitives[seq] = {
682 'name': primitive['name'],
683 'parameters': self._map_primitive_parameters(
Adam Israel42d88e62018-07-16 14:18:41 -0400684 params,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400685 {'<rw_mgmt_ip>': None}
Adam Israel88a49632018-04-10 13:04:57 -0600686 ),
687 }
688
689 for primitive in sorted(primitives):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400690 uuids.append(
691 await self.ExecutePrimitive(
692 model_name,
693 application_name,
694 primitives[primitive]['name'],
695 callback,
696 callback_args,
697 **primitives[primitive]['parameters'],
698 )
Adam Israel88a49632018-04-10 13:04:57 -0600699 )
700 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400701 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600702 "[N2VC] Exception executing primitive: {}".format(e)
703 )
704 raise
Adam Israel5e08a0e2018-09-06 19:22:47 -0400705 return uuids
Adam Israel88a49632018-04-10 13:04:57 -0600706
Adam Israel5e08a0e2018-09-06 19:22:47 -0400707 async def ExecutePrimitive(self, model_name, application_name, primitive,
708 callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400709 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600710
Adam Israelc9df96f2018-05-03 14:49:56 -0400711 Execute a primitive defined in the VNF descriptor.
712
713 :param str model_name: The name of the network service.
714 :param str application_name: The name of the application
715 :param str primitive: The name of the primitive to execute.
716 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400717 :param tuple callback_args: A list of arguments to be passed to the
718 callback function.
719 :param dict params: A dictionary of key=value pairs representing the
720 primitive's parameters
Adam Israelc9df96f2018-05-03 14:49:56 -0400721 Examples::
722 {
723 'rw_mgmt_ip': '1.2.3.4',
724 # Pass the initial-config-primitives section of the vnf or vdu
725 'initial-config-primitives': {...}
726 }
Adam Israel6817f612018-04-13 08:41:43 -0600727 """
Adam Israel5e08a0e2018-09-06 19:22:47 -0400728 self.log.debug("Executing {}".format(primitive))
Adam Israel6817f612018-04-13 08:41:43 -0600729 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500730 try:
731 if not self.authenticated:
732 await self.login()
733
734 # FIXME: This is hard-coded until model-per-ns is added
735 model_name = 'default'
736
Adam Israel5e08a0e2018-09-06 19:22:47 -0400737 model = await self.get_model(model_name)
Adam Israelb5214512018-05-03 10:00:04 -0400738
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500739 if primitive == 'config':
740 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400741 await self.set_config(
742 model,
743 application_name,
744 params['params'],
745 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500746 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500747 app = await self.get_application(model, application_name)
748 if app:
749 # Run against the first (and probably only) unit in the app
750 unit = app.units[0]
751 if unit:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500752 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600753 uuid = action.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500754 except Exception as e:
Adam Israelb0943662018-08-02 15:32:00 -0400755 self.log.debug(
756 "Caught exception while executing primitive: {}".format(e)
757 )
Adam Israel7d871fb2018-07-17 12:17:06 -0400758 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600759 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500760
Adam Israel5e08a0e2018-09-06 19:22:47 -0400761 async def RemoveCharms(self, model_name, application_name, callback=None,
762 *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400763 """Remove a charm from the VCA.
764
765 Remove a charm referenced in a VNF Descriptor.
766
767 :param str model_name: The name of the network service.
768 :param str application_name: The name of the application
769 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400770 :param tuple callback_args: A list of arguments to be passed to the
771 callback function.
Adam Israelc9df96f2018-05-03 14:49:56 -0400772 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500773 try:
774 if not self.authenticated:
775 await self.login()
776
777 model = await self.get_model(model_name)
778 app = await self.get_application(model, application_name)
779 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400780 # Remove this application from event monitoring
781 self.monitors[model_name].RemoveApplication(application_name)
782
783 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400784 self.log.debug(
785 "Removing the application {}".format(application_name)
786 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500787 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400788
789 # Notify the callback that this charm has been removed.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400790 self.notify_callback(
791 model_name,
792 application_name,
793 "removed",
794 callback,
795 *callback_args,
796 )
Adam Israel28a43c02018-04-23 16:04:54 -0400797
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500798 except Exception as e:
799 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600800 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500801 raise e
802
803 async def DestroyNetworkService(self, nsd):
804 raise NotImplementedError()
805
Adam Israelb5214512018-05-03 10:00:04 -0400806 async def GetMetrics(self, model_name, application_name):
807 """Get the metrics collected by the VCA.
808
809 :param model_name The name of the model
810 :param application_name The name of the application
811 """
812 metrics = {}
813 model = await self.get_model(model_name)
814 app = await self.get_application(model, application_name)
815 if app:
816 metrics = await app.get_metrics()
817
818 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500819
Adam Israelfa329072018-09-14 11:26:13 -0400820 async def HasApplication(self, model_name, application_name):
821 model = await self.get_model(model_name)
822 app = await self.get_application(model, application_name)
823 if app:
824 return True
825 return False
826
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500827 # Non-public methods
Adam Israel136186e2018-09-14 12:01:12 -0400828 async def add_relation(self, model_name, relation1, relation2):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500829 """
830 Add a relation between two application endpoints.
831
Adam Israel136186e2018-09-14 12:01:12 -0400832 :param str model_name Name of the network service.
833 :param str relation1 '<application>[:<relation_name>]'
834 :param str relation12 '<application>[:<relation_name>]'
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500835 """
Adam Israel136186e2018-09-14 12:01:12 -0400836
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500837 if not self.authenticated:
838 await self.login()
839
Adam Israel136186e2018-09-14 12:01:12 -0400840 m = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500841 try:
Adam Israel136186e2018-09-14 12:01:12 -0400842 await m.add_relation(relation1, relation2)
843 except JujuAPIError as e:
844 # If one of the applications in the relationship doesn't exist,
845 # or the relation has already been added, let the operation fail
846 # silently.
847 if 'not found' in e.message:
848 return
849 if 'already exists' in e.message:
850 return
851
852 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500853
Adam Israelb5214512018-05-03 10:00:04 -0400854 # async def apply_config(self, config, application):
855 # """Apply a configuration to the application."""
856 # print("JujuApi: Applying configuration to {}.".format(
857 # application
858 # ))
859 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500860
861 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600862 """Transform the yang config primitive to dict.
863
864 Expected result:
865
866 config = {
867 'config':
868 }
869 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500870 config = {}
871 for primitive in config_primitive:
872 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600873 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500874 for parameter in primitive['parameter']:
875 param = str(parameter['name'])
876 if parameter['value'] == "<rw_mgmt_ip>":
877 config[param] = str(values[parameter['value']])
878 else:
879 config[param] = str(parameter['value'])
880
881 return config
882
Adam Israel88a49632018-04-10 13:04:57 -0600883 def _map_primitive_parameters(self, parameters, values):
884 params = {}
885 for parameter in parameters:
886 param = str(parameter['name'])
Adam Israel5e08a0e2018-09-06 19:22:47 -0400887
888 # Typecast parameter value, if present
889 if 'data-type' in parameter:
890 paramtype = str(parameter['data-type']).lower()
891 value = None
892
893 if paramtype == "integer":
894 value = int(parameter['value'])
895 elif paramtype == "boolean":
896 value = bool(parameter['value'])
897 else:
898 value = str(parameter['value'])
899
Adam Israel88a49632018-04-10 13:04:57 -0600900 if parameter['value'] == "<rw_mgmt_ip>":
901 params[param] = str(values[parameter['value']])
902 else:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400903 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -0600904 return params
905
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500906 def _get_config_from_yang(self, config_primitive, values):
907 """Transform the yang config primitive to dict."""
908 config = {}
909 for primitive in config_primitive.values():
910 if primitive['name'] == 'config':
911 for parameter in primitive['parameter'].values():
912 param = str(parameter['name'])
913 if parameter['value'] == "<rw_mgmt_ip>":
914 config[param] = str(values[parameter['value']])
915 else:
916 config[param] = str(parameter['value'])
917
918 return config
919
Adam Israel5e08a0e2018-09-06 19:22:47 -0400920 @staticmethod
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500921 def FormatApplicationName(self, *args):
922 """
923 Generate a Juju-compatible Application name
924
925 :param args tuple: Positional arguments to be used to construct the
926 application name.
927
928 Limitations::
929 - Only accepts characters a-z and non-consequitive dashes (-)
930 - Application name should not exceed 50 characters
931
932 Examples::
933
934 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
935 """
936
937 appname = ""
938 for c in "-".join(list(args)):
939 if c.isdigit():
940 c = chr(97 + int(c))
941 elif not c.isalpha():
942 c = "-"
943 appname += c
944 return re.sub('\-+', '-', appname.lower())
945
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500946 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
947 # """Format the name of the application
948 #
949 # Limitations:
950 # - Only accepts characters a-z and non-consequitive dashes (-)
951 # - Application name should not exceed 50 characters
952 # """
953 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
954 # new_name = ''
955 # for c in name:
956 # if c.isdigit():
957 # c = chr(97 + int(c))
958 # elif not c.isalpha():
959 # c = "-"
960 # new_name += c
961 # return re.sub('\-+', '-', new_name.lower())
962
963 def format_model_name(self, name):
964 """Format the name of model.
965
966 Model names may only contain lowercase letters, digits and hyphens
967 """
968
969 return name.replace('_', '-').lower()
970
971 async def get_application(self, model, application):
972 """Get the deployed application."""
973 if not self.authenticated:
974 await self.login()
975
976 app = None
977 if application and model:
978 if model.applications:
979 if application in model.applications:
980 app = model.applications[application]
981
982 return app
983
984 async def get_model(self, model_name='default'):
985 """Get a model from the Juju Controller.
986
987 Note: Model objects returned must call disconnected() before it goes
988 out of scope."""
989 if not self.authenticated:
990 await self.login()
991
992 if model_name not in self.models:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400993 self.models[model_name] = await self.controller.get_model(
994 model_name,
995 )
Adam Israelfc511ed2018-09-21 14:20:55 +0200996 self.refcount['model'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500997
Adam Israel28a43c02018-04-23 16:04:54 -0400998 # Create an observer for this model
999 self.monitors[model_name] = VCAMonitor(model_name)
1000 self.models[model_name].add_observer(self.monitors[model_name])
1001
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001002 return self.models[model_name]
1003
1004 async def login(self):
1005 """Login to the Juju controller."""
1006
1007 if self.authenticated:
1008 return
1009
1010 self.connecting = True
1011
1012 self.log.debug("JujuApi: Logging into controller")
1013
1014 cacert = None
Adam Israel5e08a0e2018-09-06 19:22:47 -04001015 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001016
1017 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001018 self.log.debug(
1019 "Connecting to controller... ws://{}:{} as {}/{}".format(
1020 self.endpoint,
1021 self.port,
1022 self.user,
1023 self.secret,
1024 )
1025 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001026 await self.controller.connect(
1027 endpoint=self.endpoint,
1028 username=self.user,
1029 password=self.secret,
1030 cacert=cacert,
1031 )
Adam Israelfc511ed2018-09-21 14:20:55 +02001032 self.refcount['controller'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001033 else:
1034 # current_controller no longer exists
1035 # self.log.debug("Connecting to current controller...")
1036 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -06001037 # await self.controller.connect(
1038 # endpoint=self.endpoint,
1039 # username=self.user,
1040 # cacert=cacert,
1041 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001042 self.log.fatal("VCA credentials not configured.")
1043
1044 self.authenticated = True
1045 self.log.debug("JujuApi: Logged into controller")
1046
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001047 async def logout(self):
1048 """Logout of the Juju controller."""
1049 if not self.authenticated:
1050 return
1051
1052 try:
1053 if self.default_model:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001054 self.log.debug("Disconnecting model {}".format(
1055 self.default_model
1056 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001057 await self.default_model.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +02001058 self.refcount['model'] -= 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001059 self.default_model = None
1060
1061 for model in self.models:
1062 await self.models[model].disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +02001063 self.refcount['model'] -= 1
1064 self.models[model] = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001065
1066 if self.controller:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001067 self.log.debug("Disconnecting controller {}".format(
1068 self.controller
1069 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001070 await self.controller.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +02001071 self.refcount['controller'] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -04001072 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001073
1074 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +02001075
1076 self.log.debug(self.refcount)
1077
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001078 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001079 self.log.fatal(
1080 "Fatal error logging out of Juju Controller: {}".format(e)
1081 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001082 raise e
1083
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001084 # async def remove_application(self, name):
1085 # """Remove the application."""
1086 # if not self.authenticated:
1087 # await self.login()
1088 #
1089 # app = await self.get_application(name)
1090 # if app:
1091 # self.log.debug("JujuApi: Destroying application {}".format(
1092 # name,
1093 # ))
1094 #
1095 # await app.destroy()
1096
1097 async def remove_relation(self, a, b):
1098 """
1099 Remove a relation between two application endpoints
1100
1101 :param a An application endpoint
1102 :param b An application endpoint
1103 """
1104 if not self.authenticated:
1105 await self.login()
1106
1107 m = await self.get_model()
1108 try:
1109 m.remove_relation(a, b)
1110 finally:
1111 await m.disconnect()
1112
1113 async def resolve_error(self, application=None):
1114 """Resolve units in error state."""
1115 if not self.authenticated:
1116 await self.login()
1117
1118 app = await self.get_application(self.default_model, application)
1119 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001120 self.log.debug(
1121 "JujuApi: Resolving errors for application {}".format(
1122 application,
1123 )
1124 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001125
1126 for unit in app.units:
1127 app.resolved(retry=True)
1128
1129 async def run_action(self, application, action_name, **params):
1130 """Execute an action and return an Action object."""
1131 if not self.authenticated:
1132 await self.login()
1133 result = {
1134 'status': '',
1135 'action': {
1136 'tag': None,
1137 'results': None,
1138 }
1139 }
1140 app = await self.get_application(self.default_model, application)
1141 if app:
1142 # We currently only have one unit per application
1143 # so use the first unit available.
1144 unit = app.units[0]
1145
Adam Israel5e08a0e2018-09-06 19:22:47 -04001146 self.log.debug(
1147 "JujuApi: Running Action {} against Application {}".format(
1148 action_name,
1149 application,
1150 )
1151 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001152
1153 action = await unit.run_action(action_name, **params)
1154
1155 # Wait for the action to complete
1156 await action.wait()
1157
1158 result['status'] = action.status
1159 result['action']['tag'] = action.data['id']
1160 result['action']['results'] = action.results
1161
1162 return result
1163
Adam Israelb5214512018-05-03 10:00:04 -04001164 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001165 """Apply a configuration to the application."""
1166 if not self.authenticated:
1167 await self.login()
1168
Adam Israelb5214512018-05-03 10:00:04 -04001169 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001170 if app:
1171 self.log.debug("JujuApi: Setting config for Application {}".format(
1172 application,
1173 ))
1174 await app.set_config(config)
1175
1176 # Verify the config is set
1177 newconf = await app.get_config()
1178 for key in config:
1179 if config[key] != newconf[key]['value']:
1180 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1181
Adam Israelb5214512018-05-03 10:00:04 -04001182 # async def set_parameter(self, parameter, value, application=None):
1183 # """Set a config parameter for a service."""
1184 # if not self.authenticated:
1185 # await self.login()
1186 #
1187 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1188 # parameter,
1189 # value,
1190 # application,
1191 # ))
1192 # return await self.apply_config(
1193 # {parameter: value},
1194 # application=application,
1195 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001196
Adam Israel5e08a0e2018-09-06 19:22:47 -04001197 async def wait_for_application(self, model_name, application_name,
1198 timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001199 """Wait for an application to become active."""
1200 if not self.authenticated:
1201 await self.login()
1202
Adam Israel5e08a0e2018-09-06 19:22:47 -04001203 # TODO: In a point release, we will use a model per deployed network
1204 # service. In the meantime, we will always use the 'default' model.
1205 model_name = 'default'
1206 model = await self.get_model(model_name)
1207
1208 app = await self.get_application(model, application_name)
1209 self.log.debug("Application: {}".format(app))
1210 # app = await self.get_application(model_name, application_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001211 if app:
1212 self.log.debug(
1213 "JujuApi: Waiting {} seconds for Application {}".format(
1214 timeout,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001215 application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001216 )
1217 )
1218
Adam Israel5e08a0e2018-09-06 19:22:47 -04001219 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001220 lambda: all(
Adam Israel5e08a0e2018-09-06 19:22:47 -04001221 unit.agent_status == 'idle' and unit.workload_status in
1222 ['active', 'unknown'] for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001223 ),
1224 timeout=timeout
1225 )