blob: a1fcfe3b60279fc32b7ab5aadb87d118e8b18f7e [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 Israelc3e6c2e2018-03-01 09:31:50 -050022
23
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 Israel5e08a0e2018-09-06 19:22:47 -0400257 async def DeployCharms(self, model_name, application_name, vnfd,
258 charm_path, params={}, machine_spec={},
259 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500260 """Deploy one or more charms associated with a VNF.
261
262 Deploy the charm(s) referenced in a VNF Descriptor.
263
Adam Israelc9df96f2018-05-03 14:49:56 -0400264 :param str model_name: The name of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500265 :param str application_name: The name of the application
266 :param dict vnfd: The name of the application
267 :param str charm_path: The path to the Juju charm
268 :param dict params: A dictionary of runtime parameters
269 Examples::
270 {
Adam Israel88a49632018-04-10 13:04:57 -0600271 'rw_mgmt_ip': '1.2.3.4',
272 # Pass the initial-config-primitives section of the vnf or vdu
273 'initial-config-primitives': {...}
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500274 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400275 :param dict machine_spec: A dictionary describing the machine to
276 install to
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500277 Examples::
278 {
279 'hostname': '1.2.3.4',
280 'username': 'ubuntu',
281 }
282 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400283 :param tuple callback_args: A list of arguments to be passed to the
284 callback
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500285 """
286
287 ########################################################
288 # Verify the path to the charm exists and is readable. #
289 ########################################################
290 if not os.path.exists(charm_path):
291 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400292 self.notify_callback(
293 model_name,
294 application_name,
295 "failed",
296 callback,
297 *callback_args,
298 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500299 raise JujuCharmNotFound("No artifacts configured.")
300
301 ################################
302 # Login to the Juju controller #
303 ################################
304 if not self.authenticated:
305 self.log.debug("Authenticating with Juju")
306 await self.login()
307
308 ##########################################
309 # Get the model for this network service #
310 ##########################################
311 # TODO: In a point release, we will use a model per deployed network
312 # service. In the meantime, we will always use the 'default' model.
313 model_name = 'default'
314 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500315
316 ########################################
317 # Verify the application doesn't exist #
318 ########################################
319 app = await self.get_application(model, application_name)
320 if app:
Adam Israel42d88e62018-07-16 14:18:41 -0400321 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500322
Adam Israel28a43c02018-04-23 16:04:54 -0400323 ################################################################
324 # Register this application with the model-level event monitor #
325 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500326 if callback:
Adam Israel28a43c02018-04-23 16:04:54 -0400327 self.monitors[model_name].AddApplication(
328 application_name,
329 callback,
330 *callback_args
331 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500332
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500333 ########################################################
334 # Check for specific machine placement (native charms) #
335 ########################################################
336 to = ""
337 if machine_spec.keys():
Adam Israelfa329072018-09-14 11:26:13 -0400338 if all(k in machine_spec for k in ['host', 'user']):
339 # Enlist an existing machine as a Juju unit
340 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
341 machine_spec['user'],
342 machine_spec['host'],
343 self.GetPrivateKeyPath(),
344 ))
345 to = machine.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500346
347 #######################################
348 # Get the initial charm configuration #
349 #######################################
350
351 rw_mgmt_ip = None
352 if 'rw_mgmt_ip' in params:
353 rw_mgmt_ip = params['rw_mgmt_ip']
354
Adam Israel5afe0542018-08-08 12:54:55 -0400355 if 'initial-config-primitive' not in params:
356 params['initial-config-primitive'] = {}
357
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500358 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600359 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500360 {'<rw_mgmt_ip>': rw_mgmt_ip}
361 )
362
Adam Israel88a49632018-04-10 13:04:57 -0600363 self.log.debug("JujuApi: Deploying charm ({}) from {}".format(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500364 application_name,
365 charm_path,
366 to=to,
367 ))
368
369 ########################################################
370 # Deploy the charm and apply the initial configuration #
371 ########################################################
372 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600373 # We expect charm_path to be either the path to the charm on disk
374 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500375 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600376 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500377 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600378 # Proxy charms should use the current LTS. This will need to be
379 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500380 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600381 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500382 config=initial_config,
Adam Israelfa329072018-09-14 11:26:13 -0400383 # Where to deploy the charm to.
384 to=to,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500385 )
386
Adam Israel88a49632018-04-10 13:04:57 -0600387 # #######################################
388 # # Execute initial config primitive(s) #
389 # #######################################
Adam Israel5e08a0e2018-09-06 19:22:47 -0400390 await self.ExecuteInitialPrimitives(
391 model_name,
392 application_name,
393 params,
394 )
395
396 # primitives = {}
397 #
398 # # Build a sequential list of the primitives to execute
399 # for primitive in params['initial-config-primitive']:
400 # try:
401 # if primitive['name'] == 'config':
402 # # This is applied when the Application is deployed
403 # pass
404 # else:
405 # seq = primitive['seq']
406 #
407 # params = {}
408 # if 'parameter' in primitive:
409 # params = primitive['parameter']
410 #
411 # primitives[seq] = {
412 # 'name': primitive['name'],
413 # 'parameters': self._map_primitive_parameters(
414 # params,
415 # {'<rw_mgmt_ip>': rw_mgmt_ip}
416 # ),
417 # }
418 #
419 # for primitive in sorted(primitives):
420 # await self.ExecutePrimitive(
421 # model_name,
422 # application_name,
423 # primitives[primitive]['name'],
424 # callback,
425 # callback_args,
426 # **primitives[primitive]['parameters'],
427 # )
428 # except N2VCPrimitiveExecutionFailed as e:
429 # self.log.debug(
430 # "[N2VC] Exception executing primitive: {}".format(e)
431 # )
432 # raise
433
434 async def GetPrimitiveStatus(self, model_name, uuid):
435 """Get the status of an executed Primitive.
436
437 The status of an executed Primitive will be one of three values:
438 - completed
439 - failed
440 - running
441 """
442 status = None
443 try:
444 if not self.authenticated:
445 await self.login()
446
447 # FIXME: This is hard-coded until model-per-ns is added
448 model_name = 'default'
449
450 model = await self.get_model(model_name)
451
452 results = await model.get_action_status(uuid)
453
454 if uuid in results:
455 status = results[uuid]
456
457 except Exception as e:
458 self.log.debug(
459 "Caught exception while getting primitive status: {}".format(e)
460 )
461 raise N2VCPrimitiveExecutionFailed(e)
462
463 return status
464
465 async def GetPrimitiveOutput(self, model_name, uuid):
466 """Get the output of an executed Primitive.
467
468 Note: this only returns output for a successfully executed primitive.
469 """
470 results = None
471 try:
472 if not self.authenticated:
473 await self.login()
474
475 # FIXME: This is hard-coded until model-per-ns is added
476 model_name = 'default'
477
478 model = await self.get_model(model_name)
479 results = await model.get_action_output(uuid, 60)
480 except Exception as e:
481 self.log.debug(
482 "Caught exception while getting primitive status: {}".format(e)
483 )
484 raise N2VCPrimitiveExecutionFailed(e)
485
486 return results
487
Adam Israelfa329072018-09-14 11:26:13 -0400488 # async def ProvisionMachine(self, model_name, hostname, username):
489 # """Provision machine for usage with Juju.
490 #
491 # Provisions a previously instantiated machine for use with Juju.
492 # """
493 # try:
494 # if not self.authenticated:
495 # await self.login()
496 #
497 # # FIXME: This is hard-coded until model-per-ns is added
498 # model_name = 'default'
499 #
500 # model = await self.get_model(model_name)
501 # model.add_machine(spec={})
502 #
503 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
504 # "ubuntu",
505 # host['address'],
506 # private_key_path,
507 # ))
508 # return machine.id
509 #
510 # except Exception as e:
511 # self.log.debug(
512 # "Caught exception while getting primitive status: {}".format(e)
513 # )
514 # raise N2VCPrimitiveExecutionFailed(e)
515
516 def GetPrivateKeyPath(self):
517 homedir = os.environ['HOME']
518 sshdir = "{}/.ssh".format(homedir)
519 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
520 return private_key_path
521
522 async def GetPublicKey(self):
523 """Get the N2VC SSH public key.abs
524
525 Returns the SSH public key, to be injected into virtual machines to
526 be managed by the VCA.
527
528 The first time this is run, a ssh keypair will be created. The public
529 key is injected into a VM so that we can provision the machine with
530 Juju, after which Juju will communicate with the VM directly via the
531 juju agent.
532 """
533 public_key = ""
534
535 # Find the path to where we expect our key to live.
536 homedir = os.environ['HOME']
537 sshdir = "{}/.ssh".format(homedir)
538 if not os.path.exists(sshdir):
539 os.mkdir(sshdir)
540
541 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
542 public_key_path = "{}.pub".format(private_key_path)
543
544 # If we don't have a key generated, generate it.
545 if not os.path.exists(private_key_path):
546 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
547 "rsa",
548 "4096",
549 private_key_path
550 )
551 subprocess.check_output(shlex.split(cmd))
552
553 # Read the public key
554 with open(public_key_path, "r") as f:
555 public_key = f.readline()
556
557 return public_key
558
Adam Israel5e08a0e2018-09-06 19:22:47 -0400559 async def ExecuteInitialPrimitives(self, model_name, application_name,
560 params, callback=None, *callback_args):
561 """Execute multiple primitives.
562
563 Execute multiple primitives as declared in initial-config-primitive.
564 This is useful in cases where the primitives initially failed -- for
565 example, if the charm is a proxy but the proxy hasn't been configured
566 yet.
567 """
568 uuids = []
Adam Israel88a49632018-04-10 13:04:57 -0600569 primitives = {}
570
571 # Build a sequential list of the primitives to execute
572 for primitive in params['initial-config-primitive']:
573 try:
574 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600575 pass
576 else:
Adam Israel88a49632018-04-10 13:04:57 -0600577 seq = primitive['seq']
578
Adam Israel42d88e62018-07-16 14:18:41 -0400579 params = {}
580 if 'parameter' in primitive:
581 params = primitive['parameter']
582
Adam Israel88a49632018-04-10 13:04:57 -0600583 primitives[seq] = {
584 'name': primitive['name'],
585 'parameters': self._map_primitive_parameters(
Adam Israel42d88e62018-07-16 14:18:41 -0400586 params,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400587 {'<rw_mgmt_ip>': None}
Adam Israel88a49632018-04-10 13:04:57 -0600588 ),
589 }
590
591 for primitive in sorted(primitives):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400592 uuids.append(
593 await self.ExecutePrimitive(
594 model_name,
595 application_name,
596 primitives[primitive]['name'],
597 callback,
598 callback_args,
599 **primitives[primitive]['parameters'],
600 )
Adam Israel88a49632018-04-10 13:04:57 -0600601 )
602 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400603 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600604 "[N2VC] Exception executing primitive: {}".format(e)
605 )
606 raise
Adam Israel5e08a0e2018-09-06 19:22:47 -0400607 return uuids
Adam Israel88a49632018-04-10 13:04:57 -0600608
Adam Israel5e08a0e2018-09-06 19:22:47 -0400609 async def ExecutePrimitive(self, model_name, application_name, primitive,
610 callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400611 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600612
Adam Israelc9df96f2018-05-03 14:49:56 -0400613 Execute a primitive defined in the VNF descriptor.
614
615 :param str model_name: The name of the network service.
616 :param str application_name: The name of the application
617 :param str primitive: The name of the primitive to execute.
618 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400619 :param tuple callback_args: A list of arguments to be passed to the
620 callback function.
621 :param dict params: A dictionary of key=value pairs representing the
622 primitive's parameters
Adam Israelc9df96f2018-05-03 14:49:56 -0400623 Examples::
624 {
625 'rw_mgmt_ip': '1.2.3.4',
626 # Pass the initial-config-primitives section of the vnf or vdu
627 'initial-config-primitives': {...}
628 }
Adam Israel6817f612018-04-13 08:41:43 -0600629 """
Adam Israel5e08a0e2018-09-06 19:22:47 -0400630 self.log.debug("Executing {}".format(primitive))
Adam Israel6817f612018-04-13 08:41:43 -0600631 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500632 try:
633 if not self.authenticated:
634 await self.login()
635
636 # FIXME: This is hard-coded until model-per-ns is added
637 model_name = 'default'
638
Adam Israel5e08a0e2018-09-06 19:22:47 -0400639 model = await self.get_model(model_name)
Adam Israelb5214512018-05-03 10:00:04 -0400640
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500641 if primitive == 'config':
642 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400643 await self.set_config(
644 model,
645 application_name,
646 params['params'],
647 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500648 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500649 app = await self.get_application(model, application_name)
650 if app:
651 # Run against the first (and probably only) unit in the app
652 unit = app.units[0]
653 if unit:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500654 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600655 uuid = action.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500656 except Exception as e:
Adam Israelb0943662018-08-02 15:32:00 -0400657 self.log.debug(
658 "Caught exception while executing primitive: {}".format(e)
659 )
Adam Israel7d871fb2018-07-17 12:17:06 -0400660 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600661 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500662
Adam Israel5e08a0e2018-09-06 19:22:47 -0400663 async def RemoveCharms(self, model_name, application_name, callback=None,
664 *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400665 """Remove a charm from the VCA.
666
667 Remove a charm referenced in a VNF Descriptor.
668
669 :param str model_name: The name of the network service.
670 :param str application_name: The name of the application
671 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400672 :param tuple callback_args: A list of arguments to be passed to the
673 callback function.
Adam Israelc9df96f2018-05-03 14:49:56 -0400674 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500675 try:
676 if not self.authenticated:
677 await self.login()
678
679 model = await self.get_model(model_name)
680 app = await self.get_application(model, application_name)
681 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400682 # Remove this application from event monitoring
683 self.monitors[model_name].RemoveApplication(application_name)
684
685 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400686 self.log.debug(
687 "Removing the application {}".format(application_name)
688 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500689 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400690
691 # Notify the callback that this charm has been removed.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400692 self.notify_callback(
693 model_name,
694 application_name,
695 "removed",
696 callback,
697 *callback_args,
698 )
Adam Israel28a43c02018-04-23 16:04:54 -0400699
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500700 except Exception as e:
701 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600702 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500703 raise e
704
705 async def DestroyNetworkService(self, nsd):
706 raise NotImplementedError()
707
Adam Israelb5214512018-05-03 10:00:04 -0400708 async def GetMetrics(self, model_name, application_name):
709 """Get the metrics collected by the VCA.
710
711 :param model_name The name of the model
712 :param application_name The name of the application
713 """
714 metrics = {}
715 model = await self.get_model(model_name)
716 app = await self.get_application(model, application_name)
717 if app:
718 metrics = await app.get_metrics()
719
720 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500721
Adam Israelfa329072018-09-14 11:26:13 -0400722 async def HasApplication(self, model_name, application_name):
723 model = await self.get_model(model_name)
724 app = await self.get_application(model, application_name)
725 if app:
726 return True
727 return False
728
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500729 # Non-public methods
730 async def add_relation(self, a, b, via=None):
731 """
732 Add a relation between two application endpoints.
733
734 :param a An application endpoint
735 :param b An application endpoint
736 :param via The egress subnet(s) for outbound traffic, e.g.,
737 (192.168.0.0/16,10.0.0.0/8)
738 """
739 if not self.authenticated:
740 await self.login()
741
742 m = await self.get_model()
743 try:
744 m.add_relation(a, b, via)
745 finally:
746 await m.disconnect()
747
Adam Israelb5214512018-05-03 10:00:04 -0400748 # async def apply_config(self, config, application):
749 # """Apply a configuration to the application."""
750 # print("JujuApi: Applying configuration to {}.".format(
751 # application
752 # ))
753 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500754
755 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600756 """Transform the yang config primitive to dict.
757
758 Expected result:
759
760 config = {
761 'config':
762 }
763 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500764 config = {}
765 for primitive in config_primitive:
766 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600767 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500768 for parameter in primitive['parameter']:
769 param = str(parameter['name'])
770 if parameter['value'] == "<rw_mgmt_ip>":
771 config[param] = str(values[parameter['value']])
772 else:
773 config[param] = str(parameter['value'])
774
775 return config
776
Adam Israel88a49632018-04-10 13:04:57 -0600777 def _map_primitive_parameters(self, parameters, values):
778 params = {}
779 for parameter in parameters:
780 param = str(parameter['name'])
Adam Israel5e08a0e2018-09-06 19:22:47 -0400781
782 # Typecast parameter value, if present
783 if 'data-type' in parameter:
784 paramtype = str(parameter['data-type']).lower()
785 value = None
786
787 if paramtype == "integer":
788 value = int(parameter['value'])
789 elif paramtype == "boolean":
790 value = bool(parameter['value'])
791 else:
792 value = str(parameter['value'])
793
Adam Israel88a49632018-04-10 13:04:57 -0600794 if parameter['value'] == "<rw_mgmt_ip>":
795 params[param] = str(values[parameter['value']])
796 else:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400797 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -0600798 return params
799
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500800 def _get_config_from_yang(self, config_primitive, values):
801 """Transform the yang config primitive to dict."""
802 config = {}
803 for primitive in config_primitive.values():
804 if primitive['name'] == 'config':
805 for parameter in primitive['parameter'].values():
806 param = str(parameter['name'])
807 if parameter['value'] == "<rw_mgmt_ip>":
808 config[param] = str(values[parameter['value']])
809 else:
810 config[param] = str(parameter['value'])
811
812 return config
813
Adam Israel5e08a0e2018-09-06 19:22:47 -0400814 @staticmethod
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500815 def FormatApplicationName(self, *args):
816 """
817 Generate a Juju-compatible Application name
818
819 :param args tuple: Positional arguments to be used to construct the
820 application name.
821
822 Limitations::
823 - Only accepts characters a-z and non-consequitive dashes (-)
824 - Application name should not exceed 50 characters
825
826 Examples::
827
828 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
829 """
830
831 appname = ""
832 for c in "-".join(list(args)):
833 if c.isdigit():
834 c = chr(97 + int(c))
835 elif not c.isalpha():
836 c = "-"
837 appname += c
838 return re.sub('\-+', '-', appname.lower())
839
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500840 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
841 # """Format the name of the application
842 #
843 # Limitations:
844 # - Only accepts characters a-z and non-consequitive dashes (-)
845 # - Application name should not exceed 50 characters
846 # """
847 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
848 # new_name = ''
849 # for c in name:
850 # if c.isdigit():
851 # c = chr(97 + int(c))
852 # elif not c.isalpha():
853 # c = "-"
854 # new_name += c
855 # return re.sub('\-+', '-', new_name.lower())
856
857 def format_model_name(self, name):
858 """Format the name of model.
859
860 Model names may only contain lowercase letters, digits and hyphens
861 """
862
863 return name.replace('_', '-').lower()
864
865 async def get_application(self, model, application):
866 """Get the deployed application."""
867 if not self.authenticated:
868 await self.login()
869
870 app = None
871 if application and model:
872 if model.applications:
873 if application in model.applications:
874 app = model.applications[application]
875
876 return app
877
878 async def get_model(self, model_name='default'):
879 """Get a model from the Juju Controller.
880
881 Note: Model objects returned must call disconnected() before it goes
882 out of scope."""
883 if not self.authenticated:
884 await self.login()
885
886 if model_name not in self.models:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400887 self.models[model_name] = await self.controller.get_model(
888 model_name,
889 )
Adam Israelfc511ed2018-09-21 14:20:55 +0200890 self.refcount['model'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500891
Adam Israel28a43c02018-04-23 16:04:54 -0400892 # Create an observer for this model
893 self.monitors[model_name] = VCAMonitor(model_name)
894 self.models[model_name].add_observer(self.monitors[model_name])
895
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500896 return self.models[model_name]
897
898 async def login(self):
899 """Login to the Juju controller."""
900
901 if self.authenticated:
902 return
903
904 self.connecting = True
905
906 self.log.debug("JujuApi: Logging into controller")
907
908 cacert = None
Adam Israel5e08a0e2018-09-06 19:22:47 -0400909 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500910
911 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400912 self.log.debug(
913 "Connecting to controller... ws://{}:{} as {}/{}".format(
914 self.endpoint,
915 self.port,
916 self.user,
917 self.secret,
918 )
919 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500920 await self.controller.connect(
921 endpoint=self.endpoint,
922 username=self.user,
923 password=self.secret,
924 cacert=cacert,
925 )
Adam Israelfc511ed2018-09-21 14:20:55 +0200926 self.refcount['controller'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500927 else:
928 # current_controller no longer exists
929 # self.log.debug("Connecting to current controller...")
930 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -0600931 # await self.controller.connect(
932 # endpoint=self.endpoint,
933 # username=self.user,
934 # cacert=cacert,
935 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500936 self.log.fatal("VCA credentials not configured.")
937
938 self.authenticated = True
939 self.log.debug("JujuApi: Logged into controller")
940
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500941 async def logout(self):
942 """Logout of the Juju controller."""
943 if not self.authenticated:
944 return
945
946 try:
947 if self.default_model:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400948 self.log.debug("Disconnecting model {}".format(
949 self.default_model
950 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500951 await self.default_model.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +0200952 self.refcount['model'] -= 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500953 self.default_model = None
954
955 for model in self.models:
956 await self.models[model].disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +0200957 self.refcount['model'] -= 1
958 self.models[model] = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500959
960 if self.controller:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400961 self.log.debug("Disconnecting controller {}".format(
962 self.controller
963 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500964 await self.controller.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +0200965 self.refcount['controller'] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -0400966 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500967
968 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +0200969
970 self.log.debug(self.refcount)
971
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500972 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400973 self.log.fatal(
974 "Fatal error logging out of Juju Controller: {}".format(e)
975 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500976 raise e
977
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500978 # async def remove_application(self, name):
979 # """Remove the application."""
980 # if not self.authenticated:
981 # await self.login()
982 #
983 # app = await self.get_application(name)
984 # if app:
985 # self.log.debug("JujuApi: Destroying application {}".format(
986 # name,
987 # ))
988 #
989 # await app.destroy()
990
991 async def remove_relation(self, a, b):
992 """
993 Remove a relation between two application endpoints
994
995 :param a An application endpoint
996 :param b An application endpoint
997 """
998 if not self.authenticated:
999 await self.login()
1000
1001 m = await self.get_model()
1002 try:
1003 m.remove_relation(a, b)
1004 finally:
1005 await m.disconnect()
1006
1007 async def resolve_error(self, application=None):
1008 """Resolve units in error state."""
1009 if not self.authenticated:
1010 await self.login()
1011
1012 app = await self.get_application(self.default_model, application)
1013 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001014 self.log.debug(
1015 "JujuApi: Resolving errors for application {}".format(
1016 application,
1017 )
1018 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001019
1020 for unit in app.units:
1021 app.resolved(retry=True)
1022
1023 async def run_action(self, application, action_name, **params):
1024 """Execute an action and return an Action object."""
1025 if not self.authenticated:
1026 await self.login()
1027 result = {
1028 'status': '',
1029 'action': {
1030 'tag': None,
1031 'results': None,
1032 }
1033 }
1034 app = await self.get_application(self.default_model, application)
1035 if app:
1036 # We currently only have one unit per application
1037 # so use the first unit available.
1038 unit = app.units[0]
1039
Adam Israel5e08a0e2018-09-06 19:22:47 -04001040 self.log.debug(
1041 "JujuApi: Running Action {} against Application {}".format(
1042 action_name,
1043 application,
1044 )
1045 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001046
1047 action = await unit.run_action(action_name, **params)
1048
1049 # Wait for the action to complete
1050 await action.wait()
1051
1052 result['status'] = action.status
1053 result['action']['tag'] = action.data['id']
1054 result['action']['results'] = action.results
1055
1056 return result
1057
Adam Israelb5214512018-05-03 10:00:04 -04001058 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001059 """Apply a configuration to the application."""
1060 if not self.authenticated:
1061 await self.login()
1062
Adam Israelb5214512018-05-03 10:00:04 -04001063 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001064 if app:
1065 self.log.debug("JujuApi: Setting config for Application {}".format(
1066 application,
1067 ))
1068 await app.set_config(config)
1069
1070 # Verify the config is set
1071 newconf = await app.get_config()
1072 for key in config:
1073 if config[key] != newconf[key]['value']:
1074 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1075
Adam Israelb5214512018-05-03 10:00:04 -04001076 # async def set_parameter(self, parameter, value, application=None):
1077 # """Set a config parameter for a service."""
1078 # if not self.authenticated:
1079 # await self.login()
1080 #
1081 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1082 # parameter,
1083 # value,
1084 # application,
1085 # ))
1086 # return await self.apply_config(
1087 # {parameter: value},
1088 # application=application,
1089 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001090
Adam Israel5e08a0e2018-09-06 19:22:47 -04001091 async def wait_for_application(self, model_name, application_name,
1092 timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001093 """Wait for an application to become active."""
1094 if not self.authenticated:
1095 await self.login()
1096
Adam Israel5e08a0e2018-09-06 19:22:47 -04001097 # TODO: In a point release, we will use a model per deployed network
1098 # service. In the meantime, we will always use the 'default' model.
1099 model_name = 'default'
1100 model = await self.get_model(model_name)
1101
1102 app = await self.get_application(model, application_name)
1103 self.log.debug("Application: {}".format(app))
1104 # app = await self.get_application(model_name, application_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001105 if app:
1106 self.log.debug(
1107 "JujuApi: Waiting {} seconds for Application {}".format(
1108 timeout,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001109 application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001110 )
1111 )
1112
Adam Israel5e08a0e2018-09-06 19:22:47 -04001113 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001114 lambda: all(
Adam Israel5e08a0e2018-09-06 19:22:47 -04001115 unit.agent_status == 'idle' and unit.workload_status in
1116 ['active', 'unknown'] for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001117 ),
1118 timeout=timeout
1119 )