blob: 87d5df409fc5bf1afe1d3a3c0ac720f54be81e5b [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:
Adam Israelc92163f2019-05-27 08:39:19 -0400342 juju = cfg['juju']
Adam Israel136186e2018-09-14 12:01:12 -0400343 if 'relation' in juju:
344 for rel in juju['relation']:
345 try:
346
347 # get the application name for the provides
348 (name, endpoint) = rel['provides'].split(':')
349 application_name = _get_application_name(name)
350
351 provides = "{}:{}".format(
352 application_name,
353 endpoint
354 )
355
356 # get the application name for thr requires
357 (name, endpoint) = rel['requires'].split(':')
358 application_name = _get_application_name(name)
359
360 requires = "{}:{}".format(
361 application_name,
362 endpoint
363 )
364 self.log.debug("Relation: {} <-> {}".format(
365 provides,
366 requires
367 ))
368 await self.add_relation(
Adam Israel85a4b212018-11-29 20:30:24 -0500369 model_name,
Adam Israel136186e2018-09-14 12:01:12 -0400370 provides,
371 requires,
372 )
373 except Exception as e:
374 self.log.debug("Exception: {}".format(e))
375
376 return
377
Adam Israel5e08a0e2018-09-06 19:22:47 -0400378 async def DeployCharms(self, model_name, application_name, vnfd,
379 charm_path, params={}, machine_spec={},
380 callback=None, *callback_args):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500381 """Deploy one or more charms associated with a VNF.
382
383 Deploy the charm(s) referenced in a VNF Descriptor.
384
Adam Israel85a4b212018-11-29 20:30:24 -0500385 :param str model_name: The name or unique id of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500386 :param str application_name: The name of the application
387 :param dict vnfd: The name of the application
388 :param str charm_path: The path to the Juju charm
389 :param dict params: A dictionary of runtime parameters
390 Examples::
391 {
Adam Israel88a49632018-04-10 13:04:57 -0600392 'rw_mgmt_ip': '1.2.3.4',
393 # Pass the initial-config-primitives section of the vnf or vdu
394 'initial-config-primitives': {...}
tierno1afb30a2018-12-21 13:42:43 +0000395 'user_values': dictionary with the day-1 parameters provided at instantiation time. It will replace values
396 inside < >. rw_mgmt_ip will be included here also
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500397 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400398 :param dict machine_spec: A dictionary describing the machine to
399 install to
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500400 Examples::
401 {
402 'hostname': '1.2.3.4',
403 'username': 'ubuntu',
404 }
405 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400406 :param tuple callback_args: A list of arguments to be passed to the
407 callback
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500408 """
409
410 ########################################################
411 # Verify the path to the charm exists and is readable. #
412 ########################################################
413 if not os.path.exists(charm_path):
414 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400415 self.notify_callback(
416 model_name,
417 application_name,
418 "failed",
419 callback,
420 *callback_args,
421 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500422 raise JujuCharmNotFound("No artifacts configured.")
423
424 ################################
425 # Login to the Juju controller #
426 ################################
427 if not self.authenticated:
428 self.log.debug("Authenticating with Juju")
429 await self.login()
430
431 ##########################################
432 # Get the model for this network service #
433 ##########################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500434 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500435
436 ########################################
437 # Verify the application doesn't exist #
438 ########################################
439 app = await self.get_application(model, application_name)
440 if app:
Adam Israel42d88e62018-07-16 14:18:41 -0400441 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name, model_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500442
Adam Israel28a43c02018-04-23 16:04:54 -0400443 ################################################################
444 # Register this application with the model-level event monitor #
445 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500446 if callback:
Adam Israel04eee1f2019-04-29 14:59:45 -0400447 self.log.debug("JujuApi: Registering callback for {}".format(
Adam Israel28a43c02018-04-23 16:04:54 -0400448 application_name,
Adam Israel04eee1f2019-04-29 14:59:45 -0400449 ))
450 await self.Subscribe(model_name, application_name, callback, *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500451
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500452 ########################################################
453 # Check for specific machine placement (native charms) #
454 ########################################################
455 to = ""
456 if machine_spec.keys():
Adam Israel5963cb42018-09-14 11:26:13 -0400457 if all(k in machine_spec for k in ['host', 'user']):
458 # Enlist an existing machine as a Juju unit
459 machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
Adam Israelce31bc32019-05-22 16:30:25 -0400460 machine_spec['username'],
461 machine_spec['hostname'],
Adam Israel5963cb42018-09-14 11:26:13 -0400462 self.GetPrivateKeyPath(),
463 ))
Adam Israelfa329072018-09-14 11:26:13 -0400464 to = machine.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500465
466 #######################################
467 # Get the initial charm configuration #
468 #######################################
469
470 rw_mgmt_ip = None
471 if 'rw_mgmt_ip' in params:
472 rw_mgmt_ip = params['rw_mgmt_ip']
473
Adam Israel5afe0542018-08-08 12:54:55 -0400474 if 'initial-config-primitive' not in params:
475 params['initial-config-primitive'] = {}
476
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500477 initial_config = self._get_config_from_dict(
Adam Israel88a49632018-04-10 13:04:57 -0600478 params['initial-config-primitive'],
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500479 {'<rw_mgmt_ip>': rw_mgmt_ip}
480 )
481
Adam Israel85a4b212018-11-29 20:30:24 -0500482 self.log.debug("JujuApi: Deploying charm ({}/{}) from {}".format(
483 model_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500484 application_name,
485 charm_path,
486 to=to,
487 ))
488
489 ########################################################
490 # Deploy the charm and apply the initial configuration #
491 ########################################################
492 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600493 # We expect charm_path to be either the path to the charm on disk
494 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500495 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600496 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500497 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600498 # Proxy charms should use the current LTS. This will need to be
499 # changed for native charms.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500500 series='xenial',
Adam Israel88a49632018-04-10 13:04:57 -0600501 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500502 config=initial_config,
Adam Israelfa329072018-09-14 11:26:13 -0400503 # Where to deploy the charm to.
504 to=to,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500505 )
Adam Israel38bf1642019-05-31 09:59:52 -0400506 #############################
507 # Map the vdu id<->app name #
508 #############################
509 try:
510 await self.Relate(model_name, vnfd)
511 except KeyError as ex:
512 # We don't currently support relations between NS and VNF/VDU charms
513 self.log.warn("[N2VC] Relations not supported: {}".format(ex))
514 except Exception as ex:
515 # This may happen if not all of the charms needed by the relation
516 # are ready. We can safely ignore this, because Relate will be
517 # retried when the endpoint of the relation is deployed.
518 self.log.warn("[N2VC] Relations not ready")
Adam Israel136186e2018-09-14 12:01:12 -0400519
Adam Israel88a49632018-04-10 13:04:57 -0600520 # #######################################
521 # # Execute initial config primitive(s) #
522 # #######################################
Adam Israelcf253202018-10-31 16:29:09 -0700523 uuids = await self.ExecuteInitialPrimitives(
Adam Israel5e08a0e2018-09-06 19:22:47 -0400524 model_name,
525 application_name,
526 params,
527 )
Adam Israelcf253202018-10-31 16:29:09 -0700528 return uuids
Adam Israel5e08a0e2018-09-06 19:22:47 -0400529
530 # primitives = {}
531 #
532 # # Build a sequential list of the primitives to execute
533 # for primitive in params['initial-config-primitive']:
534 # try:
535 # if primitive['name'] == 'config':
536 # # This is applied when the Application is deployed
537 # pass
538 # else:
539 # seq = primitive['seq']
540 #
541 # params = {}
542 # if 'parameter' in primitive:
543 # params = primitive['parameter']
544 #
545 # primitives[seq] = {
546 # 'name': primitive['name'],
547 # 'parameters': self._map_primitive_parameters(
548 # params,
549 # {'<rw_mgmt_ip>': rw_mgmt_ip}
550 # ),
551 # }
552 #
553 # for primitive in sorted(primitives):
554 # await self.ExecutePrimitive(
555 # model_name,
556 # application_name,
557 # primitives[primitive]['name'],
558 # callback,
559 # callback_args,
560 # **primitives[primitive]['parameters'],
561 # )
562 # except N2VCPrimitiveExecutionFailed as e:
563 # self.log.debug(
564 # "[N2VC] Exception executing primitive: {}".format(e)
565 # )
566 # raise
567
568 async def GetPrimitiveStatus(self, model_name, uuid):
569 """Get the status of an executed Primitive.
570
571 The status of an executed Primitive will be one of three values:
572 - completed
573 - failed
574 - running
575 """
576 status = None
577 try:
578 if not self.authenticated:
579 await self.login()
580
Adam Israel5e08a0e2018-09-06 19:22:47 -0400581 model = await self.get_model(model_name)
582
583 results = await model.get_action_status(uuid)
584
585 if uuid in results:
586 status = results[uuid]
587
588 except Exception as e:
589 self.log.debug(
590 "Caught exception while getting primitive status: {}".format(e)
591 )
592 raise N2VCPrimitiveExecutionFailed(e)
593
594 return status
595
596 async def GetPrimitiveOutput(self, model_name, uuid):
597 """Get the output of an executed Primitive.
598
599 Note: this only returns output for a successfully executed primitive.
600 """
601 results = None
602 try:
603 if not self.authenticated:
604 await self.login()
605
Adam Israel5e08a0e2018-09-06 19:22:47 -0400606 model = await self.get_model(model_name)
607 results = await model.get_action_output(uuid, 60)
608 except Exception as e:
609 self.log.debug(
610 "Caught exception while getting primitive status: {}".format(e)
611 )
612 raise N2VCPrimitiveExecutionFailed(e)
613
614 return results
615
Adam Israelfa329072018-09-14 11:26:13 -0400616 # async def ProvisionMachine(self, model_name, hostname, username):
617 # """Provision machine for usage with Juju.
618 #
619 # Provisions a previously instantiated machine for use with Juju.
620 # """
621 # try:
622 # if not self.authenticated:
623 # await self.login()
624 #
625 # # FIXME: This is hard-coded until model-per-ns is added
626 # model_name = 'default'
627 #
628 # model = await self.get_model(model_name)
629 # model.add_machine(spec={})
630 #
631 # machine = await model.add_machine(spec='ssh:{}@{}:{}'.format(
632 # "ubuntu",
633 # host['address'],
634 # private_key_path,
635 # ))
636 # return machine.id
637 #
638 # except Exception as e:
639 # self.log.debug(
640 # "Caught exception while getting primitive status: {}".format(e)
641 # )
642 # raise N2VCPrimitiveExecutionFailed(e)
643
644 def GetPrivateKeyPath(self):
645 homedir = os.environ['HOME']
646 sshdir = "{}/.ssh".format(homedir)
647 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
648 return private_key_path
649
650 async def GetPublicKey(self):
651 """Get the N2VC SSH public key.abs
652
653 Returns the SSH public key, to be injected into virtual machines to
654 be managed by the VCA.
655
656 The first time this is run, a ssh keypair will be created. The public
657 key is injected into a VM so that we can provision the machine with
658 Juju, after which Juju will communicate with the VM directly via the
659 juju agent.
660 """
661 public_key = ""
662
663 # Find the path to where we expect our key to live.
664 homedir = os.environ['HOME']
665 sshdir = "{}/.ssh".format(homedir)
666 if not os.path.exists(sshdir):
667 os.mkdir(sshdir)
668
669 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
670 public_key_path = "{}.pub".format(private_key_path)
671
672 # If we don't have a key generated, generate it.
673 if not os.path.exists(private_key_path):
674 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
675 "rsa",
676 "4096",
677 private_key_path
678 )
679 subprocess.check_output(shlex.split(cmd))
680
681 # Read the public key
682 with open(public_key_path, "r") as f:
683 public_key = f.readline()
684
685 return public_key
686
Adam Israel5e08a0e2018-09-06 19:22:47 -0400687 async def ExecuteInitialPrimitives(self, model_name, application_name,
688 params, callback=None, *callback_args):
689 """Execute multiple primitives.
690
691 Execute multiple primitives as declared in initial-config-primitive.
692 This is useful in cases where the primitives initially failed -- for
693 example, if the charm is a proxy but the proxy hasn't been configured
694 yet.
695 """
696 uuids = []
Adam Israel88a49632018-04-10 13:04:57 -0600697 primitives = {}
698
699 # Build a sequential list of the primitives to execute
700 for primitive in params['initial-config-primitive']:
701 try:
702 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600703 pass
704 else:
Adam Israel88a49632018-04-10 13:04:57 -0600705 seq = primitive['seq']
706
tierno1afb30a2018-12-21 13:42:43 +0000707 params_ = {}
Adam Israel42d88e62018-07-16 14:18:41 -0400708 if 'parameter' in primitive:
tierno1afb30a2018-12-21 13:42:43 +0000709 params_ = primitive['parameter']
710
711 user_values = params.get("user_values", {})
712 if 'rw_mgmt_ip' not in user_values:
713 user_values['rw_mgmt_ip'] = None
714 # just for backward compatibility, because it will be provided always by modern version of LCM
Adam Israel42d88e62018-07-16 14:18:41 -0400715
Adam Israel88a49632018-04-10 13:04:57 -0600716 primitives[seq] = {
717 'name': primitive['name'],
718 'parameters': self._map_primitive_parameters(
tierno1afb30a2018-12-21 13:42:43 +0000719 params_,
720 user_values
Adam Israel88a49632018-04-10 13:04:57 -0600721 ),
722 }
723
724 for primitive in sorted(primitives):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400725 uuids.append(
726 await self.ExecutePrimitive(
727 model_name,
728 application_name,
729 primitives[primitive]['name'],
730 callback,
731 callback_args,
732 **primitives[primitive]['parameters'],
733 )
Adam Israel88a49632018-04-10 13:04:57 -0600734 )
735 except N2VCPrimitiveExecutionFailed as e:
Adam Israel7d871fb2018-07-17 12:17:06 -0400736 self.log.debug(
Adam Israel88a49632018-04-10 13:04:57 -0600737 "[N2VC] Exception executing primitive: {}".format(e)
738 )
739 raise
Adam Israel5e08a0e2018-09-06 19:22:47 -0400740 return uuids
Adam Israel88a49632018-04-10 13:04:57 -0600741
Adam Israel5e08a0e2018-09-06 19:22:47 -0400742 async def ExecutePrimitive(self, model_name, application_name, primitive,
743 callback, *callback_args, **params):
Adam Israelc9df96f2018-05-03 14:49:56 -0400744 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600745
Adam Israelc9df96f2018-05-03 14:49:56 -0400746 Execute a primitive defined in the VNF descriptor.
747
Adam Israel85a4b212018-11-29 20:30:24 -0500748 :param str model_name: The name or unique id of the network service.
Adam Israelc9df96f2018-05-03 14:49:56 -0400749 :param str application_name: The name of the application
750 :param str primitive: The name of the primitive to execute.
751 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400752 :param tuple callback_args: A list of arguments to be passed to the
753 callback function.
754 :param dict params: A dictionary of key=value pairs representing the
755 primitive's parameters
Adam Israelc9df96f2018-05-03 14:49:56 -0400756 Examples::
757 {
758 'rw_mgmt_ip': '1.2.3.4',
759 # Pass the initial-config-primitives section of the vnf or vdu
760 'initial-config-primitives': {...}
761 }
Adam Israel6817f612018-04-13 08:41:43 -0600762 """
tierno1afb30a2018-12-21 13:42:43 +0000763 self.log.debug("Executing primitive={} params={}".format(primitive, params))
Adam Israel6817f612018-04-13 08:41:43 -0600764 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500765 try:
766 if not self.authenticated:
767 await self.login()
768
Adam Israel5e08a0e2018-09-06 19:22:47 -0400769 model = await self.get_model(model_name)
Adam Israelb5214512018-05-03 10:00:04 -0400770
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500771 if primitive == 'config':
772 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400773 await self.set_config(
774 model,
775 application_name,
776 params['params'],
777 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500778 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500779 app = await self.get_application(model, application_name)
780 if app:
781 # Run against the first (and probably only) unit in the app
782 unit = app.units[0]
783 if unit:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500784 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600785 uuid = action.id
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500786 except Exception as e:
Adam Israelb0943662018-08-02 15:32:00 -0400787 self.log.debug(
788 "Caught exception while executing primitive: {}".format(e)
789 )
Adam Israel7d871fb2018-07-17 12:17:06 -0400790 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600791 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500792
Adam Israel5e08a0e2018-09-06 19:22:47 -0400793 async def RemoveCharms(self, model_name, application_name, callback=None,
794 *callback_args):
Adam Israelc9df96f2018-05-03 14:49:56 -0400795 """Remove a charm from the VCA.
796
797 Remove a charm referenced in a VNF Descriptor.
798
799 :param str model_name: The name of the network service.
800 :param str application_name: The name of the application
801 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400802 :param tuple callback_args: A list of arguments to be passed to the
803 callback function.
Adam Israelc9df96f2018-05-03 14:49:56 -0400804 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500805 try:
806 if not self.authenticated:
807 await self.login()
808
809 model = await self.get_model(model_name)
810 app = await self.get_application(model, application_name)
811 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400812 # Remove this application from event monitoring
Adam Israel04eee1f2019-04-29 14:59:45 -0400813 await self.Unsubscribe(model_name, application_name)
Adam Israel28a43c02018-04-23 16:04:54 -0400814
815 # self.notify_callback(model_name, application_name, "removing", callback, *callback_args)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400816 self.log.debug(
817 "Removing the application {}".format(application_name)
818 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500819 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400820
Adam Israel85a4b212018-11-29 20:30:24 -0500821 await self.disconnect_model(self.monitors[model_name])
822
Adam Israel5e08a0e2018-09-06 19:22:47 -0400823 self.notify_callback(
824 model_name,
825 application_name,
826 "removed",
Adam Israelc4f393e2019-03-19 16:33:30 -0400827 "Removing charm {}".format(application_name),
Adam Israel5e08a0e2018-09-06 19:22:47 -0400828 callback,
829 *callback_args,
830 )
Adam Israel28a43c02018-04-23 16:04:54 -0400831
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500832 except Exception as e:
833 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600834 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500835 raise e
836
Adam Israel6d84dbd2019-03-08 18:33:35 -0500837 async def CreateNetworkService(self, ns_uuid):
838 """Create a new Juju model for the Network Service.
839
840 Creates a new Model in the Juju Controller.
841
842 :param str ns_uuid: A unique id representing an instaance of a
843 Network Service.
844
845 :returns: True if the model was created. Raises JujuError on failure.
846 """
847 if not self.authenticated:
848 await self.login()
849
850 models = await self.controller.list_models()
851 if ns_uuid not in models:
852 try:
853 self.models[ns_uuid] = await self.controller.add_model(
854 ns_uuid
855 )
856 except JujuError as e:
857 if "already exists" not in e.message:
858 raise e
Adam Israel7bf2f4d2019-03-15 15:28:47 -0400859
860 # Create an observer for this model
861 await self.create_model_monitor(ns_uuid)
862
Adam Israel6d84dbd2019-03-08 18:33:35 -0500863 return True
864
865 async def DestroyNetworkService(self, ns_uuid):
866 """Destroy a Network Service.
867
868 Destroy the Network Service and any deployed charms.
869
870 :param ns_uuid The unique id of the Network Service
871
872 :returns: True if the model was created. Raises JujuError on failure.
873 """
874
875 # Do not delete the default model. The default model was used by all
876 # Network Services, prior to the implementation of a model per NS.
Adam Israelc4f393e2019-03-19 16:33:30 -0400877 if ns_uuid.lower() == "default":
Adam Israel6d84dbd2019-03-08 18:33:35 -0500878 return False
879
880 if not self.authenticated:
881 self.log.debug("Authenticating with Juju")
882 await self.login()
883
884 # Disconnect from the Model
885 if ns_uuid in self.models:
886 await self.disconnect_model(self.models[ns_uuid])
887
888 try:
889 await self.controller.destroy_models(ns_uuid)
Adam Israelc4f393e2019-03-19 16:33:30 -0400890 except JujuError:
Adam Israel6d84dbd2019-03-08 18:33:35 -0500891 raise NetworkServiceDoesNotExist(
892 "The Network Service '{}' does not exist".format(ns_uuid)
893 )
894
895 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500896
Adam Israelb5214512018-05-03 10:00:04 -0400897 async def GetMetrics(self, model_name, application_name):
898 """Get the metrics collected by the VCA.
899
Adam Israel85a4b212018-11-29 20:30:24 -0500900 :param model_name The name or unique id of the network service
Adam Israelb5214512018-05-03 10:00:04 -0400901 :param application_name The name of the application
902 """
903 metrics = {}
904 model = await self.get_model(model_name)
905 app = await self.get_application(model, application_name)
906 if app:
907 metrics = await app.get_metrics()
908
909 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500910
Adam Israelfa329072018-09-14 11:26:13 -0400911 async def HasApplication(self, model_name, application_name):
912 model = await self.get_model(model_name)
913 app = await self.get_application(model, application_name)
914 if app:
915 return True
916 return False
917
Adam Israel04eee1f2019-04-29 14:59:45 -0400918 async def Subscribe(self, ns_name, application_name, callback, *callback_args):
919 """Subscribe to callbacks for an application.
920
921 :param ns_name str: The name of the Network Service
922 :param application_name str: The name of the application
923 :param callback obj: The callback method
924 :param callback_args list: The list of arguments to append to calls to
925 the callback method
926 """
927 self.monitors[ns_name].AddApplication(
928 application_name,
929 callback,
930 *callback_args
931 )
932
933 async def Unsubscribe(self, ns_name, application_name):
934 """Unsubscribe to callbacks for an application.
935
936 Unsubscribes the caller from notifications from a deployed application.
937
938 :param ns_name str: The name of the Network Service
939 :param application_name str: The name of the application
940 """
941 self.monitors[ns_name].RemoveApplication(
942 application_name,
943 )
944
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500945 # Non-public methods
Adam Israel136186e2018-09-14 12:01:12 -0400946 async def add_relation(self, model_name, relation1, relation2):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500947 """
948 Add a relation between two application endpoints.
949
Adam Israel85a4b212018-11-29 20:30:24 -0500950 :param str model_name: The name or unique id of the network service
951 :param str relation1: '<application>[:<relation_name>]'
952 :param str relation2: '<application>[:<relation_name>]'
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500953 """
Adam Israel136186e2018-09-14 12:01:12 -0400954
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500955 if not self.authenticated:
956 await self.login()
957
Adam Israel136186e2018-09-14 12:01:12 -0400958 m = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500959 try:
Adam Israel136186e2018-09-14 12:01:12 -0400960 await m.add_relation(relation1, relation2)
961 except JujuAPIError as e:
962 # If one of the applications in the relationship doesn't exist,
963 # or the relation has already been added, let the operation fail
964 # silently.
965 if 'not found' in e.message:
966 return
967 if 'already exists' in e.message:
968 return
969
970 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500971
Adam Israelb5214512018-05-03 10:00:04 -0400972 # async def apply_config(self, config, application):
973 # """Apply a configuration to the application."""
974 # print("JujuApi: Applying configuration to {}.".format(
975 # application
976 # ))
977 # return await self.set_config(application=application, config=config)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500978
979 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -0600980 """Transform the yang config primitive to dict.
981
982 Expected result:
983
984 config = {
985 'config':
986 }
987 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500988 config = {}
989 for primitive in config_primitive:
990 if primitive['name'] == 'config':
Adam Israel88a49632018-04-10 13:04:57 -0600991 # config = self._map_primitive_parameters()
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500992 for parameter in primitive['parameter']:
993 param = str(parameter['name'])
994 if parameter['value'] == "<rw_mgmt_ip>":
995 config[param] = str(values[parameter['value']])
996 else:
997 config[param] = str(parameter['value'])
998
999 return config
1000
tierno1afb30a2018-12-21 13:42:43 +00001001 def _map_primitive_parameters(self, parameters, user_values):
Adam Israel88a49632018-04-10 13:04:57 -06001002 params = {}
1003 for parameter in parameters:
1004 param = str(parameter['name'])
tierno1afb30a2018-12-21 13:42:43 +00001005 value = parameter.get('value')
1006
1007 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user_values.
1008 # Must exist at user_values except if there is a default value
1009 if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
1010 if parameter['value'][1:-1] in user_values:
1011 value = user_values[parameter['value'][1:-1]]
1012 elif 'default-value' in parameter:
1013 value = parameter['default-value']
1014 else:
1015 raise KeyError("parameter {}='{}' not supplied ".format(param, value))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001016
Adam Israelbf793522018-11-20 13:54:13 -05001017 # If there's no value, use the default-value (if set)
tierno1afb30a2018-12-21 13:42:43 +00001018 if value is None and 'default-value' in parameter:
Adam Israelbf793522018-11-20 13:54:13 -05001019 value = parameter['default-value']
1020
Adam Israel5e08a0e2018-09-06 19:22:47 -04001021 # Typecast parameter value, if present
tierno1afb30a2018-12-21 13:42:43 +00001022 paramtype = "string"
1023 try:
1024 if 'data-type' in parameter:
1025 paramtype = str(parameter['data-type']).lower()
Adam Israel5e08a0e2018-09-06 19:22:47 -04001026
tierno1afb30a2018-12-21 13:42:43 +00001027 if paramtype == "integer":
1028 value = int(value)
1029 elif paramtype == "boolean":
1030 value = bool(value)
1031 else:
1032 value = str(value)
Adam Israel5e08a0e2018-09-06 19:22:47 -04001033 else:
tierno1afb30a2018-12-21 13:42:43 +00001034 # If there's no data-type, assume the value is a string
1035 value = str(value)
1036 except ValueError:
1037 raise ValueError("parameter {}='{}' cannot be converted to type {}".format(param, value, paramtype))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001038
tierno1afb30a2018-12-21 13:42:43 +00001039 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -06001040 return params
1041
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001042 def _get_config_from_yang(self, config_primitive, values):
1043 """Transform the yang config primitive to dict."""
1044 config = {}
1045 for primitive in config_primitive.values():
1046 if primitive['name'] == 'config':
1047 for parameter in primitive['parameter'].values():
1048 param = str(parameter['name'])
1049 if parameter['value'] == "<rw_mgmt_ip>":
1050 config[param] = str(values[parameter['value']])
1051 else:
1052 config[param] = str(parameter['value'])
1053
1054 return config
1055
1056 def FormatApplicationName(self, *args):
1057 """
1058 Generate a Juju-compatible Application name
1059
1060 :param args tuple: Positional arguments to be used to construct the
1061 application name.
1062
1063 Limitations::
1064 - Only accepts characters a-z and non-consequitive dashes (-)
1065 - Application name should not exceed 50 characters
1066
1067 Examples::
1068
1069 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
1070 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001071 appname = ""
1072 for c in "-".join(list(args)):
1073 if c.isdigit():
1074 c = chr(97 + int(c))
1075 elif not c.isalpha():
1076 c = "-"
1077 appname += c
Adam Israel6d84dbd2019-03-08 18:33:35 -05001078 return re.sub('-+', '-', appname.lower())
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001079
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001080 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
1081 # """Format the name of the application
1082 #
1083 # Limitations:
1084 # - Only accepts characters a-z and non-consequitive dashes (-)
1085 # - Application name should not exceed 50 characters
1086 # """
1087 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
1088 # new_name = ''
1089 # for c in name:
1090 # if c.isdigit():
1091 # c = chr(97 + int(c))
1092 # elif not c.isalpha():
1093 # c = "-"
1094 # new_name += c
1095 # return re.sub('\-+', '-', new_name.lower())
1096
1097 def format_model_name(self, name):
1098 """Format the name of model.
1099
1100 Model names may only contain lowercase letters, digits and hyphens
1101 """
1102
1103 return name.replace('_', '-').lower()
1104
1105 async def get_application(self, model, application):
1106 """Get the deployed application."""
1107 if not self.authenticated:
1108 await self.login()
1109
1110 app = None
1111 if application and model:
1112 if model.applications:
1113 if application in model.applications:
1114 app = model.applications[application]
1115
1116 return app
1117
Adam Israel85a4b212018-11-29 20:30:24 -05001118 async def get_model(self, model_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001119 """Get a model from the Juju Controller.
1120
1121 Note: Model objects returned must call disconnected() before it goes
1122 out of scope."""
1123 if not self.authenticated:
1124 await self.login()
1125
1126 if model_name not in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001127 # Get the models in the controller
1128 models = await self.controller.list_models()
1129
1130 if model_name not in models:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001131 try:
1132 self.models[model_name] = await self.controller.add_model(
1133 model_name
1134 )
1135 except JujuError as e:
1136 if "already exists" not in e.message:
1137 raise e
Adam Israel85a4b212018-11-29 20:30:24 -05001138 else:
1139 self.models[model_name] = await self.controller.get_model(
1140 model_name
1141 )
1142
Adam Israelfc511ed2018-09-21 14:20:55 +02001143 self.refcount['model'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001144
Adam Israel28a43c02018-04-23 16:04:54 -04001145 # Create an observer for this model
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001146 await self.create_model_monitor(model_name)
1147
1148 return self.models[model_name]
1149
1150 async def create_model_monitor(self, model_name):
1151 """Create a monitor for the model, if none exists."""
1152 if not self.authenticated:
1153 await self.login()
1154
1155 if model_name not in self.monitors:
Adam Israel28a43c02018-04-23 16:04:54 -04001156 self.monitors[model_name] = VCAMonitor(model_name)
1157 self.models[model_name].add_observer(self.monitors[model_name])
1158
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001159 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001160
1161 async def login(self):
1162 """Login to the Juju controller."""
1163
1164 if self.authenticated:
1165 return
1166
1167 self.connecting = True
1168
1169 self.log.debug("JujuApi: Logging into controller")
1170
Adam Israel5e08a0e2018-09-06 19:22:47 -04001171 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001172
1173 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001174 self.log.debug(
1175 "Connecting to controller... ws://{}:{} as {}/{}".format(
1176 self.endpoint,
1177 self.port,
1178 self.user,
1179 self.secret,
1180 )
1181 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001182 await self.controller.connect(
1183 endpoint=self.endpoint,
1184 username=self.user,
1185 password=self.secret,
Adam Israelb2a07f52019-04-25 17:17:05 -04001186 cacert=self.ca_cert,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001187 )
Adam Israelfc511ed2018-09-21 14:20:55 +02001188 self.refcount['controller'] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001189 else:
1190 # current_controller no longer exists
1191 # self.log.debug("Connecting to current controller...")
1192 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -06001193 # await self.controller.connect(
1194 # endpoint=self.endpoint,
1195 # username=self.user,
1196 # cacert=cacert,
1197 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001198 self.log.fatal("VCA credentials not configured.")
1199
1200 self.authenticated = True
1201 self.log.debug("JujuApi: Logged into controller")
1202
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001203 async def logout(self):
1204 """Logout of the Juju controller."""
1205 if not self.authenticated:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001206 return False
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001207
1208 try:
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001209 for model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001210 await self.disconnect_model(model)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001211
1212 if self.controller:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001213 self.log.debug("Disconnecting controller {}".format(
1214 self.controller
1215 ))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001216 await self.controller.disconnect()
Adam Israelfc511ed2018-09-21 14:20:55 +02001217 self.refcount['controller'] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -04001218 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001219
1220 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +02001221
1222 self.log.debug(self.refcount)
1223
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001224 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001225 self.log.fatal(
1226 "Fatal error logging out of Juju Controller: {}".format(e)
1227 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001228 raise e
Adam Israel6d84dbd2019-03-08 18:33:35 -05001229 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001230
Adam Israel85a4b212018-11-29 20:30:24 -05001231 async def disconnect_model(self, model):
1232 self.log.debug("Disconnecting model {}".format(model))
1233 if model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001234 print("Disconnecting model")
1235 await self.models[model].disconnect()
1236 self.refcount['model'] -= 1
1237 self.models[model] = None
1238
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001239 # async def remove_application(self, name):
1240 # """Remove the application."""
1241 # if not self.authenticated:
1242 # await self.login()
1243 #
1244 # app = await self.get_application(name)
1245 # if app:
1246 # self.log.debug("JujuApi: Destroying application {}".format(
1247 # name,
1248 # ))
1249 #
1250 # await app.destroy()
1251
1252 async def remove_relation(self, a, b):
1253 """
1254 Remove a relation between two application endpoints
1255
1256 :param a An application endpoint
1257 :param b An application endpoint
1258 """
1259 if not self.authenticated:
1260 await self.login()
1261
1262 m = await self.get_model()
1263 try:
1264 m.remove_relation(a, b)
1265 finally:
1266 await m.disconnect()
1267
Adam Israel85a4b212018-11-29 20:30:24 -05001268 async def resolve_error(self, model_name, application=None):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001269 """Resolve units in error state."""
1270 if not self.authenticated:
1271 await self.login()
1272
Adam Israel85a4b212018-11-29 20:30:24 -05001273 model = await self.get_model(model_name)
1274
1275 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001276 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001277 self.log.debug(
1278 "JujuApi: Resolving errors for application {}".format(
1279 application,
1280 )
1281 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001282
1283 for unit in app.units:
1284 app.resolved(retry=True)
1285
Adam Israel85a4b212018-11-29 20:30:24 -05001286 async def run_action(self, model_name, application, action_name, **params):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001287 """Execute an action and return an Action object."""
1288 if not self.authenticated:
1289 await self.login()
1290 result = {
1291 'status': '',
1292 'action': {
1293 'tag': None,
1294 'results': None,
1295 }
1296 }
Adam Israel85a4b212018-11-29 20:30:24 -05001297
1298 model = await self.get_model(model_name)
1299
1300 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001301 if app:
1302 # We currently only have one unit per application
1303 # so use the first unit available.
1304 unit = app.units[0]
1305
Adam Israel5e08a0e2018-09-06 19:22:47 -04001306 self.log.debug(
1307 "JujuApi: Running Action {} against Application {}".format(
1308 action_name,
1309 application,
1310 )
1311 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001312
1313 action = await unit.run_action(action_name, **params)
1314
1315 # Wait for the action to complete
1316 await action.wait()
1317
1318 result['status'] = action.status
1319 result['action']['tag'] = action.data['id']
1320 result['action']['results'] = action.results
1321
1322 return result
1323
Adam Israelb5214512018-05-03 10:00:04 -04001324 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001325 """Apply a configuration to the application."""
1326 if not self.authenticated:
1327 await self.login()
1328
Adam Israelb5214512018-05-03 10:00:04 -04001329 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001330 if app:
1331 self.log.debug("JujuApi: Setting config for Application {}".format(
1332 application,
1333 ))
1334 await app.set_config(config)
1335
1336 # Verify the config is set
1337 newconf = await app.get_config()
1338 for key in config:
1339 if config[key] != newconf[key]['value']:
1340 self.log.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key, config[key], newconf[key]))
1341
Adam Israelb5214512018-05-03 10:00:04 -04001342 # async def set_parameter(self, parameter, value, application=None):
1343 # """Set a config parameter for a service."""
1344 # if not self.authenticated:
1345 # await self.login()
1346 #
1347 # self.log.debug("JujuApi: Setting {}={} for Application {}".format(
1348 # parameter,
1349 # value,
1350 # application,
1351 # ))
1352 # return await self.apply_config(
1353 # {parameter: value},
1354 # application=application,
1355 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001356
Adam Israel5e08a0e2018-09-06 19:22:47 -04001357 async def wait_for_application(self, model_name, application_name,
1358 timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001359 """Wait for an application to become active."""
1360 if not self.authenticated:
1361 await self.login()
1362
Adam Israel5e08a0e2018-09-06 19:22:47 -04001363 model = await self.get_model(model_name)
1364
1365 app = await self.get_application(model, application_name)
1366 self.log.debug("Application: {}".format(app))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001367 if app:
1368 self.log.debug(
1369 "JujuApi: Waiting {} seconds for Application {}".format(
1370 timeout,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001371 application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001372 )
1373 )
1374
Adam Israel5e08a0e2018-09-06 19:22:47 -04001375 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001376 lambda: all(
Adam Israel5e08a0e2018-09-06 19:22:47 -04001377 unit.agent_status == 'idle' and unit.workload_status in
1378 ['active', 'unknown'] for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001379 ),
1380 timeout=timeout
1381 )