10 # FIXME: this should load the juju inside or modules without having to
11 # explicitly install it. Check why it's not working.
12 # Load our subtree of the juju library
13 path
= os
.path
.abspath(os
.path
.join(os
.path
.dirname(__file__
), '..'))
14 path
= os
.path
.join(path
, "modules/libjuju/")
15 if path
not in sys
.path
:
16 sys
.path
.insert(1, path
)
18 from juju
.controller
import Controller
19 from juju
.model
import Model
, ModelObserver
22 # We might need this to connect to the websocket securely, but test and verify.
24 ssl
._create
_default
_https
_context
= ssl
._create
_unverified
_context
25 except AttributeError:
26 # Legacy Python doesn't verify by default (see pep-0476)
27 # https://www.python.org/dev/peps/pep-0476/
32 class JujuCharmNotFound(Exception):
33 """The Charm can't be found or is not readable."""
36 class JujuApplicationExists(Exception):
37 """The Application already exists."""
39 class N2VCPrimitiveExecutionFailed(Exception):
40 """Something failed while attempting to execute a primitive."""
43 # Quiet the debug logging
44 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
45 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
46 logging
.getLogger('juju.model').setLevel(logging
.WARN
)
47 logging
.getLogger('juju.machine').setLevel(logging
.WARN
)
49 class VCAMonitor(ModelObserver
):
50 """Monitor state changes within the Juju Model."""
55 application_name
= None
57 def __init__(self
, ns_name
, application_name
, callback
, *args
):
58 self
.log
= logging
.getLogger(__name__
)
60 self
.ns_name
= ns_name
61 self
.application_name
= application_name
62 self
.callback
= callback
63 self
.callback_args
= args
65 async def on_change(self
, delta
, old
, new
, model
):
66 """React to changes in the Juju model."""
68 if delta
.entity
== "unit":
71 old_status
= old
.workload_status
72 new_status
= new
.workload_status
74 if old_status
== new_status
:
76 """The workload status may fluctuate around certain events,
77 so wait until the status has stabilized before triggering
82 self
.application_name
,
85 except Exception as e
:
86 self
.log
.debug("[1] notify_callback exception {}".format(e
))
87 elif delta
.entity
== "action":
88 # TODO: Decide how we want to notify the user of actions
90 # uuid = delta.data['id'] # The Action's unique id
91 # msg = delta.data['message'] # The output of the action
93 # if delta.data['status'] == "pending":
94 # # The action is queued
96 # elif delta.data['status'] == "completed""
97 # # The action was successful
99 # elif delta.data['status'] == "failed":
100 # # The action failed.
108 # Create unique models per network service
109 # Document all public functions
118 authenticated
= False
142 :param vcaconfig dict A dictionary containing the VCA configuration
144 :param artifacts str The directory where charms required by a vnfd are
148 n2vc = N2VC(vcaconfig={
149 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
151 'ip-address': '10.44.127.137',
153 'artifacts': '/path/to/charms'
161 self
.log
= logging
.getLogger(__name__
)
163 # Quiet websocket traffic
164 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
165 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
166 logging
.getLogger('model').setLevel(logging
.WARN
)
167 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
169 self
.log
.debug('JujuApi: instantiated')
175 if user
.startswith('user-'):
178 self
.user
= 'user-{}'.format(user
)
180 self
.endpoint
= '%s:%d' % (server
, int(port
))
182 self
.artifacts
= artifacts
185 """Close any open connections."""
188 def notify_callback(self
, model_name
, application_name
, status
, callback
=None, *callback_args
):
191 callback(model_name
, application_name
, status
, *callback_args
)
192 except Exception as e
:
193 self
.log
.error("[0] notify_callback exception {}".format(e
))
198 async def CreateNetworkService(self
, nsd
):
199 """Create a new model to encapsulate this network service.
201 Create a new model in the Juju controller to encapsulate the
202 charms associated with a network service.
204 You can pass either the nsd record or the id of the network
205 service, but this method will fail without one of them.
207 if not self
.authenticated
:
210 # Ideally, we will create a unique model per network service.
211 # This change will require all components, i.e., LCM and SO, to use
212 # N2VC for 100% compatibility. If we adopt unique models for the LCM,
213 # services deployed via LCM would't be manageable via SO and vice versa
215 return self
.default_model
217 async def DeployCharms(self
, model_name
, application_name
, vnfd
, charm_path
, params
={}, machine_spec
={}, callback
=None, *callback_args
):
218 """Deploy one or more charms associated with a VNF.
220 Deploy the charm(s) referenced in a VNF Descriptor.
222 You can pass either the nsd record or the id of the network
223 service, but this method will fail without one of them.
225 :param str ns_name: The name of the network service
226 :param str application_name: The name of the application
227 :param dict vnfd: The name of the application
228 :param str charm_path: The path to the Juju charm
229 :param dict params: A dictionary of runtime parameters
232 'rw_mgmt_ip': '1.2.3.4',
233 # Pass the initial-config-primitives section of the vnf or vdu
234 'initial-config-primitives': {...}
236 :param dict machine_spec: A dictionary describing the machine to install to
239 'hostname': '1.2.3.4',
240 'username': 'ubuntu',
242 :param obj callback: A callback function to receive status changes.
243 :param tuple callback_args: A list of arguments to be passed to the callback
246 ########################################################
247 # Verify the path to the charm exists and is readable. #
248 ########################################################
249 if not os
.path
.exists(charm_path
):
250 self
.log
.debug("Charm path doesn't exist: {}".format(charm_path
))
251 self
.notify_callback(model_name
, application_name
, "failed", callback
, *callback_args
)
252 raise JujuCharmNotFound("No artifacts configured.")
254 ################################
255 # Login to the Juju controller #
256 ################################
257 if not self
.authenticated
:
258 self
.log
.debug("Authenticating with Juju")
261 ##########################################
262 # Get the model for this network service #
263 ##########################################
264 # TODO: In a point release, we will use a model per deployed network
265 # service. In the meantime, we will always use the 'default' model.
266 model_name
= 'default'
267 model
= await self
.get_model(model_name
)
269 ########################################
270 # Verify the application doesn't exist #
271 ########################################
272 app
= await self
.get_application(model
, application_name
)
274 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model
))
276 ############################################################
277 # Create a monitor to watch for application status changes #
278 ############################################################
280 self
.log
.debug("Setting monitor<->callback")
281 self
.monitors
[application_name
] = VCAMonitor(model_name
, application_name
, callback
, *callback_args
)
282 model
.add_observer(self
.monitors
[application_name
])
284 ########################################################
285 # Check for specific machine placement (native charms) #
286 ########################################################
288 if machine_spec
.keys():
289 # TODO: This needs to be tested.
290 # if all(k in machine_spec for k in ['hostname', 'username']):
291 # # Enlist the existing machine in Juju
292 # machine = await self.model.add_machine(spec='ssh:%@%'.format(
299 #######################################
300 # Get the initial charm configuration #
301 #######################################
304 if 'rw_mgmt_ip' in params
:
305 rw_mgmt_ip
= params
['rw_mgmt_ip']
307 initial_config
= self
._get
_config
_from
_dict
(
308 params
['initial-config-primitive'],
309 {'<rw_mgmt_ip>': rw_mgmt_ip
}
312 self
.log
.debug("JujuApi: Deploying charm ({}) from {}".format(
318 ########################################################
319 # Deploy the charm and apply the initial configuration #
320 ########################################################
321 app
= await model
.deploy(
322 # We expect charm_path to be either the path to the charm on disk
323 # or in the format of cs:series/name
325 # This is the formatted, unique name for this charm
326 application_name
=application_name
,
327 # Proxy charms should use the current LTS. This will need to be
328 # changed for native charms.
330 # Apply the initial 'config' primitive during deployment
331 config
=initial_config
,
332 # TBD: Where to deploy the charm to.
336 # #######################################
337 # # Execute initial config primitive(s) #
338 # #######################################
341 # Build a sequential list of the primitives to execute
342 for primitive
in params
['initial-config-primitive']:
344 if primitive
['name'] == 'config':
345 # This is applied when the Application is deployed
348 # TODO: We need to sort by seq, and queue the actions in order.
350 seq
= primitive
['seq']
353 'name': primitive
['name'],
354 'parameters': self
._map
_primitive
_parameters
(
355 primitive
['parameter'],
356 {'<rw_mgmt_ip>': rw_mgmt_ip
}
360 for primitive
in sorted(primitives
):
361 await self
.ExecutePrimitive(
364 primitives
[primitive
]['name'],
367 **primitives
[primitive
]['parameters'],
369 except N2VCPrimitiveExecutionFailed
as e
:
371 "[N2VC] Exception executing primitive: {}".format(e
)
375 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
, callback
, *callback_args
, **params
):
377 if not self
.authenticated
:
380 # FIXME: This is hard-coded until model-per-ns is added
381 model_name
= 'default'
383 if primitive
== 'config':
384 # config is special, and expecting params to be a dictionary
385 await self
.set_config(application_name
, params
['params'])
387 model
= await self
.controller
.get_model(model_name
)
388 app
= await self
.get_application(model
, application_name
)
390 # Run against the first (and probably only) unit in the app
393 self
.log
.debug("Executing primitive {}".format(primitive
))
394 action
= await unit
.run_action(primitive
, **params
)
395 # action = await action.wait()
396 await model
.disconnect()
397 except Exception as e
:
398 self
.log
.debug("Caught exception while executing primitive: {}".format(e
))
401 async def RemoveCharms(self
, model_name
, application_name
, callback
=None, *callback_args
):
403 if not self
.authenticated
:
406 model
= await self
.get_model(model_name
)
407 app
= await self
.get_application(model
, application_name
)
409 self
.notify_callback(model_name
, application_name
, "removing", callback
, *callback_args
)
411 self
.notify_callback(model_name
, application_name
, "removed", callback
, *callback_args
)
412 except Exception as e
:
413 print("Caught exception: {}".format(e
))
417 async def DestroyNetworkService(self
, nsd
):
418 raise NotImplementedError()
420 async def GetMetrics(self
, nsd
, vnfd
):
421 """Get the metrics collected by the VCA."""
422 raise NotImplementedError()
425 async def add_relation(self
, a
, b
, via
=None):
427 Add a relation between two application endpoints.
429 :param a An application endpoint
430 :param b An application endpoint
431 :param via The egress subnet(s) for outbound traffic, e.g.,
432 (192.168.0.0/16,10.0.0.0/8)
434 if not self
.authenticated
:
437 m
= await self
.get_model()
439 m
.add_relation(a
, b
, via
)
443 async def apply_config(self
, config
, application
):
444 """Apply a configuration to the application."""
445 print("JujuApi: Applying configuration to {}.".format(
448 return await self
.set_config(application
=application
, config
=config
)
450 def _get_config_from_dict(self
, config_primitive
, values
):
451 """Transform the yang config primitive to dict.
460 for primitive
in config_primitive
:
461 if primitive
['name'] == 'config':
462 # config = self._map_primitive_parameters()
463 for parameter
in primitive
['parameter']:
464 param
= str(parameter
['name'])
465 if parameter
['value'] == "<rw_mgmt_ip>":
466 config
[param
] = str(values
[parameter
['value']])
468 config
[param
] = str(parameter
['value'])
472 def _map_primitive_parameters(self
, parameters
, values
):
474 for parameter
in parameters
:
475 param
= str(parameter
['name'])
476 if parameter
['value'] == "<rw_mgmt_ip>":
477 params
[param
] = str(values
[parameter
['value']])
479 params
[param
] = str(parameter
['value'])
482 def _get_config_from_yang(self
, config_primitive
, values
):
483 """Transform the yang config primitive to dict."""
485 for primitive
in config_primitive
.values():
486 if primitive
['name'] == 'config':
487 for parameter
in primitive
['parameter'].values():
488 param
= str(parameter
['name'])
489 if parameter
['value'] == "<rw_mgmt_ip>":
490 config
[param
] = str(values
[parameter
['value']])
492 config
[param
] = str(parameter
['value'])
496 def FormatApplicationName(self
, *args
):
498 Generate a Juju-compatible Application name
500 :param args tuple: Positional arguments to be used to construct the
504 - Only accepts characters a-z and non-consequitive dashes (-)
505 - Application name should not exceed 50 characters
509 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
513 for c
in "-".join(list(args
)):
516 elif not c
.isalpha():
519 return re
.sub('\-+', '-', appname
.lower())
522 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
523 # """Format the name of the application
526 # - Only accepts characters a-z and non-consequitive dashes (-)
527 # - Application name should not exceed 50 characters
529 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
533 # c = chr(97 + int(c))
534 # elif not c.isalpha():
537 # return re.sub('\-+', '-', new_name.lower())
539 def format_model_name(self
, name
):
540 """Format the name of model.
542 Model names may only contain lowercase letters, digits and hyphens
545 return name
.replace('_', '-').lower()
547 async def get_application(self
, model
, application
):
548 """Get the deployed application."""
549 if not self
.authenticated
:
553 if application
and model
:
554 if model
.applications
:
555 if application
in model
.applications
:
556 app
= model
.applications
[application
]
560 async def get_model(self
, model_name
='default'):
561 """Get a model from the Juju Controller.
563 Note: Model objects returned must call disconnected() before it goes
565 if not self
.authenticated
:
568 if model_name
not in self
.models
:
569 print("connecting to model {}".format(model_name
))
570 self
.models
[model_name
] = await self
.controller
.get_model(model_name
)
572 return self
.models
[model_name
]
574 async def login(self
):
575 """Login to the Juju controller."""
577 if self
.authenticated
:
580 self
.connecting
= True
582 self
.log
.debug("JujuApi: Logging into controller")
585 self
.controller
= Controller()
588 self
.log
.debug("Connecting to controller... ws://{}:{} as {}/{}".format(self
.endpoint
, self
.port
, self
.user
, self
.secret
))
589 await self
.controller
.connect(
590 endpoint
=self
.endpoint
,
592 password
=self
.secret
,
596 # current_controller no longer exists
597 # self.log.debug("Connecting to current controller...")
598 # await self.controller.connect_current()
599 # await self.controller.connect(
600 # endpoint=self.endpoint,
601 # username=self.user,
604 self
.log
.fatal("VCA credentials not configured.")
606 self
.authenticated
= True
607 self
.log
.debug("JujuApi: Logged into controller")
609 # self.default_model = await self.controller.get_model("default")
611 async def logout(self
):
612 """Logout of the Juju controller."""
613 if not self
.authenticated
:
617 if self
.default_model
:
618 self
.log
.debug("Disconnecting model {}".format(self
.default_model
))
619 await self
.default_model
.disconnect()
620 self
.default_model
= None
622 for model
in self
.models
:
623 await self
.models
[model
].disconnect()
626 self
.log
.debug("Disconnecting controller {}".format(self
.controller
))
627 await self
.controller
.disconnect()
628 # self.controller = None
630 self
.authenticated
= False
631 except Exception as e
:
632 self
.log
.fail("Fatal error logging out of Juju Controller: {}".format(e
))
636 # async def remove_application(self, name):
637 # """Remove the application."""
638 # if not self.authenticated:
641 # app = await self.get_application(name)
643 # self.log.debug("JujuApi: Destroying application {}".format(
647 # await app.destroy()
649 async def remove_relation(self
, a
, b
):
651 Remove a relation between two application endpoints
653 :param a An application endpoint
654 :param b An application endpoint
656 if not self
.authenticated
:
659 m
= await self
.get_model()
661 m
.remove_relation(a
, b
)
665 async def resolve_error(self
, application
=None):
666 """Resolve units in error state."""
667 if not self
.authenticated
:
670 app
= await self
.get_application(self
.default_model
, application
)
672 self
.log
.debug("JujuApi: Resolving errors for application {}".format(
676 for unit
in app
.units
:
677 app
.resolved(retry
=True)
679 async def run_action(self
, application
, action_name
, **params
):
680 """Execute an action and return an Action object."""
681 if not self
.authenticated
:
690 app
= await self
.get_application(self
.default_model
, application
)
692 # We currently only have one unit per application
693 # so use the first unit available.
696 self
.log
.debug("JujuApi: Running Action {} against Application {}".format(
701 action
= await unit
.run_action(action_name
, **params
)
703 # Wait for the action to complete
706 result
['status'] = action
.status
707 result
['action']['tag'] = action
.data
['id']
708 result
['action']['results'] = action
.results
712 async def set_config(self
, application
, config
):
713 """Apply a configuration to the application."""
714 if not self
.authenticated
:
717 app
= await self
.get_application(self
.default_model
, application
)
719 self
.log
.debug("JujuApi: Setting config for Application {}".format(
722 await app
.set_config(config
)
724 # Verify the config is set
725 newconf
= await app
.get_config()
727 if config
[key
] != newconf
[key
]['value']:
728 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
730 async def set_parameter(self
, parameter
, value
, application
=None):
731 """Set a config parameter for a service."""
732 if not self
.authenticated
:
735 self
.log
.debug("JujuApi: Setting {}={} for Application {}".format(
740 return await self
.apply_config(
742 application
=application
,
745 async def wait_for_application(self
, name
, timeout
=300):
746 """Wait for an application to become active."""
747 if not self
.authenticated
:
750 app
= await self
.get_application(self
.default_model
, name
)
753 "JujuApi: Waiting {} seconds for Application {}".format(
759 await self
.default_model
.block_until(
761 unit
.agent_status
== 'idle'
762 and unit
.workload_status
763 in ['active', 'unknown'] for unit
in app
.units