1 |
|
# Copyright 2019 Canonical Ltd. |
2 |
|
# |
3 |
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
4 |
|
# you may not use this file except in compliance with the License. |
5 |
|
# You may obtain a copy of the License at |
6 |
|
# |
7 |
|
# http://www.apache.org/licenses/LICENSE-2.0 |
8 |
|
# |
9 |
|
# Unless required by applicable law or agreed to in writing, software |
10 |
|
# distributed under the License is distributed on an "AS IS" BASIS, |
11 |
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 |
|
# See the License for the specific language governing permissions and |
13 |
|
# limitations under the License. |
14 |
|
|
15 |
0 |
import asyncio |
16 |
0 |
import base64 |
17 |
0 |
import binascii |
18 |
0 |
import logging |
19 |
0 |
import os.path |
20 |
0 |
import re |
21 |
0 |
import shlex |
22 |
0 |
import ssl |
23 |
0 |
import subprocess |
24 |
|
|
25 |
0 |
from juju.client import client |
26 |
0 |
from juju.controller import Controller |
27 |
0 |
from juju.errors import JujuAPIError, JujuError |
28 |
0 |
from juju.model import ModelObserver |
29 |
|
|
30 |
0 |
import n2vc.exceptions |
31 |
0 |
from n2vc.provisioner import SSHProvisioner |
32 |
|
|
33 |
|
|
34 |
|
# import time |
35 |
|
# FIXME: this should load the juju inside or modules without having to |
36 |
|
# explicitly install it. Check why it's not working. |
37 |
|
# Load our subtree of the juju library |
38 |
|
# path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) |
39 |
|
# path = os.path.join(path, "modules/libjuju/") |
40 |
|
# if path not in sys.path: |
41 |
|
# sys.path.insert(1, path) |
42 |
|
# We might need this to connect to the websocket securely, but test and verify. |
43 |
0 |
try: |
44 |
0 |
ssl._create_default_https_context = ssl._create_unverified_context |
45 |
0 |
except AttributeError: |
46 |
|
# Legacy Python doesn't verify by default (see pep-0476) |
47 |
|
# https://www.python.org/dev/peps/pep-0476/ |
48 |
0 |
pass |
49 |
|
|
50 |
|
|
51 |
|
# Custom exceptions |
52 |
|
# Deprecated. Please use n2vc.exceptions namespace. |
53 |
0 |
class JujuCharmNotFound(Exception): |
54 |
|
"""The Charm can't be found or is not readable.""" |
55 |
|
|
56 |
|
|
57 |
0 |
class JujuApplicationExists(Exception): |
58 |
|
"""The Application already exists.""" |
59 |
|
|
60 |
|
|
61 |
0 |
class N2VCPrimitiveExecutionFailed(Exception): |
62 |
|
"""Something failed while attempting to execute a primitive.""" |
63 |
|
|
64 |
|
|
65 |
0 |
class NetworkServiceDoesNotExist(Exception): |
66 |
|
"""The Network Service being acted against does not exist.""" |
67 |
|
|
68 |
|
|
69 |
0 |
class PrimitiveDoesNotExist(Exception): |
70 |
|
"""The Primitive being executed does not exist.""" |
71 |
|
|
72 |
|
|
73 |
|
# Quiet the debug logging |
74 |
0 |
logging.getLogger("websockets.protocol").setLevel(logging.INFO) |
75 |
0 |
logging.getLogger("juju.client.connection").setLevel(logging.WARN) |
76 |
0 |
logging.getLogger("juju.model").setLevel(logging.WARN) |
77 |
0 |
logging.getLogger("juju.machine").setLevel(logging.WARN) |
78 |
|
|
79 |
|
|
80 |
0 |
class VCAMonitor(ModelObserver): |
81 |
|
"""Monitor state changes within the Juju Model.""" |
82 |
|
|
83 |
0 |
log = None |
84 |
|
|
85 |
0 |
def __init__(self, ns_name): |
86 |
0 |
self.log = logging.getLogger(__name__) |
87 |
|
|
88 |
0 |
self.ns_name = ns_name |
89 |
0 |
self.applications = {} |
90 |
|
|
91 |
0 |
def AddApplication(self, application_name, callback, *callback_args): |
92 |
0 |
if application_name not in self.applications: |
93 |
0 |
self.applications[application_name] = { |
94 |
|
"callback": callback, |
95 |
|
"callback_args": callback_args, |
96 |
|
} |
97 |
|
|
98 |
0 |
def RemoveApplication(self, application_name): |
99 |
0 |
if application_name in self.applications: |
100 |
0 |
del self.applications[application_name] |
101 |
|
|
102 |
0 |
async def on_change(self, delta, old, new, model): |
103 |
|
"""React to changes in the Juju model.""" |
104 |
|
|
105 |
0 |
if delta.entity == "unit": |
106 |
|
# Ignore change events from other applications |
107 |
0 |
if delta.data["application"] not in self.applications.keys(): |
108 |
0 |
return |
109 |
|
|
110 |
0 |
try: |
111 |
|
|
112 |
0 |
application_name = delta.data["application"] |
113 |
|
|
114 |
0 |
callback = self.applications[application_name]["callback"] |
115 |
0 |
callback_args = self.applications[application_name]["callback_args"] |
116 |
|
|
117 |
0 |
if old and new: |
118 |
|
# Fire off a callback with the application state |
119 |
0 |
if callback: |
120 |
0 |
callback( |
121 |
|
self.ns_name, |
122 |
|
delta.data["application"], |
123 |
|
new.workload_status, |
124 |
|
new.workload_status_message, |
125 |
|
*callback_args, |
126 |
|
) |
127 |
|
|
128 |
0 |
if old and not new: |
129 |
|
# This is a charm being removed |
130 |
0 |
if callback: |
131 |
0 |
callback( |
132 |
|
self.ns_name, |
133 |
|
delta.data["application"], |
134 |
|
"removed", |
135 |
|
"", |
136 |
|
*callback_args, |
137 |
|
) |
138 |
0 |
except Exception as e: |
139 |
0 |
self.log.debug("[1] notify_callback exception: {}".format(e)) |
140 |
|
|
141 |
0 |
elif delta.entity == "action": |
142 |
|
# TODO: Decide how we want to notify the user of actions |
143 |
|
|
144 |
|
# uuid = delta.data['id'] # The Action's unique id |
145 |
|
# msg = delta.data['message'] # The output of the action |
146 |
|
# |
147 |
|
# if delta.data['status'] == "pending": |
148 |
|
# # The action is queued |
149 |
|
# pass |
150 |
|
# elif delta.data['status'] == "completed"" |
151 |
|
# # The action was successful |
152 |
|
# pass |
153 |
|
# elif delta.data['status'] == "failed": |
154 |
|
# # The action failed. |
155 |
|
# pass |
156 |
|
|
157 |
0 |
pass |
158 |
|
|
159 |
|
|
160 |
|
######## |
161 |
|
# TODO |
162 |
|
# |
163 |
|
# Create unique models per network service |
164 |
|
# Document all public functions |
165 |
|
|
166 |
|
|
167 |
0 |
class N2VC: |
168 |
0 |
def __init__( |
169 |
|
self, |
170 |
|
log=None, |
171 |
|
server="127.0.0.1", |
172 |
|
port=17070, |
173 |
|
user="admin", |
174 |
|
secret=None, |
175 |
|
artifacts=None, |
176 |
|
loop=None, |
177 |
|
juju_public_key=None, |
178 |
|
ca_cert=None, |
179 |
|
api_proxy=None, |
180 |
|
): |
181 |
|
"""Initialize N2VC |
182 |
|
|
183 |
|
Initializes the N2VC object, allowing the caller to interoperate with the VCA. |
184 |
|
|
185 |
|
|
186 |
|
:param log obj: The logging object to log to |
187 |
|
:param server str: The IP Address or Hostname of the Juju controller |
188 |
|
:param port int: The port of the Juju Controller |
189 |
|
:param user str: The Juju username to authenticate with |
190 |
|
:param secret str: The Juju password to authenticate with |
191 |
|
:param artifacts str: The directory where charms required by a vnfd are |
192 |
|
stored. |
193 |
|
:param loop obj: The loop to use. |
194 |
|
:param juju_public_key str: The contents of the Juju public SSH key |
195 |
|
:param ca_cert str: The CA certificate to use to authenticate |
196 |
|
:param api_proxy str: The IP of the host machine |
197 |
|
|
198 |
|
:Example: |
199 |
|
client = n2vc.vnf.N2VC( |
200 |
|
log=log, |
201 |
|
server='10.1.1.28', |
202 |
|
port=17070, |
203 |
|
user='admin', |
204 |
|
secret='admin', |
205 |
|
artifacts='/app/storage/myvnf/charms', |
206 |
|
loop=loop, |
207 |
|
juju_public_key='<contents of the juju public key>', |
208 |
|
ca_cert='<contents of CA certificate>', |
209 |
|
api_proxy='192.168.1.155' |
210 |
|
) |
211 |
|
""" |
212 |
|
|
213 |
|
# Initialize instance-level variables |
214 |
0 |
self.api = None |
215 |
0 |
self.log = None |
216 |
0 |
self.controller = None |
217 |
0 |
self.connecting = False |
218 |
0 |
self.authenticated = False |
219 |
0 |
self.api_proxy = api_proxy |
220 |
|
|
221 |
0 |
if log: |
222 |
0 |
self.log = log |
223 |
|
else: |
224 |
0 |
self.log = logging.getLogger(__name__) |
225 |
|
|
226 |
|
# For debugging |
227 |
0 |
self.refcount = { |
228 |
|
"controller": 0, |
229 |
|
"model": 0, |
230 |
|
} |
231 |
|
|
232 |
0 |
self.models = {} |
233 |
|
|
234 |
|
# Model Observers |
235 |
0 |
self.monitors = {} |
236 |
|
|
237 |
|
# VCA config |
238 |
0 |
self.hostname = "" |
239 |
0 |
self.port = 17070 |
240 |
0 |
self.username = "" |
241 |
0 |
self.secret = "" |
242 |
|
|
243 |
0 |
self.juju_public_key = juju_public_key |
244 |
0 |
if juju_public_key: |
245 |
0 |
self._create_juju_public_key(juju_public_key) |
246 |
|
else: |
247 |
0 |
self.juju_public_key = "" |
248 |
|
|
249 |
|
# TODO: Verify ca_cert is valid before using. VCA will crash |
250 |
|
# if the ca_cert isn't formatted correctly. |
251 |
0 |
def base64_to_cacert(b64string): |
252 |
|
"""Convert the base64-encoded string containing the VCA CACERT. |
253 |
|
|
254 |
|
The input string.... |
255 |
|
|
256 |
|
""" |
257 |
0 |
try: |
258 |
0 |
cacert = base64.b64decode(b64string).decode("utf-8") |
259 |
|
|
260 |
0 |
cacert = re.sub(r"\\n", r"\n", cacert,) |
261 |
0 |
except binascii.Error as e: |
262 |
0 |
self.log.debug("Caught binascii.Error: {}".format(e)) |
263 |
0 |
raise n2vc.exceptions.N2VCInvalidCertificate("Invalid CA Certificate") |
264 |
|
|
265 |
0 |
return cacert |
266 |
|
|
267 |
0 |
self.ca_cert = None |
268 |
0 |
if ca_cert: |
269 |
0 |
self.ca_cert = base64_to_cacert(ca_cert) |
270 |
|
|
271 |
|
# Quiet websocket traffic |
272 |
0 |
logging.getLogger("websockets.protocol").setLevel(logging.INFO) |
273 |
0 |
logging.getLogger("juju.client.connection").setLevel(logging.WARN) |
274 |
0 |
logging.getLogger("model").setLevel(logging.WARN) |
275 |
|
# logging.getLogger('websockets.protocol').setLevel(logging.DEBUG) |
276 |
|
|
277 |
0 |
self.log.debug("JujuApi: instantiated") |
278 |
|
|
279 |
0 |
self.server = server |
280 |
0 |
self.port = port |
281 |
|
|
282 |
0 |
self.secret = secret |
283 |
0 |
if user.startswith("user-"): |
284 |
0 |
self.user = user |
285 |
|
else: |
286 |
0 |
self.user = "user-{}".format(user) |
287 |
|
|
288 |
0 |
self.endpoint = "%s:%d" % (server, int(port)) |
289 |
|
|
290 |
0 |
self.artifacts = artifacts |
291 |
|
|
292 |
0 |
self.loop = loop or asyncio.get_event_loop() |
293 |
|
|
294 |
0 |
def __del__(self): |
295 |
|
"""Close any open connections.""" |
296 |
0 |
yield self.logout() |
297 |
|
|
298 |
0 |
def _create_juju_public_key(self, public_key): |
299 |
|
"""Recreate the Juju public key on disk. |
300 |
|
|
301 |
|
Certain libjuju commands expect to be run from the same machine as Juju |
302 |
|
is bootstrapped to. This method will write the public key to disk in |
303 |
|
that location: ~/.local/share/juju/ssh/juju_id_rsa.pub |
304 |
|
""" |
305 |
|
# Make sure that we have a public key before writing to disk |
306 |
0 |
if public_key is None or len(public_key) == 0: |
307 |
0 |
if "OSM_VCA_PUBKEY" in os.environ: |
308 |
0 |
public_key = os.getenv("OSM_VCA_PUBKEY", "") |
309 |
0 |
if len(public_key == 0): |
310 |
0 |
return |
311 |
|
else: |
312 |
0 |
return |
313 |
|
|
314 |
0 |
path = "{}/.local/share/juju/ssh".format(os.path.expanduser("~"),) |
315 |
0 |
if not os.path.exists(path): |
316 |
0 |
os.makedirs(path) |
317 |
|
|
318 |
0 |
with open("{}/juju_id_rsa.pub".format(path), "w") as f: |
319 |
0 |
f.write(public_key) |
320 |
|
|
321 |
0 |
def notify_callback( |
322 |
|
self, |
323 |
|
model_name, |
324 |
|
application_name, |
325 |
|
status, |
326 |
|
message, |
327 |
|
callback=None, |
328 |
|
*callback_args |
329 |
|
): |
330 |
0 |
try: |
331 |
0 |
if callback: |
332 |
0 |
callback( |
333 |
|
model_name, application_name, status, message, *callback_args, |
334 |
|
) |
335 |
0 |
except Exception as e: |
336 |
0 |
self.log.error("[0] notify_callback exception {}".format(e)) |
337 |
0 |
raise e |
338 |
0 |
return True |
339 |
|
|
340 |
|
# Public methods |
341 |
0 |
async def Relate(self, model_name, vnfd): |
342 |
|
"""Create a relation between the charm-enabled VDUs in a VNF. |
343 |
|
|
344 |
|
The Relation mapping has two parts: the id of the vdu owning the endpoint, and |
345 |
|
the name of the endpoint. |
346 |
|
|
347 |
|
vdu: |
348 |
|
... |
349 |
|
vca-relationships: |
350 |
|
relation: |
351 |
|
- provides: dataVM:db |
352 |
|
requires: mgmtVM:app |
353 |
|
|
354 |
|
This tells N2VC that the charm referred to by the dataVM vdu offers a relation |
355 |
|
named 'db', and the mgmtVM vdu |
356 |
|
has an 'app' endpoint that should be connected to a database. |
357 |
|
|
358 |
|
:param str ns_name: The name of the network service. |
359 |
|
:param dict vnfd: The parsed yaml VNF descriptor. |
360 |
|
""" |
361 |
|
|
362 |
|
# Currently, the call to Relate() is made automatically after the |
363 |
|
# deployment of each charm; if the relation depends on a charm that |
364 |
|
# hasn't been deployed yet, the call will fail silently. This will |
365 |
|
# prevent an API breakage, with the intent of making this an explicitly |
366 |
|
# required call in a more object-oriented refactor of the N2VC API. |
367 |
|
|
368 |
0 |
configs = [] |
369 |
0 |
vnf_config = vnfd.get("vnf-configuration") |
370 |
0 |
if vnf_config: |
371 |
0 |
juju = vnf_config["juju"] |
372 |
0 |
if juju: |
373 |
0 |
configs.append(vnf_config) |
374 |
|
|
375 |
0 |
for vdu in vnfd["vdu"]: |
376 |
0 |
vdu_config = vdu.get("vdu-configuration") |
377 |
0 |
if vdu_config: |
378 |
0 |
juju = vdu_config["juju"] |
379 |
0 |
if juju: |
380 |
0 |
configs.append(vdu_config) |
381 |
|
|
382 |
0 |
def _get_application_name(name): |
383 |
|
"""Get the application name that's mapped to a vnf/vdu.""" |
384 |
0 |
vnf_member_index = 0 |
385 |
0 |
vnf_name = vnfd["name"] |
386 |
|
|
387 |
0 |
for vdu in vnfd.get("vdu"): |
388 |
|
# Compare the named portion of the relation to the vdu's id |
389 |
0 |
if vdu["id"] == name: |
390 |
0 |
application_name = self.FormatApplicationName( |
391 |
|
model_name, vnf_name, str(vnf_member_index), |
392 |
|
) |
393 |
0 |
return application_name |
394 |
|
else: |
395 |
0 |
vnf_member_index += 1 |
396 |
|
|
397 |
0 |
return None |
398 |
|
|
399 |
|
# Loop through relations |
400 |
0 |
for cfg in configs: |
401 |
0 |
if "juju" in cfg: |
402 |
0 |
juju = cfg["juju"] |
403 |
0 |
if ( |
404 |
|
"vca-relationships" in juju |
405 |
|
and "relation" in juju["vca-relationships"] |
406 |
|
): |
407 |
0 |
for rel in juju["vca-relationships"]["relation"]: |
408 |
0 |
try: |
409 |
|
|
410 |
|
# get the application name for the provides |
411 |
0 |
(name, endpoint) = rel["provides"].split(":") |
412 |
0 |
application_name = _get_application_name(name) |
413 |
|
|
414 |
0 |
provides = "{}:{}".format(application_name, endpoint) |
415 |
|
|
416 |
|
# get the application name for thr requires |
417 |
0 |
(name, endpoint) = rel["requires"].split(":") |
418 |
0 |
application_name = _get_application_name(name) |
419 |
|
|
420 |
0 |
requires = "{}:{}".format(application_name, endpoint) |
421 |
0 |
self.log.debug( |
422 |
|
"Relation: {} <-> {}".format(provides, requires) |
423 |
|
) |
424 |
0 |
await self.add_relation( |
425 |
|
model_name, provides, requires, |
426 |
|
) |
427 |
0 |
except Exception as e: |
428 |
0 |
self.log.debug("Exception: {}".format(e)) |
429 |
|
|
430 |
0 |
return |
431 |
|
|
432 |
0 |
async def DeployCharms( |
433 |
|
self, |
434 |
|
model_name, |
435 |
|
application_name, |
436 |
|
vnfd, |
437 |
|
charm_path, |
438 |
|
params={}, |
439 |
|
machine_spec={}, |
440 |
|
callback=None, |
441 |
|
*callback_args |
442 |
|
): |
443 |
|
"""Deploy one or more charms associated with a VNF. |
444 |
|
|
445 |
|
Deploy the charm(s) referenced in a VNF Descriptor. |
446 |
|
|
447 |
|
:param str model_name: The name or unique id of the network service. |
448 |
|
:param str application_name: The name of the application |
449 |
|
:param dict vnfd: The name of the application |
450 |
|
:param str charm_path: The path to the Juju charm |
451 |
|
:param dict params: A dictionary of runtime parameters |
452 |
|
Examples:: |
453 |
|
{ |
454 |
|
'rw_mgmt_ip': '1.2.3.4', |
455 |
|
# Pass the initial-config-primitives section of the vnf or vdu |
456 |
|
'initial-config-primitives': {...} |
457 |
|
'user_values': dictionary with the day-1 parameters provided at |
458 |
|
instantiation time. It will replace values |
459 |
|
inside < >. rw_mgmt_ip will be included here also |
460 |
|
} |
461 |
|
:param dict machine_spec: A dictionary describing the machine to |
462 |
|
install to |
463 |
|
Examples:: |
464 |
|
{ |
465 |
|
'hostname': '1.2.3.4', |
466 |
|
'username': 'ubuntu', |
467 |
|
} |
468 |
|
:param obj callback: A callback function to receive status changes. |
469 |
|
:param tuple callback_args: A list of arguments to be passed to the |
470 |
|
callback |
471 |
|
""" |
472 |
|
|
473 |
|
######################################################## |
474 |
|
# Verify the path to the charm exists and is readable. # |
475 |
|
######################################################## |
476 |
0 |
if not os.path.exists(charm_path): |
477 |
0 |
self.log.debug("Charm path doesn't exist: {}".format(charm_path)) |
478 |
0 |
self.notify_callback( |
479 |
|
model_name, |
480 |
|
application_name, |
481 |
|
"error", |
482 |
|
"failed", |
483 |
|
callback, |
484 |
|
*callback_args, |
485 |
|
) |
486 |
0 |
raise JujuCharmNotFound("No artifacts configured.") |
487 |
|
|
488 |
|
################################ |
489 |
|
# Login to the Juju controller # |
490 |
|
################################ |
491 |
0 |
if not self.authenticated: |
492 |
0 |
self.log.debug("Authenticating with Juju") |
493 |
0 |
await self.login() |
494 |
|
|
495 |
|
########################################## |
496 |
|
# Get the model for this network service # |
497 |
|
########################################## |
498 |
0 |
model = await self.get_model(model_name) |
499 |
|
|
500 |
|
######################################## |
501 |
|
# Verify the application doesn't exist # |
502 |
|
######################################## |
503 |
0 |
app = await self.get_application(model, application_name) |
504 |
0 |
if app: |
505 |
0 |
raise JujuApplicationExists( |
506 |
|
( |
507 |
|
'Can\'t deploy application "{}" to model ' |
508 |
|
' "{}" because it already exists.' |
509 |
|
).format(application_name, model_name) |
510 |
|
) |
511 |
|
|
512 |
|
################################################################ |
513 |
|
# Register this application with the model-level event monitor # |
514 |
|
################################################################ |
515 |
0 |
if callback: |
516 |
0 |
self.log.debug( |
517 |
|
"JujuApi: Registering callback for {}".format(application_name,) |
518 |
|
) |
519 |
0 |
await self.Subscribe(model_name, application_name, callback, *callback_args) |
520 |
|
|
521 |
|
####################################### |
522 |
|
# Get the initial charm configuration # |
523 |
|
####################################### |
524 |
|
|
525 |
0 |
rw_mgmt_ip = None |
526 |
0 |
if "rw_mgmt_ip" in params: |
527 |
0 |
rw_mgmt_ip = params["rw_mgmt_ip"] |
528 |
|
|
529 |
0 |
if "initial-config-primitive" not in params: |
530 |
0 |
params["initial-config-primitive"] = {} |
531 |
|
|
532 |
0 |
initial_config = self._get_config_from_dict( |
533 |
|
params["initial-config-primitive"], {"<rw_mgmt_ip>": rw_mgmt_ip} |
534 |
|
) |
535 |
|
|
536 |
|
######################################################## |
537 |
|
# Check for specific machine placement (native charms) # |
538 |
|
######################################################## |
539 |
0 |
to = "" |
540 |
0 |
series = "xenial" |
541 |
|
|
542 |
0 |
if machine_spec.keys(): |
543 |
0 |
if all(k in machine_spec for k in ["hostname", "username"]): |
544 |
|
|
545 |
|
# Allow series to be derived from the native charm |
546 |
0 |
series = None |
547 |
|
|
548 |
0 |
self.log.debug( |
549 |
|
"Provisioning manual machine {}@{}".format( |
550 |
|
machine_spec["username"], machine_spec["hostname"], |
551 |
|
) |
552 |
|
) |
553 |
|
|
554 |
|
"""Native Charm support |
555 |
|
|
556 |
|
Taking a bare VM (assumed to be an Ubuntu cloud image), |
557 |
|
the provisioning process will: |
558 |
|
- Create an ubuntu user w/sudo access |
559 |
|
- Detect hardware |
560 |
|
- Detect architecture |
561 |
|
- Download and install Juju agent from controller |
562 |
|
- Enable Juju agent |
563 |
|
- Add an iptables rule to route traffic to the API proxy |
564 |
|
""" |
565 |
|
|
566 |
0 |
to = await self.provision_machine( |
567 |
|
model_name=model_name, |
568 |
|
username=machine_spec["username"], |
569 |
|
hostname=machine_spec["hostname"], |
570 |
|
private_key_path=self.GetPrivateKeyPath(), |
571 |
|
) |
572 |
0 |
self.log.debug("Provisioned machine id {}".format(to)) |
573 |
|
|
574 |
|
# TODO: If to is none, raise an exception |
575 |
|
|
576 |
|
# The native charm won't have the sshproxy layer, typically, but LCM |
577 |
|
# uses the config primitive |
578 |
|
# to interpret what the values are. That's a gap to fill. |
579 |
|
|
580 |
|
""" |
581 |
|
The ssh-* config parameters are unique to the sshproxy layer, |
582 |
|
which most native charms will not be aware of. |
583 |
|
|
584 |
|
Setting invalid config parameters will cause the deployment to |
585 |
|
fail. |
586 |
|
|
587 |
|
For the moment, we will strip the ssh-* parameters from native |
588 |
|
charms, until the feature gap is addressed in the information |
589 |
|
model. |
590 |
|
""" |
591 |
|
|
592 |
|
# Native charms don't include the ssh-* config values, so strip them |
593 |
|
# from the initial_config, otherwise the deploy will raise an error. |
594 |
|
# self.log.debug("Removing ssh-* from initial-config") |
595 |
0 |
for k in ["ssh-hostname", "ssh-username", "ssh-password"]: |
596 |
0 |
if k in initial_config: |
597 |
0 |
self.log.debug("Removing parameter {}".format(k)) |
598 |
0 |
del initial_config[k] |
599 |
|
|
600 |
0 |
self.log.debug( |
601 |
|
"JujuApi: Deploying charm ({}/{}) from {} to {}".format( |
602 |
|
model_name, application_name, charm_path, to, |
603 |
|
) |
604 |
|
) |
605 |
|
|
606 |
|
######################################################## |
607 |
|
# Deploy the charm and apply the initial configuration # |
608 |
|
######################################################## |
609 |
0 |
app = await model.deploy( |
610 |
|
# We expect charm_path to be either the path to the charm on disk |
611 |
|
# or in the format of cs:series/name |
612 |
|
charm_path, |
613 |
|
# This is the formatted, unique name for this charm |
614 |
|
application_name=application_name, |
615 |
|
# Proxy charms should use the current LTS. This will need to be |
616 |
|
# changed for native charms. |
617 |
|
series=series, |
618 |
|
# Apply the initial 'config' primitive during deployment |
619 |
|
config=initial_config, |
620 |
|
# Where to deploy the charm to. |
621 |
|
to=to, |
622 |
|
) |
623 |
|
|
624 |
|
############################# |
625 |
|
# Map the vdu id<->app name # |
626 |
|
############################# |
627 |
0 |
try: |
628 |
0 |
await self.Relate(model_name, vnfd) |
629 |
0 |
except KeyError as ex: |
630 |
|
# We don't currently support relations between NS and VNF/VDU charms |
631 |
0 |
self.log.warn("[N2VC] Relations not supported: {}".format(ex)) |
632 |
0 |
except Exception: |
633 |
|
# This may happen if not all of the charms needed by the relation |
634 |
|
# are ready. We can safely ignore this, because Relate will be |
635 |
|
# retried when the endpoint of the relation is deployed. |
636 |
0 |
self.log.warn("[N2VC] Relations not ready") |
637 |
|
|
638 |
|
# ####################################### |
639 |
|
# # Execute initial config primitive(s) # |
640 |
|
# ####################################### |
641 |
0 |
uuids = await self.ExecuteInitialPrimitives( |
642 |
|
model_name, application_name, params, |
643 |
|
) |
644 |
0 |
return uuids |
645 |
|
|
646 |
|
# primitives = {} |
647 |
|
# |
648 |
|
# # Build a sequential list of the primitives to execute |
649 |
|
# for primitive in params['initial-config-primitive']: |
650 |
|
# try: |
651 |
|
# if primitive['name'] == 'config': |
652 |
|
# # This is applied when the Application is deployed |
653 |
|
# pass |
654 |
|
# else: |
655 |
|
# seq = primitive['seq'] |
656 |
|
# |
657 |
|
# params = {} |
658 |
|
# if 'parameter' in primitive: |
659 |
|
# params = primitive['parameter'] |
660 |
|
# |
661 |
|
# primitives[seq] = { |
662 |
|
# 'name': primitive['name'], |
663 |
|
# 'parameters': self._map_primitive_parameters( |
664 |
|
# params, |
665 |
|
# {'<rw_mgmt_ip>': rw_mgmt_ip} |
666 |
|
# ), |
667 |
|
# } |
668 |
|
# |
669 |
|
# for primitive in sorted(primitives): |
670 |
|
# await self.ExecutePrimitive( |
671 |
|
# model_name, |
672 |
|
# application_name, |
673 |
|
# primitives[primitive]['name'], |
674 |
|
# callback, |
675 |
|
# callback_args, |
676 |
|
# **primitives[primitive]['parameters'], |
677 |
|
# ) |
678 |
|
# except N2VCPrimitiveExecutionFailed as e: |
679 |
|
# self.log.debug( |
680 |
|
# "[N2VC] Exception executing primitive: {}".format(e) |
681 |
|
# ) |
682 |
|
# raise |
683 |
|
|
684 |
0 |
async def GetPrimitiveStatus(self, model_name, uuid): |
685 |
|
"""Get the status of an executed Primitive. |
686 |
|
|
687 |
|
The status of an executed Primitive will be one of three values: |
688 |
|
- completed |
689 |
|
- failed |
690 |
|
- running |
691 |
|
""" |
692 |
0 |
status = None |
693 |
0 |
try: |
694 |
0 |
if not self.authenticated: |
695 |
0 |
await self.login() |
696 |
|
|
697 |
0 |
model = await self.get_model(model_name) |
698 |
|
|
699 |
0 |
results = await model.get_action_status(uuid) |
700 |
|
|
701 |
0 |
if uuid in results: |
702 |
0 |
status = results[uuid] |
703 |
|
|
704 |
0 |
except Exception as e: |
705 |
0 |
self.log.debug( |
706 |
|
"Caught exception while getting primitive status: {}".format(e) |
707 |
|
) |
708 |
0 |
raise N2VCPrimitiveExecutionFailed(e) |
709 |
|
|
710 |
0 |
return status |
711 |
|
|
712 |
0 |
async def GetPrimitiveOutput(self, model_name, uuid): |
713 |
|
"""Get the output of an executed Primitive. |
714 |
|
|
715 |
|
Note: this only returns output for a successfully executed primitive. |
716 |
|
""" |
717 |
0 |
results = None |
718 |
0 |
try: |
719 |
0 |
if not self.authenticated: |
720 |
0 |
await self.login() |
721 |
|
|
722 |
0 |
model = await self.get_model(model_name) |
723 |
0 |
results = await model.get_action_output(uuid, 60) |
724 |
0 |
except Exception as e: |
725 |
0 |
self.log.debug( |
726 |
|
"Caught exception while getting primitive status: {}".format(e) |
727 |
|
) |
728 |
0 |
raise N2VCPrimitiveExecutionFailed(e) |
729 |
|
|
730 |
0 |
return results |
731 |
|
|
732 |
|
# async def ProvisionMachine(self, model_name, hostname, username): |
733 |
|
# """Provision machine for usage with Juju. |
734 |
|
# |
735 |
|
# Provisions a previously instantiated machine for use with Juju. |
736 |
|
# """ |
737 |
|
# try: |
738 |
|
# if not self.authenticated: |
739 |
|
# await self.login() |
740 |
|
# |
741 |
|
# # FIXME: This is hard-coded until model-per-ns is added |
742 |
|
# model_name = 'default' |
743 |
|
# |
744 |
|
# model = await self.get_model(model_name) |
745 |
|
# model.add_machine(spec={}) |
746 |
|
# |
747 |
|
# machine = await model.add_machine(spec='ssh:{}@{}:{}'.format( |
748 |
|
# "ubuntu", |
749 |
|
# host['address'], |
750 |
|
# private_key_path, |
751 |
|
# )) |
752 |
|
# return machine.id |
753 |
|
# |
754 |
|
# except Exception as e: |
755 |
|
# self.log.debug( |
756 |
|
# "Caught exception while getting primitive status: {}".format(e) |
757 |
|
# ) |
758 |
|
# raise N2VCPrimitiveExecutionFailed(e) |
759 |
|
|
760 |
0 |
def GetPrivateKeyPath(self): |
761 |
0 |
homedir = os.environ["HOME"] |
762 |
0 |
sshdir = "{}/.ssh".format(homedir) |
763 |
0 |
private_key_path = "{}/id_n2vc_rsa".format(sshdir) |
764 |
0 |
return private_key_path |
765 |
|
|
766 |
0 |
async def GetPublicKey(self): |
767 |
|
"""Get the N2VC SSH public key.abs |
768 |
|
|
769 |
|
Returns the SSH public key, to be injected into virtual machines to |
770 |
|
be managed by the VCA. |
771 |
|
|
772 |
|
The first time this is run, a ssh keypair will be created. The public |
773 |
|
key is injected into a VM so that we can provision the machine with |
774 |
|
Juju, after which Juju will communicate with the VM directly via the |
775 |
|
juju agent. |
776 |
|
""" |
777 |
|
# public_key = "" |
778 |
|
|
779 |
|
# Find the path to where we expect our key to live. |
780 |
0 |
homedir = os.environ["HOME"] |
781 |
0 |
sshdir = "{}/.ssh".format(homedir) |
782 |
0 |
if not os.path.exists(sshdir): |
783 |
0 |
os.mkdir(sshdir) |
784 |
|
|
785 |
0 |
private_key_path = "{}/id_n2vc_rsa".format(sshdir) |
786 |
0 |
public_key_path = "{}.pub".format(private_key_path) |
787 |
|
|
788 |
|
# If we don't have a key generated, generate it. |
789 |
0 |
if not os.path.exists(private_key_path): |
790 |
0 |
cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format( |
791 |
|
"rsa", "4096", private_key_path |
792 |
|
) |
793 |
0 |
subprocess.check_output(shlex.split(cmd)) |
794 |
|
|
795 |
|
# Read the public key |
796 |
0 |
with open(public_key_path, "r") as f: |
797 |
0 |
public_key = f.readline() |
798 |
|
|
799 |
0 |
return public_key |
800 |
|
|
801 |
0 |
async def ExecuteInitialPrimitives( |
802 |
|
self, model_name, application_name, params, callback=None, *callback_args |
803 |
|
): |
804 |
|
"""Execute multiple primitives. |
805 |
|
|
806 |
|
Execute multiple primitives as declared in initial-config-primitive. |
807 |
|
This is useful in cases where the primitives initially failed -- for |
808 |
|
example, if the charm is a proxy but the proxy hasn't been configured |
809 |
|
yet. |
810 |
|
""" |
811 |
0 |
uuids = [] |
812 |
0 |
primitives = {} |
813 |
|
|
814 |
|
# Build a sequential list of the primitives to execute |
815 |
0 |
for primitive in params["initial-config-primitive"]: |
816 |
0 |
try: |
817 |
0 |
if primitive["name"] == "config": |
818 |
0 |
pass |
819 |
|
else: |
820 |
0 |
seq = primitive["seq"] |
821 |
|
|
822 |
0 |
params_ = {} |
823 |
0 |
if "parameter" in primitive: |
824 |
0 |
params_ = primitive["parameter"] |
825 |
|
|
826 |
0 |
user_values = params.get("user_values", {}) |
827 |
0 |
if "rw_mgmt_ip" not in user_values: |
828 |
0 |
user_values["rw_mgmt_ip"] = None |
829 |
|
# just for backward compatibility, because it will be provided |
830 |
|
# always by modern version of LCM |
831 |
|
|
832 |
0 |
primitives[seq] = { |
833 |
|
"name": primitive["name"], |
834 |
|
"parameters": self._map_primitive_parameters( |
835 |
|
params_, user_values |
836 |
|
), |
837 |
|
} |
838 |
|
|
839 |
0 |
for primitive in sorted(primitives): |
840 |
0 |
try: |
841 |
|
# self.log.debug("Queuing action {}".format( |
842 |
|
# primitives[primitive]['name'])) |
843 |
0 |
uuids.append( |
844 |
|
await self.ExecutePrimitive( |
845 |
|
model_name, |
846 |
|
application_name, |
847 |
|
primitives[primitive]["name"], |
848 |
|
callback, |
849 |
|
callback_args, |
850 |
|
**primitives[primitive]["parameters"], |
851 |
|
) |
852 |
|
) |
853 |
0 |
except PrimitiveDoesNotExist as e: |
854 |
0 |
self.log.debug( |
855 |
|
"Ignoring exception PrimitiveDoesNotExist: {}".format(e) |
856 |
|
) |
857 |
0 |
pass |
858 |
0 |
except Exception as e: |
859 |
0 |
self.log.debug( |
860 |
|
( |
861 |
|
"XXXXXXXXXXXXXXXXXXXXXXXXX Unexpected exception: {}" |
862 |
|
).format(e) |
863 |
|
) |
864 |
0 |
raise e |
865 |
|
|
866 |
0 |
except N2VCPrimitiveExecutionFailed as e: |
867 |
0 |
self.log.debug("[N2VC] Exception executing primitive: {}".format(e)) |
868 |
0 |
raise |
869 |
0 |
return uuids |
870 |
|
|
871 |
0 |
async def ExecutePrimitive( |
872 |
|
self, |
873 |
|
model_name, |
874 |
|
application_name, |
875 |
|
primitive, |
876 |
|
callback, |
877 |
|
*callback_args, |
878 |
|
**params |
879 |
|
): |
880 |
|
"""Execute a primitive of a charm for Day 1 or Day 2 configuration. |
881 |
|
|
882 |
|
Execute a primitive defined in the VNF descriptor. |
883 |
|
|
884 |
|
:param str model_name: The name or unique id of the network service. |
885 |
|
:param str application_name: The name of the application |
886 |
|
:param str primitive: The name of the primitive to execute. |
887 |
|
:param obj callback: A callback function to receive status changes. |
888 |
|
:param tuple callback_args: A list of arguments to be passed to the |
889 |
|
callback function. |
890 |
|
:param dict params: A dictionary of key=value pairs representing the |
891 |
|
primitive's parameters |
892 |
|
Examples:: |
893 |
|
{ |
894 |
|
'rw_mgmt_ip': '1.2.3.4', |
895 |
|
# Pass the initial-config-primitives section of the vnf or vdu |
896 |
|
'initial-config-primitives': {...} |
897 |
|
} |
898 |
|
""" |
899 |
0 |
self.log.debug("Executing primitive={} params={}".format(primitive, params)) |
900 |
0 |
uuid = None |
901 |
0 |
try: |
902 |
0 |
if not self.authenticated: |
903 |
0 |
await self.login() |
904 |
|
|
905 |
0 |
model = await self.get_model(model_name) |
906 |
|
|
907 |
0 |
if primitive == "config": |
908 |
|
# config is special, and expecting params to be a dictionary |
909 |
0 |
await self.set_config( |
910 |
|
model, application_name, params["params"], |
911 |
|
) |
912 |
|
else: |
913 |
0 |
app = await self.get_application(model, application_name) |
914 |
0 |
if app: |
915 |
|
# Does this primitive exist? |
916 |
0 |
actions = await app.get_actions() |
917 |
|
|
918 |
0 |
if primitive not in actions.keys(): |
919 |
0 |
raise PrimitiveDoesNotExist( |
920 |
|
"Primitive {} does not exist".format(primitive) |
921 |
|
) |
922 |
|
|
923 |
|
# Run against the first (and probably only) unit in the app |
924 |
0 |
unit = app.units[0] |
925 |
0 |
if unit: |
926 |
0 |
action = await unit.run_action(primitive, **params) |
927 |
0 |
uuid = action.id |
928 |
0 |
except PrimitiveDoesNotExist as e: |
929 |
|
# Catch and raise this exception if it's thrown from the inner block |
930 |
0 |
raise e |
931 |
0 |
except Exception as e: |
932 |
|
# An unexpected exception was caught |
933 |
0 |
self.log.debug("Caught exception while executing primitive: {}".format(e)) |
934 |
0 |
raise N2VCPrimitiveExecutionFailed(e) |
935 |
0 |
return uuid |
936 |
|
|
937 |
0 |
async def RemoveCharms( |
938 |
|
self, model_name, application_name, callback=None, *callback_args |
939 |
|
): |
940 |
|
"""Remove a charm from the VCA. |
941 |
|
|
942 |
|
Remove a charm referenced in a VNF Descriptor. |
943 |
|
|
944 |
|
:param str model_name: The name of the network service. |
945 |
|
:param str application_name: The name of the application |
946 |
|
:param obj callback: A callback function to receive status changes. |
947 |
|
:param tuple callback_args: A list of arguments to be passed to the |
948 |
|
callback function. |
949 |
|
""" |
950 |
0 |
try: |
951 |
0 |
if not self.authenticated: |
952 |
0 |
await self.login() |
953 |
|
|
954 |
0 |
model = await self.get_model(model_name) |
955 |
0 |
app = await self.get_application(model, application_name) |
956 |
0 |
if app: |
957 |
|
# Remove this application from event monitoring |
958 |
0 |
await self.Unsubscribe(model_name, application_name) |
959 |
|
|
960 |
|
# self.notify_callback(model_name, application_name, "removing", |
961 |
|
# callback, *callback_args) |
962 |
0 |
self.log.debug("Removing the application {}".format(application_name)) |
963 |
0 |
await app.remove() |
964 |
|
|
965 |
|
# await self.disconnect_model(self.monitors[model_name]) |
966 |
|
|
967 |
0 |
self.notify_callback( |
968 |
|
model_name, |
969 |
|
application_name, |
970 |
|
"removed", |
971 |
|
"Removing charm {}".format(application_name), |
972 |
|
callback, |
973 |
|
*callback_args, |
974 |
|
) |
975 |
|
|
976 |
0 |
except Exception as e: |
977 |
0 |
print("Caught exception: {}".format(e)) |
978 |
0 |
self.log.debug(e) |
979 |
0 |
raise e |
980 |
|
|
981 |
0 |
async def CreateNetworkService(self, ns_uuid): |
982 |
|
"""Create a new Juju model for the Network Service. |
983 |
|
|
984 |
|
Creates a new Model in the Juju Controller. |
985 |
|
|
986 |
|
:param str ns_uuid: A unique id representing an instaance of a |
987 |
|
Network Service. |
988 |
|
|
989 |
|
:returns: True if the model was created. Raises JujuError on failure. |
990 |
|
""" |
991 |
0 |
if not self.authenticated: |
992 |
0 |
await self.login() |
993 |
|
|
994 |
0 |
models = await self.controller.list_models() |
995 |
0 |
if ns_uuid not in models: |
996 |
|
# Get the new model |
997 |
0 |
await self.get_model(ns_uuid) |
998 |
|
|
999 |
0 |
return True |
1000 |
|
|
1001 |
0 |
async def DestroyNetworkService(self, ns_uuid): |
1002 |
|
"""Destroy a Network Service. |
1003 |
|
|
1004 |
|
Destroy the Network Service and any deployed charms. |
1005 |
|
|
1006 |
|
:param ns_uuid The unique id of the Network Service |
1007 |
|
|
1008 |
|
:returns: True if the model was created. Raises JujuError on failure. |
1009 |
|
""" |
1010 |
|
|
1011 |
|
# Do not delete the default model. The default model was used by all |
1012 |
|
# Network Services, prior to the implementation of a model per NS. |
1013 |
0 |
if ns_uuid.lower() == "default": |
1014 |
0 |
return False |
1015 |
|
|
1016 |
0 |
if not self.authenticated: |
1017 |
0 |
await self.login() |
1018 |
|
|
1019 |
0 |
models = await self.controller.list_models() |
1020 |
0 |
if ns_uuid in models: |
1021 |
0 |
model = await self.controller.get_model(ns_uuid) |
1022 |
|
|
1023 |
0 |
for application in model.applications: |
1024 |
0 |
app = model.applications[application] |
1025 |
|
|
1026 |
0 |
await self.RemoveCharms(ns_uuid, application) |
1027 |
|
|
1028 |
0 |
self.log.debug("Unsubscribing Watcher for {}".format(application)) |
1029 |
0 |
await self.Unsubscribe(ns_uuid, application) |
1030 |
|
|
1031 |
0 |
self.log.debug("Waiting for application to terminate") |
1032 |
0 |
timeout = 30 |
1033 |
0 |
try: |
1034 |
0 |
await model.block_until( |
1035 |
|
lambda: all( |
1036 |
|
unit.workload_status in ["terminated"] for unit in app.units |
1037 |
|
), |
1038 |
|
timeout=timeout, |
1039 |
|
) |
1040 |
0 |
except Exception: |
1041 |
0 |
self.log.debug( |
1042 |
|
"Timed out waiting for {} to terminate.".format(application) |
1043 |
|
) |
1044 |
|
|
1045 |
0 |
for machine in model.machines: |
1046 |
0 |
try: |
1047 |
0 |
self.log.debug("Destroying machine {}".format(machine)) |
1048 |
0 |
await model.machines[machine].destroy(force=True) |
1049 |
0 |
except JujuAPIError as e: |
1050 |
0 |
if "does not exist" in str(e): |
1051 |
|
# Our cached model may be stale, because the machine |
1052 |
|
# has already been removed. It's safe to continue. |
1053 |
0 |
continue |
1054 |
|
else: |
1055 |
0 |
self.log.debug("Caught exception: {}".format(e)) |
1056 |
0 |
raise e |
1057 |
|
|
1058 |
|
# Disconnect from the Model |
1059 |
0 |
if ns_uuid in self.models: |
1060 |
0 |
self.log.debug("Disconnecting model {}".format(ns_uuid)) |
1061 |
|
# await self.disconnect_model(self.models[ns_uuid]) |
1062 |
0 |
await self.disconnect_model(ns_uuid) |
1063 |
|
|
1064 |
0 |
try: |
1065 |
0 |
self.log.debug("Destroying model {}".format(ns_uuid)) |
1066 |
0 |
await self.controller.destroy_models(ns_uuid) |
1067 |
0 |
except JujuError: |
1068 |
0 |
raise NetworkServiceDoesNotExist( |
1069 |
|
"The Network Service '{}' does not exist".format(ns_uuid) |
1070 |
|
) |
1071 |
|
|
1072 |
0 |
return True |
1073 |
|
|
1074 |
0 |
async def GetMetrics(self, model_name, application_name): |
1075 |
|
"""Get the metrics collected by the VCA. |
1076 |
|
|
1077 |
|
:param model_name The name or unique id of the network service |
1078 |
|
:param application_name The name of the application |
1079 |
|
""" |
1080 |
0 |
metrics = {} |
1081 |
0 |
model = await self.get_model(model_name) |
1082 |
0 |
app = await self.get_application(model, application_name) |
1083 |
0 |
if app: |
1084 |
0 |
metrics = await app.get_metrics() |
1085 |
|
|
1086 |
0 |
return metrics |
1087 |
|
|
1088 |
0 |
async def HasApplication(self, model_name, application_name): |
1089 |
0 |
model = await self.get_model(model_name) |
1090 |
0 |
app = await self.get_application(model, application_name) |
1091 |
0 |
if app: |
1092 |
0 |
return True |
1093 |
0 |
return False |
1094 |
|
|
1095 |
0 |
async def Subscribe(self, ns_name, application_name, callback, *callback_args): |
1096 |
|
"""Subscribe to callbacks for an application. |
1097 |
|
|
1098 |
|
:param ns_name str: The name of the Network Service |
1099 |
|
:param application_name str: The name of the application |
1100 |
|
:param callback obj: The callback method |
1101 |
|
:param callback_args list: The list of arguments to append to calls to |
1102 |
|
the callback method |
1103 |
|
""" |
1104 |
0 |
self.monitors[ns_name].AddApplication( |
1105 |
|
application_name, callback, *callback_args |
1106 |
|
) |
1107 |
|
|
1108 |
0 |
async def Unsubscribe(self, ns_name, application_name): |
1109 |
|
"""Unsubscribe to callbacks for an application. |
1110 |
|
|
1111 |
|
Unsubscribes the caller from notifications from a deployed application. |
1112 |
|
|
1113 |
|
:param ns_name str: The name of the Network Service |
1114 |
|
:param application_name str: The name of the application |
1115 |
|
""" |
1116 |
0 |
self.monitors[ns_name].RemoveApplication(application_name,) |
1117 |
|
|
1118 |
|
# Non-public methods |
1119 |
0 |
async def add_relation(self, model_name, relation1, relation2): |
1120 |
|
""" |
1121 |
|
Add a relation between two application endpoints. |
1122 |
|
|
1123 |
|
:param str model_name: The name or unique id of the network service |
1124 |
|
:param str relation1: '<application>[:<relation_name>]' |
1125 |
|
:param str relation2: '<application>[:<relation_name>]' |
1126 |
|
""" |
1127 |
|
|
1128 |
0 |
if not self.authenticated: |
1129 |
0 |
await self.login() |
1130 |
|
|
1131 |
0 |
m = await self.get_model(model_name) |
1132 |
0 |
try: |
1133 |
0 |
await m.add_relation(relation1, relation2) |
1134 |
0 |
except JujuAPIError as e: |
1135 |
|
# If one of the applications in the relationship doesn't exist, |
1136 |
|
# or the relation has already been added, let the operation fail |
1137 |
|
# silently. |
1138 |
0 |
if "not found" in e.message: |
1139 |
0 |
return |
1140 |
0 |
if "already exists" in e.message: |
1141 |
0 |
return |
1142 |
|
|
1143 |
0 |
raise e |
1144 |
|
|
1145 |
|
# async def apply_config(self, config, application): |
1146 |
|
# """Apply a configuration to the application.""" |
1147 |
|
# print("JujuApi: Applying configuration to {}.".format( |
1148 |
|
# application |
1149 |
|
# )) |
1150 |
|
# return await self.set_config(application=application, config=config) |
1151 |
|
|
1152 |
0 |
def _get_config_from_dict(self, config_primitive, values): |
1153 |
|
"""Transform the yang config primitive to dict. |
1154 |
|
|
1155 |
|
Expected result: |
1156 |
|
|
1157 |
|
config = { |
1158 |
|
'config': |
1159 |
|
} |
1160 |
|
""" |
1161 |
0 |
config = {} |
1162 |
0 |
for primitive in config_primitive: |
1163 |
0 |
if primitive["name"] == "config": |
1164 |
|
# config = self._map_primitive_parameters() |
1165 |
0 |
for parameter in primitive["parameter"]: |
1166 |
0 |
param = str(parameter["name"]) |
1167 |
0 |
if parameter["value"] == "<rw_mgmt_ip>": |
1168 |
0 |
config[param] = str(values[parameter["value"]]) |
1169 |
|
else: |
1170 |
0 |
config[param] = str(parameter["value"]) |
1171 |
|
|
1172 |
0 |
return config |
1173 |
|
|
1174 |
0 |
def _map_primitive_parameters(self, parameters, user_values): |
1175 |
0 |
params = {} |
1176 |
0 |
for parameter in parameters: |
1177 |
0 |
param = str(parameter["name"]) |
1178 |
0 |
value = parameter.get("value") |
1179 |
|
|
1180 |
|
# map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user |
1181 |
|
# _values. |
1182 |
|
# Must exist at user_values except if there is a default value |
1183 |
0 |
if isinstance(value, str) and value.startswith("<") and value.endswith(">"): |
1184 |
0 |
if parameter["value"][1:-1] in user_values: |
1185 |
0 |
value = user_values[parameter["value"][1:-1]] |
1186 |
0 |
elif "default-value" in parameter: |
1187 |
0 |
value = parameter["default-value"] |
1188 |
|
else: |
1189 |
0 |
raise KeyError( |
1190 |
|
"parameter {}='{}' not supplied ".format(param, value) |
1191 |
|
) |
1192 |
|
|
1193 |
|
# If there's no value, use the default-value (if set) |
1194 |
0 |
if value is None and "default-value" in parameter: |
1195 |
0 |
value = parameter["default-value"] |
1196 |
|
|
1197 |
|
# Typecast parameter value, if present |
1198 |
0 |
paramtype = "string" |
1199 |
0 |
try: |
1200 |
0 |
if "data-type" in parameter: |
1201 |
0 |
paramtype = str(parameter["data-type"]).lower() |
1202 |
|
|
1203 |
0 |
if paramtype == "integer": |
1204 |
0 |
value = int(value) |
1205 |
0 |
elif paramtype == "boolean": |
1206 |
0 |
value = bool(value) |
1207 |
|
else: |
1208 |
0 |
value = str(value) |
1209 |
|
else: |
1210 |
|
# If there's no data-type, assume the value is a string |
1211 |
0 |
value = str(value) |
1212 |
0 |
except ValueError: |
1213 |
0 |
raise ValueError( |
1214 |
|
"parameter {}='{}' cannot be converted to type {}".format( |
1215 |
|
param, value, paramtype |
1216 |
|
) |
1217 |
|
) |
1218 |
|
|
1219 |
0 |
params[param] = value |
1220 |
0 |
return params |
1221 |
|
|
1222 |
0 |
def _get_config_from_yang(self, config_primitive, values): |
1223 |
|
"""Transform the yang config primitive to dict.""" |
1224 |
0 |
config = {} |
1225 |
0 |
for primitive in config_primitive.values(): |
1226 |
0 |
if primitive["name"] == "config": |
1227 |
0 |
for parameter in primitive["parameter"].values(): |
1228 |
0 |
param = str(parameter["name"]) |
1229 |
0 |
if parameter["value"] == "<rw_mgmt_ip>": |
1230 |
0 |
config[param] = str(values[parameter["value"]]) |
1231 |
|
else: |
1232 |
0 |
config[param] = str(parameter["value"]) |
1233 |
|
|
1234 |
0 |
return config |
1235 |
|
|
1236 |
0 |
def FormatApplicationName(self, *args): |
1237 |
|
""" |
1238 |
|
Generate a Juju-compatible Application name |
1239 |
|
|
1240 |
|
:param args tuple: Positional arguments to be used to construct the |
1241 |
|
application name. |
1242 |
|
|
1243 |
|
Limitations:: |
1244 |
|
- Only accepts characters a-z and non-consequitive dashes (-) |
1245 |
|
- Application name should not exceed 50 characters |
1246 |
|
|
1247 |
|
Examples:: |
1248 |
|
|
1249 |
|
FormatApplicationName("ping_pong_ns", "ping_vnf", "a") |
1250 |
|
""" |
1251 |
0 |
appname = "" |
1252 |
0 |
for c in "-".join(list(args)): |
1253 |
0 |
if c.isdigit(): |
1254 |
0 |
c = chr(97 + int(c)) |
1255 |
0 |
elif not c.isalpha(): |
1256 |
0 |
c = "-" |
1257 |
0 |
appname += c |
1258 |
0 |
return re.sub("-+", "-", appname.lower()) |
1259 |
|
|
1260 |
|
# def format_application_name(self, nsd_name, vnfr_name, member_vnf_index=0): |
1261 |
|
# """Format the name of the application |
1262 |
|
# |
1263 |
|
# Limitations: |
1264 |
|
# - Only accepts characters a-z and non-consequitive dashes (-) |
1265 |
|
# - Application name should not exceed 50 characters |
1266 |
|
# """ |
1267 |
|
# name = "{}-{}-{}".format(nsd_name, vnfr_name, member_vnf_index) |
1268 |
|
# new_name = '' |
1269 |
|
# for c in name: |
1270 |
|
# if c.isdigit(): |
1271 |
|
# c = chr(97 + int(c)) |
1272 |
|
# elif not c.isalpha(): |
1273 |
|
# c = "-" |
1274 |
|
# new_name += c |
1275 |
|
# return re.sub('\-+', '-', new_name.lower()) |
1276 |
|
|
1277 |
0 |
def format_model_name(self, name): |
1278 |
|
"""Format the name of model. |
1279 |
|
|
1280 |
|
Model names may only contain lowercase letters, digits and hyphens |
1281 |
|
""" |
1282 |
|
|
1283 |
0 |
return name.replace("_", "-").lower() |
1284 |
|
|
1285 |
0 |
async def get_application(self, model, application): |
1286 |
|
"""Get the deployed application.""" |
1287 |
0 |
if not self.authenticated: |
1288 |
0 |
await self.login() |
1289 |
|
|
1290 |
0 |
app = None |
1291 |
0 |
if application and model: |
1292 |
0 |
if model.applications: |
1293 |
0 |
if application in model.applications: |
1294 |
0 |
app = model.applications[application] |
1295 |
|
|
1296 |
0 |
return app |
1297 |
|
|
1298 |
0 |
async def get_model(self, model_name): |
1299 |
|
"""Get a model from the Juju Controller. |
1300 |
|
|
1301 |
|
Note: Model objects returned must call disconnected() before it goes |
1302 |
|
out of scope.""" |
1303 |
0 |
if not self.authenticated: |
1304 |
0 |
await self.login() |
1305 |
|
|
1306 |
0 |
if model_name not in self.models: |
1307 |
|
# Get the models in the controller |
1308 |
0 |
models = await self.controller.list_models() |
1309 |
|
|
1310 |
0 |
if model_name not in models: |
1311 |
0 |
try: |
1312 |
0 |
self.models[model_name] = await self.controller.add_model( |
1313 |
|
model_name, config={"authorized-keys": self.juju_public_key} |
1314 |
|
) |
1315 |
0 |
except JujuError as e: |
1316 |
0 |
if "already exists" not in e.message: |
1317 |
0 |
raise e |
1318 |
|
else: |
1319 |
0 |
self.models[model_name] = await self.controller.get_model(model_name) |
1320 |
|
|
1321 |
0 |
self.refcount["model"] += 1 |
1322 |
|
|
1323 |
|
# Create an observer for this model |
1324 |
0 |
await self.create_model_monitor(model_name) |
1325 |
|
|
1326 |
0 |
return self.models[model_name] |
1327 |
|
|
1328 |
0 |
async def create_model_monitor(self, model_name): |
1329 |
|
"""Create a monitor for the model, if none exists.""" |
1330 |
0 |
if not self.authenticated: |
1331 |
0 |
await self.login() |
1332 |
|
|
1333 |
0 |
if model_name not in self.monitors: |
1334 |
0 |
self.monitors[model_name] = VCAMonitor(model_name) |
1335 |
0 |
self.models[model_name].add_observer(self.monitors[model_name]) |
1336 |
|
|
1337 |
0 |
return True |
1338 |
|
|
1339 |
0 |
async def login(self): |
1340 |
|
"""Login to the Juju controller.""" |
1341 |
|
|
1342 |
0 |
if self.authenticated: |
1343 |
0 |
return |
1344 |
|
|
1345 |
0 |
self.connecting = True |
1346 |
|
|
1347 |
0 |
self.log.debug("JujuApi: Logging into controller") |
1348 |
|
|
1349 |
0 |
self.controller = Controller(loop=self.loop) |
1350 |
|
|
1351 |
0 |
if self.secret: |
1352 |
0 |
self.log.debug( |
1353 |
|
"Connecting to controller... ws://{} as {}/{}".format( |
1354 |
|
self.endpoint, self.user, self.secret, |
1355 |
|
) |
1356 |
|
) |
1357 |
0 |
try: |
1358 |
0 |
await self.controller.connect( |
1359 |
|
endpoint=self.endpoint, |
1360 |
|
username=self.user, |
1361 |
|
password=self.secret, |
1362 |
|
cacert=self.ca_cert, |
1363 |
|
) |
1364 |
0 |
self.refcount["controller"] += 1 |
1365 |
0 |
self.authenticated = True |
1366 |
0 |
self.log.debug("JujuApi: Logged into controller") |
1367 |
0 |
except Exception as ex: |
1368 |
0 |
self.log.debug("Caught exception: {}".format(ex)) |
1369 |
|
else: |
1370 |
|
# current_controller no longer exists |
1371 |
|
# self.log.debug("Connecting to current controller...") |
1372 |
|
# await self.controller.connect_current() |
1373 |
|
# await self.controller.connect( |
1374 |
|
# endpoint=self.endpoint, |
1375 |
|
# username=self.user, |
1376 |
|
# cacert=cacert, |
1377 |
|
# ) |
1378 |
0 |
self.log.fatal("VCA credentials not configured.") |
1379 |
0 |
self.authenticated = False |
1380 |
|
|
1381 |
0 |
async def logout(self): |
1382 |
|
"""Logout of the Juju controller.""" |
1383 |
0 |
if not self.authenticated: |
1384 |
0 |
return False |
1385 |
|
|
1386 |
0 |
try: |
1387 |
0 |
for model in self.models: |
1388 |
0 |
await self.disconnect_model(model) |
1389 |
|
|
1390 |
0 |
if self.controller: |
1391 |
0 |
self.log.debug("Disconnecting controller {}".format(self.controller)) |
1392 |
0 |
await self.controller.disconnect() |
1393 |
0 |
self.refcount["controller"] -= 1 |
1394 |
0 |
self.controller = None |
1395 |
|
|
1396 |
0 |
self.authenticated = False |
1397 |
|
|
1398 |
0 |
self.log.debug(self.refcount) |
1399 |
|
|
1400 |
0 |
except Exception as e: |
1401 |
0 |
self.log.fatal("Fatal error logging out of Juju Controller: {}".format(e)) |
1402 |
0 |
raise e |
1403 |
0 |
return True |
1404 |
|
|
1405 |
0 |
async def disconnect_model(self, model): |
1406 |
0 |
self.log.debug("Disconnecting model {}".format(model)) |
1407 |
0 |
if model in self.models: |
1408 |
0 |
try: |
1409 |
0 |
await self.models[model].disconnect() |
1410 |
0 |
self.refcount["model"] -= 1 |
1411 |
0 |
self.models[model] = None |
1412 |
0 |
except Exception as e: |
1413 |
0 |
self.log.debug("Caught exception: {}".format(e)) |
1414 |
|
|
1415 |
0 |
async def provision_machine( |
1416 |
|
self, model_name: str, hostname: str, username: str, private_key_path: str |
1417 |
|
) -> int: |
1418 |
|
"""Provision a machine. |
1419 |
|
|
1420 |
|
This executes the SSH provisioner, which will log in to a machine via |
1421 |
|
SSH and prepare it for use with the Juju model |
1422 |
|
|
1423 |
|
:param model_name str: The name of the model |
1424 |
|
:param hostname str: The IP or hostname of the target VM |
1425 |
|
:param user str: The username to login to |
1426 |
|
:param private_key_path str: The path to the private key that's been injected |
1427 |
|
to the VM via cloud-init |
1428 |
|
:return machine_id int: Returns the id of the machine or None if provisioning |
1429 |
|
fails |
1430 |
|
""" |
1431 |
0 |
if not self.authenticated: |
1432 |
0 |
await self.login() |
1433 |
|
|
1434 |
0 |
machine_id = None |
1435 |
|
|
1436 |
0 |
if self.api_proxy: |
1437 |
0 |
self.log.debug( |
1438 |
|
"Instantiating SSH Provisioner for {}@{} ({})".format( |
1439 |
|
username, hostname, private_key_path |
1440 |
|
) |
1441 |
|
) |
1442 |
0 |
provisioner = SSHProvisioner( |
1443 |
|
host=hostname, |
1444 |
|
user=username, |
1445 |
|
private_key_path=private_key_path, |
1446 |
|
log=self.log, |
1447 |
|
) |
1448 |
|
|
1449 |
0 |
params = None |
1450 |
0 |
try: |
1451 |
0 |
params = provisioner.provision_machine() |
1452 |
0 |
except Exception as ex: |
1453 |
0 |
self.log.debug("caught exception from provision_machine: {}".format(ex)) |
1454 |
0 |
return None |
1455 |
|
|
1456 |
0 |
if params: |
1457 |
0 |
params.jobs = ["JobHostUnits"] |
1458 |
|
|
1459 |
0 |
model = await self.get_model(model_name) |
1460 |
|
|
1461 |
0 |
connection = model.connection() |
1462 |
|
|
1463 |
|
# Submit the request. |
1464 |
0 |
self.log.debug("Adding machine to model") |
1465 |
0 |
client_facade = client.ClientFacade.from_connection(connection) |
1466 |
0 |
results = await client_facade.AddMachines(params=[params]) |
1467 |
0 |
error = results.machines[0].error |
1468 |
0 |
if error: |
1469 |
0 |
raise ValueError("Error adding machine: %s" % error.message) |
1470 |
|
|
1471 |
0 |
machine_id = results.machines[0].machine |
1472 |
|
|
1473 |
|
# Need to run this after AddMachines has been called, |
1474 |
|
# as we need the machine_id |
1475 |
0 |
self.log.debug("Installing Juju agent") |
1476 |
0 |
await provisioner.install_agent( |
1477 |
|
connection, params.nonce, machine_id, self.api_proxy, |
1478 |
|
) |
1479 |
|
else: |
1480 |
0 |
self.log.debug("Missing API Proxy") |
1481 |
0 |
return machine_id |
1482 |
|
|
1483 |
|
# async def remove_application(self, name): |
1484 |
|
# """Remove the application.""" |
1485 |
|
# if not self.authenticated: |
1486 |
|
# await self.login() |
1487 |
|
# |
1488 |
|
# app = await self.get_application(name) |
1489 |
|
# if app: |
1490 |
|
# self.log.debug("JujuApi: Destroying application {}".format( |
1491 |
|
# name, |
1492 |
|
# )) |
1493 |
|
# |
1494 |
|
# await app.destroy() |
1495 |
|
|
1496 |
0 |
async def remove_relation(self, a, b): |
1497 |
|
""" |
1498 |
|
Remove a relation between two application endpoints |
1499 |
|
|
1500 |
|
:param a An application endpoint |
1501 |
|
:param b An application endpoint |
1502 |
|
""" |
1503 |
0 |
if not self.authenticated: |
1504 |
0 |
await self.login() |
1505 |
|
|
1506 |
|
# m = await self.get_model() |
1507 |
|
# try: |
1508 |
|
# m.remove_relation(a, b) |
1509 |
|
# finally: |
1510 |
|
# await m.disconnect() |
1511 |
|
|
1512 |
0 |
async def resolve_error(self, model_name, application=None): |
1513 |
|
"""Resolve units in error state.""" |
1514 |
0 |
if not self.authenticated: |
1515 |
0 |
await self.login() |
1516 |
|
|
1517 |
0 |
model = await self.get_model(model_name) |
1518 |
|
|
1519 |
0 |
app = await self.get_application(model, application) |
1520 |
0 |
if app: |
1521 |
0 |
self.log.debug( |
1522 |
|
"JujuApi: Resolving errors for application {}".format(application,) |
1523 |
|
) |
1524 |
|
|
1525 |
0 |
for _ in app.units: |
1526 |
0 |
app.resolved(retry=True) |
1527 |
|
|
1528 |
0 |
async def run_action(self, model_name, application, action_name, **params): |
1529 |
|
"""Execute an action and return an Action object.""" |
1530 |
0 |
if not self.authenticated: |
1531 |
0 |
await self.login() |
1532 |
0 |
result = {"status": "", "action": {"tag": None, "results": None}} |
1533 |
|
|
1534 |
0 |
model = await self.get_model(model_name) |
1535 |
|
|
1536 |
0 |
app = await self.get_application(model, application) |
1537 |
0 |
if app: |
1538 |
|
# We currently only have one unit per application |
1539 |
|
# so use the first unit available. |
1540 |
0 |
unit = app.units[0] |
1541 |
|
|
1542 |
0 |
self.log.debug( |
1543 |
|
"JujuApi: Running Action {} against Application {}".format( |
1544 |
|
action_name, application, |
1545 |
|
) |
1546 |
|
) |
1547 |
|
|
1548 |
0 |
action = await unit.run_action(action_name, **params) |
1549 |
|
|
1550 |
|
# Wait for the action to complete |
1551 |
0 |
await action.wait() |
1552 |
|
|
1553 |
0 |
result["status"] = action.status |
1554 |
0 |
result["action"]["tag"] = action.data["id"] |
1555 |
0 |
result["action"]["results"] = action.results |
1556 |
|
|
1557 |
0 |
return result |
1558 |
|
|
1559 |
0 |
async def set_config(self, model_name, application, config): |
1560 |
|
"""Apply a configuration to the application.""" |
1561 |
0 |
if not self.authenticated: |
1562 |
0 |
await self.login() |
1563 |
|
|
1564 |
0 |
app = await self.get_application(model_name, application) |
1565 |
0 |
if app: |
1566 |
0 |
self.log.debug( |
1567 |
|
"JujuApi: Setting config for Application {}".format(application,) |
1568 |
|
) |
1569 |
0 |
await app.set_config(config) |
1570 |
|
|
1571 |
|
# Verify the config is set |
1572 |
0 |
newconf = await app.get_config() |
1573 |
0 |
for key in config: |
1574 |
0 |
if config[key] != newconf[key]["value"]: |
1575 |
0 |
self.log.debug( |
1576 |
|
( |
1577 |
|
"JujuApi: Config not set! Key {} Value {} doesn't match {}" |
1578 |
|
).format(key, config[key], newconf[key]) |
1579 |
|
) |
1580 |
|
|
1581 |
|
# async def set_parameter(self, parameter, value, application=None): |
1582 |
|
# """Set a config parameter for a service.""" |
1583 |
|
# if not self.authenticated: |
1584 |
|
# await self.login() |
1585 |
|
# |
1586 |
|
# self.log.debug("JujuApi: Setting {}={} for Application {}".format( |
1587 |
|
# parameter, |
1588 |
|
# value, |
1589 |
|
# application, |
1590 |
|
# )) |
1591 |
|
# return await self.apply_config( |
1592 |
|
# {parameter: value}, |
1593 |
|
# application=application, |
1594 |
|
# ) |
1595 |
|
|
1596 |
0 |
async def wait_for_application(self, model_name, application_name, timeout=300): |
1597 |
|
"""Wait for an application to become active.""" |
1598 |
0 |
if not self.authenticated: |
1599 |
0 |
await self.login() |
1600 |
|
|
1601 |
0 |
model = await self.get_model(model_name) |
1602 |
|
|
1603 |
0 |
app = await self.get_application(model, application_name) |
1604 |
0 |
self.log.debug("Application: {}".format(app)) |
1605 |
0 |
if app: |
1606 |
0 |
self.log.debug( |
1607 |
|
"JujuApi: Waiting {} seconds for Application {}".format( |
1608 |
|
timeout, application_name, |
1609 |
|
) |
1610 |
|
) |
1611 |
|
|
1612 |
0 |
await model.block_until( |
1613 |
|
lambda: all( |
1614 |
|
unit.agent_status == "idle" |
1615 |
|
and unit.workload_status in ["active", "unknown"] |
1616 |
|
for unit in app.units |
1617 |
|
), |
1618 |
|
timeout=timeout, |
1619 |
|
) |