blob: a68e657c2383e49a7c07e5c441fe66d23988702b [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 """
Adam Israele3a05f82019-04-26 13:12:47 -0400249 # Make sure that we have a public key before writing to disk
Adam Israelb2a07f52019-04-25 17:17:05 -0400250 if public_key is None or len(public_key) == 0:
Adam Israele3a05f82019-04-26 13:12:47 -0400251 if 'OSM_VCA_PUBKEY' in os.environ:
252 public_key = os.getenv('OSM_VCA_PUBKEY', '')
253 if len(public_key == 0):
254 return
255 else:
256 return
257
Adam Israelb2a07f52019-04-25 17:17:05 -0400258 path = "{}/.local/share/juju/ssh".format(
259 os.path.expanduser('~'),
260 )
261 if not os.path.exists(path):
262 os.makedirs(path)
263
264 with open('{}/juju_id_rsa.pub'.format(path), 'w') as f:
265 f.write(public_key)
266
Adam Israel5e08a0e2018-09-06 19:22:47 -0400267 def notify_callback(self, model_name, application_name, status, message,
268 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500269 try:
270 if callback:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400271 callback(
272 model_name,
273 application_name,
274 status, message,
275 *callback_args,
276 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500277 except Exception as e:
278 self.log.error("[0] notify_callback exception {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600279 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500280 return True
281
282 # Public methods
Adam Israel85a4b212018-11-29 20:30:24 -0500283 async def Relate(self, model_name, vnfd):
Adam Israel136186e2018-09-14 12:01:12 -0400284 """Create a relation between the charm-enabled VDUs in a VNF.
285
286 The Relation mapping has two parts: the id of the vdu owning the endpoint, and the name of the endpoint.
287
288 vdu:
289 ...
290 relation:
291 - provides: dataVM:db
292 requires: mgmtVM:app
293
294 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.
295
296 :param str ns_name: The name of the network service.
297 :param dict vnfd: The parsed yaml VNF descriptor.
298 """
299
300 # Currently, the call to Relate() is made automatically after the
301 # deployment of each charm; if the relation depends on a charm that
302 # hasn't been deployed yet, the call will fail silently. This will
303 # prevent an API breakage, with the intent of making this an explicitly
304 # required call in a more object-oriented refactor of the N2VC API.
305
306 configs = []
307 vnf_config = vnfd.get("vnf-configuration")
308 if vnf_config:
309 juju = vnf_config['juju']
310 if juju:
311 configs.append(vnf_config)
312
313 for vdu in vnfd['vdu']:
314 vdu_config = vdu.get('vdu-configuration')
315 if vdu_config:
316 juju = vdu_config['juju']
317 if juju:
318 configs.append(vdu_config)
319
320 def _get_application_name(name):
321 """Get the application name that's mapped to a vnf/vdu."""
322 vnf_member_index = 0
323 vnf_name = vnfd['name']
324
325 for vdu in vnfd.get('vdu'):
326 # Compare the named portion of the relation to the vdu's id
327 if vdu['id'] == name:
328 application_name = self.FormatApplicationName(
Adam Israel85a4b212018-11-29 20:30:24 -0500329 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400330 vnf_name,
331 str(vnf_member_index),
332 )
333 return application_name
334 else:
335 vnf_member_index += 1
336
337 return None
338
339 # Loop through relations
340 for cfg in configs:
341 if 'juju' in cfg:
342 if 'relation' in juju:
343 for rel in juju['relation']:
344 try:
345
346 # get the application name for the provides
347 (name, endpoint) = rel['provides'].split(':')
348 application_name = _get_application_name(name)
349
350 provides = "{}:{}".format(
351 application_name,
352 endpoint
353 )
354
355 # get the application name for thr requires
356 (name, endpoint) = rel['requires'].split(':')
357 application_name = _get_application_name(name)
358
359 requires = "{}:{}".format(
360 application_name,
361 endpoint
362 )
363 self.log.debug("Relation: {} <-> {}".format(
364 provides,
365 requires
366 ))
367 await self.add_relation(
Adam Israel85a4b212018-11-29 20:30:24 -0500368 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400369 provides,
370 requires,
371 )
372 except Exception as e:
373 self.log.debug("Exception: {}".format(e))
374
375 return
376
Adam Israel5e08a0e2018-09-06 19:22:47 -0400377 async def DeployCharms(self, model_name, application_name, vnfd,
378 charm_path, params={}, machine_spec={},
379 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500380 """Deploy one or more charms associated with a VNF.
381
382 Deploy the charm(s) referenced in a VNF Descriptor.
383
Adam Israel85a4b212018-11-29 20:30:24 -0500384 :param str model_name: The name or unique id of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500385 :param str application_name: The name of the application
386 :param dict vnfd: The name of the application
387 :param str charm_path: The path to the Juju charm
388 :param dict params: A dictionary of runtime parameters
389 Examples::
390 {
Adam Israel88a49632018-04-10 13:04:57 -0600391 'rw_mgmt_ip': '1.2.3.4',
392 # Pass the initial-config-primitives section of the vnf or vdu
393 'initial-config-primitives': {...}
tierno1afb30a2018-12-21 13:42:43 +0000394 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
395 inside < >. rw_mgmt_ip will be included here also
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500396 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400397 :param dict machine_spec: A dictionary describing the machine to
398 install to
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500399 Examples::
400 {
401 'hostname': '1.2.3.4',
402 'username': 'ubuntu',
403 }
404 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400405 :param tuple callback_args: A list of arguments to be passed to the
406 callback
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500407 """
408
409 ########################################################
410 # Verify the path to the charm exists and is readable. #
411 ########################################################
412 if not os.path.exists(charm_path):
413 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400414 self.notify_callback(
415 model_name,
416 application_name,
417 "failed",
418 callback,
419 *callback_args,
420 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500421 raise JujuCharmNotFound("No artifacts configured.")
422
423 ################################
424 # Login to the Juju controller #
425 ################################
426 if not self.authenticated:
427 self.log.debug("Authenticating with Juju")
428 await self.login()
429
430 ##########################################
431 # Get the model for this network service #
432 ##########################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500433 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500434
435 ########################################
436 # Verify the application doesn't exist #
437 ########################################
438 app = await self.get_application(model, application_name)
439 if app:
Adam Israel42d88e62018-07-16 14:18:41 -0400440 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500441
Adam Israel28a43c02018-04-23 16:04:54 -0400442 ################################################################
443 # Register this application with the model-level event monitor #
444 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500445 if callback:
Adam Israel04eee1f2019-04-29 14:59:45 -0400446 self.log.debug("JujuApi: Registering callback for {}".format(
Adam Israel28a43c02018-04-23 16:04:54 -0400447 application_name,
Adam Israel04eee1f2019-04-29 14:59:45 -0400448 ))
449 await self.Subscribe(model_name, application_name, callback, *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500450
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500451 ########################################################
452 # Check for specific machine placement (native charms) #
453 ########################################################
454 to = ""
455 if machine_spec.keys():
Adam Israel5963cb42018-09-14 11:26:13 -0400456 if all(k in machine_spec for k in ['host', 'user']):
457 # Enlist an existing machine as a Juju unit
458 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
459 machine_spec['user'],
460 machine_spec['host'],
461 self.GetPrivateKeyPath(),
462 ))
Adam Israelfa329072018-09-14 11:26:13 -0400463 to = machine.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500464
465 #######################################
466 # Get the initial charm configuration #
467 #######################################
468
469 rw_mgmt_ip = None
470 if 'rw_mgmt_ip' in params:
471 rw_mgmt_ip = params['rw_mgmt_ip']
472
Adam Israel5afe0542018-08-08 12:54:55 -0400473 if 'initial-config-primitive' not in params:
474 params['initial-config-primitive'] = {}
475
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500476 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600477 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500478 {'<rw_mgmt_ip>': rw_mgmt_ip}
479 )
480
Adam Israel85a4b212018-11-29 20:30:24 -0500481 self.log.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
482 model_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500483 application_name,
484 charm_path,
485 to=to,
486 ))
487
488 ########################################################
489 # Deploy the charm and apply the initial configuration #
490 ########################################################
491 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600492 # We expect charm_path to be either the path to the charm on disk
493 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500494 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600495 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500496 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600497 # Proxy charms should use the current LTS. This will need to be
498 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500499 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600500 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500501 config=initial_config,
Adam Israelfa329072018-09-14 11:26:13 -0400502 # Where to deploy the charm to.
503 to=to,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500504 )
505
Adam Israel136186e2018-09-14 12:01:12 -0400506 # Map the vdu id<->app name,
507 #
508 await self.Relate(model_name, vnfd)
509
Adam Israel88a49632018-04-10 13:04:57 -0600510 # #######################################
511 # # Execute initial config primitive(s) #
512 # #######################################
Adam Israelcf253202018-10-31 16:29:09 -0700513 uuids = await self.ExecuteInitialPrimitives(
Adam Israel5e08a0e2018-09-06 19:22:47 -0400514 model_name,
515 application_name,
516 params,
517 )
Adam Israelcf253202018-10-31 16:29:09 -0700518 return uuids
Adam Israel5e08a0e2018-09-06 19:22:47 -0400519
520 # primitives = {}
521 #
522 # # Build a sequential list of the primitives to execute
523 # for primitive in params['initial-config-primitive']:
524 # try:
525 # if primitive['name'] == 'config':
526 # # This is applied when the Application is deployed
527 # pass
528 # else:
529 # seq = primitive['seq']
530 #
531 # params = {}
532 # if 'parameter' in primitive:
533 # params = primitive['parameter']
534 #
535 # primitives[seq] = {
536 # 'name': primitive['name'],
537 # 'parameters': self._map_primitive_parameters(
538 # params,
539 # {'<rw_mgmt_ip>': rw_mgmt_ip}
540 # ),
541 # }
542 #
543 # for primitive in sorted(primitives):
544 # await self.ExecutePrimitive(
545 # model_name,
546 # application_name,
547 # primitives[primitive]['name'],
548 # callback,
549 # callback_args,
550 # **primitives[primitive]['parameters'],
551 # )
552 # except N2VCPrimitiveExecutionFailed as e:
553 # self.log.debug(
554 # "[N2VC] Exception executing primitive: {}".format(e)
555 # )
556 # raise
557
558 async def GetPrimitiveStatus(self, model_name, uuid):
559 """Get the status of an executed Primitive.
560
561 The status of an executed Primitive will be one of three values:
562 - completed
563 - failed
564 - running
565 """
566 status = None
567 try:
568 if not self.authenticated:
569 await self.login()
570
Adam Israel5e08a0e2018-09-06 19:22:47 -0400571 model = await self.get_model(model_name)
572
573 results = await model.get_action_status(uuid)
574
575 if uuid in results:
576 status = results[uuid]
577
578 except Exception as e:
579 self.log.debug(
580 "Caught exception while getting primitive status: {}".format(e)
581 )
582 raise N2VCPrimitiveExecutionFailed(e)
583
584 return status
585
586 async def GetPrimitiveOutput(self, model_name, uuid):
587 """Get the output of an executed Primitive.
588
589 Note: this only returns output for a successfully executed primitive.
590 """
591 results = None
592 try:
593 if not self.authenticated:
594 await self.login()
595
Adam Israel5e08a0e2018-09-06 19:22:47 -0400596 model = await self.get_model(model_name)
597 results = await model.get_action_output(uuid, 60)
598 except Exception as e:
599 self.log.debug(
600 "Caught exception while getting primitive status: {}".format(e)
601 )
602 raise N2VCPrimitiveExecutionFailed(e)
603
604 return results
605
Adam Israelfa329072018-09-14 11:26:13 -0400606 # async def ProvisionMachine(self, model_name, hostname, username):
607 # """Provision machine for usage with Juju.
608 #
609 # Provisions a previously instantiated machine for use with Juju.
610 # """
611 # try:
612 # if not self.authenticated:
613 # await self.login()
614 #
615 # # FIXME: This is hard-coded until model-per-ns is added
616 # model_name = 'default'
617 #
618 # model = await self.get_model(model_name)
619 # model.add_machine(spec={})
620 #
621 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
622 # "ubuntu",
623 # host['address'],
624 # private_key_path,
625 # ))
626 # return machine.id
627 #
628 # except Exception as e:
629 # self.log.debug(
630 # "Caught exception while getting primitive status: {}".format(e)
631 # )
632 # raise N2VCPrimitiveExecutionFailed(e)
633
634 def GetPrivateKeyPath(self):
635 homedir = os.environ['HOME']
636 sshdir = "{}/.ssh".format(homedir)
637 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
638 return private_key_path
639
640 async def GetPublicKey(self):
641 """Get the N2VC SSH public key.abs
642
643 Returns the SSH public key, to be injected into virtual machines to
644 be managed by the VCA.
645
646 The first time this is run, a ssh keypair will be created. The public
647 key is injected into a VM so that we can provision the machine with
648 Juju, after which Juju will communicate with the VM directly via the
649 juju agent.
650 """
651 public_key = ""
652
653 # Find the path to where we expect our key to live.
654 homedir = os.environ['HOME']
655 sshdir = "{}/.ssh".format(homedir)
656 if not os.path.exists(sshdir):
657 os.mkdir(sshdir)
658
659 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
660 public_key_path = "{}.pub".format(private_key_path)
661
662 # If we don't have a key generated, generate it.
663 if not os.path.exists(private_key_path):
664 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
665 "rsa",
666 "4096",
667 private_key_path
668 )
669 subprocess.check_output(shlex.split(cmd))
670
671 # Read the public key
672 with open(public_key_path, "r") as f:
673 public_key = f.readline()
674
675 return public_key
676
Adam Israel5e08a0e2018-09-06 19:22:47 -0400677 async def ExecuteInitialPrimitives(self, model_name, application_name,
678 params, callback=None, *callback_args):
679 """Execute multiple primitives.
680
681 Execute multiple primitives as declared in initial-config-primitive.
682 This is useful in cases where the primitives initially failed -- for
683 example, if the charm is a proxy but the proxy hasn't been configured
684 yet.
685 """
686 uuids = []
Adam Israel88a49632018-04-10 13:04:57 -0600687 primitives = {}
688
689 # Build a sequential list of the primitives to execute
690 for primitive in params['initial-config-primitive']:
691 try:
692 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600693 pass
694 else:
Adam Israel88a49632018-04-10 13:04:57 -0600695 seq = primitive['seq']
696
tierno1afb30a2018-12-21 13:42:43 +0000697 params_ = {}
Adam Israel42d88e62018-07-16 14:18:41 -0400698 if 'parameter' in primitive:
tierno1afb30a2018-12-21 13:42:43 +0000699 params_ = primitive['parameter']
700
701 user_values = params.get("user_values", {})
702 if 'rw_mgmt_ip' not in user_values:
703 user_values['rw_mgmt_ip'] = None
704 # just for backward compatibility, because it will be provided always by modern version of LCM
Adam Israel42d88e62018-07-16 14:18:41 -0400705
Adam Israel88a49632018-04-10 13:04:57 -0600706 primitives[seq] = {
707 'name': primitive['name'],
708 'parameters': self._map_primitive_parameters(
tierno1afb30a2018-12-21 13:42:43 +0000709 params_,
710 user_values
Adam Israel88a49632018-04-10 13:04:57 -0600711 ),
712 }
713
714 for primitive in sorted(primitives):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400715 uuids.append(
716 await self.ExecutePrimitive(
717 model_name,
718 application_name,
719 primitives[primitive]['name'],
720 callback,
721 callback_args,
722 **primitives[primitive]['parameters'],
723 )
Adam Israel88a49632018-04-10 13:04:57 -0600724 )
725 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400726 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600727 "[N2VC] Exception executing primitive: {}".format(e)
728 )
729 raise
Adam Israel5e08a0e2018-09-06 19:22:47 -0400730 return uuids
Adam Israel88a49632018-04-10 13:04:57 -0600731
Adam Israel5e08a0e2018-09-06 19:22:47 -0400732 async def ExecutePrimitive(self, model_name, application_name, primitive,
733 callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400734 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600735
Adam Israelc9df96f2018-05-03 14:49:56 -0400736 Execute a primitive defined in the VNF descriptor.
737
Adam Israel85a4b212018-11-29 20:30:24 -0500738 :param str model_name: The name or unique id of the network service.
Adam Israelc9df96f2018-05-03 14:49:56 -0400739 :param str application_name: The name of the application
740 :param str primitive: The name of the primitive to execute.
741 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400742 :param tuple callback_args: A list of arguments to be passed to the
743 callback function.
744 :param dict params: A dictionary of key=value pairs representing the
745 primitive's parameters
Adam Israelc9df96f2018-05-03 14:49:56 -0400746 Examples::
747 {
748 'rw_mgmt_ip': '1.2.3.4',
749 # Pass the initial-config-primitives section of the vnf or vdu
750 'initial-config-primitives': {...}
751 }
Adam Israel6817f612018-04-13 08:41:43 -0600752 """
tierno1afb30a2018-12-21 13:42:43 +0000753 self.log.debug("Executing primitive={} params={}".format(primitive, params))
Adam Israel6817f612018-04-13 08:41:43 -0600754 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500755 try:
756 if not self.authenticated:
757 await self.login()
758
Adam Israel5e08a0e2018-09-06 19:22:47 -0400759 model = await self.get_model(model_name)
Adam Israelb5214512018-05-03 10:00:04 -0400760
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500761 if primitive == 'config':
762 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400763 await self.set_config(
764 model,
765 application_name,
766 params['params'],
767 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500768 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500769 app = await self.get_application(model, application_name)
770 if app:
771 # Run against the first (and probably only) unit in the app
772 unit = app.units[0]
773 if unit:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500774 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600775 uuid = action.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500776 except Exception as e:
Adam Israelb0943662018-08-02 15:32:00 -0400777 self.log.debug(
778 "Caught exception while executing primitive: {}".format(e)
779 )
Adam Israel7d871fb2018-07-17 12:17:06 -0400780 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600781 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500782
Adam Israel5e08a0e2018-09-06 19:22:47 -0400783 async def RemoveCharms(self, model_name, application_name, callback=None,
784 *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400785 """Remove a charm from the VCA.
786
787 Remove a charm referenced in a VNF Descriptor.
788
789 :param str model_name: The name of the network service.
790 :param str application_name: The name of the application
791 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400792 :param tuple callback_args: A list of arguments to be passed to the
793 callback function.
Adam Israelc9df96f2018-05-03 14:49:56 -0400794 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500795 try:
796 if not self.authenticated:
797 await self.login()
798
799 model = await self.get_model(model_name)
800 app = await self.get_application(model, application_name)
801 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400802 # Remove this application from event monitoring
Adam Israel04eee1f2019-04-29 14:59:45 -0400803 await self.Unsubscribe(model_name, application_name)
Adam Israel28a43c02018-04-23 16:04:54 -0400804
805 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400806 self.log.debug(
807 "Removing the application {}".format(application_name)
808 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500809 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400810
Adam Israel85a4b212018-11-29 20:30:24 -0500811 await self.disconnect_model(self.monitors[model_name])
812
Adam Israel5e08a0e2018-09-06 19:22:47 -0400813 self.notify_callback(
814 model_name,
815 application_name,
816 "removed",
Adam Israelc4f393e2019-03-19 16:33:30 -0400817 "Removing charm {}".format(application_name),
Adam Israel5e08a0e2018-09-06 19:22:47 -0400818 callback,
819 *callback_args,
820 )
Adam Israel28a43c02018-04-23 16:04:54 -0400821
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500822 except Exception as e:
823 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600824 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500825 raise e
826
Adam Israel6d84dbd2019-03-08 18:33:35 -0500827 async def CreateNetworkService(self, ns_uuid):
828 """Create a new Juju model for the Network Service.
829
830 Creates a new Model in the Juju Controller.
831
832 :param str ns_uuid: A unique id representing an instaance of a
833 Network Service.
834
835 :returns: True if the model was created. Raises JujuError on failure.
836 """
837 if not self.authenticated:
838 await self.login()
839
840 models = await self.controller.list_models()
841 if ns_uuid not in models:
842 try:
843 self.models[ns_uuid] = await self.controller.add_model(
844 ns_uuid
845 )
846 except JujuError as e:
847 if "already exists" not in e.message:
848 raise e
Adam Israel7bf2f4d2019-03-15 15:28:47 -0400849
850 # Create an observer for this model
851 await self.create_model_monitor(ns_uuid)
852
Adam Israel6d84dbd2019-03-08 18:33:35 -0500853 return True
854
855 async def DestroyNetworkService(self, ns_uuid):
856 """Destroy a Network Service.
857
858 Destroy the Network Service and any deployed charms.
859
860 :param ns_uuid The unique id of the Network Service
861
862 :returns: True if the model was created. Raises JujuError on failure.
863 """
864
865 # Do not delete the default model. The default model was used by all
866 # Network Services, prior to the implementation of a model per NS.
Adam Israelc4f393e2019-03-19 16:33:30 -0400867 if ns_uuid.lower() == "default":
Adam Israel6d84dbd2019-03-08 18:33:35 -0500868 return False
869
870 if not self.authenticated:
871 self.log.debug("Authenticating with Juju")
872 await self.login()
873
874 # Disconnect from the Model
875 if ns_uuid in self.models:
876 await self.disconnect_model(self.models[ns_uuid])
877
878 try:
879 await self.controller.destroy_models(ns_uuid)
Adam Israelc4f393e2019-03-19 16:33:30 -0400880 except JujuError:
Adam Israel6d84dbd2019-03-08 18:33:35 -0500881 raise NetworkServiceDoesNotExist(
882 "The Network Service '{}' does not exist".format(ns_uuid)
883 )
884
885 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500886
Adam Israelb5214512018-05-03 10:00:04 -0400887 async def GetMetrics(self, model_name, application_name):
888 """Get the metrics collected by the VCA.
889
Adam Israel85a4b212018-11-29 20:30:24 -0500890 :param model_name The name or unique id of the network service
Adam Israelb5214512018-05-03 10:00:04 -0400891 :param application_name The name of the application
892 """
893 metrics = {}
894 model = await self.get_model(model_name)
895 app = await self.get_application(model, application_name)
896 if app:
897 metrics = await app.get_metrics()
898
899 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500900
Adam Israelfa329072018-09-14 11:26:13 -0400901 async def HasApplication(self, model_name, application_name):
902 model = await self.get_model(model_name)
903 app = await self.get_application(model, application_name)
904 if app:
905 return True
906 return False
907
Adam Israel04eee1f2019-04-29 14:59:45 -0400908 async def Subscribe(self, ns_name, application_name, callback, *callback_args):
909 """Subscribe to callbacks for an application.
910
911 :param ns_name str: The name of the Network Service
912 :param application_name str: The name of the application
913 :param callback obj: The callback method
914 :param callback_args list: The list of arguments to append to calls to
915 the callback method
916 """
917 self.monitors[ns_name].AddApplication(
918 application_name,
919 callback,
920 *callback_args
921 )
922
923 async def Unsubscribe(self, ns_name, application_name):
924 """Unsubscribe to callbacks for an application.
925
926 Unsubscribes the caller from notifications from a deployed application.
927
928 :param ns_name str: The name of the Network Service
929 :param application_name str: The name of the application
930 """
931 self.monitors[ns_name].RemoveApplication(
932 application_name,
933 )
934
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500935 # Non-public methods
Adam Israel136186e2018-09-14 12:01:12 -0400936 async def add_relation(self, model_name, relation1, relation2):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500937 """
938 Add a relation between two application endpoints.
939
Adam Israel85a4b212018-11-29 20:30:24 -0500940 :param str model_name: The name or unique id of the network service
941 :param str relation1: '<application>[:<relation_name>]'
942 :param str relation2: '<application>[:<relation_name>]'
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500943 """
Adam Israel136186e2018-09-14 12:01:12 -0400944
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500945 if not self.authenticated:
946 await self.login()
947
Adam Israel136186e2018-09-14 12:01:12 -0400948 m = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500949 try:
Adam Israel136186e2018-09-14 12:01:12 -0400950 await m.add_relation(relation1, relation2)
951 except JujuAPIError as e:
952 # If one of the applications in the relationship doesn't exist,
953 # or the relation has already been added, let the operation fail
954 # silently.
955 if 'not found' in e.message:
956 return
957 if 'already exists' in e.message:
958 return
959
960 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500961
Adam Israelb5214512018-05-03 10:00:04 -0400962 # async def apply_config(self, config, application):
963 # """Apply a configuration to the application."""
964 # print("JujuApi: Applying configuration to {}.".format(
965 # application
966 # ))
967 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500968
969 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600970 """Transform the yang config primitive to dict.
971
972 Expected result:
973
974 config = {
975 'config':
976 }
977 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500978 config = {}
979 for primitive in config_primitive:
980 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600981 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500982 for parameter in primitive['parameter']:
983 param = str(parameter['name'])
984 if parameter['value'] == "<rw_mgmt_ip>":
985 config[param] = str(values[parameter['value']])
986 else:
987 config[param] = str(parameter['value'])
988
989 return config
990
tierno1afb30a2018-12-21 13:42:43 +0000991 def _map_primitive_parameters(self, parameters, user_values):
Adam Israel88a49632018-04-10 13:04:57 -0600992 params = {}
993 for parameter in parameters:
994 param = str(parameter['name'])
tierno1afb30a2018-12-21 13:42:43 +0000995 value = parameter.get('value')
996
997 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
998 # Must exist at user_values except if there is a default value
999 if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
1000 if parameter['value'][1:-1] in user_values:
1001 value = user_values[parameter['value'][1:-1]]
1002 elif 'default-value' in parameter:
1003 value = parameter['default-value']
1004 else:
1005 raise KeyError("parameter {}='{}' not supplied ".format(param, value))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001006
Adam Israelbf793522018-11-20 13:54:13 -05001007 # If there's no value, use the default-value (if set)
tierno1afb30a2018-12-21 13:42:43 +00001008 if value is None and 'default-value' in parameter:
Adam Israelbf793522018-11-20 13:54:13 -05001009 value = parameter['default-value']
1010
Adam Israel5e08a0e2018-09-06 19:22:47 -04001011 # Typecast parameter value, if present
tierno1afb30a2018-12-21 13:42:43 +00001012 paramtype = "string"
1013 try:
1014 if 'data-type' in parameter:
1015 paramtype = str(parameter['data-type']).lower()
Adam Israel5e08a0e2018-09-06 19:22:47 -04001016
tierno1afb30a2018-12-21 13:42:43 +00001017 if paramtype == "integer":
1018 value = int(value)
1019 elif paramtype == "boolean":
1020 value = bool(value)
1021 else:
1022 value = str(value)
Adam Israel5e08a0e2018-09-06 19:22:47 -04001023 else:
tierno1afb30a2018-12-21 13:42:43 +00001024 # If there's no data-type, assume the value is a string
1025 value = str(value)
1026 except ValueError:
1027 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param, value, paramtype))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001028
tierno1afb30a2018-12-21 13:42:43 +00001029 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -06001030 return params
1031
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001032 def _get_config_from_yang(self, config_primitive, values):
1033 """Transform the yang config primitive to dict."""
1034 config = {}
1035 for primitive in config_primitive.values():
1036 if primitive['name'] == 'config':
1037 for parameter in primitive['parameter'].values():
1038 param = str(parameter['name'])
1039 if parameter['value'] == "<rw_mgmt_ip>":
1040 config[param] = str(values[parameter['value']])
1041 else:
1042 config[param] = str(parameter['value'])
1043
1044 return config
1045
1046 def FormatApplicationName(self, *args):
1047 """
1048 Generate a Juju-compatible Application name
1049
1050 :param args tuple: Positional arguments to be used to construct the
1051 application name.
1052
1053 Limitations::
1054 - Only accepts characters a-z and non-consequitive dashes (-)
1055 - Application name should not exceed 50 characters
1056
1057 Examples::
1058
1059 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
1060 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001061 appname = ""
1062 for c in "-".join(list(args)):
1063 if c.isdigit():
1064 c = chr(97 + int(c))
1065 elif not c.isalpha():
1066 c = "-"
1067 appname += c
Adam Israel6d84dbd2019-03-08 18:33:35 -05001068 return re.sub('-+', '-', appname.lower())
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001069
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001070 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1071 # """Format the name of the application
1072 #
1073 # Limitations:
1074 # - Only accepts characters a-z and non-consequitive dashes (-)
1075 # - Application name should not exceed 50 characters
1076 # """
1077 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1078 # new_name = ''
1079 # for c in name:
1080 # if c.isdigit():
1081 # c = chr(97 + int(c))
1082 # elif not c.isalpha():
1083 # c = "-"
1084 # new_name += c
1085 # return re.sub('\-+', '-', new_name.lower())
1086
1087 def format_model_name(self, name):
1088 """Format the name of model.
1089
1090 Model names may only contain lowercase letters, digits and hyphens
1091 """
1092
1093 return name.replace('_', '-').lower()
1094
1095 async def get_application(self, model, application):
1096 """Get the deployed application."""
1097 if not self.authenticated:
1098 await self.login()
1099
1100 app = None
1101 if application and model:
1102 if model.applications:
1103 if application in model.applications:
1104 app = model.applications[application]
1105
1106 return app
1107
Adam Israel85a4b212018-11-29 20:30:24 -05001108 async def get_model(self, model_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001109 """Get a model from the Juju Controller.
1110
1111 Note: Model objects returned must call disconnected() before it goes
1112 out of scope."""
1113 if not self.authenticated:
1114 await self.login()
1115
1116 if model_name not in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001117 # Get the models in the controller
1118 models = await self.controller.list_models()
1119
1120 if model_name not in models:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001121 try:
1122 self.models[model_name] = await self.controller.add_model(
1123 model_name
1124 )
1125 except JujuError as e:
1126 if "already exists" not in e.message:
1127 raise e
Adam Israel85a4b212018-11-29 20:30:24 -05001128 else:
1129 self.models[model_name] = await self.controller.get_model(
1130 model_name
1131 )
1132
Adam Israelfc511ed2018-09-21 14:20:55 +02001133 self.refcount['model'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001134
Adam Israel28a43c02018-04-23 16:04:54 -04001135 # Create an observer for this model
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001136 await self.create_model_monitor(model_name)
1137
1138 return self.models[model_name]
1139
1140 async def create_model_monitor(self, model_name):
1141 """Create a monitor for the model, if none exists."""
1142 if not self.authenticated:
1143 await self.login()
1144
1145 if model_name not in self.monitors:
Adam Israel28a43c02018-04-23 16:04:54 -04001146 self.monitors[model_name] = VCAMonitor(model_name)
1147 self.models[model_name].add_observer(self.monitors[model_name])
1148
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001149 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001150
1151 async def login(self):
1152 """Login to the Juju controller."""
1153
1154 if self.authenticated:
1155 return
1156
1157 self.connecting = True
1158
1159 self.log.debug("JujuApi: Logging into controller")
1160
Adam Israel5e08a0e2018-09-06 19:22:47 -04001161 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001162
1163 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001164 self.log.debug(
1165 "Connecting to controller... ws://{}:{} as {}/{}".format(
1166 self.endpoint,
1167 self.port,
1168 self.user,
1169 self.secret,
1170 )
1171 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001172 await self.controller.connect(
1173 endpoint=self.endpoint,
1174 username=self.user,
1175 password=self.secret,
Adam Israelb2a07f52019-04-25 17:17:05 -04001176 cacert=self.ca_cert,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001177 )
Adam Israelfc511ed2018-09-21 14:20:55 +02001178 self.refcount['controller'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001179 else:
1180 # current_controller no longer exists
1181 # self.log.debug("Connecting to current controller...")
1182 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -06001183 # await self.controller.connect(
1184 # endpoint=self.endpoint,
1185 # username=self.user,
1186 # cacert=cacert,
1187 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001188 self.log.fatal("VCA credentials not configured.")
1189
1190 self.authenticated = True
1191 self.log.debug("JujuApi: Logged into controller")
1192
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001193 async def logout(self):
1194 """Logout of the Juju controller."""
1195 if not self.authenticated:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001196 return False
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001197
1198 try:
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001199 for model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001200 await self.disconnect_model(model)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001201
1202 if self.controller:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001203 self.log.debug("Disconnecting controller {}".format(
1204 self.controller
1205 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001206 await self.controller.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +02001207 self.refcount['controller'] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -04001208 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001209
1210 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +02001211
1212 self.log.debug(self.refcount)
1213
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001214 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001215 self.log.fatal(
1216 "Fatal error logging out of Juju Controller: {}".format(e)
1217 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001218 raise e
Adam Israel6d84dbd2019-03-08 18:33:35 -05001219 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001220
Adam Israel85a4b212018-11-29 20:30:24 -05001221 async def disconnect_model(self, model):
1222 self.log.debug("Disconnecting model {}".format(model))
1223 if model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001224 print("Disconnecting model")
1225 await self.models[model].disconnect()
1226 self.refcount['model'] -= 1
1227 self.models[model] = None
1228
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001229 # async def remove_application(self, name):
1230 # """Remove the application."""
1231 # if not self.authenticated:
1232 # await self.login()
1233 #
1234 # app = await self.get_application(name)
1235 # if app:
1236 # self.log.debug("JujuApi: Destroying application {}".format(
1237 # name,
1238 # ))
1239 #
1240 # await app.destroy()
1241
1242 async def remove_relation(self, a, b):
1243 """
1244 Remove a relation between two application endpoints
1245
1246 :param a An application endpoint
1247 :param b An application endpoint
1248 """
1249 if not self.authenticated:
1250 await self.login()
1251
1252 m = await self.get_model()
1253 try:
1254 m.remove_relation(a, b)
1255 finally:
1256 await m.disconnect()
1257
Adam Israel85a4b212018-11-29 20:30:24 -05001258 async def resolve_error(self, model_name, application=None):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001259 """Resolve units in error state."""
1260 if not self.authenticated:
1261 await self.login()
1262
Adam Israel85a4b212018-11-29 20:30:24 -05001263 model = await self.get_model(model_name)
1264
1265 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001266 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001267 self.log.debug(
1268 "JujuApi: Resolving errors for application {}".format(
1269 application,
1270 )
1271 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001272
1273 for unit in app.units:
1274 app.resolved(retry=True)
1275
Adam Israel85a4b212018-11-29 20:30:24 -05001276 async def run_action(self, model_name, application, action_name, **params):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001277 """Execute an action and return an Action object."""
1278 if not self.authenticated:
1279 await self.login()
1280 result = {
1281 'status': '',
1282 'action': {
1283 'tag': None,
1284 'results': None,
1285 }
1286 }
Adam Israel85a4b212018-11-29 20:30:24 -05001287
1288 model = await self.get_model(model_name)
1289
1290 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001291 if app:
1292 # We currently only have one unit per application
1293 # so use the first unit available.
1294 unit = app.units[0]
1295
Adam Israel5e08a0e2018-09-06 19:22:47 -04001296 self.log.debug(
1297 "JujuApi: Running Action {} against Application {}".format(
1298 action_name,
1299 application,
1300 )
1301 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001302
1303 action = await unit.run_action(action_name, **params)
1304
1305 # Wait for the action to complete
1306 await action.wait()
1307
1308 result['status'] = action.status
1309 result['action']['tag'] = action.data['id']
1310 result['action']['results'] = action.results
1311
1312 return result
1313
Adam Israelb5214512018-05-03 10:00:04 -04001314 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001315 """Apply a configuration to the application."""
1316 if not self.authenticated:
1317 await self.login()
1318
Adam Israelb5214512018-05-03 10:00:04 -04001319 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001320 if app:
1321 self.log.debug("JujuApi: Setting config for Application {}".format(
1322 application,
1323 ))
1324 await app.set_config(config)
1325
1326 # Verify the config is set
1327 newconf = await app.get_config()
1328 for key in config:
1329 if config[key] != newconf[key]['value']:
1330 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1331
Adam Israelb5214512018-05-03 10:00:04 -04001332 # async def set_parameter(self, parameter, value, application=None):
1333 # """Set a config parameter for a service."""
1334 # if not self.authenticated:
1335 # await self.login()
1336 #
1337 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1338 # parameter,
1339 # value,
1340 # application,
1341 # ))
1342 # return await self.apply_config(
1343 # {parameter: value},
1344 # application=application,
1345 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001346
Adam Israel5e08a0e2018-09-06 19:22:47 -04001347 async def wait_for_application(self, model_name, application_name,
1348 timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001349 """Wait for an application to become active."""
1350 if not self.authenticated:
1351 await self.login()
1352
Adam Israel5e08a0e2018-09-06 19:22:47 -04001353 model = await self.get_model(model_name)
1354
1355 app = await self.get_application(model, application_name)
1356 self.log.debug("Application: {}".format(app))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001357 if app:
1358 self.log.debug(
1359 "JujuApi: Waiting {} seconds for Application {}".format(
1360 timeout,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001361 application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001362 )
1363 )
1364
Adam Israel5e08a0e2018-09-06 19:22:47 -04001365 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001366 lambda: all(
Adam Israel5e08a0e2018-09-06 19:22:47 -04001367 unit.agent_status == 'idle' and unit.workload_status in
1368 ['active', 'unknown'] for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001369 ),
1370 timeout=timeout
1371 )