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."""
40 # Quiet the debug logging
41 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
42 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
43 logging
.getLogger('juju.model').setLevel(logging
.WARN
)
44 logging
.getLogger('juju.machine').setLevel(logging
.WARN
)
46 class VCAMonitor(ModelObserver
):
47 """Monitor state changes within the Juju Model."""
52 application_name
= None
54 def __init__(self
, ns_name
, application_name
, callback
, *args
):
55 self
.log
= logging
.getLogger(__name__
)
57 self
.ns_name
= ns_name
58 self
.application_name
= application_name
59 self
.callback
= callback
60 self
.callback_args
= args
62 async def on_change(self
, delta
, old
, new
, model
):
63 """React to changes in the Juju model."""
65 if delta
.entity
== "unit":
68 old_status
= old
.workload_status
69 new_status
= new
.workload_status
70 if old_status
== new_status
:
71 """The workload status may fluctuate around certain events,
72 so wait until the status has stabilized before triggering
77 self
.application_name
,
80 except Exception as e
:
81 self
.log
.debug("[1] notify_callback exception {}".format(e
))
87 # Create unique models per network service
88 # Document all public functions
121 :param vcaconfig dict A dictionary containing the VCA configuration
123 :param artifacts str The directory where charms required by a vnfd are
127 n2vc = N2VC(vcaconfig={
128 'secret': 'MzI3MDJhOTYxYmM0YzRjNTJiYmY1Yzdm',
130 'ip-address': '10.44.127.137',
132 'artifacts': '/path/to/charms'
140 self
.log
= logging
.getLogger(__name__
)
142 # Quiet websocket traffic
143 logging
.getLogger('websockets.protocol').setLevel(logging
.INFO
)
144 logging
.getLogger('juju.client.connection').setLevel(logging
.WARN
)
145 logging
.getLogger('model').setLevel(logging
.WARN
)
146 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
148 self
.log
.debug('JujuApi: instantiated')
154 if user
.startswith('user-'):
157 self
.user
= 'user-{}'.format(user
)
159 self
.endpoint
= '%s:%d' % (server
, int(port
))
161 self
.artifacts
= artifacts
164 """Close any open connections."""
167 def notify_callback(self
, model_name
, application_name
, status
, callback
=None, *callback_args
):
170 callback(model_name
, application_name
, status
, *callback_args
)
171 except Exception as e
:
172 self
.log
.error("[0] notify_callback exception {}".format(e
))
176 async def CreateNetworkService(self
, nsd
):
177 """Create a new model to encapsulate this network service.
179 Create a new model in the Juju controller to encapsulate the
180 charms associated with a network service.
182 You can pass either the nsd record or the id of the network
183 service, but this method will fail without one of them.
185 if not self
.authenticated
:
188 # Ideally, we will create a unique model per network service.
189 # This change will require all components, i.e., LCM and SO, to use
190 # N2VC for 100% compatibility. If we adopt unique models for the LCM,
191 # services deployed via LCM would't be manageable via SO and vice versa
193 return self
.default_model
195 async def DeployCharms(self
, model_name
, application_name
, vnfd
, charm_path
, params
={}, machine_spec
={}, callback
=None, *callback_args
):
196 """Deploy one or more charms associated with a VNF.
198 Deploy the charm(s) referenced in a VNF Descriptor.
200 You can pass either the nsd record or the id of the network
201 service, but this method will fail without one of them.
203 :param str ns_name: The name of the network service
204 :param str application_name: The name of the application
205 :param dict vnfd: The name of the application
206 :param str charm_path: The path to the Juju charm
207 :param dict params: A dictionary of runtime parameters
210 'rw_mgmt_ip': '1.2.3.4'
212 :param dict machine_spec: A dictionary describing the machine to install to
215 'hostname': '1.2.3.4',
216 'username': 'ubuntu',
218 :param obj callback: A callback function to receive status changes.
219 :param tuple callback_args: A list of arguments to be passed to the callback
222 ########################################################
223 # Verify the path to the charm exists and is readable. #
224 ########################################################
225 if not os
.path
.exists(charm_path
):
226 self
.log
.debug("Charm path doesn't exist: {}".format(charm_path
))
227 self
.notify_callback(model_name
, application_name
, "failed", callback
, *callback_args
)
228 raise JujuCharmNotFound("No artifacts configured.")
230 ################################
231 # Login to the Juju controller #
232 ################################
233 if not self
.authenticated
:
234 self
.log
.debug("Authenticating with Juju")
237 ##########################################
238 # Get the model for this network service #
239 ##########################################
240 # TODO: In a point release, we will use a model per deployed network
241 # service. In the meantime, we will always use the 'default' model.
242 model_name
= 'default'
243 model
= await self
.get_model(model_name
)
244 # if model_name not in self.models:
245 # self.log.debug("Getting model {}".format(model_name))
246 # self.models[model_name] = await self.controller.get_model(model_name)
247 # model = await self.CreateNetworkService(ns_name)
249 ###################################################
250 # Get the name of the charm and its configuration #
251 ###################################################
252 config_dict
= vnfd
['vnf-configuration']
253 juju
= config_dict
['juju']
254 charm
= juju
['charm']
255 self
.log
.debug("Charm: {}".format(charm
))
257 ########################################
258 # Verify the application doesn't exist #
259 ########################################
260 app
= await self
.get_application(model
, application_name
)
262 raise JujuApplicationExists("Can't deploy application \"{}\" to model \"{}\" because it already exists.".format(application_name
, model
))
264 ############################################################
265 # Create a monitor to watch for application status changes #
266 ############################################################
268 self
.log
.debug("Setting monitor<->callback")
269 self
.monitors
[application_name
] = VCAMonitor(model_name
, application_name
, callback
, *callback_args
)
270 model
.add_observer(self
.monitors
[application_name
])
273 ########################################################
274 # Check for specific machine placement (native charms) #
275 ########################################################
277 if machine_spec
.keys():
278 # TODO: This needs to be tested.
279 # if all(k in machine_spec for k in ['hostname', 'username']):
280 # # Enlist the existing machine in Juju
281 # machine = await self.model.add_machine(spec='ssh:%@%'.format(
288 #######################################
289 # Get the initial charm configuration #
290 #######################################
293 if 'rw_mgmt_ip' in params
:
294 rw_mgmt_ip
= params
['rw_mgmt_ip']
296 initial_config
= self
._get
_config
_from
_dict
(
297 config_dict
['initial-config-primitive'],
298 {'<rw_mgmt_ip>': rw_mgmt_ip
}
301 self
.log
.debug("JujuApi: Deploying charm {} ({}) from {}".format(
308 ########################################################
309 # Deploy the charm and apply the initial configuration #
310 ########################################################
311 app
= await model
.deploy(
313 application_name
=application_name
,
315 config
=initial_config
,
319 async def ExecutePrimitive(self
, model_name
, application_name
, primitive
, callback
, *callback_args
, **params
):
321 if not self
.authenticated
:
324 # FIXME: This is hard-coded until model-per-ns is added
325 model_name
= 'default'
327 if primitive
== 'config':
328 # config is special, and expecting params to be a dictionary
329 await self
.set_config(application_name
, params
['params'])
331 model
= await self
.controller
.get_model(model_name
)
332 app
= await self
.get_application(model
, application_name
)
334 # Run against the first (and probably only) unit in the app
337 self
.log
.debug("Executing primitive {}".format(primitive
))
338 action
= await unit
.run_action(primitive
, **params
)
339 action
= await action
.wait()
340 await model
.disconnect()
341 except Exception as e
:
342 self
.log
.debug("Caught exception while executing primitive: {}".format(e
))
345 async def RemoveCharms(self
, model_name
, application_name
, callback
=None, *callback_args
):
347 if not self
.authenticated
:
350 model
= await self
.get_model(model_name
)
351 app
= await self
.get_application(model
, application_name
)
353 self
.notify_callback(model_name
, application_name
, "removing", callback
, *callback_args
)
355 self
.notify_callback(model_name
, application_name
, "removed", callback
, *callback_args
)
356 except Exception as e
:
357 print("Caught exception: {}".format(e
))
360 async def DestroyNetworkService(self
, nsd
):
361 raise NotImplementedError()
363 async def GetMetrics(self
, nsd
, vnfd
):
364 """Get the metrics collected by the VCA."""
365 raise NotImplementedError()
368 async def add_relation(self
, a
, b
, via
=None):
370 Add a relation between two application endpoints.
372 :param a An application endpoint
373 :param b An application endpoint
374 :param via The egress subnet(s) for outbound traffic, e.g.,
375 (192.168.0.0/16,10.0.0.0/8)
377 if not self
.authenticated
:
380 m
= await self
.get_model()
382 m
.add_relation(a
, b
, via
)
386 async def apply_config(self
, config
, application
):
387 """Apply a configuration to the application."""
388 print("JujuApi: Applying configuration to {}.".format(
391 return await self
.set_config(application
=application
, config
=config
)
393 def _get_config_from_dict(self
, config_primitive
, values
):
394 """Transform the yang config primitive to dict."""
396 for primitive
in config_primitive
:
397 if primitive
['name'] == 'config':
398 for parameter
in primitive
['parameter']:
399 param
= str(parameter
['name'])
400 if parameter
['value'] == "<rw_mgmt_ip>":
401 config
[param
] = str(values
[parameter
['value']])
403 config
[param
] = str(parameter
['value'])
407 def _get_config_from_yang(self
, config_primitive
, values
):
408 """Transform the yang config primitive to dict."""
410 for primitive
in config_primitive
.values():
411 if primitive
['name'] == 'config':
412 for parameter
in primitive
['parameter'].values():
413 param
= str(parameter
['name'])
414 if parameter
['value'] == "<rw_mgmt_ip>":
415 config
[param
] = str(values
[parameter
['value']])
417 config
[param
] = str(parameter
['value'])
421 def FormatApplicationName(self
, *args
):
423 Generate a Juju-compatible Application name
425 :param args tuple: Positional arguments to be used to construct the
429 - Only accepts characters a-z and non-consequitive dashes (-)
430 - Application name should not exceed 50 characters
434 FormatApplicationName("ping_pong_ns", "ping_vnf", "a")
438 for c
in "-".join(list(args
)):
441 elif not c
.isalpha():
444 return re
.sub('\-+', '-', appname
.lower())
447 # def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0):
448 # """Format the name of the application
451 # - Only accepts characters a-z and non-consequitive dashes (-)
452 # - Application name should not exceed 50 characters
454 # name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index)
458 # c = chr(97 + int(c))
459 # elif not c.isalpha():
462 # return re.sub('\-+', '-', new_name.lower())
464 def format_model_name(self
, name
):
465 """Format the name of model.
467 Model names may only contain lowercase letters, digits and hyphens
470 return name
.replace('_', '-').lower()
472 async def get_application(self
, model
, application
):
473 """Get the deployed application."""
474 if not self
.authenticated
:
478 if application
and model
:
479 if model
.applications
:
480 if application
in model
.applications
:
481 app
= model
.applications
[application
]
485 async def get_model(self
, model_name
='default'):
486 """Get a model from the Juju Controller.
488 Note: Model objects returned must call disconnected() before it goes
490 if not self
.authenticated
:
493 if model_name
not in self
.models
:
494 print("connecting to model {}".format(model_name
))
495 self
.models
[model_name
] = await self
.controller
.get_model(model_name
)
497 return self
.models
[model_name
]
499 async def login(self
):
500 """Login to the Juju controller."""
502 if self
.authenticated
:
505 self
.connecting
= True
507 self
.log
.debug("JujuApi: Logging into controller")
510 self
.controller
= Controller()
513 self
.log
.debug("Connecting to controller... ws://{}:{} as {}/{}".format(self
.endpoint
, self
.port
, self
.user
, self
.secret
))
514 await self
.controller
.connect(
515 endpoint
=self
.endpoint
,
517 password
=self
.secret
,
521 # current_controller no longer exists
522 # self.log.debug("Connecting to current controller...")
523 # await self.controller.connect_current()
524 self
.log
.fatal("VCA credentials not configured.")
526 self
.authenticated
= True
527 self
.log
.debug("JujuApi: Logged into controller")
529 # self.default_model = await self.controller.get_model("default")
531 async def logout(self
):
532 """Logout of the Juju controller."""
533 if not self
.authenticated
:
537 if self
.default_model
:
538 self
.log
.debug("Disconnecting model {}".format(self
.default_model
))
539 await self
.default_model
.disconnect()
540 self
.default_model
= None
542 for model
in self
.models
:
543 await self
.models
[model
].disconnect()
546 self
.log
.debug("Disconnecting controller {}".format(self
.controller
))
547 await self
.controller
.disconnect()
548 # self.controller = None
550 self
.authenticated
= False
551 except Exception as e
:
552 self
.log
.fail("Fatal error logging out of Juju Controller: {}".format(e
))
556 # async def remove_application(self, name):
557 # """Remove the application."""
558 # if not self.authenticated:
561 # app = await self.get_application(name)
563 # self.log.debug("JujuApi: Destroying application {}".format(
567 # await app.destroy()
569 async def remove_relation(self
, a
, b
):
571 Remove a relation between two application endpoints
573 :param a An application endpoint
574 :param b An application endpoint
576 if not self
.authenticated
:
579 m
= await self
.get_model()
581 m
.remove_relation(a
, b
)
585 async def resolve_error(self
, application
=None):
586 """Resolve units in error state."""
587 if not self
.authenticated
:
590 app
= await self
.get_application(self
.default_model
, application
)
592 self
.log
.debug("JujuApi: Resolving errors for application {}".format(
596 for unit
in app
.units
:
597 app
.resolved(retry
=True)
599 async def run_action(self
, application
, action_name
, **params
):
600 """Execute an action and return an Action object."""
601 if not self
.authenticated
:
610 app
= await self
.get_application(self
.default_model
, application
)
612 # We currently only have one unit per application
613 # so use the first unit available.
616 self
.log
.debug("JujuApi: Running Action {} against Application {}".format(
621 action
= await unit
.run_action(action_name
, **params
)
623 # Wait for the action to complete
626 result
['status'] = action
.status
627 result
['action']['tag'] = action
.data
['id']
628 result
['action']['results'] = action
.results
632 async def set_config(self
, application
, config
):
633 """Apply a configuration to the application."""
634 if not self
.authenticated
:
637 app
= await self
.get_application(self
.default_model
, application
)
639 self
.log
.debug("JujuApi: Setting config for Application {}".format(
642 await app
.set_config(config
)
644 # Verify the config is set
645 newconf
= await app
.get_config()
647 if config
[key
] != newconf
[key
]['value']:
648 self
.log
.debug("JujuApi: Config not set! Key {} Value {} doesn't match {}".format(key
, config
[key
], newconf
[key
]))
650 async def set_parameter(self
, parameter
, value
, application
=None):
651 """Set a config parameter for a service."""
652 if not self
.authenticated
:
655 self
.log
.debug("JujuApi: Setting {}={} for Application {}".format(
660 return await self
.apply_config(
662 application
=application
,
665 async def wait_for_application(self
, name
, timeout
=300):
666 """Wait for an application to become active."""
667 if not self
.authenticated
:
670 app
= await self
.get_application(self
.default_model
, name
)
673 "JujuApi: Waiting {} seconds for Application {}".format(
679 await self
.default_model
.block_until(
681 unit
.agent_status
== 'idle'
682 and unit
.workload_status
683 in ['active', 'unknown'] for unit
in app
.units