blob: 1b9efa8447316b094717084eb6e3612fb2d20cf7 [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
335 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600336 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500337 {'<rw_mgmt_ip>': rw_mgmt_ip}
338 )
339
Adam Israel88a49632018-04-10 13:04:57 -0600340 self.log.debug("JujuApi: Deploying charm ({}) from {}".format(
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500341 application_name,
342 charm_path,
343 to=to,
344 ))
345
346 ########################################################
347 # Deploy the charm and apply the initial configuration #
348 ########################################################
349 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600350 # We expect charm_path to be either the path to the charm on disk
351 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500352 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600353 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500354 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600355 # Proxy charms should use the current LTS. This will need to be
356 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500357 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600358 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500359 config=initial_config,
Adam Israel88a49632018-04-10 13:04:57 -0600360 # TBD: Where to deploy the charm to.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500361 to=None,
362 )
363
Adam Israel88a49632018-04-10 13:04:57 -0600364 # #######################################
365 # # Execute initial config primitive(s) #
366 # #######################################
367 primitives = {}
368
369 # Build a sequential list of the primitives to execute
370 for primitive in params['initial-config-primitive']:
371 try:
372 if primitive['name'] == 'config':
373 # This is applied when the Application is deployed
374 pass
375 else:
Adam Israel88a49632018-04-10 13:04:57 -0600376 seq = primitive['seq']
377
Adam Israel42d88e62018-07-16 14:18:41 -0400378 params = {}
379 if 'parameter' in primitive:
380 params = primitive['parameter']
381
Adam Israel88a49632018-04-10 13:04:57 -0600382 primitives[seq] = {
383 'name': primitive['name'],
384 'parameters': self._map_primitive_parameters(
Adam Israel42d88e62018-07-16 14:18:41 -0400385 params,
Adam Israel88a49632018-04-10 13:04:57 -0600386 {'<rw_mgmt_ip>': rw_mgmt_ip}
387 ),
388 }
389
390 for primitive in sorted(primitives):
391 await self.ExecutePrimitive(
392 model_name,
393 application_name,
394 primitives[primitive]['name'],
395 callback,
396 callback_args,
397 **primitives[primitive]['parameters'],
398 )
399 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400400 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600401 "[N2VC] Exception executing primitive: {}".format(e)
402 )
403 raise
404
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500405 async def ExecutePrimitive(self, model_name, application_name, primitive, callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400406 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600407
Adam Israelc9df96f2018-05-03 14:49:56 -0400408 Execute a primitive defined in the VNF descriptor.
409
410 :param str model_name: The name of the network service.
411 :param str application_name: The name of the application
412 :param str primitive: The name of the primitive to execute.
413 :param obj callback: A callback function to receive status changes.
414 :param tuple callback_args: A list of arguments to be passed to the callback function.
415 :param dict params: A dictionary of key=value pairs representing the primitive's parameters
416 Examples::
417 {
418 'rw_mgmt_ip': '1.2.3.4',
419 # Pass the initial-config-primitives section of the vnf or vdu
420 'initial-config-primitives': {...}
421 }
Adam Israel6817f612018-04-13 08:41:43 -0600422 """
423 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500424 try:
425 if not self.authenticated:
426 await self.login()
427
428 # FIXME: This is hard-coded until model-per-ns is added
429 model_name = 'default'
430
Adam Israelb5214512018-05-03 10:00:04 -0400431 model = await self.controller.get_model(model_name)
432
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500433 if primitive == 'config':
434 # config is special, and expecting params to be a dictionary
Adam Israelb5214512018-05-03 10:00:04 -0400435 self.log.debug("Setting charm configuration for {}".format(application_name))
436 self.log.debug(params['params'])
437 await self.set_config(model, application_name, params['params'])
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500438 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500439 app = await self.get_application(model, application_name)
440 if app:
441 # Run against the first (and probably only) unit in the app
442 unit = app.units[0]
443 if unit:
444 self.log.debug("Executing primitive {}".format(primitive))
445 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600446 uuid = action.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500447 await model.disconnect()
448 except Exception as e:
449 self.log.debug("Caught exception while executing primitive: {}".format(e))
Adam Israel7d871fb2018-07-17 12:17:06 -0400450 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600451 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500452
453 async def RemoveCharms(self, model_name, application_name, callback=None, *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400454 """Remove a charm from the VCA.
455
456 Remove a charm referenced in a VNF Descriptor.
457
458 :param str model_name: The name of the network service.
459 :param str application_name: The name of the application
460 :param obj callback: A callback function to receive status changes.
461 :param tuple callback_args: A list of arguments to be passed to the callback function.
462 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500463 try:
464 if not self.authenticated:
465 await self.login()
466
467 model = await self.get_model(model_name)
468 app = await self.get_application(model, application_name)
469 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400470 # Remove this application from event monitoring
471 self.monitors[model_name].RemoveApplication(application_name)
472
473 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
474 self.log.debug("Removing the application {}".format(application_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500475 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400476
477 # Notify the callback that this charm has been removed.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500478 self.notify_callback(model_name, application_name, "removed", callback, *callback_args)
Adam Israel28a43c02018-04-23 16:04:54 -0400479
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500480 except Exception as e:
481 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600482 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500483 raise e
484
485 async def DestroyNetworkService(self, nsd):
486 raise NotImplementedError()
487
Adam Israelb5214512018-05-03 10:00:04 -0400488 async def GetMetrics(self, model_name, application_name):
489 """Get the metrics collected by the VCA.
490
491 :param model_name The name of the model
492 :param application_name The name of the application
493 """
494 metrics = {}
495 model = await self.get_model(model_name)
496 app = await self.get_application(model, application_name)
497 if app:
498 metrics = await app.get_metrics()
499
500 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500501
502 # Non-public methods
503 async def add_relation(self, a, b, via=None):
504 """
505 Add a relation between two application endpoints.
506
507 :param a An application endpoint
508 :param b An application endpoint
509 :param via The egress subnet(s) for outbound traffic, e.g.,
510 (192.168.0.0/16,10.0.0.0/8)
511 """
512 if not self.authenticated:
513 await self.login()
514
515 m = await self.get_model()
516 try:
517 m.add_relation(a, b, via)
518 finally:
519 await m.disconnect()
520
Adam Israelb5214512018-05-03 10:00:04 -0400521 # async def apply_config(self, config, application):
522 # """Apply a configuration to the application."""
523 # print("JujuApi: Applying configuration to {}.".format(
524 # application
525 # ))
526 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500527
528 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600529 """Transform the yang config primitive to dict.
530
531 Expected result:
532
533 config = {
534 'config':
535 }
536 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500537 config = {}
538 for primitive in config_primitive:
539 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600540 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500541 for parameter in primitive['parameter']:
542 param = str(parameter['name'])
543 if parameter['value'] == "<rw_mgmt_ip>":
544 config[param] = str(values[parameter['value']])
545 else:
546 config[param] = str(parameter['value'])
547
548 return config
549
Adam Israel88a49632018-04-10 13:04:57 -0600550 def _map_primitive_parameters(self, parameters, values):
551 params = {}
552 for parameter in parameters:
553 param = str(parameter['name'])
554 if parameter['value'] == "<rw_mgmt_ip>":
555 params[param] = str(values[parameter['value']])
556 else:
557 params[param] = str(parameter['value'])
558 return params
559
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500560 def _get_config_from_yang(self, config_primitive, values):
561 """Transform the yang config primitive to dict."""
562 config = {}
563 for primitive in config_primitive.values():
564 if primitive['name'] == 'config':
565 for parameter in primitive['parameter'].values():
566 param = str(parameter['name'])
567 if parameter['value'] == "<rw_mgmt_ip>":
568 config[param] = str(values[parameter['value']])
569 else:
570 config[param] = str(parameter['value'])
571
572 return config
573
574 def FormatApplicationName(self, *args):
575 """
576 Generate a Juju-compatible Application name
577
578 :param args tuple: Positional arguments to be used to construct the
579 application name.
580
581 Limitations::
582 - Only accepts characters a-z and non-consequitive dashes (-)
583 - Application name should not exceed 50 characters
584
585 Examples::
586
587 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
588 """
589
590 appname = ""
591 for c in "-".join(list(args)):
592 if c.isdigit():
593 c = chr(97 + int(c))
594 elif not c.isalpha():
595 c = "-"
596 appname += c
597 return re.sub('\-+', '-', appname.lower())
598
599
600 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
601 # """Format the name of the application
602 #
603 # Limitations:
604 # - Only accepts characters a-z and non-consequitive dashes (-)
605 # - Application name should not exceed 50 characters
606 # """
607 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
608 # new_name = ''
609 # for c in name:
610 # if c.isdigit():
611 # c = chr(97 + int(c))
612 # elif not c.isalpha():
613 # c = "-"
614 # new_name += c
615 # return re.sub('\-+', '-', new_name.lower())
616
617 def format_model_name(self, name):
618 """Format the name of model.
619
620 Model names may only contain lowercase letters, digits and hyphens
621 """
622
623 return name.replace('_', '-').lower()
624
625 async def get_application(self, model, application):
626 """Get the deployed application."""
627 if not self.authenticated:
628 await self.login()
629
630 app = None
631 if application and model:
632 if model.applications:
633 if application in model.applications:
634 app = model.applications[application]
635
636 return app
637
638 async def get_model(self, model_name='default'):
639 """Get a model from the Juju Controller.
640
641 Note: Model objects returned must call disconnected() before it goes
642 out of scope."""
643 if not self.authenticated:
644 await self.login()
645
646 if model_name not in self.models:
647 print("connecting to model {}".format(model_name))
648 self.models[model_name] = await self.controller.get_model(model_name)
649
Adam Israel28a43c02018-04-23 16:04:54 -0400650 # Create an observer for this model
651 self.monitors[model_name] = VCAMonitor(model_name)
652 self.models[model_name].add_observer(self.monitors[model_name])
653
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500654 return self.models[model_name]
655
656 async def login(self):
657 """Login to the Juju controller."""
658
659 if self.authenticated:
660 return
661
662 self.connecting = True
663
664 self.log.debug("JujuApi: Logging into controller")
665
666 cacert = None
667 self.controller = Controller()
668
669 if self.secret:
670 self.log.debug("Connecting to controller... ws://{}:{} as {}/{}".format(self.endpoint, self.port, self.user, self.secret))
671 await self.controller.connect(
672 endpoint=self.endpoint,
673 username=self.user,
674 password=self.secret,
675 cacert=cacert,
676 )
677 else:
678 # current_controller no longer exists
679 # self.log.debug("Connecting to current controller...")
680 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -0600681 # await self.controller.connect(
682 # endpoint=self.endpoint,
683 # username=self.user,
684 # cacert=cacert,
685 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500686 self.log.fatal("VCA credentials not configured.")
687
688 self.authenticated = True
689 self.log.debug("JujuApi: Logged into controller")
690
691 # self.default_model = await self.controller.get_model("default")
692
693 async def logout(self):
694 """Logout of the Juju controller."""
695 if not self.authenticated:
696 return
697
698 try:
699 if self.default_model:
700 self.log.debug("Disconnecting model {}".format(self.default_model))
701 await self.default_model.disconnect()
702 self.default_model = None
703
704 for model in self.models:
705 await self.models[model].disconnect()
706
707 if self.controller:
708 self.log.debug("Disconnecting controller {}".format(self.controller))
709 await self.controller.disconnect()
710 # self.controller = None
711
712 self.authenticated = False
713 except Exception as e:
714 self.log.fail("Fatal error logging out of Juju Controller: {}".format(e))
715 raise e
716
717
718 # async def remove_application(self, name):
719 # """Remove the application."""
720 # if not self.authenticated:
721 # await self.login()
722 #
723 # app = await self.get_application(name)
724 # if app:
725 # self.log.debug("JujuApi: Destroying application {}".format(
726 # name,
727 # ))
728 #
729 # await app.destroy()
730
731 async def remove_relation(self, a, b):
732 """
733 Remove a relation between two application endpoints
734
735 :param a An application endpoint
736 :param b An application endpoint
737 """
738 if not self.authenticated:
739 await self.login()
740
741 m = await self.get_model()
742 try:
743 m.remove_relation(a, b)
744 finally:
745 await m.disconnect()
746
747 async def resolve_error(self, application=None):
748 """Resolve units in error state."""
749 if not self.authenticated:
750 await self.login()
751
752 app = await self.get_application(self.default_model, application)
753 if app:
754 self.log.debug("JujuApi: Resolving errors for application {}".format(
755 application,
756 ))
757
758 for unit in app.units:
759 app.resolved(retry=True)
760
761 async def run_action(self, application, action_name, **params):
762 """Execute an action and return an Action object."""
763 if not self.authenticated:
764 await self.login()
765 result = {
766 'status': '',
767 'action': {
768 'tag': None,
769 'results': None,
770 }
771 }
772 app = await self.get_application(self.default_model, application)
773 if app:
774 # We currently only have one unit per application
775 # so use the first unit available.
776 unit = app.units[0]
777
778 self.log.debug("JujuApi: Running Action {} against Application {}".format(
779 action_name,
780 application,
781 ))
782
783 action = await unit.run_action(action_name, **params)
784
785 # Wait for the action to complete
786 await action.wait()
787
788 result['status'] = action.status
789 result['action']['tag'] = action.data['id']
790 result['action']['results'] = action.results
791
792 return result
793
Adam Israelb5214512018-05-03 10:00:04 -0400794 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500795 """Apply a configuration to the application."""
796 if not self.authenticated:
797 await self.login()
798
Adam Israelb5214512018-05-03 10:00:04 -0400799 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500800 if app:
801 self.log.debug("JujuApi: Setting config for Application {}".format(
802 application,
803 ))
804 await app.set_config(config)
805
806 # Verify the config is set
807 newconf = await app.get_config()
808 for key in config:
809 if config[key] != newconf[key]['value']:
810 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
811
Adam Israelb5214512018-05-03 10:00:04 -0400812 # async def set_parameter(self, parameter, value, application=None):
813 # """Set a config parameter for a service."""
814 # if not self.authenticated:
815 # await self.login()
816 #
817 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
818 # parameter,
819 # value,
820 # application,
821 # ))
822 # return await self.apply_config(
823 # {parameter: value},
824 # application=application,
825 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500826
827 async def wait_for_application(self, name, timeout=300):
828 """Wait for an application to become active."""
829 if not self.authenticated:
830 await self.login()
831
832 app = await self.get_application(self.default_model, name)
833 if app:
834 self.log.debug(
835 "JujuApi: Waiting {} seconds for Application {}".format(
836 timeout,
837 name,
838 )
839 )
840
841 await self.default_model.block_until(
842 lambda: all(
843 unit.agent_status == 'idle'
844 and unit.workload_status
845 in ['active', 'unknown'] for unit in app.units
846 ),
847 timeout=timeout
848 )