blob: 8064cb3489680501d918c3a8dd5dbcecebc87e04 [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
6import ssl
7import sys
Adam Israel5e08a0e2018-09-06 19:22:47 -04008# import time
Adam Israelc3e6c2e2018-03-01 09:31:50 -05009
10# FIXME: this should load the juju inside or modules without having to
11# explicitly install it. Check why it's not working.
12# Load our subtree of the juju library
13path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
14path = os.path.join(path, "modules/libjuju/")
15if path not in sys.path:
16 sys.path.insert(1, path)
17
18from juju.controller import Controller
Adam Israel5e08a0e2018-09-06 19:22:47 -040019from juju.model import ModelObserver
Adam Israelc3e6c2e2018-03-01 09:31:50 -050020
21
22# We might need this to connect to the websocket securely, but test and verify.
23try:
24 ssl._create_default_https_context = ssl._create_unverified_context
25except AttributeError:
26 # Legacy Python doesn't verify by default (see pep-0476)
27 # https://www.python.org/dev/peps/pep-0476/
28 pass
29
30
31# Custom exceptions
32class JujuCharmNotFound(Exception):
33 """The Charm can't be found or is not readable."""
34
35
36class JujuApplicationExists(Exception):
37 """The Application already exists."""
38
Adam Israelb5214512018-05-03 10:00:04 -040039
Adam Israel88a49632018-04-10 13:04:57 -060040class N2VCPrimitiveExecutionFailed(Exception):
41 """Something failed while attempting to execute a primitive."""
42
Adam Israelc3e6c2e2018-03-01 09:31:50 -050043
44# Quiet the debug logging
45logging.getLogger('websockets.protocol').setLevel(logging.INFO)
46logging.getLogger('juju.client.connection').setLevel(logging.WARN)
47logging.getLogger('juju.model').setLevel(logging.WARN)
48logging.getLogger('juju.machine').setLevel(logging.WARN)
49
Adam Israelb5214512018-05-03 10:00:04 -040050
Adam Israelc3e6c2e2018-03-01 09:31:50 -050051class VCAMonitor(ModelObserver):
52 """Monitor state changes within the Juju Model."""
Adam Israelc3e6c2e2018-03-01 09:31:50 -050053 log = None
54 ns_name = None
Adam Israel28a43c02018-04-23 16:04:54 -040055 applications = {}
Adam Israelc3e6c2e2018-03-01 09:31:50 -050056
Adam Israel28a43c02018-04-23 16:04:54 -040057 def __init__(self, ns_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050058 self.log = logging.getLogger(__name__)
59
60 self.ns_name = ns_name
Adam Israel28a43c02018-04-23 16:04:54 -040061
62 def AddApplication(self, application_name, callback, *callback_args):
63 if application_name not in self.applications:
64 self.applications[application_name] = {
65 'callback': callback,
66 'callback_args': callback_args
67 }
68
69 def RemoveApplication(self, application_name):
70 if application_name in self.applications:
71 del self.applications[application_name]
Adam Israelc3e6c2e2018-03-01 09:31:50 -050072
73 async def on_change(self, delta, old, new, model):
74 """React to changes in the Juju model."""
75
76 if delta.entity == "unit":
Adam Israel28a43c02018-04-23 16:04:54 -040077 # Ignore change events from other applications
78 if delta.data['application'] not in self.applications.keys():
79 return
80
Adam Israelc3e6c2e2018-03-01 09:31:50 -050081 try:
Adam Israel28a43c02018-04-23 16:04:54 -040082
83 application_name = delta.data['application']
84
85 callback = self.applications[application_name]['callback']
Adam Israel5e08a0e2018-09-06 19:22:47 -040086 callback_args = \
87 self.applications[application_name]['callback_args']
Adam Israel28a43c02018-04-23 16:04:54 -040088
Adam Israelc3e6c2e2018-03-01 09:31:50 -050089 if old and new:
Adam Israelfc511ed2018-09-21 14:20:55 +020090 # Fire off a callback with the application state
91 if callback:
92 callback(
93 self.ns_name,
94 delta.data['application'],
95 new.workload_status,
96 new.workload_status_message,
97 *callback_args)
Adam Israel28a43c02018-04-23 16:04:54 -040098
99 if old and not new:
100 # This is a charm being removed
101 if callback:
102 callback(
103 self.ns_name,
104 delta.data['application'],
105 "removed",
Adam Israel9562f432018-05-09 13:55:28 -0400106 "",
Adam Israel28a43c02018-04-23 16:04:54 -0400107 *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500108 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400109 self.log.debug("[1] notify_callback exception: {}".format(e))
110
Adam Israel88a49632018-04-10 13:04:57 -0600111 elif delta.entity == "action":
112 # TODO: Decide how we want to notify the user of actions
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500113
Adam Israel88a49632018-04-10 13:04:57 -0600114 # uuid = delta.data['id'] # The Action's unique id
115 # msg = delta.data['message'] # The output of the action
116 #
117 # if delta.data['status'] == "pending":
118 # # The action is queued
119 # pass
120 # elif delta.data['status'] == "completed""
121 # # The action was successful
122 # pass
123 # elif delta.data['status'] == "failed":
124 # # The action failed.
125 # pass
126
127 pass
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500128
129########
130# TODO
131#
132# Create unique models per network service
133# Document all public functions
134
Adam Israelb5214512018-05-03 10:00:04 -0400135
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500136class N2VC:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500137 def __init__(self,
138 log=None,
139 server='127.0.0.1',
140 port=17070,
141 user='admin',
142 secret=None,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400143 artifacts=None,
144 loop=None,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500145 ):
146 """Initialize N2VC
147
148 :param vcaconfig dict A dictionary containing the VCA configuration
149
150 :param artifacts str The directory where charms required by a vnfd are
151 stored.
152
153 :Example:
154 n2vc = N2VC(vcaconfig={
155 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
156 'user': 'admin',
157 'ip-address': '10.44.127.137',
158 'port': 17070,
159 'artifacts': '/path/to/charms'
160 })
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500161 """
162
Adam Israel5e08a0e2018-09-06 19:22:47 -0400163 # Initialize instance-level variables
164 self.api = None
165 self.log = None
166 self.controller = None
167 self.connecting = False
168 self.authenticated = False
169
Adam Israelfc511ed2018-09-21 14:20:55 +0200170 # For debugging
171 self.refcount = {
172 'controller': 0,
173 'model': 0,
174 }
175
Adam Israel5e08a0e2018-09-06 19:22:47 -0400176 self.models = {}
177 self.default_model = None
178
179 # Model Observers
180 self.monitors = {}
181
182 # VCA config
183 self.hostname = ""
184 self.port = 17070
185 self.username = ""
186 self.secret = ""
187
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500188 if log:
189 self.log = log
190 else:
191 self.log = logging.getLogger(__name__)
192
193 # Quiet websocket traffic
194 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
195 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
196 logging.getLogger('model').setLevel(logging.WARN)
197 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
198
199 self.log.debug('JujuApi: instantiated')
200
201 self.server = server
202 self.port = port
203
204 self.secret = secret
205 if user.startswith('user-'):
206 self.user = user
207 else:
208 self.user = 'user-{}'.format(user)
209
210 self.endpoint = '%s:%d' % (server, int(port))
211
212 self.artifacts = artifacts
213
Adam Israel5e08a0e2018-09-06 19:22:47 -0400214 self.loop = loop or asyncio.get_event_loop()
215
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500216 def __del__(self):
217 """Close any open connections."""
218 yield self.logout()
219
Adam Israel5e08a0e2018-09-06 19:22:47 -0400220 def notify_callback(self, model_name, application_name, status, message,
221 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500222 try:
223 if callback:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400224 callback(
225 model_name,
226 application_name,
227 status, message,
228 *callback_args,
229 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500230 except Exception as e:
231 self.log.error("[0] notify_callback exception {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600232 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500233 return True
234
235 # Public methods
236 async def CreateNetworkService(self, nsd):
237 """Create a new model to encapsulate this network service.
238
239 Create a new model in the Juju controller to encapsulate the
240 charms associated with a network service.
241
242 You can pass either the nsd record or the id of the network
243 service, but this method will fail without one of them.
244 """
245 if not self.authenticated:
246 await self.login()
247
248 # Ideally, we will create a unique model per network service.
249 # This change will require all components, i.e., LCM and SO, to use
250 # N2VC for 100% compatibility. If we adopt unique models for the LCM,
251 # services deployed via LCM would't be manageable via SO and vice versa
252
253 return self.default_model
254
Adam Israel5e08a0e2018-09-06 19:22:47 -0400255 async def DeployCharms(self, model_name, application_name, vnfd,
256 charm_path, params={}, machine_spec={},
257 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500258 """Deploy one or more charms associated with a VNF.
259
260 Deploy the charm(s) referenced in a VNF Descriptor.
261
Adam Israelc9df96f2018-05-03 14:49:56 -0400262 :param str model_name: The name of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500263 :param str application_name: The name of the application
264 :param dict vnfd: The name of the application
265 :param str charm_path: The path to the Juju charm
266 :param dict params: A dictionary of runtime parameters
267 Examples::
268 {
Adam Israel88a49632018-04-10 13:04:57 -0600269 'rw_mgmt_ip': '1.2.3.4',
270 # Pass the initial-config-primitives section of the vnf or vdu
271 'initial-config-primitives': {...}
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500272 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400273 :param dict machine_spec: A dictionary describing the machine to
274 install to
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500275 Examples::
276 {
277 'hostname': '1.2.3.4',
278 'username': 'ubuntu',
279 }
280 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400281 :param tuple callback_args: A list of arguments to be passed to the
282 callback
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500283 """
284
285 ########################################################
286 # Verify the path to the charm exists and is readable. #
287 ########################################################
288 if not os.path.exists(charm_path):
289 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400290 self.notify_callback(
291 model_name,
292 application_name,
293 "failed",
294 callback,
295 *callback_args,
296 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500297 raise JujuCharmNotFound("No artifacts configured.")
298
299 ################################
300 # Login to the Juju controller #
301 ################################
302 if not self.authenticated:
303 self.log.debug("Authenticating with Juju")
304 await self.login()
305
306 ##########################################
307 # Get the model for this network service #
308 ##########################################
309 # TODO: In a point release, we will use a model per deployed network
310 # service. In the meantime, we will always use the 'default' model.
311 model_name = 'default'
312 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500313
314 ########################################
315 # Verify the application doesn't exist #
316 ########################################
317 app = await self.get_application(model, application_name)
318 if app:
Adam Israel42d88e62018-07-16 14:18:41 -0400319 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500320
Adam Israel28a43c02018-04-23 16:04:54 -0400321 ################################################################
322 # Register this application with the model-level event monitor #
323 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500324 if callback:
Adam Israel28a43c02018-04-23 16:04:54 -0400325 self.monitors[model_name].AddApplication(
326 application_name,
327 callback,
328 *callback_args
329 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500330
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500331 ########################################################
332 # Check for specific machine placement (native charms) #
333 ########################################################
334 to = ""
335 if machine_spec.keys():
336 # TODO: This needs to be tested.
337 # if all(k in machine_spec for k in ['hostname', 'username']):
338 # # Enlist the existing machine in Juju
339 # machine = await self.model.add_machine(spec='ssh:%@%'.format(
340 # specs['host'],
341 # specs['user'],
342 # ))
343 # to = machine.id
344 pass
345
346 #######################################
347 # Get the initial charm configuration #
348 #######################################
349
350 rw_mgmt_ip = None
351 if 'rw_mgmt_ip' in params:
352 rw_mgmt_ip = params['rw_mgmt_ip']
353
Adam Israel5afe0542018-08-08 12:54:55 -0400354 # initial_config = {}
Adam Israel5e08a0e2018-09-06 19:22:47 -0400355 # self.log.debug(type(params))
356 # self.log.debug("Params: {}".format(params))
Adam Israel5afe0542018-08-08 12:54:55 -0400357 if 'initial-config-primitive' not in params:
358 params['initial-config-primitive'] = {}
359
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500360 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600361 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500362 {'<rw_mgmt_ip>': rw_mgmt_ip}
363 )
364
Adam Israel88a49632018-04-10 13:04:57 -0600365 self.log.debug("JujuApi: Deploying charm ({}) from {}".format(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500366 application_name,
367 charm_path,
368 to=to,
369 ))
370
371 ########################################################
372 # Deploy the charm and apply the initial configuration #
373 ########################################################
374 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600375 # We expect charm_path to be either the path to the charm on disk
376 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500377 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600378 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500379 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600380 # Proxy charms should use the current LTS. This will need to be
381 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500382 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600383 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500384 config=initial_config,
Adam Israel88a49632018-04-10 13:04:57 -0600385 # TBD: Where to deploy the charm to.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500386 to=None,
387 )
388
Adam Israel88a49632018-04-10 13:04:57 -0600389 # #######################################
390 # # Execute initial config primitive(s) #
391 # #######################################
Adam Israel5e08a0e2018-09-06 19:22:47 -0400392 await self.ExecuteInitialPrimitives(
393 model_name,
394 application_name,
395 params,
396 )
397
398 # primitives = {}
399 #
400 # # Build a sequential list of the primitives to execute
401 # for primitive in params['initial-config-primitive']:
402 # try:
403 # if primitive['name'] == 'config':
404 # # This is applied when the Application is deployed
405 # pass
406 # else:
407 # seq = primitive['seq']
408 #
409 # params = {}
410 # if 'parameter' in primitive:
411 # params = primitive['parameter']
412 #
413 # primitives[seq] = {
414 # 'name': primitive['name'],
415 # 'parameters': self._map_primitive_parameters(
416 # params,
417 # {'<rw_mgmt_ip>': rw_mgmt_ip}
418 # ),
419 # }
420 #
421 # for primitive in sorted(primitives):
422 # await self.ExecutePrimitive(
423 # model_name,
424 # application_name,
425 # primitives[primitive]['name'],
426 # callback,
427 # callback_args,
428 # **primitives[primitive]['parameters'],
429 # )
430 # except N2VCPrimitiveExecutionFailed as e:
431 # self.log.debug(
432 # "[N2VC] Exception executing primitive: {}".format(e)
433 # )
434 # raise
435
436 async def GetPrimitiveStatus(self, model_name, uuid):
437 """Get the status of an executed Primitive.
438
439 The status of an executed Primitive will be one of three values:
440 - completed
441 - failed
442 - running
443 """
444 status = None
445 try:
446 if not self.authenticated:
447 await self.login()
448
449 # FIXME: This is hard-coded until model-per-ns is added
450 model_name = 'default'
451
452 model = await self.get_model(model_name)
453
454 results = await model.get_action_status(uuid)
455
456 if uuid in results:
457 status = results[uuid]
458
459 except Exception as e:
460 self.log.debug(
461 "Caught exception while getting primitive status: {}".format(e)
462 )
463 raise N2VCPrimitiveExecutionFailed(e)
464
465 return status
466
467 async def GetPrimitiveOutput(self, model_name, uuid):
468 """Get the output of an executed Primitive.
469
470 Note: this only returns output for a successfully executed primitive.
471 """
472 results = None
473 try:
474 if not self.authenticated:
475 await self.login()
476
477 # FIXME: This is hard-coded until model-per-ns is added
478 model_name = 'default'
479
480 model = await self.get_model(model_name)
481 results = await model.get_action_output(uuid, 60)
482 except Exception as e:
483 self.log.debug(
484 "Caught exception while getting primitive status: {}".format(e)
485 )
486 raise N2VCPrimitiveExecutionFailed(e)
487
488 return results
489
490 async def ExecuteInitialPrimitives(self, model_name, application_name,
491 params, callback=None, *callback_args):
492 """Execute multiple primitives.
493
494 Execute multiple primitives as declared in initial-config-primitive.
495 This is useful in cases where the primitives initially failed -- for
496 example, if the charm is a proxy but the proxy hasn't been configured
497 yet.
498 """
499 uuids = []
Adam Israel88a49632018-04-10 13:04:57 -0600500 primitives = {}
501
502 # Build a sequential list of the primitives to execute
503 for primitive in params['initial-config-primitive']:
504 try:
505 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600506 pass
507 else:
Adam Israel88a49632018-04-10 13:04:57 -0600508 seq = primitive['seq']
509
Adam Israel42d88e62018-07-16 14:18:41 -0400510 params = {}
511 if 'parameter' in primitive:
512 params = primitive['parameter']
513
Adam Israel88a49632018-04-10 13:04:57 -0600514 primitives[seq] = {
515 'name': primitive['name'],
516 'parameters': self._map_primitive_parameters(
Adam Israel42d88e62018-07-16 14:18:41 -0400517 params,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400518 {'<rw_mgmt_ip>': None}
Adam Israel88a49632018-04-10 13:04:57 -0600519 ),
520 }
521
522 for primitive in sorted(primitives):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400523 uuids.append(
524 await self.ExecutePrimitive(
525 model_name,
526 application_name,
527 primitives[primitive]['name'],
528 callback,
529 callback_args,
530 **primitives[primitive]['parameters'],
531 )
Adam Israel88a49632018-04-10 13:04:57 -0600532 )
533 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400534 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600535 "[N2VC] Exception executing primitive: {}".format(e)
536 )
537 raise
Adam Israel5e08a0e2018-09-06 19:22:47 -0400538 return uuids
Adam Israel88a49632018-04-10 13:04:57 -0600539
Adam Israel5e08a0e2018-09-06 19:22:47 -0400540 async def ExecutePrimitive(self, model_name, application_name, primitive,
541 callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400542 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600543
Adam Israelc9df96f2018-05-03 14:49:56 -0400544 Execute a primitive defined in the VNF descriptor.
545
546 :param str model_name: The name of the network service.
547 :param str application_name: The name of the application
548 :param str primitive: The name of the primitive to execute.
549 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400550 :param tuple callback_args: A list of arguments to be passed to the
551 callback function.
552 :param dict params: A dictionary of key=value pairs representing the
553 primitive's parameters
Adam Israelc9df96f2018-05-03 14:49:56 -0400554 Examples::
555 {
556 'rw_mgmt_ip': '1.2.3.4',
557 # Pass the initial-config-primitives section of the vnf or vdu
558 'initial-config-primitives': {...}
559 }
Adam Israel6817f612018-04-13 08:41:43 -0600560 """
Adam Israel5e08a0e2018-09-06 19:22:47 -0400561 self.log.debug("Executing {}".format(primitive))
Adam Israel6817f612018-04-13 08:41:43 -0600562 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500563 try:
564 if not self.authenticated:
565 await self.login()
566
567 # FIXME: This is hard-coded until model-per-ns is added
568 model_name = 'default'
569
Adam Israel5e08a0e2018-09-06 19:22:47 -0400570 model = await self.get_model(model_name)
Adam Israelb5214512018-05-03 10:00:04 -0400571
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500572 if primitive == 'config':
573 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400574 await self.set_config(
575 model,
576 application_name,
577 params['params'],
578 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500579 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500580 app = await self.get_application(model, application_name)
581 if app:
582 # Run against the first (and probably only) unit in the app
583 unit = app.units[0]
584 if unit:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500585 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600586 uuid = action.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500587 except Exception as e:
Adam Israelb0943662018-08-02 15:32:00 -0400588 self.log.debug(
589 "Caught exception while executing primitive: {}".format(e)
590 )
Adam Israel7d871fb2018-07-17 12:17:06 -0400591 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600592 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500593
Adam Israel5e08a0e2018-09-06 19:22:47 -0400594 async def RemoveCharms(self, model_name, application_name, callback=None,
595 *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400596 """Remove a charm from the VCA.
597
598 Remove a charm referenced in a VNF Descriptor.
599
600 :param str model_name: The name of the network service.
601 :param str application_name: The name of the application
602 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400603 :param tuple callback_args: A list of arguments to be passed to the
604 callback function.
Adam Israelc9df96f2018-05-03 14:49:56 -0400605 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500606 try:
607 if not self.authenticated:
608 await self.login()
609
610 model = await self.get_model(model_name)
611 app = await self.get_application(model, application_name)
612 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400613 # Remove this application from event monitoring
614 self.monitors[model_name].RemoveApplication(application_name)
615
616 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400617 self.log.debug(
618 "Removing the application {}".format(application_name)
619 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500620 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400621
622 # Notify the callback that this charm has been removed.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400623 self.notify_callback(
624 model_name,
625 application_name,
626 "removed",
627 callback,
628 *callback_args,
629 )
Adam Israel28a43c02018-04-23 16:04:54 -0400630
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500631 except Exception as e:
632 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600633 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500634 raise e
635
636 async def DestroyNetworkService(self, nsd):
637 raise NotImplementedError()
638
Adam Israelb5214512018-05-03 10:00:04 -0400639 async def GetMetrics(self, model_name, application_name):
640 """Get the metrics collected by the VCA.
641
642 :param model_name The name of the model
643 :param application_name The name of the application
644 """
645 metrics = {}
646 model = await self.get_model(model_name)
647 app = await self.get_application(model, application_name)
648 if app:
649 metrics = await app.get_metrics()
650
651 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500652
653 # Non-public methods
654 async def add_relation(self, a, b, via=None):
655 """
656 Add a relation between two application endpoints.
657
658 :param a An application endpoint
659 :param b An application endpoint
660 :param via The egress subnet(s) for outbound traffic, e.g.,
661 (192.168.0.0/16,10.0.0.0/8)
662 """
663 if not self.authenticated:
664 await self.login()
665
666 m = await self.get_model()
667 try:
668 m.add_relation(a, b, via)
669 finally:
670 await m.disconnect()
671
Adam Israelb5214512018-05-03 10:00:04 -0400672 # async def apply_config(self, config, application):
673 # """Apply a configuration to the application."""
674 # print("JujuApi: Applying configuration to {}.".format(
675 # application
676 # ))
677 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500678
679 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600680 """Transform the yang config primitive to dict.
681
682 Expected result:
683
684 config = {
685 'config':
686 }
687 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500688 config = {}
689 for primitive in config_primitive:
690 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600691 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500692 for parameter in primitive['parameter']:
693 param = str(parameter['name'])
694 if parameter['value'] == "<rw_mgmt_ip>":
695 config[param] = str(values[parameter['value']])
696 else:
697 config[param] = str(parameter['value'])
698
699 return config
700
Adam Israel88a49632018-04-10 13:04:57 -0600701 def _map_primitive_parameters(self, parameters, values):
702 params = {}
703 for parameter in parameters:
704 param = str(parameter['name'])
Adam Israel5e08a0e2018-09-06 19:22:47 -0400705
706 # Typecast parameter value, if present
707 if 'data-type' in parameter:
708 paramtype = str(parameter['data-type']).lower()
709 value = None
710
711 if paramtype == "integer":
712 value = int(parameter['value'])
713 elif paramtype == "boolean":
714 value = bool(parameter['value'])
715 else:
716 value = str(parameter['value'])
717
Adam Israel88a49632018-04-10 13:04:57 -0600718 if parameter['value'] == "<rw_mgmt_ip>":
719 params[param] = str(values[parameter['value']])
720 else:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400721 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -0600722 return params
723
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500724 def _get_config_from_yang(self, config_primitive, values):
725 """Transform the yang config primitive to dict."""
726 config = {}
727 for primitive in config_primitive.values():
728 if primitive['name'] == 'config':
729 for parameter in primitive['parameter'].values():
730 param = str(parameter['name'])
731 if parameter['value'] == "<rw_mgmt_ip>":
732 config[param] = str(values[parameter['value']])
733 else:
734 config[param] = str(parameter['value'])
735
736 return config
737
Adam Israel5e08a0e2018-09-06 19:22:47 -0400738 @staticmethod
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500739 def FormatApplicationName(self, *args):
740 """
741 Generate a Juju-compatible Application name
742
743 :param args tuple: Positional arguments to be used to construct the
744 application name.
745
746 Limitations::
747 - Only accepts characters a-z and non-consequitive dashes (-)
748 - Application name should not exceed 50 characters
749
750 Examples::
751
752 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
753 """
754
755 appname = ""
756 for c in "-".join(list(args)):
757 if c.isdigit():
758 c = chr(97 + int(c))
759 elif not c.isalpha():
760 c = "-"
761 appname += c
762 return re.sub('\-+', '-', appname.lower())
763
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500764 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
765 # """Format the name of the application
766 #
767 # Limitations:
768 # - Only accepts characters a-z and non-consequitive dashes (-)
769 # - Application name should not exceed 50 characters
770 # """
771 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
772 # new_name = ''
773 # for c in name:
774 # if c.isdigit():
775 # c = chr(97 + int(c))
776 # elif not c.isalpha():
777 # c = "-"
778 # new_name += c
779 # return re.sub('\-+', '-', new_name.lower())
780
781 def format_model_name(self, name):
782 """Format the name of model.
783
784 Model names may only contain lowercase letters, digits and hyphens
785 """
786
787 return name.replace('_', '-').lower()
788
789 async def get_application(self, model, application):
790 """Get the deployed application."""
791 if not self.authenticated:
792 await self.login()
793
794 app = None
795 if application and model:
796 if model.applications:
797 if application in model.applications:
798 app = model.applications[application]
799
800 return app
801
802 async def get_model(self, model_name='default'):
803 """Get a model from the Juju Controller.
804
805 Note: Model objects returned must call disconnected() before it goes
806 out of scope."""
807 if not self.authenticated:
808 await self.login()
809
810 if model_name not in self.models:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400811 self.models[model_name] = await self.controller.get_model(
812 model_name,
813 )
Adam Israelfc511ed2018-09-21 14:20:55 +0200814 self.refcount['model'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500815
Adam Israel28a43c02018-04-23 16:04:54 -0400816 # Create an observer for this model
817 self.monitors[model_name] = VCAMonitor(model_name)
818 self.models[model_name].add_observer(self.monitors[model_name])
819
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500820 return self.models[model_name]
821
822 async def login(self):
823 """Login to the Juju controller."""
824
825 if self.authenticated:
826 return
827
828 self.connecting = True
829
830 self.log.debug("JujuApi: Logging into controller")
831
832 cacert = None
Adam Israel5e08a0e2018-09-06 19:22:47 -0400833 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500834
835 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400836 self.log.debug(
837 "Connecting to controller... ws://{}:{} as {}/{}".format(
838 self.endpoint,
839 self.port,
840 self.user,
841 self.secret,
842 )
843 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500844 await self.controller.connect(
845 endpoint=self.endpoint,
846 username=self.user,
847 password=self.secret,
848 cacert=cacert,
849 )
Adam Israelfc511ed2018-09-21 14:20:55 +0200850 self.refcount['controller'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500851 else:
852 # current_controller no longer exists
853 # self.log.debug("Connecting to current controller...")
854 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -0600855 # await self.controller.connect(
856 # endpoint=self.endpoint,
857 # username=self.user,
858 # cacert=cacert,
859 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500860 self.log.fatal("VCA credentials not configured.")
861
862 self.authenticated = True
863 self.log.debug("JujuApi: Logged into controller")
864
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500865 async def logout(self):
866 """Logout of the Juju controller."""
867 if not self.authenticated:
868 return
869
870 try:
871 if self.default_model:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400872 self.log.debug("Disconnecting model {}".format(
873 self.default_model
874 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500875 await self.default_model.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +0200876 self.refcount['model'] -= 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500877 self.default_model = None
878
879 for model in self.models:
880 await self.models[model].disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +0200881 self.refcount['model'] -= 1
882 self.models[model] = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500883
884 if self.controller:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400885 self.log.debug("Disconnecting controller {}".format(
886 self.controller
887 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500888 await self.controller.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +0200889 self.refcount['controller'] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -0400890 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500891
892 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +0200893
894 self.log.debug(self.refcount)
895
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500896 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400897 self.log.fatal(
898 "Fatal error logging out of Juju Controller: {}".format(e)
899 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500900 raise e
901
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500902 # async def remove_application(self, name):
903 # """Remove the application."""
904 # if not self.authenticated:
905 # await self.login()
906 #
907 # app = await self.get_application(name)
908 # if app:
909 # self.log.debug("JujuApi: Destroying application {}".format(
910 # name,
911 # ))
912 #
913 # await app.destroy()
914
915 async def remove_relation(self, a, b):
916 """
917 Remove a relation between two application endpoints
918
919 :param a An application endpoint
920 :param b An application endpoint
921 """
922 if not self.authenticated:
923 await self.login()
924
925 m = await self.get_model()
926 try:
927 m.remove_relation(a, b)
928 finally:
929 await m.disconnect()
930
931 async def resolve_error(self, application=None):
932 """Resolve units in error state."""
933 if not self.authenticated:
934 await self.login()
935
936 app = await self.get_application(self.default_model, application)
937 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400938 self.log.debug(
939 "JujuApi: Resolving errors for application {}".format(
940 application,
941 )
942 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500943
944 for unit in app.units:
945 app.resolved(retry=True)
946
947 async def run_action(self, application, action_name, **params):
948 """Execute an action and return an Action object."""
949 if not self.authenticated:
950 await self.login()
951 result = {
952 'status': '',
953 'action': {
954 'tag': None,
955 'results': None,
956 }
957 }
958 app = await self.get_application(self.default_model, application)
959 if app:
960 # We currently only have one unit per application
961 # so use the first unit available.
962 unit = app.units[0]
963
Adam Israel5e08a0e2018-09-06 19:22:47 -0400964 self.log.debug(
965 "JujuApi: Running Action {} against Application {}".format(
966 action_name,
967 application,
968 )
969 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500970
971 action = await unit.run_action(action_name, **params)
972
973 # Wait for the action to complete
974 await action.wait()
975
976 result['status'] = action.status
977 result['action']['tag'] = action.data['id']
978 result['action']['results'] = action.results
979
980 return result
981
Adam Israelb5214512018-05-03 10:00:04 -0400982 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500983 """Apply a configuration to the application."""
984 if not self.authenticated:
985 await self.login()
986
Adam Israelb5214512018-05-03 10:00:04 -0400987 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500988 if app:
989 self.log.debug("JujuApi: Setting config for Application {}".format(
990 application,
991 ))
992 await app.set_config(config)
993
994 # Verify the config is set
995 newconf = await app.get_config()
996 for key in config:
997 if config[key] != newconf[key]['value']:
998 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
999
Adam Israelb5214512018-05-03 10:00:04 -04001000 # async def set_parameter(self, parameter, value, application=None):
1001 # """Set a config parameter for a service."""
1002 # if not self.authenticated:
1003 # await self.login()
1004 #
1005 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1006 # parameter,
1007 # value,
1008 # application,
1009 # ))
1010 # return await self.apply_config(
1011 # {parameter: value},
1012 # application=application,
1013 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001014
Adam Israel5e08a0e2018-09-06 19:22:47 -04001015 async def wait_for_application(self, model_name, application_name,
1016 timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001017 """Wait for an application to become active."""
1018 if not self.authenticated:
1019 await self.login()
1020
Adam Israel5e08a0e2018-09-06 19:22:47 -04001021 # TODO: In a point release, we will use a model per deployed network
1022 # service. In the meantime, we will always use the 'default' model.
1023 model_name = 'default'
1024 model = await self.get_model(model_name)
1025
1026 app = await self.get_application(model, application_name)
1027 self.log.debug("Application: {}".format(app))
1028 # app = await self.get_application(model_name, application_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001029 if app:
1030 self.log.debug(
1031 "JujuApi: Waiting {} seconds for Application {}".format(
1032 timeout,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001033 application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001034 )
1035 )
1036
Adam Israel5e08a0e2018-09-06 19:22:47 -04001037 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001038 lambda: all(
Adam Israel5e08a0e2018-09-06 19:22:47 -04001039 unit.agent_status == 'idle' and unit.workload_status in
1040 ['active', 'unknown'] for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001041 ),
1042 timeout=timeout
1043 )