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 Queue the execution of a primitive
379 returns the UUID of the executed primitive
383 if not self
.authenticated
:
386 # FIXME: This is hard-coded until model-per-ns is added
387 model_name
= 'default'
389 if primitive
== 'config':
390 # config is special, and expecting params to be a dictionary
391 await self
.set_config(application_name
, params
['params'])
393 model
= await self
.controller
.get_model(model_name
)
394 app
= await self
.get_application(model
, application_name
)
396 # Run against the first (and probably only) unit in the app
399 self
.log
.debug("Executing primitive {}".format(primitive
))
400 action
= await unit
.run_action(primitive
, **params
)
402 await model
.disconnect()
403 except Exception as e
:
404 self
.log
.debug("Caught exception while executing primitive: {}".format(e
))
408 async def RemoveCharms(self
, model_name
, application_name
, callback
=None, *callback_args
):
410 if not self
.authenticated
:
413 model
= await self
.get_model(model_name
)
414 app
= await self
.get_application(model
, application_name
)
416 self
.notify_callback(model_name
, application_name
, "removing", callback
, *callback_args
)
418 self
.notify_callback(model_name
, application_name
, "removed", callback
, *callback_args
)
419 except Exception as e
:
420 print("Caught exception: {}".format(e
))
424 async def DestroyNetworkService(self
, nsd
):
425 raise NotImplementedError()
427 async def GetMetrics(self
, nsd
, vnfd
):
428 """Get the metrics collected by the VCA."""
429 raise NotImplementedError()
432 async def add_relation(self
, a
, b
, via
=None):
434 Add a relation between two application endpoints.
436 :param a An application endpoint
437 :param b An application endpoint
438 :param via The egress subnet(s) for outbound traffic, e.g.,
439 (192.168.0.0/16,10.0.0.0/8)
441 if not self
.authenticated
:
444 m
= await self
.get_model()
446 m
.add_relation(a
, b
, via
)
450 async def apply_config(self
, config
, application
):
451 """Apply a configuration to the application."""
452 print("JujuApi: Applying configuration to {}.".format(
455 return await self
.set_config(application
=application
, config
=config
)
457 def _get_config_from_dict(self
, config_primitive
, values
):
458 """Transform the yang config primitive to dict.
467 for primitive
in config_primitive
:
468 if primitive
['name'] == 'config':
469 # config = self._map_primitive_parameters()
470 for parameter
in primitive
['parameter']:
471 param
= str(parameter
['name'])
472 if parameter
['value'] == "<rw_mgmt_ip>":
473 config
[param
] = str(values
[parameter
['value']])
475 config
[param
] = str(parameter
['value'])
479 def _map_primitive_parameters(self
, parameters
, values
):
481 for parameter
in parameters
:
482 param
= str(parameter
['name'])
483 if parameter
['value'] == "<rw_mgmt_ip>":
484 params
[param
] = str(values
[parameter
['value']])
486 params
[param
] = str(parameter
['value'])
489 def _get_config_from_yang(self
, config_primitive
, values
):
490 """Transform the yang config primitive to dict."""
492 for primitive
in config_primitive
.values():
493 if primitive
['name'] == 'config':
494 for parameter
in primitive
['parameter'].values():
495 param
= str(parameter
['name'])
496 if parameter
['value'] == "<rw_mgmt_ip>":
497 config
[param
] = str(values
[parameter
['value']])
499 config
[param
] = str(parameter
['value'])
503 def FormatApplicationName(self
, *args
):
505 Generate a Juju-compatible Application name
507 :param args tuple: Positional arguments to be used to construct the
511 - Only accepts characters a-z and non-consequitive dashes (-)
512 - Application name should not exceed 50 characters
516 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
520 for c
in "-".join(list(args
)):
523 elif not c
.isalpha():
526 return re
.sub('\-+', '-', appname
.lower())
529 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
530 # """Format the name of the application
533 # - Only accepts characters a-z and non-consequitive dashes (-)
534 # - Application name should not exceed 50 characters
536 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
540 # c = chr(97 + int(c))
541 # elif not c.isalpha():
544 # return re.sub('\-+', '-', new_name.lower())
546 def format_model_name(self
, name
):
547 """Format the name of model.
549 Model names may only contain lowercase letters, digits and hyphens
552 return name
.replace('_', '-').lower()
554 async def get_application(self
, model
, application
):
555 """Get the deployed application."""
556 if not self
.authenticated
:
560 if application
and model
:
561 if model
.applications
:
562 if application
in model
.applications
:
563 app
= model
.applications
[application
]
567 async def get_model(self
, model_name
='default'):
568 """Get a model from the Juju Controller.
570 Note: Model objects returned must call disconnected() before it goes
572 if not self
.authenticated
:
575 if model_name
not in self
.models
:
576 print("connecting to model {}".format(model_name
))
577 self
.models
[model_name
] = await self
.controller
.get_model(model_name
)
579 return self
.models
[model_name
]
581 async def login(self
):
582 """Login to the Juju controller."""
584 if self
.authenticated
:
587 self
.connecting
= True
589 self
.log
.debug("JujuApi: Logging into controller")
592 self
.controller
= Controller()
595 self
.log
.debug("Connecting to controller... ws://{}:{} as {}/{}".format(self
.endpoint
, self
.port
, self
.user
, self
.secret
))
596 await self
.controller
.connect(
597 endpoint
=self
.endpoint
,
599 password
=self
.secret
,
603 # current_controller no longer exists
604 # self.log.debug("Connecting to current controller...")
605 # await self.controller.connect_current()
606 # await self.controller.connect(
607 # endpoint=self.endpoint,
608 # username=self.user,
611 self
.log
.fatal("VCA credentials not configured.")
613 self
.authenticated
= True
614 self
.log
.debug("JujuApi: Logged into controller")
616 # self.default_model = await self.controller.get_model("default")
618 async def logout(self
):
619 """Logout of the Juju controller."""
620 if not self
.authenticated
:
624 if self
.default_model
:
625 self
.log
.debug("Disconnecting model {}".format(self
.default_model
))
626 await self
.default_model
.disconnect()
627 self
.default_model
= None
629 for model
in self
.models
:
630 await self
.models
[model
].disconnect()
633 self
.log
.debug("Disconnecting controller {}".format(self
.controller
))
634 await self
.controller
.disconnect()
635 # self.controller = None
637 self
.authenticated
= False
638 except Exception as e
:
639 self
.log
.fail("Fatal error logging out of Juju Controller: {}".format(e
))
643 # async def remove_application(self, name):
644 # """Remove the application."""
645 # if not self.authenticated:
648 # app = await self.get_application(name)
650 # self.log.debug("JujuApi: Destroying application {}".format(
654 # await app.destroy()
656 async def remove_relation(self
, a
, b
):
658 Remove a relation between two application endpoints
660 :param a An application endpoint
661 :param b An application endpoint
663 if not self
.authenticated
:
666 m
= await self
.get_model()
668 m
.remove_relation(a
, b
)
672 async def resolve_error(self
, application
=None):
673 """Resolve units in error state."""
674 if not self
.authenticated
:
677 app
= await self
.get_application(self
.default_model
, application
)
679 self
.log
.debug("JujuApi: Resolving errors for application {}".format(
683 for unit
in app
.units
:
684 app
.resolved(retry
=True)
686 async def run_action(self
, application
, action_name
, **params
):
687 """Execute an action and return an Action object."""
688 if not self
.authenticated
:
697 app
= await self
.get_application(self
.default_model
, application
)
699 # We currently only have one unit per application
700 # so use the first unit available.
703 self
.log
.debug("JujuApi: Running Action {} against Application {}".format(
708 action
= await unit
.run_action(action_name
, **params
)
710 # Wait for the action to complete
713 result
['status'] = action
.status
714 result
['action']['tag'] = action
.data
['id']
715 result
['action']['results'] = action
.results
719 async def set_config(self
, application
, config
):
720 """Apply a configuration to the application."""
721 if not self
.authenticated
:
724 app
= await self
.get_application(self
.default_model
, application
)
726 self
.log
.debug("JujuApi: Setting config for Application {}".format(
729 await app
.set_config(config
)
731 # Verify the config is set
732 newconf
= await app
.get_config()
734 if config
[key
] != newconf
[key
]['value']:
735 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
737 async def set_parameter(self
, parameter
, value
, application
=None):
738 """Set a config parameter for a service."""
739 if not self
.authenticated
:
742 self
.log
.debug("JujuApi: Setting {}={} for Application {}".format(
747 return await self
.apply_config(
749 application
=application
,
752 async def wait_for_application(self
, name
, timeout
=300):
753 """Wait for an application to become active."""
754 if not self
.authenticated
:
757 app
= await self
.get_application(self
.default_model
, name
)
760 "JujuApi: Waiting {} seconds for Application {}".format(
766 await self
.default_model
.block_until(
768 unit
.agent_status
== 'idle'
769 and unit
.workload_status
770 in ['active', 'unknown'] for unit
in app
.units