blob: 38a9d154482d64925772c25836f7c8b8832072f4 [file] [log] [blame]
Adam Israel5e08a0e2018-09-06 19:22:47 -04001import asyncio
Adam Israelc3e6c2e2018-03-01 09:31:50 -05002import logging
3import os
4import os.path
5import re
Adam Israelfa329072018-09-14 11:26:13 -04006import shlex
Adam Israelc3e6c2e2018-03-01 09:31:50 -05007import ssl
Adam Israelfa329072018-09-14 11:26:13 -04008import subprocess
Adam Israelc3e6c2e2018-03-01 09:31:50 -05009import sys
Adam Israel5e08a0e2018-09-06 19:22:47 -040010# import time
Adam Israelc3e6c2e2018-03-01 09:31:50 -050011
12# FIXME: this should load the juju inside or modules without having to
13# explicitly install it. Check why it's not working.
14# Load our subtree of the juju library
15path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
16path = os.path.join(path, "modules/libjuju/")
17if path not in sys.path:
18 sys.path.insert(1, path)
19
20from juju.controller import Controller
Adam Israel5e08a0e2018-09-06 19:22:47 -040021from juju.model import ModelObserver
Adam Israel6d84dbd2019-03-08 18:33:35 -050022from juju.errors import JujuAPIError, JujuError
Adam Israelc3e6c2e2018-03-01 09:31:50 -050023
24# We might need this to connect to the websocket securely, but test and verify.
25try:
26 ssl._create_default_https_context = ssl._create_unverified_context
27except AttributeError:
28 # Legacy Python doesn't verify by default (see pep-0476)
29 # https://www.python.org/dev/peps/pep-0476/
30 pass
31
32
33# Custom exceptions
34class JujuCharmNotFound(Exception):
35 """The Charm can't be found or is not readable."""
36
37
38class JujuApplicationExists(Exception):
39 """The Application already exists."""
40
Adam Israelb5214512018-05-03 10:00:04 -040041
Adam Israel88a49632018-04-10 13:04:57 -060042class N2VCPrimitiveExecutionFailed(Exception):
43 """Something failed while attempting to execute a primitive."""
44
Adam Israelc3e6c2e2018-03-01 09:31:50 -050045
Adam Israel6d84dbd2019-03-08 18:33:35 -050046class NetworkServiceDoesNotExist(Exception):
47 """The Network Service being acted against does not exist."""
48
49
Adam Israelc3e6c2e2018-03-01 09:31:50 -050050# Quiet the debug logging
51logging.getLogger('websockets.protocol').setLevel(logging.INFO)
52logging.getLogger('juju.client.connection').setLevel(logging.WARN)
53logging.getLogger('juju.model').setLevel(logging.WARN)
54logging.getLogger('juju.machine').setLevel(logging.WARN)
55
Adam Israelb5214512018-05-03 10:00:04 -040056
Adam Israelc3e6c2e2018-03-01 09:31:50 -050057class VCAMonitor(ModelObserver):
58 """Monitor state changes within the Juju Model."""
Adam Israelc3e6c2e2018-03-01 09:31:50 -050059 log = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -050060
Adam Israel28a43c02018-04-23 16:04:54 -040061 def __init__(self, ns_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050062 self.log = logging.getLogger(__name__)
63
64 self.ns_name = ns_name
Adam Israeld420a8b2019-04-09 16:07:53 -040065 self.applications = {}
Adam Israel28a43c02018-04-23 16:04:54 -040066
67 def AddApplication(self, application_name, callback, *callback_args):
68 if application_name not in self.applications:
69 self.applications[application_name] = {
70 'callback': callback,
71 'callback_args': callback_args
72 }
73
74 def RemoveApplication(self, application_name):
75 if application_name in self.applications:
76 del self.applications[application_name]
Adam Israelc3e6c2e2018-03-01 09:31:50 -050077
78 async def on_change(self, delta, old, new, model):
79 """React to changes in the Juju model."""
80
81 if delta.entity == "unit":
Adam Israel28a43c02018-04-23 16:04:54 -040082 # Ignore change events from other applications
83 if delta.data['application'] not in self.applications.keys():
84 return
85
Adam Israelc3e6c2e2018-03-01 09:31:50 -050086 try:
Adam Israel28a43c02018-04-23 16:04:54 -040087
88 application_name = delta.data['application']
89
90 callback = self.applications[application_name]['callback']
Adam Israel5e08a0e2018-09-06 19:22:47 -040091 callback_args = \
92 self.applications[application_name]['callback_args']
Adam Israel28a43c02018-04-23 16:04:54 -040093
Adam Israelc3e6c2e2018-03-01 09:31:50 -050094 if old and new:
Adam Israelfc511ed2018-09-21 14:20:55 +020095 # Fire off a callback with the application state
96 if callback:
97 callback(
98 self.ns_name,
99 delta.data['application'],
100 new.workload_status,
101 new.workload_status_message,
102 *callback_args)
Adam Israel28a43c02018-04-23 16:04:54 -0400103
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:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400114 self.log.debug("[1] notify_callback exception: {}".format(e))
115
Adam Israel88a49632018-04-10 13:04:57 -0600116 elif delta.entity == "action":
117 # TODO: Decide how we want to notify the user of actions
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500118
Adam Israel88a49632018-04-10 13:04:57 -0600119 # uuid = delta.data['id'] # The Action's unique id
120 # msg = delta.data['message'] # The output of the action
121 #
122 # if delta.data['status'] == "pending":
123 # # The action is queued
124 # pass
125 # elif delta.data['status'] == "completed""
126 # # The action was successful
127 # pass
128 # elif delta.data['status'] == "failed":
129 # # The action failed.
130 # pass
131
132 pass
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500133
134########
135# TODO
136#
137# Create unique models per network service
138# Document all public functions
139
Adam Israelb5214512018-05-03 10:00:04 -0400140
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500141class N2VC:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500142 def __init__(self,
143 log=None,
144 server='127.0.0.1',
145 port=17070,
146 user='admin',
147 secret=None,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400148 artifacts=None,
149 loop=None,
Adam Israelb2a07f52019-04-25 17:17:05 -0400150 juju_public_key=None,
151 ca_cert=None,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500152 ):
153 """Initialize N2VC
Adam Israelb2a07f52019-04-25 17:17:05 -0400154 :param log obj: The logging object to log to
155 :param server str: The IP Address or Hostname of the Juju controller
156 :param port int: The port of the Juju Controller
157 :param user str: The Juju username to authenticate with
158 :param secret str: The Juju password to authenticate with
159 :param artifacts str: The directory where charms required by a vnfd are
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500160 stored.
Adam Israelb2a07f52019-04-25 17:17:05 -0400161 :param loop obj: The loop to use.
162 :param juju_public_key str: The contents of the Juju public SSH key
163 :param ca_cert str: The CA certificate to use to authenticate
164
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500165
166 :Example:
Adam Israelb2a07f52019-04-25 17:17:05 -0400167 client = n2vc.vnf.N2VC(
168 log=log,
169 server='10.1.1.28',
170 port=17070,
171 user='admin',
172 secret='admin',
173 artifacts='/app/storage/myvnf/charms',
174 loop=loop,
175 juju_public_key='<contents of the juju public key>',
176 ca_cert='<contents of CA certificate>',
177 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500178 """
179
Adam Israel5e08a0e2018-09-06 19:22:47 -0400180 # Initialize instance-level variables
181 self.api = None
182 self.log = None
183 self.controller = None
184 self.connecting = False
185 self.authenticated = False
186
Adam Israelfc511ed2018-09-21 14:20:55 +0200187 # For debugging
188 self.refcount = {
189 'controller': 0,
190 'model': 0,
191 }
192
Adam Israel5e08a0e2018-09-06 19:22:47 -0400193 self.models = {}
Adam Israel5e08a0e2018-09-06 19:22:47 -0400194
195 # Model Observers
196 self.monitors = {}
197
198 # VCA config
199 self.hostname = ""
200 self.port = 17070
201 self.username = ""
202 self.secret = ""
203
Adam Israelb2a07f52019-04-25 17:17:05 -0400204 self.juju_public_key = juju_public_key
205 if juju_public_key:
206 self._create_juju_public_key(juju_public_key)
207
208 self.ca_cert = ca_cert
209
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500210 if log:
211 self.log = log
212 else:
213 self.log = logging.getLogger(__name__)
214
215 # Quiet websocket traffic
216 logging.getLogger('websockets.protocol').setLevel(logging.INFO)
217 logging.getLogger('juju.client.connection').setLevel(logging.WARN)
218 logging.getLogger('model').setLevel(logging.WARN)
219 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
220
221 self.log.debug('JujuApi: instantiated')
222
223 self.server = server
224 self.port = port
225
226 self.secret = secret
227 if user.startswith('user-'):
228 self.user = user
229 else:
230 self.user = 'user-{}'.format(user)
231
232 self.endpoint = '%s:%d' % (server, int(port))
233
234 self.artifacts = artifacts
235
Adam Israel5e08a0e2018-09-06 19:22:47 -0400236 self.loop = loop or asyncio.get_event_loop()
237
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500238 def __del__(self):
239 """Close any open connections."""
240 yield self.logout()
241
Adam Israelb2a07f52019-04-25 17:17:05 -0400242 def _create_juju_public_key(self, public_key):
243 """Recreate the Juju public key on disk.
244
245 Certain libjuju commands expect to be run from the same machine as Juju
246 is bootstrapped to. This method will write the public key to disk in
247 that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
248 """
249 if public_key is None or len(public_key) == 0:
250 return
251
252 path = "{}/.local/share/juju/ssh".format(
253 os.path.expanduser('~'),
254 )
255 if not os.path.exists(path):
256 os.makedirs(path)
257
258 with open('{}/juju_id_rsa.pub'.format(path), 'w') as f:
259 f.write(public_key)
260
Adam Israel5e08a0e2018-09-06 19:22:47 -0400261 def notify_callback(self, model_name, application_name, status, message,
262 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500263 try:
264 if callback:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400265 callback(
266 model_name,
267 application_name,
268 status, message,
269 *callback_args,
270 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500271 except Exception as e:
272 self.log.error("[0] notify_callback exception {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600273 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500274 return True
275
276 # Public methods
Adam Israel85a4b212018-11-29 20:30:24 -0500277 async def Relate(self, model_name, vnfd):
Adam Israel136186e2018-09-14 12:01:12 -0400278 """Create a relation between the charm-enabled VDUs in a VNF.
279
280 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
281
282 vdu:
283 ...
284 relation:
285 - provides: dataVM:db
286 requires: mgmtVM:app
287
288 This tells N2VC that the charm referred to by the dataVM vdu offers a relation named 'db', and the mgmtVM vdu has an 'app' endpoint that should be connected to a database.
289
290 :param str ns_name: The name of the network service.
291 :param dict vnfd: The parsed yaml VNF descriptor.
292 """
293
294 # Currently, the call to Relate() is made automatically after the
295 # deployment of each charm; if the relation depends on a charm that
296 # hasn't been deployed yet, the call will fail silently. This will
297 # prevent an API breakage, with the intent of making this an explicitly
298 # required call in a more object-oriented refactor of the N2VC API.
299
300 configs = []
301 vnf_config = vnfd.get("vnf-configuration")
302 if vnf_config:
303 juju = vnf_config['juju']
304 if juju:
305 configs.append(vnf_config)
306
307 for vdu in vnfd['vdu']:
308 vdu_config = vdu.get('vdu-configuration')
309 if vdu_config:
310 juju = vdu_config['juju']
311 if juju:
312 configs.append(vdu_config)
313
314 def _get_application_name(name):
315 """Get the application name that's mapped to a vnf/vdu."""
316 vnf_member_index = 0
317 vnf_name = vnfd['name']
318
319 for vdu in vnfd.get('vdu'):
320 # Compare the named portion of the relation to the vdu's id
321 if vdu['id'] == name:
322 application_name = self.FormatApplicationName(
Adam Israel85a4b212018-11-29 20:30:24 -0500323 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400324 vnf_name,
325 str(vnf_member_index),
326 )
327 return application_name
328 else:
329 vnf_member_index += 1
330
331 return None
332
333 # Loop through relations
334 for cfg in configs:
335 if 'juju' in cfg:
336 if 'relation' in juju:
337 for rel in juju['relation']:
338 try:
339
340 # get the application name for the provides
341 (name, endpoint) = rel['provides'].split(':')
342 application_name = _get_application_name(name)
343
344 provides = "{}:{}".format(
345 application_name,
346 endpoint
347 )
348
349 # get the application name for thr requires
350 (name, endpoint) = rel['requires'].split(':')
351 application_name = _get_application_name(name)
352
353 requires = "{}:{}".format(
354 application_name,
355 endpoint
356 )
357 self.log.debug("Relation: {} <-> {}".format(
358 provides,
359 requires
360 ))
361 await self.add_relation(
Adam Israel85a4b212018-11-29 20:30:24 -0500362 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400363 provides,
364 requires,
365 )
366 except Exception as e:
367 self.log.debug("Exception: {}".format(e))
368
369 return
370
Adam Israel5e08a0e2018-09-06 19:22:47 -0400371 async def DeployCharms(self, model_name, application_name, vnfd,
372 charm_path, params={}, machine_spec={},
373 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500374 """Deploy one or more charms associated with a VNF.
375
376 Deploy the charm(s) referenced in a VNF Descriptor.
377
Adam Israel85a4b212018-11-29 20:30:24 -0500378 :param str model_name: The name or unique id of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500379 :param str application_name: The name of the application
380 :param dict vnfd: The name of the application
381 :param str charm_path: The path to the Juju charm
382 :param dict params: A dictionary of runtime parameters
383 Examples::
384 {
Adam Israel88a49632018-04-10 13:04:57 -0600385 'rw_mgmt_ip': '1.2.3.4',
386 # Pass the initial-config-primitives section of the vnf or vdu
387 'initial-config-primitives': {...}
tierno1afb30a2018-12-21 13:42:43 +0000388 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
389 inside < >. rw_mgmt_ip will be included here also
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500390 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400391 :param dict machine_spec: A dictionary describing the machine to
392 install to
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500393 Examples::
394 {
395 'hostname': '1.2.3.4',
396 'username': 'ubuntu',
397 }
398 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400399 :param tuple callback_args: A list of arguments to be passed to the
400 callback
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500401 """
402
403 ########################################################
404 # Verify the path to the charm exists and is readable. #
405 ########################################################
406 if not os.path.exists(charm_path):
407 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400408 self.notify_callback(
409 model_name,
410 application_name,
411 "failed",
412 callback,
413 *callback_args,
414 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500415 raise JujuCharmNotFound("No artifacts configured.")
416
417 ################################
418 # Login to the Juju controller #
419 ################################
420 if not self.authenticated:
421 self.log.debug("Authenticating with Juju")
422 await self.login()
423
424 ##########################################
425 # Get the model for this network service #
426 ##########################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500427 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500428
429 ########################################
430 # Verify the application doesn't exist #
431 ########################################
432 app = await self.get_application(model, application_name)
433 if app:
Adam Israel42d88e62018-07-16 14:18:41 -0400434 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500435
Adam Israel28a43c02018-04-23 16:04:54 -0400436 ################################################################
437 # Register this application with the model-level event monitor #
438 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500439 if callback:
Adam Israel28a43c02018-04-23 16:04:54 -0400440 self.monitors[model_name].AddApplication(
441 application_name,
442 callback,
443 *callback_args
444 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500445
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500446 ########################################################
447 # Check for specific machine placement (native charms) #
448 ########################################################
449 to = ""
450 if machine_spec.keys():
Adam Israel5963cb42018-09-14 11:26:13 -0400451 if all(k in machine_spec for k in ['host', 'user']):
452 # Enlist an existing machine as a Juju unit
453 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
454 machine_spec['user'],
455 machine_spec['host'],
456 self.GetPrivateKeyPath(),
457 ))
Adam Israelfa329072018-09-14 11:26:13 -0400458 to = machine.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500459
460 #######################################
461 # Get the initial charm configuration #
462 #######################################
463
464 rw_mgmt_ip = None
465 if 'rw_mgmt_ip' in params:
466 rw_mgmt_ip = params['rw_mgmt_ip']
467
Adam Israel5afe0542018-08-08 12:54:55 -0400468 if 'initial-config-primitive' not in params:
469 params['initial-config-primitive'] = {}
470
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500471 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600472 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500473 {'<rw_mgmt_ip>': rw_mgmt_ip}
474 )
475
Adam Israel85a4b212018-11-29 20:30:24 -0500476 self.log.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
477 model_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500478 application_name,
479 charm_path,
480 to=to,
481 ))
482
483 ########################################################
484 # Deploy the charm and apply the initial configuration #
485 ########################################################
486 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600487 # We expect charm_path to be either the path to the charm on disk
488 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500489 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600490 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500491 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600492 # Proxy charms should use the current LTS. This will need to be
493 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500494 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600495 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500496 config=initial_config,
Adam Israelfa329072018-09-14 11:26:13 -0400497 # Where to deploy the charm to.
498 to=to,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500499 )
500
Adam Israel136186e2018-09-14 12:01:12 -0400501 # Map the vdu id<->app name,
502 #
503 await self.Relate(model_name, vnfd)
504
Adam Israel88a49632018-04-10 13:04:57 -0600505 # #######################################
506 # # Execute initial config primitive(s) #
507 # #######################################
Adam Israelcf253202018-10-31 16:29:09 -0700508 uuids = await self.ExecuteInitialPrimitives(
Adam Israel5e08a0e2018-09-06 19:22:47 -0400509 model_name,
510 application_name,
511 params,
512 )
Adam Israelcf253202018-10-31 16:29:09 -0700513 return uuids
Adam Israel5e08a0e2018-09-06 19:22:47 -0400514
515 # primitives = {}
516 #
517 # # Build a sequential list of the primitives to execute
518 # for primitive in params['initial-config-primitive']:
519 # try:
520 # if primitive['name'] == 'config':
521 # # This is applied when the Application is deployed
522 # pass
523 # else:
524 # seq = primitive['seq']
525 #
526 # params = {}
527 # if 'parameter' in primitive:
528 # params = primitive['parameter']
529 #
530 # primitives[seq] = {
531 # 'name': primitive['name'],
532 # 'parameters': self._map_primitive_parameters(
533 # params,
534 # {'<rw_mgmt_ip>': rw_mgmt_ip}
535 # ),
536 # }
537 #
538 # for primitive in sorted(primitives):
539 # await self.ExecutePrimitive(
540 # model_name,
541 # application_name,
542 # primitives[primitive]['name'],
543 # callback,
544 # callback_args,
545 # **primitives[primitive]['parameters'],
546 # )
547 # except N2VCPrimitiveExecutionFailed as e:
548 # self.log.debug(
549 # "[N2VC] Exception executing primitive: {}".format(e)
550 # )
551 # raise
552
553 async def GetPrimitiveStatus(self, model_name, uuid):
554 """Get the status of an executed Primitive.
555
556 The status of an executed Primitive will be one of three values:
557 - completed
558 - failed
559 - running
560 """
561 status = None
562 try:
563 if not self.authenticated:
564 await self.login()
565
Adam Israel5e08a0e2018-09-06 19:22:47 -0400566 model = await self.get_model(model_name)
567
568 results = await model.get_action_status(uuid)
569
570 if uuid in results:
571 status = results[uuid]
572
573 except Exception as e:
574 self.log.debug(
575 "Caught exception while getting primitive status: {}".format(e)
576 )
577 raise N2VCPrimitiveExecutionFailed(e)
578
579 return status
580
581 async def GetPrimitiveOutput(self, model_name, uuid):
582 """Get the output of an executed Primitive.
583
584 Note: this only returns output for a successfully executed primitive.
585 """
586 results = None
587 try:
588 if not self.authenticated:
589 await self.login()
590
Adam Israel5e08a0e2018-09-06 19:22:47 -0400591 model = await self.get_model(model_name)
592 results = await model.get_action_output(uuid, 60)
593 except Exception as e:
594 self.log.debug(
595 "Caught exception while getting primitive status: {}".format(e)
596 )
597 raise N2VCPrimitiveExecutionFailed(e)
598
599 return results
600
Adam Israelfa329072018-09-14 11:26:13 -0400601 # async def ProvisionMachine(self, model_name, hostname, username):
602 # """Provision machine for usage with Juju.
603 #
604 # Provisions a previously instantiated machine for use with Juju.
605 # """
606 # try:
607 # if not self.authenticated:
608 # await self.login()
609 #
610 # # FIXME: This is hard-coded until model-per-ns is added
611 # model_name = 'default'
612 #
613 # model = await self.get_model(model_name)
614 # model.add_machine(spec={})
615 #
616 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
617 # "ubuntu",
618 # host['address'],
619 # private_key_path,
620 # ))
621 # return machine.id
622 #
623 # except Exception as e:
624 # self.log.debug(
625 # "Caught exception while getting primitive status: {}".format(e)
626 # )
627 # raise N2VCPrimitiveExecutionFailed(e)
628
629 def GetPrivateKeyPath(self):
630 homedir = os.environ['HOME']
631 sshdir = "{}/.ssh".format(homedir)
632 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
633 return private_key_path
634
635 async def GetPublicKey(self):
636 """Get the N2VC SSH public key.abs
637
638 Returns the SSH public key, to be injected into virtual machines to
639 be managed by the VCA.
640
641 The first time this is run, a ssh keypair will be created. The public
642 key is injected into a VM so that we can provision the machine with
643 Juju, after which Juju will communicate with the VM directly via the
644 juju agent.
645 """
646 public_key = ""
647
648 # Find the path to where we expect our key to live.
649 homedir = os.environ['HOME']
650 sshdir = "{}/.ssh".format(homedir)
651 if not os.path.exists(sshdir):
652 os.mkdir(sshdir)
653
654 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
655 public_key_path = "{}.pub".format(private_key_path)
656
657 # If we don't have a key generated, generate it.
658 if not os.path.exists(private_key_path):
659 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
660 "rsa",
661 "4096",
662 private_key_path
663 )
664 subprocess.check_output(shlex.split(cmd))
665
666 # Read the public key
667 with open(public_key_path, "r") as f:
668 public_key = f.readline()
669
670 return public_key
671
Adam Israel5e08a0e2018-09-06 19:22:47 -0400672 async def ExecuteInitialPrimitives(self, model_name, application_name,
673 params, callback=None, *callback_args):
674 """Execute multiple primitives.
675
676 Execute multiple primitives as declared in initial-config-primitive.
677 This is useful in cases where the primitives initially failed -- for
678 example, if the charm is a proxy but the proxy hasn't been configured
679 yet.
680 """
681 uuids = []
Adam Israel88a49632018-04-10 13:04:57 -0600682 primitives = {}
683
684 # Build a sequential list of the primitives to execute
685 for primitive in params['initial-config-primitive']:
686 try:
687 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600688 pass
689 else:
Adam Israel88a49632018-04-10 13:04:57 -0600690 seq = primitive['seq']
691
tierno1afb30a2018-12-21 13:42:43 +0000692 params_ = {}
Adam Israel42d88e62018-07-16 14:18:41 -0400693 if 'parameter' in primitive:
tierno1afb30a2018-12-21 13:42:43 +0000694 params_ = primitive['parameter']
695
696 user_values = params.get("user_values", {})
697 if 'rw_mgmt_ip' not in user_values:
698 user_values['rw_mgmt_ip'] = None
699 # just for backward compatibility, because it will be provided always by modern version of LCM
Adam Israel42d88e62018-07-16 14:18:41 -0400700
Adam Israel88a49632018-04-10 13:04:57 -0600701 primitives[seq] = {
702 'name': primitive['name'],
703 'parameters': self._map_primitive_parameters(
tierno1afb30a2018-12-21 13:42:43 +0000704 params_,
705 user_values
Adam Israel88a49632018-04-10 13:04:57 -0600706 ),
707 }
708
709 for primitive in sorted(primitives):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400710 uuids.append(
711 await self.ExecutePrimitive(
712 model_name,
713 application_name,
714 primitives[primitive]['name'],
715 callback,
716 callback_args,
717 **primitives[primitive]['parameters'],
718 )
Adam Israel88a49632018-04-10 13:04:57 -0600719 )
720 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400721 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600722 "[N2VC] Exception executing primitive: {}".format(e)
723 )
724 raise
Adam Israel5e08a0e2018-09-06 19:22:47 -0400725 return uuids
Adam Israel88a49632018-04-10 13:04:57 -0600726
Adam Israel5e08a0e2018-09-06 19:22:47 -0400727 async def ExecutePrimitive(self, model_name, application_name, primitive,
728 callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400729 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600730
Adam Israelc9df96f2018-05-03 14:49:56 -0400731 Execute a primitive defined in the VNF descriptor.
732
Adam Israel85a4b212018-11-29 20:30:24 -0500733 :param str model_name: The name or unique id of the network service.
Adam Israelc9df96f2018-05-03 14:49:56 -0400734 :param str application_name: The name of the application
735 :param str primitive: The name of the primitive to execute.
736 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400737 :param tuple callback_args: A list of arguments to be passed to the
738 callback function.
739 :param dict params: A dictionary of key=value pairs representing the
740 primitive's parameters
Adam Israelc9df96f2018-05-03 14:49:56 -0400741 Examples::
742 {
743 'rw_mgmt_ip': '1.2.3.4',
744 # Pass the initial-config-primitives section of the vnf or vdu
745 'initial-config-primitives': {...}
746 }
Adam Israel6817f612018-04-13 08:41:43 -0600747 """
tierno1afb30a2018-12-21 13:42:43 +0000748 self.log.debug("Executing primitive={} params={}".format(primitive, params))
Adam Israel6817f612018-04-13 08:41:43 -0600749 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500750 try:
751 if not self.authenticated:
752 await self.login()
753
Adam Israel5e08a0e2018-09-06 19:22:47 -0400754 model = await self.get_model(model_name)
Adam Israelb5214512018-05-03 10:00:04 -0400755
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500756 if primitive == 'config':
757 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400758 await self.set_config(
759 model,
760 application_name,
761 params['params'],
762 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500763 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500764 app = await self.get_application(model, application_name)
765 if app:
766 # Run against the first (and probably only) unit in the app
767 unit = app.units[0]
768 if unit:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500769 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600770 uuid = action.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500771 except Exception as e:
Adam Israelb0943662018-08-02 15:32:00 -0400772 self.log.debug(
773 "Caught exception while executing primitive: {}".format(e)
774 )
Adam Israel7d871fb2018-07-17 12:17:06 -0400775 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600776 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500777
Adam Israel5e08a0e2018-09-06 19:22:47 -0400778 async def RemoveCharms(self, model_name, application_name, callback=None,
779 *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400780 """Remove a charm from the VCA.
781
782 Remove a charm referenced in a VNF Descriptor.
783
784 :param str model_name: The name of the network service.
785 :param str application_name: The name of the application
786 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400787 :param tuple callback_args: A list of arguments to be passed to the
788 callback function.
Adam Israelc9df96f2018-05-03 14:49:56 -0400789 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500790 try:
791 if not self.authenticated:
792 await self.login()
793
794 model = await self.get_model(model_name)
795 app = await self.get_application(model, application_name)
796 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400797 # Remove this application from event monitoring
798 self.monitors[model_name].RemoveApplication(application_name)
799
800 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400801 self.log.debug(
802 "Removing the application {}".format(application_name)
803 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500804 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400805
Adam Israel85a4b212018-11-29 20:30:24 -0500806 await self.disconnect_model(self.monitors[model_name])
807
Adam Israel5e08a0e2018-09-06 19:22:47 -0400808 self.notify_callback(
809 model_name,
810 application_name,
811 "removed",
Adam Israelc4f393e2019-03-19 16:33:30 -0400812 "Removing charm {}".format(application_name),
Adam Israel5e08a0e2018-09-06 19:22:47 -0400813 callback,
814 *callback_args,
815 )
Adam Israel28a43c02018-04-23 16:04:54 -0400816
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500817 except Exception as e:
818 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600819 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500820 raise e
821
Adam Israel6d84dbd2019-03-08 18:33:35 -0500822 async def CreateNetworkService(self, ns_uuid):
823 """Create a new Juju model for the Network Service.
824
825 Creates a new Model in the Juju Controller.
826
827 :param str ns_uuid: A unique id representing an instaance of a
828 Network Service.
829
830 :returns: True if the model was created. Raises JujuError on failure.
831 """
832 if not self.authenticated:
833 await self.login()
834
835 models = await self.controller.list_models()
836 if ns_uuid not in models:
837 try:
838 self.models[ns_uuid] = await self.controller.add_model(
839 ns_uuid
840 )
841 except JujuError as e:
842 if "already exists" not in e.message:
843 raise e
Adam Israel7bf2f4d2019-03-15 15:28:47 -0400844
845 # Create an observer for this model
846 await self.create_model_monitor(ns_uuid)
847
Adam Israel6d84dbd2019-03-08 18:33:35 -0500848 return True
849
850 async def DestroyNetworkService(self, ns_uuid):
851 """Destroy a Network Service.
852
853 Destroy the Network Service and any deployed charms.
854
855 :param ns_uuid The unique id of the Network Service
856
857 :returns: True if the model was created. Raises JujuError on failure.
858 """
859
860 # Do not delete the default model. The default model was used by all
861 # Network Services, prior to the implementation of a model per NS.
Adam Israelc4f393e2019-03-19 16:33:30 -0400862 if ns_uuid.lower() == "default":
Adam Israel6d84dbd2019-03-08 18:33:35 -0500863 return False
864
865 if not self.authenticated:
866 self.log.debug("Authenticating with Juju")
867 await self.login()
868
869 # Disconnect from the Model
870 if ns_uuid in self.models:
871 await self.disconnect_model(self.models[ns_uuid])
872
873 try:
874 await self.controller.destroy_models(ns_uuid)
Adam Israelc4f393e2019-03-19 16:33:30 -0400875 except JujuError:
Adam Israel6d84dbd2019-03-08 18:33:35 -0500876 raise NetworkServiceDoesNotExist(
877 "The Network Service '{}' does not exist".format(ns_uuid)
878 )
879
880 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500881
Adam Israelb5214512018-05-03 10:00:04 -0400882 async def GetMetrics(self, model_name, application_name):
883 """Get the metrics collected by the VCA.
884
Adam Israel85a4b212018-11-29 20:30:24 -0500885 :param model_name The name or unique id of the network service
Adam Israelb5214512018-05-03 10:00:04 -0400886 :param application_name The name of the application
887 """
888 metrics = {}
889 model = await self.get_model(model_name)
890 app = await self.get_application(model, application_name)
891 if app:
892 metrics = await app.get_metrics()
893
894 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500895
Adam Israelfa329072018-09-14 11:26:13 -0400896 async def HasApplication(self, model_name, application_name):
897 model = await self.get_model(model_name)
898 app = await self.get_application(model, application_name)
899 if app:
900 return True
901 return False
902
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500903 # Non-public methods
Adam Israel136186e2018-09-14 12:01:12 -0400904 async def add_relation(self, model_name, relation1, relation2):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500905 """
906 Add a relation between two application endpoints.
907
Adam Israel85a4b212018-11-29 20:30:24 -0500908 :param str model_name: The name or unique id of the network service
909 :param str relation1: '<application>[:<relation_name>]'
910 :param str relation2: '<application>[:<relation_name>]'
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500911 """
Adam Israel136186e2018-09-14 12:01:12 -0400912
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500913 if not self.authenticated:
914 await self.login()
915
Adam Israel136186e2018-09-14 12:01:12 -0400916 m = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500917 try:
Adam Israel136186e2018-09-14 12:01:12 -0400918 await m.add_relation(relation1, relation2)
919 except JujuAPIError as e:
920 # If one of the applications in the relationship doesn't exist,
921 # or the relation has already been added, let the operation fail
922 # silently.
923 if 'not found' in e.message:
924 return
925 if 'already exists' in e.message:
926 return
927
928 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500929
Adam Israelb5214512018-05-03 10:00:04 -0400930 # async def apply_config(self, config, application):
931 # """Apply a configuration to the application."""
932 # print("JujuApi: Applying configuration to {}.".format(
933 # application
934 # ))
935 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500936
937 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600938 """Transform the yang config primitive to dict.
939
940 Expected result:
941
942 config = {
943 'config':
944 }
945 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500946 config = {}
947 for primitive in config_primitive:
948 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600949 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500950 for parameter in primitive['parameter']:
951 param = str(parameter['name'])
952 if parameter['value'] == "<rw_mgmt_ip>":
953 config[param] = str(values[parameter['value']])
954 else:
955 config[param] = str(parameter['value'])
956
957 return config
958
tierno1afb30a2018-12-21 13:42:43 +0000959 def _map_primitive_parameters(self, parameters, user_values):
Adam Israel88a49632018-04-10 13:04:57 -0600960 params = {}
961 for parameter in parameters:
962 param = str(parameter['name'])
tierno1afb30a2018-12-21 13:42:43 +0000963 value = parameter.get('value')
964
965 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
966 # Must exist at user_values except if there is a default value
967 if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
968 if parameter['value'][1:-1] in user_values:
969 value = user_values[parameter['value'][1:-1]]
970 elif 'default-value' in parameter:
971 value = parameter['default-value']
972 else:
973 raise KeyError("parameter {}='{}' not supplied ".format(param, value))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400974
Adam Israelbf793522018-11-20 13:54:13 -0500975 # If there's no value, use the default-value (if set)
tierno1afb30a2018-12-21 13:42:43 +0000976 if value is None and 'default-value' in parameter:
Adam Israelbf793522018-11-20 13:54:13 -0500977 value = parameter['default-value']
978
Adam Israel5e08a0e2018-09-06 19:22:47 -0400979 # Typecast parameter value, if present
tierno1afb30a2018-12-21 13:42:43 +0000980 paramtype = "string"
981 try:
982 if 'data-type' in parameter:
983 paramtype = str(parameter['data-type']).lower()
Adam Israel5e08a0e2018-09-06 19:22:47 -0400984
tierno1afb30a2018-12-21 13:42:43 +0000985 if paramtype == "integer":
986 value = int(value)
987 elif paramtype == "boolean":
988 value = bool(value)
989 else:
990 value = str(value)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400991 else:
tierno1afb30a2018-12-21 13:42:43 +0000992 # If there's no data-type, assume the value is a string
993 value = str(value)
994 except ValueError:
995 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param, value, paramtype))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400996
tierno1afb30a2018-12-21 13:42:43 +0000997 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -0600998 return params
999
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001000 def _get_config_from_yang(self, config_primitive, values):
1001 """Transform the yang config primitive to dict."""
1002 config = {}
1003 for primitive in config_primitive.values():
1004 if primitive['name'] == 'config':
1005 for parameter in primitive['parameter'].values():
1006 param = str(parameter['name'])
1007 if parameter['value'] == "<rw_mgmt_ip>":
1008 config[param] = str(values[parameter['value']])
1009 else:
1010 config[param] = str(parameter['value'])
1011
1012 return config
1013
1014 def FormatApplicationName(self, *args):
1015 """
1016 Generate a Juju-compatible Application name
1017
1018 :param args tuple: Positional arguments to be used to construct the
1019 application name.
1020
1021 Limitations::
1022 - Only accepts characters a-z and non-consequitive dashes (-)
1023 - Application name should not exceed 50 characters
1024
1025 Examples::
1026
1027 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
1028 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001029 appname = ""
1030 for c in "-".join(list(args)):
1031 if c.isdigit():
1032 c = chr(97 + int(c))
1033 elif not c.isalpha():
1034 c = "-"
1035 appname += c
Adam Israel6d84dbd2019-03-08 18:33:35 -05001036 return re.sub('-+', '-', appname.lower())
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001037
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001038 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1039 # """Format the name of the application
1040 #
1041 # Limitations:
1042 # - Only accepts characters a-z and non-consequitive dashes (-)
1043 # - Application name should not exceed 50 characters
1044 # """
1045 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1046 # new_name = ''
1047 # for c in name:
1048 # if c.isdigit():
1049 # c = chr(97 + int(c))
1050 # elif not c.isalpha():
1051 # c = "-"
1052 # new_name += c
1053 # return re.sub('\-+', '-', new_name.lower())
1054
1055 def format_model_name(self, name):
1056 """Format the name of model.
1057
1058 Model names may only contain lowercase letters, digits and hyphens
1059 """
1060
1061 return name.replace('_', '-').lower()
1062
1063 async def get_application(self, model, application):
1064 """Get the deployed application."""
1065 if not self.authenticated:
1066 await self.login()
1067
1068 app = None
1069 if application and model:
1070 if model.applications:
1071 if application in model.applications:
1072 app = model.applications[application]
1073
1074 return app
1075
Adam Israel85a4b212018-11-29 20:30:24 -05001076 async def get_model(self, model_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001077 """Get a model from the Juju Controller.
1078
1079 Note: Model objects returned must call disconnected() before it goes
1080 out of scope."""
1081 if not self.authenticated:
1082 await self.login()
1083
1084 if model_name not in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001085 # Get the models in the controller
1086 models = await self.controller.list_models()
1087
1088 if model_name not in models:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001089 try:
1090 self.models[model_name] = await self.controller.add_model(
1091 model_name
1092 )
1093 except JujuError as e:
1094 if "already exists" not in e.message:
1095 raise e
Adam Israel85a4b212018-11-29 20:30:24 -05001096 else:
1097 self.models[model_name] = await self.controller.get_model(
1098 model_name
1099 )
1100
Adam Israelfc511ed2018-09-21 14:20:55 +02001101 self.refcount['model'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001102
Adam Israel28a43c02018-04-23 16:04:54 -04001103 # Create an observer for this model
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001104 await self.create_model_monitor(model_name)
1105
1106 return self.models[model_name]
1107
1108 async def create_model_monitor(self, model_name):
1109 """Create a monitor for the model, if none exists."""
1110 if not self.authenticated:
1111 await self.login()
1112
1113 if model_name not in self.monitors:
Adam Israel28a43c02018-04-23 16:04:54 -04001114 self.monitors[model_name] = VCAMonitor(model_name)
1115 self.models[model_name].add_observer(self.monitors[model_name])
1116
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001117 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001118
1119 async def login(self):
1120 """Login to the Juju controller."""
1121
1122 if self.authenticated:
1123 return
1124
1125 self.connecting = True
1126
1127 self.log.debug("JujuApi: Logging into controller")
1128
Adam Israel5e08a0e2018-09-06 19:22:47 -04001129 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001130
1131 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001132 self.log.debug(
1133 "Connecting to controller... ws://{}:{} as {}/{}".format(
1134 self.endpoint,
1135 self.port,
1136 self.user,
1137 self.secret,
1138 )
1139 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001140 await self.controller.connect(
1141 endpoint=self.endpoint,
1142 username=self.user,
1143 password=self.secret,
Adam Israelb2a07f52019-04-25 17:17:05 -04001144 cacert=self.ca_cert,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001145 )
Adam Israelfc511ed2018-09-21 14:20:55 +02001146 self.refcount['controller'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001147 else:
1148 # current_controller no longer exists
1149 # self.log.debug("Connecting to current controller...")
1150 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -06001151 # await self.controller.connect(
1152 # endpoint=self.endpoint,
1153 # username=self.user,
1154 # cacert=cacert,
1155 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001156 self.log.fatal("VCA credentials not configured.")
1157
1158 self.authenticated = True
1159 self.log.debug("JujuApi: Logged into controller")
1160
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001161 async def logout(self):
1162 """Logout of the Juju controller."""
1163 if not self.authenticated:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001164 return False
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001165
1166 try:
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001167 for model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001168 await self.disconnect_model(model)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001169
1170 if self.controller:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001171 self.log.debug("Disconnecting controller {}".format(
1172 self.controller
1173 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001174 await self.controller.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +02001175 self.refcount['controller'] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -04001176 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001177
1178 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +02001179
1180 self.log.debug(self.refcount)
1181
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001182 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001183 self.log.fatal(
1184 "Fatal error logging out of Juju Controller: {}".format(e)
1185 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001186 raise e
Adam Israel6d84dbd2019-03-08 18:33:35 -05001187 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001188
Adam Israel85a4b212018-11-29 20:30:24 -05001189 async def disconnect_model(self, model):
1190 self.log.debug("Disconnecting model {}".format(model))
1191 if model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001192 print("Disconnecting model")
1193 await self.models[model].disconnect()
1194 self.refcount['model'] -= 1
1195 self.models[model] = None
1196
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001197 # async def remove_application(self, name):
1198 # """Remove the application."""
1199 # if not self.authenticated:
1200 # await self.login()
1201 #
1202 # app = await self.get_application(name)
1203 # if app:
1204 # self.log.debug("JujuApi: Destroying application {}".format(
1205 # name,
1206 # ))
1207 #
1208 # await app.destroy()
1209
1210 async def remove_relation(self, a, b):
1211 """
1212 Remove a relation between two application endpoints
1213
1214 :param a An application endpoint
1215 :param b An application endpoint
1216 """
1217 if not self.authenticated:
1218 await self.login()
1219
1220 m = await self.get_model()
1221 try:
1222 m.remove_relation(a, b)
1223 finally:
1224 await m.disconnect()
1225
Adam Israel85a4b212018-11-29 20:30:24 -05001226 async def resolve_error(self, model_name, application=None):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001227 """Resolve units in error state."""
1228 if not self.authenticated:
1229 await self.login()
1230
Adam Israel85a4b212018-11-29 20:30:24 -05001231 model = await self.get_model(model_name)
1232
1233 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001234 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001235 self.log.debug(
1236 "JujuApi: Resolving errors for application {}".format(
1237 application,
1238 )
1239 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001240
1241 for unit in app.units:
1242 app.resolved(retry=True)
1243
Adam Israel85a4b212018-11-29 20:30:24 -05001244 async def run_action(self, model_name, application, action_name, **params):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001245 """Execute an action and return an Action object."""
1246 if not self.authenticated:
1247 await self.login()
1248 result = {
1249 'status': '',
1250 'action': {
1251 'tag': None,
1252 'results': None,
1253 }
1254 }
Adam Israel85a4b212018-11-29 20:30:24 -05001255
1256 model = await self.get_model(model_name)
1257
1258 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001259 if app:
1260 # We currently only have one unit per application
1261 # so use the first unit available.
1262 unit = app.units[0]
1263
Adam Israel5e08a0e2018-09-06 19:22:47 -04001264 self.log.debug(
1265 "JujuApi: Running Action {} against Application {}".format(
1266 action_name,
1267 application,
1268 )
1269 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001270
1271 action = await unit.run_action(action_name, **params)
1272
1273 # Wait for the action to complete
1274 await action.wait()
1275
1276 result['status'] = action.status
1277 result['action']['tag'] = action.data['id']
1278 result['action']['results'] = action.results
1279
1280 return result
1281
Adam Israelb5214512018-05-03 10:00:04 -04001282 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001283 """Apply a configuration to the application."""
1284 if not self.authenticated:
1285 await self.login()
1286
Adam Israelb5214512018-05-03 10:00:04 -04001287 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001288 if app:
1289 self.log.debug("JujuApi: Setting config for Application {}".format(
1290 application,
1291 ))
1292 await app.set_config(config)
1293
1294 # Verify the config is set
1295 newconf = await app.get_config()
1296 for key in config:
1297 if config[key] != newconf[key]['value']:
1298 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1299
Adam Israelb5214512018-05-03 10:00:04 -04001300 # async def set_parameter(self, parameter, value, application=None):
1301 # """Set a config parameter for a service."""
1302 # if not self.authenticated:
1303 # await self.login()
1304 #
1305 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1306 # parameter,
1307 # value,
1308 # application,
1309 # ))
1310 # return await self.apply_config(
1311 # {parameter: value},
1312 # application=application,
1313 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001314
Adam Israel5e08a0e2018-09-06 19:22:47 -04001315 async def wait_for_application(self, model_name, application_name,
1316 timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001317 """Wait for an application to become active."""
1318 if not self.authenticated:
1319 await self.login()
1320
Adam Israel5e08a0e2018-09-06 19:22:47 -04001321 model = await self.get_model(model_name)
1322
1323 app = await self.get_application(model, application_name)
1324 self.log.debug("Application: {}".format(app))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001325 if app:
1326 self.log.debug(
1327 "JujuApi: Waiting {} seconds for Application {}".format(
1328 timeout,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001329 application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001330 )
1331 )
1332
Adam Israel5e08a0e2018-09-06 19:22:47 -04001333 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001334 lambda: all(
Adam Israel5e08a0e2018-09-06 19:22:47 -04001335 unit.agent_status == 'idle' and unit.workload_status in
1336 ['active', 'unknown'] for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001337 ),
1338 timeout=timeout
1339 )