blob: f642ead13b2d0e6d4c1facbd52664ee8609bf9ed [file] [log] [blame]
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001
2import logging
3import os
4import os.path
5import re
6import ssl
7import sys
8import time
9
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
19from juju.model import Model, ModelObserver
20
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']
86 callback_args = self.applications[application_name]['callback_args']
87
Adam Israelc3e6c2e2018-03-01 09:31:50 -050088 if old and new:
89 old_status = old.workload_status
90 new_status = new.workload_status
Adam Israel88a49632018-04-10 13:04:57 -060091
Adam Israelc3e6c2e2018-03-01 09:31:50 -050092 if old_status == new_status:
93 """The workload status may fluctuate around certain events,
94 so wait until the status has stabilized before triggering
95 the callback."""
Adam Israel28a43c02018-04-23 16:04:54 -040096 if callback:
97 callback(
Adam Israelc3e6c2e2018-03-01 09:31:50 -050098 self.ns_name,
Adam Israel28a43c02018-04-23 16:04:54 -040099 delta.data['application'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500100 new_status,
Adam Israel9562f432018-05-09 13:55:28 -0400101 new.workload_status_message,
Adam Israel28a43c02018-04-23 16:04:54 -0400102 *callback_args)
103
104 if old and not new:
105 # This is a charm being removed
106 if callback:
107 callback(
108 self.ns_name,
109 delta.data['application'],
110 "removed",
Adam Israel9562f432018-05-09 13:55:28 -0400111 "",
Adam Israel28a43c02018-04-23 16:04:54 -0400112 *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500113 except Exception as e:
114 self.log.debug("[1] notify_callback exception {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600115 elif delta.entity == "action":
116 # TODO: Decide how we want to notify the user of actions
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500117
Adam Israel88a49632018-04-10 13:04:57 -0600118 # uuid = delta.data['id'] # The Action's unique id
119 # msg = delta.data['message'] # The output of the action
120 #
121 # if delta.data['status'] == "pending":
122 # # The action is queued
123 # pass
124 # elif delta.data['status'] == "completed""
125 # # The action was successful
126 # pass
127 # elif delta.data['status'] == "failed":
128 # # The action failed.
129 # pass
130
131 pass
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500132
133########
134# TODO
135#
136# Create unique models per network service
137# Document all public functions
138
Adam Israelb5214512018-05-03 10:00:04 -0400139
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500140class N2VC:
141
142 # Juju API
143 api = None
144 log = None
145 controller = None
146 connecting = False
147 authenticated = False
148
149 models = {}
150 default_model = None
151
152 # Model Observers
153 monitors = {}
154
155 # VCA config
156 hostname = ""
157 port = 17070
158 username = ""
159 secret = ""
160
161 def __init__(self,
162 log=None,
163 server='127.0.0.1',
164 port=17070,
165 user='admin',
166 secret=None,
167 artifacts=None
168 ):
169 """Initialize N2VC
170
171 :param vcaconfig dict A dictionary containing the VCA configuration
172
173 :param artifacts str The directory where charms required by a vnfd are
174 stored.
175
176 :Example:
177 n2vc = N2VC(vcaconfig={
178 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
179 'user': 'admin',
180 'ip-address': '10.44.127.137',
181 'port': 17070,
182 'artifacts': '/path/to/charms'
183 })
184
185 """
186
187 if log:
188 self.log = log
189 else:
190 self.log = logging.getLogger(__name__)
191
192 # Quiet websocket traffic
193 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
194 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
195 logging.getLogger('model').setLevel(logging.WARN)
196 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
197
198 self.log.debug('JujuApi: instantiated')
199
200 self.server = server
201 self.port = port
202
203 self.secret = secret
204 if user.startswith('user-'):
205 self.user = user
206 else:
207 self.user = 'user-{}'.format(user)
208
209 self.endpoint = '%s:%d' % (server, int(port))
210
211 self.artifacts = artifacts
212
213 def __del__(self):
214 """Close any open connections."""
215 yield self.logout()
216
Adam Israel9562f432018-05-09 13:55:28 -0400217 def notify_callback(self, model_name, application_name, status, message, callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500218 try:
219 if callback:
Adam Israel9562f432018-05-09 13:55:28 -0400220 callback(model_name, application_name, status, message, *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500221 except Exception as e:
222 self.log.error("[0] notify_callback exception {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600223 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500224 return True
225
226 # Public methods
227 async def CreateNetworkService(self, nsd):
228 """Create a new model to encapsulate this network service.
229
230 Create a new model in the Juju controller to encapsulate the
231 charms associated with a network service.
232
233 You can pass either the nsd record or the id of the network
234 service, but this method will fail without one of them.
235 """
236 if not self.authenticated:
237 await self.login()
238
239 # Ideally, we will create a unique model per network service.
240 # This change will require all components, i.e., LCM and SO, to use
241 # N2VC for 100% compatibility. If we adopt unique models for the LCM,
242 # services deployed via LCM would't be manageable via SO and vice versa
243
244 return self.default_model
245
246 async def DeployCharms(self, model_name, application_name, vnfd, charm_path, params={}, machine_spec={}, callback=None, *callback_args):
247 """Deploy one or more charms associated with a VNF.
248
249 Deploy the charm(s) referenced in a VNF Descriptor.
250
Adam Israelc9df96f2018-05-03 14:49:56 -0400251 :param str model_name: The name of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500252 :param str application_name: The name of the application
253 :param dict vnfd: The name of the application
254 :param str charm_path: The path to the Juju charm
255 :param dict params: A dictionary of runtime parameters
256 Examples::
257 {
Adam Israel88a49632018-04-10 13:04:57 -0600258 'rw_mgmt_ip': '1.2.3.4',
259 # Pass the initial-config-primitives section of the vnf or vdu
260 'initial-config-primitives': {...}
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500261 }
262 :param dict machine_spec: A dictionary describing the machine to install to
263 Examples::
264 {
265 'hostname': '1.2.3.4',
266 'username': 'ubuntu',
267 }
268 :param obj callback: A callback function to receive status changes.
269 :param tuple callback_args: A list of arguments to be passed to the callback
270 """
271
272 ########################################################
273 # Verify the path to the charm exists and is readable. #
274 ########################################################
275 if not os.path.exists(charm_path):
276 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
277 self.notify_callback(model_name, application_name, "failed", callback, *callback_args)
278 raise JujuCharmNotFound("No artifacts configured.")
279
280 ################################
281 # Login to the Juju controller #
282 ################################
283 if not self.authenticated:
284 self.log.debug("Authenticating with Juju")
285 await self.login()
286
287 ##########################################
288 # Get the model for this network service #
289 ##########################################
290 # TODO: In a point release, we will use a model per deployed network
291 # service. In the meantime, we will always use the 'default' model.
292 model_name = 'default'
293 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500294
295 ########################################
296 # Verify the application doesn't exist #
297 ########################################
298 app = await self.get_application(model, application_name)
299 if app:
Adam Israel42d88e62018-07-16 14:18:41 -0400300 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500301
Adam Israel28a43c02018-04-23 16:04:54 -0400302 ################################################################
303 # Register this application with the model-level event monitor #
304 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500305 if callback:
Adam Israel28a43c02018-04-23 16:04:54 -0400306 self.monitors[model_name].AddApplication(
307 application_name,
308 callback,
309 *callback_args
310 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500311
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500312 ########################################################
313 # Check for specific machine placement (native charms) #
314 ########################################################
315 to = ""
316 if machine_spec.keys():
317 # TODO: This needs to be tested.
318 # if all(k in machine_spec for k in ['hostname', 'username']):
319 # # Enlist the existing machine in Juju
320 # machine = await self.model.add_machine(spec='ssh:%@%'.format(
321 # specs['host'],
322 # specs['user'],
323 # ))
324 # to = machine.id
325 pass
326
327 #######################################
328 # Get the initial charm configuration #
329 #######################################
330
331 rw_mgmt_ip = None
332 if 'rw_mgmt_ip' in params:
333 rw_mgmt_ip = params['rw_mgmt_ip']
334
Adam Israel5afe0542018-08-08 12:54:55 -0400335 # initial_config = {}
336 if 'initial-config-primitive' not in params:
337 params['initial-config-primitive'] = {}
338
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500339 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600340 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500341 {'<rw_mgmt_ip>': rw_mgmt_ip}
342 )
343
Adam Israel88a49632018-04-10 13:04:57 -0600344 self.log.debug("JujuApi: Deploying charm ({}) from {}".format(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500345 application_name,
346 charm_path,
347 to=to,
348 ))
349
350 ########################################################
351 # Deploy the charm and apply the initial configuration #
352 ########################################################
353 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600354 # We expect charm_path to be either the path to the charm on disk
355 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500356 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600357 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500358 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600359 # Proxy charms should use the current LTS. This will need to be
360 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500361 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600362 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500363 config=initial_config,
Adam Israel88a49632018-04-10 13:04:57 -0600364 # TBD: Where to deploy the charm to.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500365 to=None,
366 )
367
Adam Israel88a49632018-04-10 13:04:57 -0600368 # #######################################
369 # # Execute initial config primitive(s) #
370 # #######################################
371 primitives = {}
372
373 # Build a sequential list of the primitives to execute
374 for primitive in params['initial-config-primitive']:
375 try:
376 if primitive['name'] == 'config':
377 # This is applied when the Application is deployed
378 pass
379 else:
Adam Israel88a49632018-04-10 13:04:57 -0600380 seq = primitive['seq']
381
Adam Israel42d88e62018-07-16 14:18:41 -0400382 params = {}
383 if 'parameter' in primitive:
384 params = primitive['parameter']
385
Adam Israel88a49632018-04-10 13:04:57 -0600386 primitives[seq] = {
387 'name': primitive['name'],
388 'parameters': self._map_primitive_parameters(
Adam Israel42d88e62018-07-16 14:18:41 -0400389 params,
Adam Israel88a49632018-04-10 13:04:57 -0600390 {'<rw_mgmt_ip>': rw_mgmt_ip}
391 ),
392 }
393
394 for primitive in sorted(primitives):
395 await self.ExecutePrimitive(
396 model_name,
397 application_name,
398 primitives[primitive]['name'],
399 callback,
400 callback_args,
401 **primitives[primitive]['parameters'],
402 )
403 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400404 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600405 "[N2VC] Exception executing primitive: {}".format(e)
406 )
407 raise
408
Adam Israelb0943662018-08-02 15:32:00 -0400409 async def GetPrimitiveStatus(self, model_name, uuid):
410 results = None
411 try:
412 if not self.authenticated:
413 await self.login()
414
415 # FIXME: This is hard-coded until model-per-ns is added
416 model_name = 'default'
417
418 model = await self.controller.get_model(model_name)
419
420 results = await model.get_action_output(uuid)
421
422 await model.disconnect()
423 except Exception as e:
424 self.log.debug(
425 "Caught exception while getting primitive status: {}".format(e)
426 )
427 raise N2VCPrimitiveExecutionFailed(e)
428
429 return results
430
431
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500432 async def ExecutePrimitive(self, model_name, application_name, primitive, callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400433 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600434
Adam Israelc9df96f2018-05-03 14:49:56 -0400435 Execute a primitive defined in the VNF descriptor.
436
437 :param str model_name: The name of the network service.
438 :param str application_name: The name of the application
439 :param str primitive: The name of the primitive to execute.
440 :param obj callback: A callback function to receive status changes.
441 :param tuple callback_args: A list of arguments to be passed to the callback function.
442 :param dict params: A dictionary of key=value pairs representing the primitive's parameters
443 Examples::
444 {
445 'rw_mgmt_ip': '1.2.3.4',
446 # Pass the initial-config-primitives section of the vnf or vdu
447 'initial-config-primitives': {...}
448 }
Adam Israel6817f612018-04-13 08:41:43 -0600449 """
450 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500451 try:
452 if not self.authenticated:
453 await self.login()
454
455 # FIXME: This is hard-coded until model-per-ns is added
456 model_name = 'default'
457
Adam Israelb5214512018-05-03 10:00:04 -0400458 model = await self.controller.get_model(model_name)
459
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500460 if primitive == 'config':
461 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400462 await self.set_config(
463 model,
464 application_name,
465 params['params'],
466 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500467 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500468 app = await self.get_application(model, application_name)
469 if app:
470 # Run against the first (and probably only) unit in the app
471 unit = app.units[0]
472 if unit:
Adam Israelb0943662018-08-02 15:32:00 -0400473 self.log.debug(
474 "Executing primitive {}".format(primitive)
475 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500476 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600477 uuid = action.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500478 await model.disconnect()
479 except Exception as e:
Adam Israelb0943662018-08-02 15:32:00 -0400480 self.log.debug(
481 "Caught exception while executing primitive: {}".format(e)
482 )
Adam Israel7d871fb2018-07-17 12:17:06 -0400483 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600484 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500485
486 async def RemoveCharms(self, model_name, application_name, callback=None, *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400487 """Remove a charm from the VCA.
488
489 Remove a charm referenced in a VNF Descriptor.
490
491 :param str model_name: The name of the network service.
492 :param str application_name: The name of the application
493 :param obj callback: A callback function to receive status changes.
494 :param tuple callback_args: A list of arguments to be passed to the callback function.
495 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500496 try:
497 if not self.authenticated:
498 await self.login()
499
500 model = await self.get_model(model_name)
501 app = await self.get_application(model, application_name)
502 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400503 # Remove this application from event monitoring
504 self.monitors[model_name].RemoveApplication(application_name)
505
506 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
507 self.log.debug("Removing the application {}".format(application_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500508 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400509
510 # Notify the callback that this charm has been removed.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500511 self.notify_callback(model_name, application_name, "removed", callback, *callback_args)
Adam Israel28a43c02018-04-23 16:04:54 -0400512
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500513 except Exception as e:
514 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600515 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500516 raise e
517
518 async def DestroyNetworkService(self, nsd):
519 raise NotImplementedError()
520
Adam Israelb5214512018-05-03 10:00:04 -0400521 async def GetMetrics(self, model_name, application_name):
522 """Get the metrics collected by the VCA.
523
524 :param model_name The name of the model
525 :param application_name The name of the application
526 """
527 metrics = {}
528 model = await self.get_model(model_name)
529 app = await self.get_application(model, application_name)
530 if app:
531 metrics = await app.get_metrics()
532
533 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500534
535 # Non-public methods
536 async def add_relation(self, a, b, via=None):
537 """
538 Add a relation between two application endpoints.
539
540 :param a An application endpoint
541 :param b An application endpoint
542 :param via The egress subnet(s) for outbound traffic, e.g.,
543 (192.168.0.0/16,10.0.0.0/8)
544 """
545 if not self.authenticated:
546 await self.login()
547
548 m = await self.get_model()
549 try:
550 m.add_relation(a, b, via)
551 finally:
552 await m.disconnect()
553
Adam Israelb5214512018-05-03 10:00:04 -0400554 # async def apply_config(self, config, application):
555 # """Apply a configuration to the application."""
556 # print("JujuApi: Applying configuration to {}.".format(
557 # application
558 # ))
559 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500560
561 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600562 """Transform the yang config primitive to dict.
563
564 Expected result:
565
566 config = {
567 'config':
568 }
569 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500570 config = {}
571 for primitive in config_primitive:
572 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600573 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500574 for parameter in primitive['parameter']:
575 param = str(parameter['name'])
576 if parameter['value'] == "<rw_mgmt_ip>":
577 config[param] = str(values[parameter['value']])
578 else:
579 config[param] = str(parameter['value'])
580
581 return config
582
Adam Israel88a49632018-04-10 13:04:57 -0600583 def _map_primitive_parameters(self, parameters, values):
584 params = {}
585 for parameter in parameters:
586 param = str(parameter['name'])
587 if parameter['value'] == "<rw_mgmt_ip>":
588 params[param] = str(values[parameter['value']])
589 else:
Adam Israel75a869a2018-07-23 15:39:57 -0400590 """
591 The Juju API uses strictly typed data-types, so we must make
592 sure the parameters from the VNFD match the appropriate type.
593
594 The honus will still be on the operator, to make sure the
595 data-type in the VNFD matches the one in the charm. N2VC will
596 raise N2VCPrimitiveExecutionFailed when there is a mismatch.
597
598 There are three data types supported by the YANG model:
599 # - STRING
600 # - INTEGER
601 # - BOOLEAN
602
603 Each parameter will look like this:
604 {
605 'seq': '3',
606 'name': 'testint',
607 'parameter': [
608 {
609 'name': 'interval',
610 'data-type': 'INTEGER',
611 'value': 20
612 }
613 ]
614 }
615 """
616
617 if 'value' in parameter:
618 # String is the default format
619 val = str(parameter['value'])
620
621 # If the data-type is explicitly set, cast to that type.
622 if 'data-type' in parameter:
623 dt = parameter['data-type'].upper()
624 if dt == "INTEGER":
625 val = int(val)
626
627 elif dt == "BOOLEAN":
628 if val in ['true', 'false', '0', '1']:
629 val = True
630 else:
631 val = False
632
633 params[param] = val
Adam Israel88a49632018-04-10 13:04:57 -0600634 return params
635
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500636 def _get_config_from_yang(self, config_primitive, values):
637 """Transform the yang config primitive to dict."""
638 config = {}
639 for primitive in config_primitive.values():
640 if primitive['name'] == 'config':
641 for parameter in primitive['parameter'].values():
642 param = str(parameter['name'])
643 if parameter['value'] == "<rw_mgmt_ip>":
644 config[param] = str(values[parameter['value']])
645 else:
646 config[param] = str(parameter['value'])
647
648 return config
649
650 def FormatApplicationName(self, *args):
651 """
652 Generate a Juju-compatible Application name
653
654 :param args tuple: Positional arguments to be used to construct the
655 application name.
656
657 Limitations::
658 - Only accepts characters a-z and non-consequitive dashes (-)
659 - Application name should not exceed 50 characters
660
661 Examples::
662
663 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
664 """
665
666 appname = ""
667 for c in "-".join(list(args)):
668 if c.isdigit():
669 c = chr(97 + int(c))
670 elif not c.isalpha():
671 c = "-"
672 appname += c
673 return re.sub('\-+', '-', appname.lower())
674
675
676 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
677 # """Format the name of the application
678 #
679 # Limitations:
680 # - Only accepts characters a-z and non-consequitive dashes (-)
681 # - Application name should not exceed 50 characters
682 # """
683 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
684 # new_name = ''
685 # for c in name:
686 # if c.isdigit():
687 # c = chr(97 + int(c))
688 # elif not c.isalpha():
689 # c = "-"
690 # new_name += c
691 # return re.sub('\-+', '-', new_name.lower())
692
693 def format_model_name(self, name):
694 """Format the name of model.
695
696 Model names may only contain lowercase letters, digits and hyphens
697 """
698
699 return name.replace('_', '-').lower()
700
701 async def get_application(self, model, application):
702 """Get the deployed application."""
703 if not self.authenticated:
704 await self.login()
705
706 app = None
707 if application and model:
708 if model.applications:
709 if application in model.applications:
710 app = model.applications[application]
711
712 return app
713
714 async def get_model(self, model_name='default'):
715 """Get a model from the Juju Controller.
716
717 Note: Model objects returned must call disconnected() before it goes
718 out of scope."""
719 if not self.authenticated:
720 await self.login()
721
722 if model_name not in self.models:
723 print("connecting to model {}".format(model_name))
724 self.models[model_name] = await self.controller.get_model(model_name)
725
Adam Israel28a43c02018-04-23 16:04:54 -0400726 # Create an observer for this model
727 self.monitors[model_name] = VCAMonitor(model_name)
728 self.models[model_name].add_observer(self.monitors[model_name])
729
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500730 return self.models[model_name]
731
732 async def login(self):
733 """Login to the Juju controller."""
734
735 if self.authenticated:
736 return
737
738 self.connecting = True
739
740 self.log.debug("JujuApi: Logging into controller")
741
742 cacert = None
743 self.controller = Controller()
744
745 if self.secret:
746 self.log.debug("Connecting to controller... ws://{}:{} as {}/{}".format(self.endpoint, self.port, self.user, self.secret))
747 await self.controller.connect(
748 endpoint=self.endpoint,
749 username=self.user,
750 password=self.secret,
751 cacert=cacert,
752 )
753 else:
754 # current_controller no longer exists
755 # self.log.debug("Connecting to current controller...")
756 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -0600757 # await self.controller.connect(
758 # endpoint=self.endpoint,
759 # username=self.user,
760 # cacert=cacert,
761 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500762 self.log.fatal("VCA credentials not configured.")
763
764 self.authenticated = True
765 self.log.debug("JujuApi: Logged into controller")
766
767 # self.default_model = await self.controller.get_model("default")
768
769 async def logout(self):
770 """Logout of the Juju controller."""
771 if not self.authenticated:
772 return
773
774 try:
775 if self.default_model:
776 self.log.debug("Disconnecting model {}".format(self.default_model))
777 await self.default_model.disconnect()
778 self.default_model = None
779
780 for model in self.models:
781 await self.models[model].disconnect()
782
783 if self.controller:
784 self.log.debug("Disconnecting controller {}".format(self.controller))
785 await self.controller.disconnect()
786 # self.controller = None
787
788 self.authenticated = False
789 except Exception as e:
790 self.log.fail("Fatal error logging out of Juju Controller: {}".format(e))
791 raise e
792
793
794 # async def remove_application(self, name):
795 # """Remove the application."""
796 # if not self.authenticated:
797 # await self.login()
798 #
799 # app = await self.get_application(name)
800 # if app:
801 # self.log.debug("JujuApi: Destroying application {}".format(
802 # name,
803 # ))
804 #
805 # await app.destroy()
806
807 async def remove_relation(self, a, b):
808 """
809 Remove a relation between two application endpoints
810
811 :param a An application endpoint
812 :param b An application endpoint
813 """
814 if not self.authenticated:
815 await self.login()
816
817 m = await self.get_model()
818 try:
819 m.remove_relation(a, b)
820 finally:
821 await m.disconnect()
822
823 async def resolve_error(self, application=None):
824 """Resolve units in error state."""
825 if not self.authenticated:
826 await self.login()
827
828 app = await self.get_application(self.default_model, application)
829 if app:
830 self.log.debug("JujuApi: Resolving errors for application {}".format(
831 application,
832 ))
833
834 for unit in app.units:
835 app.resolved(retry=True)
836
837 async def run_action(self, application, action_name, **params):
838 """Execute an action and return an Action object."""
839 if not self.authenticated:
840 await self.login()
841 result = {
842 'status': '',
843 'action': {
844 'tag': None,
845 'results': None,
846 }
847 }
848 app = await self.get_application(self.default_model, application)
849 if app:
850 # We currently only have one unit per application
851 # so use the first unit available.
852 unit = app.units[0]
853
854 self.log.debug("JujuApi: Running Action {} against Application {}".format(
855 action_name,
856 application,
857 ))
858
859 action = await unit.run_action(action_name, **params)
860
861 # Wait for the action to complete
862 await action.wait()
863
864 result['status'] = action.status
865 result['action']['tag'] = action.data['id']
866 result['action']['results'] = action.results
867
868 return result
869
Adam Israelb5214512018-05-03 10:00:04 -0400870 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500871 """Apply a configuration to the application."""
872 if not self.authenticated:
873 await self.login()
874
Adam Israelb5214512018-05-03 10:00:04 -0400875 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500876 if app:
877 self.log.debug("JujuApi: Setting config for Application {}".format(
878 application,
879 ))
880 await app.set_config(config)
881
882 # Verify the config is set
883 newconf = await app.get_config()
884 for key in config:
885 if config[key] != newconf[key]['value']:
886 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
887
Adam Israelb5214512018-05-03 10:00:04 -0400888 # async def set_parameter(self, parameter, value, application=None):
889 # """Set a config parameter for a service."""
890 # if not self.authenticated:
891 # await self.login()
892 #
893 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
894 # parameter,
895 # value,
896 # application,
897 # ))
898 # return await self.apply_config(
899 # {parameter: value},
900 # application=application,
901 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500902
903 async def wait_for_application(self, name, timeout=300):
904 """Wait for an application to become active."""
905 if not self.authenticated:
906 await self.login()
907
908 app = await self.get_application(self.default_model, name)
909 if app:
910 self.log.debug(
911 "JujuApi: Waiting {} seconds for Application {}".format(
912 timeout,
913 name,
914 )
915 )
916
917 await self.default_model.block_until(
918 lambda: all(
919 unit.agent_status == 'idle'
920 and unit.workload_status
921 in ['active', 'unknown'] for unit in app.units
922 ),
923 timeout=timeout
924 )