blob: 4e467469f800c590ab573d9204171309dc446888 [file] [log] [blame]
Adam Israel0cd1c022019-09-03 18:26:08 -04001# 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
Adam Israel5e08a0e2018-09-06 19:22:47 -040015import asyncio
Adam Israel19c5cfc2019-10-03 12:35:38 -040016import base64
17import binascii
Adam Israelc3e6c2e2018-03-01 09:31:50 -050018import logging
Adam Israelc3e6c2e2018-03-01 09:31:50 -050019import os.path
20import re
Adam Israelfa329072018-09-14 11:26:13 -040021import shlex
Adam Israelc3e6c2e2018-03-01 09:31:50 -050022import ssl
Adam Israelfa329072018-09-14 11:26:13 -040023import subprocess
beierlm32862bb2020-04-21 16:36:35 -040024
25from juju.client import client
26from juju.controller import Controller
27from juju.errors import JujuAPIError, JujuError
28from juju.model import ModelObserver
29
Adam Israel19c5cfc2019-10-03 12:35:38 -040030import n2vc.exceptions
Adam Israel0cd1c022019-09-03 18:26:08 -040031from n2vc.provisioner import SSHProvisioner
Adam Israelc3e6c2e2018-03-01 09:31:50 -050032
beierlm32862bb2020-04-21 16:36:35 -040033
34# import time
Adam Israelc3e6c2e2018-03-01 09:31:50 -050035# 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
Adam Israel19c5cfc2019-10-03 12:35:38 -040038# 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)
Adam Israelc3e6c2e2018-03-01 09:31:50 -050042# We might need this to connect to the websocket securely, but test and verify.
43try:
44 ssl._create_default_https_context = ssl._create_unverified_context
45except AttributeError:
46 # Legacy Python doesn't verify by default (see pep-0476)
47 # https://www.python.org/dev/peps/pep-0476/
48 pass
49
50
51# Custom exceptions
Adam Israel0cd1c022019-09-03 18:26:08 -040052# Deprecated. Please use n2vc.exceptions namespace.
Adam Israelc3e6c2e2018-03-01 09:31:50 -050053class JujuCharmNotFound(Exception):
54 """The Charm can't be found or is not readable."""
55
56
57class JujuApplicationExists(Exception):
58 """The Application already exists."""
59
Adam Israelb5214512018-05-03 10:00:04 -040060
Adam Israel88a49632018-04-10 13:04:57 -060061class N2VCPrimitiveExecutionFailed(Exception):
62 """Something failed while attempting to execute a primitive."""
63
Adam Israelc3e6c2e2018-03-01 09:31:50 -050064
Adam Israel6d84dbd2019-03-08 18:33:35 -050065class NetworkServiceDoesNotExist(Exception):
66 """The Network Service being acted against does not exist."""
67
68
Adam Israel32a15192019-06-24 11:44:47 -040069class PrimitiveDoesNotExist(Exception):
70 """The Primitive being executed does not exist."""
71
Adam Israel0cd1c022019-09-03 18:26:08 -040072
Adam Israelc3e6c2e2018-03-01 09:31:50 -050073# Quiet the debug logging
beierlm32862bb2020-04-21 16:36:35 -040074logging.getLogger("websockets.protocol").setLevel(logging.INFO)
75logging.getLogger("juju.client.connection").setLevel(logging.WARN)
76logging.getLogger("juju.model").setLevel(logging.WARN)
77logging.getLogger("juju.machine").setLevel(logging.WARN)
Adam Israelc3e6c2e2018-03-01 09:31:50 -050078
Adam Israelb5214512018-05-03 10:00:04 -040079
Adam Israelc3e6c2e2018-03-01 09:31:50 -050080class VCAMonitor(ModelObserver):
81 """Monitor state changes within the Juju Model."""
beierlm32862bb2020-04-21 16:36:35 -040082
Adam Israelc3e6c2e2018-03-01 09:31:50 -050083 log = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -050084
Adam Israel28a43c02018-04-23 16:04:54 -040085 def __init__(self, ns_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050086 self.log = logging.getLogger(__name__)
87
88 self.ns_name = ns_name
Adam Israeld420a8b2019-04-09 16:07:53 -040089 self.applications = {}
Adam Israel28a43c02018-04-23 16:04:54 -040090
91 def AddApplication(self, application_name, callback, *callback_args):
92 if application_name not in self.applications:
93 self.applications[application_name] = {
beierlm32862bb2020-04-21 16:36:35 -040094 "callback": callback,
95 "callback_args": callback_args,
Adam Israel28a43c02018-04-23 16:04:54 -040096 }
97
98 def RemoveApplication(self, application_name):
99 if application_name in self.applications:
100 del self.applications[application_name]
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500101
102 async def on_change(self, delta, old, new, model):
103 """React to changes in the Juju model."""
104
105 if delta.entity == "unit":
Adam Israel28a43c02018-04-23 16:04:54 -0400106 # Ignore change events from other applications
beierlm32862bb2020-04-21 16:36:35 -0400107 if delta.data["application"] not in self.applications.keys():
Adam Israel28a43c02018-04-23 16:04:54 -0400108 return
109
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500110 try:
Adam Israel28a43c02018-04-23 16:04:54 -0400111
beierlm32862bb2020-04-21 16:36:35 -0400112 application_name = delta.data["application"]
Adam Israel28a43c02018-04-23 16:04:54 -0400113
beierlm32862bb2020-04-21 16:36:35 -0400114 callback = self.applications[application_name]["callback"]
115 callback_args = self.applications[application_name]["callback_args"]
Adam Israel28a43c02018-04-23 16:04:54 -0400116
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500117 if old and new:
Adam Israelfc511ed2018-09-21 14:20:55 +0200118 # Fire off a callback with the application state
119 if callback:
120 callback(
121 self.ns_name,
beierlm32862bb2020-04-21 16:36:35 -0400122 delta.data["application"],
Adam Israelfc511ed2018-09-21 14:20:55 +0200123 new.workload_status,
124 new.workload_status_message,
beierlm32862bb2020-04-21 16:36:35 -0400125 *callback_args,
126 )
Adam Israel28a43c02018-04-23 16:04:54 -0400127
128 if old and not new:
129 # This is a charm being removed
130 if callback:
131 callback(
132 self.ns_name,
beierlm32862bb2020-04-21 16:36:35 -0400133 delta.data["application"],
Adam Israel28a43c02018-04-23 16:04:54 -0400134 "removed",
Adam Israel9562f432018-05-09 13:55:28 -0400135 "",
beierlm32862bb2020-04-21 16:36:35 -0400136 *callback_args,
137 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500138 except Exception as e:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400139 self.log.debug("[1] notify_callback exception: {}".format(e))
140
Adam Israel88a49632018-04-10 13:04:57 -0600141 elif delta.entity == "action":
142 # TODO: Decide how we want to notify the user of actions
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500143
Adam Israel88a49632018-04-10 13:04:57 -0600144 # 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 pass
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500158
beierlm32862bb2020-04-21 16:36:35 -0400159
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500160########
161# TODO
162#
163# Create unique models per network service
164# Document all public functions
165
Adam Israelb5214512018-05-03 10:00:04 -0400166
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500167class N2VC:
beierlm32862bb2020-04-21 16:36:35 -0400168 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 ):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500181 """Initialize N2VC
Adam Israel0cd1c022019-09-03 18:26:08 -0400182
183 Initializes the N2VC object, allowing the caller to interoperate with the VCA.
184
185
Adam Israelb2a07f52019-04-25 17:17:05 -0400186 :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
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500192 stored.
Adam Israelb2a07f52019-04-25 17:17:05 -0400193 :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
Adam Israel0cd1c022019-09-03 18:26:08 -0400196 :param api_proxy str: The IP of the host machine
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500197
198 :Example:
Adam Israelb2a07f52019-04-25 17:17:05 -0400199 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>',
Adam Israel0cd1c022019-09-03 18:26:08 -0400209 api_proxy='192.168.1.155'
Adam Israelb2a07f52019-04-25 17:17:05 -0400210 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500211 """
212
Adam Israel5e08a0e2018-09-06 19:22:47 -0400213 # Initialize instance-level variables
214 self.api = None
215 self.log = None
216 self.controller = None
217 self.connecting = False
218 self.authenticated = False
Adam Israel0cd1c022019-09-03 18:26:08 -0400219 self.api_proxy = api_proxy
Adam Israel5e08a0e2018-09-06 19:22:47 -0400220
Adam Israel19c5cfc2019-10-03 12:35:38 -0400221 if log:
222 self.log = log
223 else:
224 self.log = logging.getLogger(__name__)
225
Adam Israelfc511ed2018-09-21 14:20:55 +0200226 # For debugging
227 self.refcount = {
beierlm32862bb2020-04-21 16:36:35 -0400228 "controller": 0,
229 "model": 0,
Adam Israelfc511ed2018-09-21 14:20:55 +0200230 }
231
Adam Israel5e08a0e2018-09-06 19:22:47 -0400232 self.models = {}
Adam Israel5e08a0e2018-09-06 19:22:47 -0400233
234 # Model Observers
235 self.monitors = {}
236
237 # VCA config
238 self.hostname = ""
239 self.port = 17070
240 self.username = ""
241 self.secret = ""
Adam Israel19c5cfc2019-10-03 12:35:38 -0400242
Adam Israelb2a07f52019-04-25 17:17:05 -0400243 self.juju_public_key = juju_public_key
244 if juju_public_key:
245 self._create_juju_public_key(juju_public_key)
Adam Israel19c5cfc2019-10-03 12:35:38 -0400246 else:
beierlm32862bb2020-04-21 16:36:35 -0400247 self.juju_public_key = ""
Adam Israelb2a07f52019-04-25 17:17:05 -0400248
Adam Israel0cd1c022019-09-03 18:26:08 -0400249 # TODO: Verify ca_cert is valid before using. VCA will crash
250 # if the ca_cert isn't formatted correctly.
Adam Israel19c5cfc2019-10-03 12:35:38 -0400251 def base64_to_cacert(b64string):
252 """Convert the base64-encoded string containing the VCA CACERT.
Adam Israelb2a07f52019-04-25 17:17:05 -0400253
Adam Israel19c5cfc2019-10-03 12:35:38 -0400254 The input string....
255
256 """
257 try:
258 cacert = base64.b64decode(b64string).decode("utf-8")
259
beierlm32862bb2020-04-21 16:36:35 -0400260 cacert = re.sub(r"\\n", r"\n", cacert,)
Adam Israel19c5cfc2019-10-03 12:35:38 -0400261 except binascii.Error as e:
262 self.log.debug("Caught binascii.Error: {}".format(e))
beierlm32862bb2020-04-21 16:36:35 -0400263 raise n2vc.exceptions.N2VCInvalidCertificate("Invalid CA Certificate")
Adam Israel19c5cfc2019-10-03 12:35:38 -0400264
265 return cacert
266
tiernof5b4b202019-11-11 17:45:17 +0000267 self.ca_cert = None
268 if ca_cert:
269 self.ca_cert = base64_to_cacert(ca_cert)
Adam Israel19c5cfc2019-10-03 12:35:38 -0400270
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500271 # Quiet websocket traffic
beierlm32862bb2020-04-21 16:36:35 -0400272 logging.getLogger("websockets.protocol").setLevel(logging.INFO)
273 logging.getLogger("juju.client.connection").setLevel(logging.WARN)
274 logging.getLogger("model").setLevel(logging.WARN)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500275 # logging.getLogger('websockets.protocol').setLevel(logging.DEBUG)
276
beierlm32862bb2020-04-21 16:36:35 -0400277 self.log.debug("JujuApi: instantiated")
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500278
279 self.server = server
280 self.port = port
281
282 self.secret = secret
beierlm32862bb2020-04-21 16:36:35 -0400283 if user.startswith("user-"):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500284 self.user = user
285 else:
beierlm32862bb2020-04-21 16:36:35 -0400286 self.user = "user-{}".format(user)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500287
beierlm32862bb2020-04-21 16:36:35 -0400288 self.endpoint = "%s:%d" % (server, int(port))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500289
290 self.artifacts = artifacts
291
Adam Israel5e08a0e2018-09-06 19:22:47 -0400292 self.loop = loop or asyncio.get_event_loop()
293
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500294 def __del__(self):
295 """Close any open connections."""
296 yield self.logout()
297
Adam Israelb2a07f52019-04-25 17:17:05 -0400298 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 """
Adam Israele3a05f82019-04-26 13:12:47 -0400305 # Make sure that we have a public key before writing to disk
Adam Israelb2a07f52019-04-25 17:17:05 -0400306 if public_key is None or len(public_key) == 0:
beierlm32862bb2020-04-21 16:36:35 -0400307 if "OSM_VCA_PUBKEY" in os.environ:
308 public_key = os.getenv("OSM_VCA_PUBKEY", "")
Adam Israele3a05f82019-04-26 13:12:47 -0400309 if len(public_key == 0):
310 return
311 else:
312 return
313
beierlm32862bb2020-04-21 16:36:35 -0400314 path = "{}/.local/share/juju/ssh".format(os.path.expanduser("~"),)
Adam Israelb2a07f52019-04-25 17:17:05 -0400315 if not os.path.exists(path):
316 os.makedirs(path)
317
beierlm32862bb2020-04-21 16:36:35 -0400318 with open("{}/juju_id_rsa.pub".format(path), "w") as f:
Adam Israelb2a07f52019-04-25 17:17:05 -0400319 f.write(public_key)
320
beierlm32862bb2020-04-21 16:36:35 -0400321 def notify_callback(
322 self,
323 model_name,
324 application_name,
325 status,
326 message,
327 callback=None,
328 *callback_args
329 ):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500330 try:
331 if callback:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400332 callback(
beierlm32862bb2020-04-21 16:36:35 -0400333 model_name, application_name, status, message, *callback_args,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400334 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500335 except Exception as e:
336 self.log.error("[0] notify_callback exception {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600337 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500338 return True
339
340 # Public methods
Adam Israel85a4b212018-11-29 20:30:24 -0500341 async def Relate(self, model_name, vnfd):
Adam Israel136186e2018-09-14 12:01:12 -0400342 """Create a relation between the charm-enabled VDUs in a VNF.
343
beierlm32862bb2020-04-21 16:36:35 -0400344 The Relation mapping has two parts: the id of the vdu owning the endpoint, and
345 the name of the endpoint.
Adam Israel136186e2018-09-14 12:01:12 -0400346
347 vdu:
348 ...
Adam Israelbc0daf82019-06-12 17:08:36 -0400349 vca-relationships:
Adam Israel47998e02019-06-03 11:21:27 -0400350 relation:
351 - provides: dataVM:db
352 requires: mgmtVM:app
Adam Israel136186e2018-09-14 12:01:12 -0400353
beierlm32862bb2020-04-21 16:36:35 -0400354 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.
Adam Israel136186e2018-09-14 12:01:12 -0400357
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 configs = []
369 vnf_config = vnfd.get("vnf-configuration")
370 if vnf_config:
beierlm32862bb2020-04-21 16:36:35 -0400371 juju = vnf_config["juju"]
Adam Israel136186e2018-09-14 12:01:12 -0400372 if juju:
373 configs.append(vnf_config)
374
beierlm32862bb2020-04-21 16:36:35 -0400375 for vdu in vnfd["vdu"]:
376 vdu_config = vdu.get("vdu-configuration")
Adam Israel136186e2018-09-14 12:01:12 -0400377 if vdu_config:
beierlm32862bb2020-04-21 16:36:35 -0400378 juju = vdu_config["juju"]
Adam Israel136186e2018-09-14 12:01:12 -0400379 if juju:
380 configs.append(vdu_config)
381
382 def _get_application_name(name):
383 """Get the application name that's mapped to a vnf/vdu."""
384 vnf_member_index = 0
beierlm32862bb2020-04-21 16:36:35 -0400385 vnf_name = vnfd["name"]
Adam Israel136186e2018-09-14 12:01:12 -0400386
beierlm32862bb2020-04-21 16:36:35 -0400387 for vdu in vnfd.get("vdu"):
Adam Israel136186e2018-09-14 12:01:12 -0400388 # Compare the named portion of the relation to the vdu's id
beierlm32862bb2020-04-21 16:36:35 -0400389 if vdu["id"] == name:
Adam Israel136186e2018-09-14 12:01:12 -0400390 application_name = self.FormatApplicationName(
beierlm32862bb2020-04-21 16:36:35 -0400391 model_name, vnf_name, str(vnf_member_index),
Adam Israel136186e2018-09-14 12:01:12 -0400392 )
393 return application_name
394 else:
395 vnf_member_index += 1
396
397 return None
398
399 # Loop through relations
400 for cfg in configs:
beierlm32862bb2020-04-21 16:36:35 -0400401 if "juju" in cfg:
402 juju = cfg["juju"]
403 if (
404 "vca-relationships" in juju
405 and "relation" in juju["vca-relationships"]
406 ):
407 for rel in juju["vca-relationships"]["relation"]:
Adam Israel136186e2018-09-14 12:01:12 -0400408 try:
409
410 # get the application name for the provides
beierlm32862bb2020-04-21 16:36:35 -0400411 (name, endpoint) = rel["provides"].split(":")
Adam Israel136186e2018-09-14 12:01:12 -0400412 application_name = _get_application_name(name)
413
beierlm32862bb2020-04-21 16:36:35 -0400414 provides = "{}:{}".format(application_name, endpoint)
Adam Israel136186e2018-09-14 12:01:12 -0400415
416 # get the application name for thr requires
beierlm32862bb2020-04-21 16:36:35 -0400417 (name, endpoint) = rel["requires"].split(":")
Adam Israel136186e2018-09-14 12:01:12 -0400418 application_name = _get_application_name(name)
419
beierlm32862bb2020-04-21 16:36:35 -0400420 requires = "{}:{}".format(application_name, endpoint)
421 self.log.debug(
422 "Relation: {} <-> {}".format(provides, requires)
Adam Israel136186e2018-09-14 12:01:12 -0400423 )
Adam Israel136186e2018-09-14 12:01:12 -0400424 await self.add_relation(
beierlm32862bb2020-04-21 16:36:35 -0400425 model_name, provides, requires,
Adam Israel136186e2018-09-14 12:01:12 -0400426 )
427 except Exception as e:
428 self.log.debug("Exception: {}".format(e))
429
430 return
431
beierlm32862bb2020-04-21 16:36:35 -0400432 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 ):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500443 """Deploy one or more charms associated with a VNF.
444
445 Deploy the charm(s) referenced in a VNF Descriptor.
446
Adam Israel85a4b212018-11-29 20:30:24 -0500447 :param str model_name: The name or unique id of the network service.
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500448 :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 {
Adam Israel88a49632018-04-10 13:04:57 -0600454 'rw_mgmt_ip': '1.2.3.4',
455 # Pass the initial-config-primitives section of the vnf or vdu
456 'initial-config-primitives': {...}
beierlm32862bb2020-04-21 16:36:35 -0400457 'user_values': dictionary with the day-1 parameters provided at
458 instantiation time. It will replace values
tierno1afb30a2018-12-21 13:42:43 +0000459 inside < >. rw_mgmt_ip will be included here also
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500460 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400461 :param dict machine_spec: A dictionary describing the machine to
462 install to
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500463 Examples::
464 {
465 'hostname': '1.2.3.4',
466 'username': 'ubuntu',
467 }
468 :param obj callback: A callback function to receive status changes.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400469 :param tuple callback_args: A list of arguments to be passed to the
470 callback
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500471 """
472
473 ########################################################
474 # Verify the path to the charm exists and is readable. #
475 ########################################################
476 if not os.path.exists(charm_path):
477 self.log.debug("Charm path doesn't exist: {}".format(charm_path))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400478 self.notify_callback(
479 model_name,
480 application_name,
Dominik Fleischmannd909b072019-11-28 16:27:36 +0100481 "error",
Adam Israel5e08a0e2018-09-06 19:22:47 -0400482 "failed",
483 callback,
484 *callback_args,
485 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500486 raise JujuCharmNotFound("No artifacts configured.")
487
488 ################################
489 # Login to the Juju controller #
490 ################################
491 if not self.authenticated:
492 self.log.debug("Authenticating with Juju")
493 await self.login()
494
495 ##########################################
496 # Get the model for this network service #
497 ##########################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500498 model = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500499
500 ########################################
501 # Verify the application doesn't exist #
502 ########################################
503 app = await self.get_application(model, application_name)
504 if app:
beierlm32862bb2020-04-21 16:36:35 -0400505 raise JujuApplicationExists(
506 (
507 'Can\'t deploy application "{}" to model '
508 ' "{}" because it already exists.'
509 ).format(application_name, model_name)
510 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500511
Adam Israel28a43c02018-04-23 16:04:54 -0400512 ################################################################
513 # Register this application with the model-level event monitor #
514 ################################################################
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500515 if callback:
beierlm32862bb2020-04-21 16:36:35 -0400516 self.log.debug(
517 "JujuApi: Registering callback for {}".format(application_name,)
518 )
Adam Israel04eee1f2019-04-29 14:59:45 -0400519 await self.Subscribe(model_name, application_name, callback, *callback_args)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500520
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500521 #######################################
522 # Get the initial charm configuration #
523 #######################################
524
525 rw_mgmt_ip = None
beierlm32862bb2020-04-21 16:36:35 -0400526 if "rw_mgmt_ip" in params:
527 rw_mgmt_ip = params["rw_mgmt_ip"]
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500528
beierlm32862bb2020-04-21 16:36:35 -0400529 if "initial-config-primitive" not in params:
530 params["initial-config-primitive"] = {}
Adam Israel5afe0542018-08-08 12:54:55 -0400531
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500532 initial_config = self._get_config_from_dict(
beierlm32862bb2020-04-21 16:36:35 -0400533 params["initial-config-primitive"], {"<rw_mgmt_ip>": rw_mgmt_ip}
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500534 )
535
Adam Israel0cd1c022019-09-03 18:26:08 -0400536 ########################################################
537 # Check for specific machine placement (native charms) #
538 ########################################################
539 to = ""
540 series = "xenial"
541
542 if machine_spec.keys():
beierlm32862bb2020-04-21 16:36:35 -0400543 if all(k in machine_spec for k in ["hostname", "username"]):
Adam Israel0cd1c022019-09-03 18:26:08 -0400544
545 # Allow series to be derived from the native charm
546 series = None
547
beierlm32862bb2020-04-21 16:36:35 -0400548 self.log.debug(
549 "Provisioning manual machine {}@{}".format(
550 machine_spec["username"], machine_spec["hostname"],
551 )
552 )
Adam Israel0cd1c022019-09-03 18:26:08 -0400553
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 to = await self.provision_machine(
567 model_name=model_name,
beierlm32862bb2020-04-21 16:36:35 -0400568 username=machine_spec["username"],
569 hostname=machine_spec["hostname"],
Adam Israel0cd1c022019-09-03 18:26:08 -0400570 private_key_path=self.GetPrivateKeyPath(),
571 )
572 self.log.debug("Provisioned machine id {}".format(to))
573
574 # TODO: If to is none, raise an exception
575
beierlm32862bb2020-04-21 16:36:35 -0400576 # The native charm won't have the sshproxy layer, typically, but LCM
577 # uses the config primitive
Adam Israel0cd1c022019-09-03 18:26:08 -0400578 # 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")
beierlm32862bb2020-04-21 16:36:35 -0400595 for k in ["ssh-hostname", "ssh-username", "ssh-password"]:
Adam Israel0cd1c022019-09-03 18:26:08 -0400596 if k in initial_config:
597 self.log.debug("Removing parameter {}".format(k))
598 del initial_config[k]
599
beierlm32862bb2020-04-21 16:36:35 -0400600 self.log.debug(
601 "JujuApi: Deploying charm ({}/{}) from {} to {}".format(
602 model_name, application_name, charm_path, to,
603 )
604 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500605
606 ########################################################
607 # Deploy the charm and apply the initial configuration #
608 ########################################################
609 app = await model.deploy(
Adam Israel88a49632018-04-10 13:04:57 -0600610 # We expect charm_path to be either the path to the charm on disk
611 # or in the format of cs:series/name
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500612 charm_path,
Adam Israel88a49632018-04-10 13:04:57 -0600613 # This is the formatted, unique name for this charm
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500614 application_name=application_name,
Adam Israel88a49632018-04-10 13:04:57 -0600615 # Proxy charms should use the current LTS. This will need to be
616 # changed for native charms.
Adam Israel0cd1c022019-09-03 18:26:08 -0400617 series=series,
Adam Israel88a49632018-04-10 13:04:57 -0600618 # Apply the initial 'config' primitive during deployment
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500619 config=initial_config,
Adam Israelfa329072018-09-14 11:26:13 -0400620 # Where to deploy the charm to.
621 to=to,
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500622 )
Adam Israel0cd1c022019-09-03 18:26:08 -0400623
Adam Israel38bf1642019-05-31 09:59:52 -0400624 #############################
625 # Map the vdu id<->app name #
626 #############################
627 try:
628 await self.Relate(model_name, vnfd)
629 except KeyError as ex:
630 # We don't currently support relations between NS and VNF/VDU charms
631 self.log.warn("[N2VC] Relations not supported: {}".format(ex))
beierlm32862bb2020-04-21 16:36:35 -0400632 except Exception:
Adam Israel38bf1642019-05-31 09:59:52 -0400633 # 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 self.log.warn("[N2VC] Relations not ready")
Adam Israel136186e2018-09-14 12:01:12 -0400637
Adam Israel88a49632018-04-10 13:04:57 -0600638 # #######################################
639 # # Execute initial config primitive(s) #
640 # #######################################
Adam Israelcf253202018-10-31 16:29:09 -0700641 uuids = await self.ExecuteInitialPrimitives(
beierlm32862bb2020-04-21 16:36:35 -0400642 model_name, application_name, params,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400643 )
Adam Israelcf253202018-10-31 16:29:09 -0700644 return uuids
Adam Israel5e08a0e2018-09-06 19:22:47 -0400645
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 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 status = None
693 try:
694 if not self.authenticated:
695 await self.login()
696
Adam Israel5e08a0e2018-09-06 19:22:47 -0400697 model = await self.get_model(model_name)
698
699 results = await model.get_action_status(uuid)
700
701 if uuid in results:
702 status = results[uuid]
703
704 except Exception as e:
705 self.log.debug(
706 "Caught exception while getting primitive status: {}".format(e)
707 )
708 raise N2VCPrimitiveExecutionFailed(e)
709
710 return status
711
712 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 results = None
718 try:
719 if not self.authenticated:
720 await self.login()
721
Adam Israel5e08a0e2018-09-06 19:22:47 -0400722 model = await self.get_model(model_name)
723 results = await model.get_action_output(uuid, 60)
724 except Exception as e:
725 self.log.debug(
726 "Caught exception while getting primitive status: {}".format(e)
727 )
728 raise N2VCPrimitiveExecutionFailed(e)
729
730 return results
731
Adam Israelfa329072018-09-14 11:26:13 -0400732 # 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 def GetPrivateKeyPath(self):
beierlm32862bb2020-04-21 16:36:35 -0400761 homedir = os.environ["HOME"]
Adam Israelfa329072018-09-14 11:26:13 -0400762 sshdir = "{}/.ssh".format(homedir)
763 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
764 return private_key_path
765
766 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 """
beierlm32862bb2020-04-21 16:36:35 -0400777 # public_key = ""
Adam Israelfa329072018-09-14 11:26:13 -0400778
779 # Find the path to where we expect our key to live.
beierlm32862bb2020-04-21 16:36:35 -0400780 homedir = os.environ["HOME"]
Adam Israelfa329072018-09-14 11:26:13 -0400781 sshdir = "{}/.ssh".format(homedir)
782 if not os.path.exists(sshdir):
783 os.mkdir(sshdir)
784
785 private_key_path = "{}/id_n2vc_rsa".format(sshdir)
786 public_key_path = "{}.pub".format(private_key_path)
787
788 # If we don't have a key generated, generate it.
789 if not os.path.exists(private_key_path):
790 cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format(
beierlm32862bb2020-04-21 16:36:35 -0400791 "rsa", "4096", private_key_path
Adam Israelfa329072018-09-14 11:26:13 -0400792 )
793 subprocess.check_output(shlex.split(cmd))
794
795 # Read the public key
796 with open(public_key_path, "r") as f:
797 public_key = f.readline()
798
799 return public_key
800
beierlm32862bb2020-04-21 16:36:35 -0400801 async def ExecuteInitialPrimitives(
802 self, model_name, application_name, params, callback=None, *callback_args
803 ):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400804 """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 uuids = []
Adam Israel88a49632018-04-10 13:04:57 -0600812 primitives = {}
813
814 # Build a sequential list of the primitives to execute
beierlm32862bb2020-04-21 16:36:35 -0400815 for primitive in params["initial-config-primitive"]:
Adam Israel88a49632018-04-10 13:04:57 -0600816 try:
beierlm32862bb2020-04-21 16:36:35 -0400817 if primitive["name"] == "config":
Adam Israel88a49632018-04-10 13:04:57 -0600818 pass
819 else:
beierlm32862bb2020-04-21 16:36:35 -0400820 seq = primitive["seq"]
Adam Israel88a49632018-04-10 13:04:57 -0600821
tierno1afb30a2018-12-21 13:42:43 +0000822 params_ = {}
beierlm32862bb2020-04-21 16:36:35 -0400823 if "parameter" in primitive:
824 params_ = primitive["parameter"]
tierno1afb30a2018-12-21 13:42:43 +0000825
826 user_values = params.get("user_values", {})
beierlm32862bb2020-04-21 16:36:35 -0400827 if "rw_mgmt_ip" not in user_values:
828 user_values["rw_mgmt_ip"] = None
829 # just for backward compatibility, because it will be provided
830 # always by modern version of LCM
Adam Israel42d88e62018-07-16 14:18:41 -0400831
Adam Israel88a49632018-04-10 13:04:57 -0600832 primitives[seq] = {
beierlm32862bb2020-04-21 16:36:35 -0400833 "name": primitive["name"],
834 "parameters": self._map_primitive_parameters(
835 params_, user_values
Adam Israel88a49632018-04-10 13:04:57 -0600836 ),
837 }
838
839 for primitive in sorted(primitives):
Adam Israel32a15192019-06-24 11:44:47 -0400840 try:
beierlm32862bb2020-04-21 16:36:35 -0400841 # self.log.debug("Queuing action {}".format(
842 # primitives[primitive]['name']))
Adam Israel32a15192019-06-24 11:44:47 -0400843 uuids.append(
844 await self.ExecutePrimitive(
845 model_name,
846 application_name,
beierlm32862bb2020-04-21 16:36:35 -0400847 primitives[primitive]["name"],
Adam Israel32a15192019-06-24 11:44:47 -0400848 callback,
849 callback_args,
beierlm32862bb2020-04-21 16:36:35 -0400850 **primitives[primitive]["parameters"],
Adam Israel32a15192019-06-24 11:44:47 -0400851 )
Adam Israel5e08a0e2018-09-06 19:22:47 -0400852 )
Adam Israel32a15192019-06-24 11:44:47 -0400853 except PrimitiveDoesNotExist as e:
beierlm32862bb2020-04-21 16:36:35 -0400854 self.log.debug(
855 "Ignoring exception PrimitiveDoesNotExist: {}".format(e)
856 )
Adam Israel32a15192019-06-24 11:44:47 -0400857 pass
858 except Exception as e:
beierlm32862bb2020-04-21 16:36:35 -0400859 self.log.debug(
860 (
861 "XXXXXXXXXXXXXXXXXXXXXXXXX Unexpected exception: {}"
862 ).format(e)
863 )
Adam Israel32a15192019-06-24 11:44:47 -0400864 raise e
865
Adam Israel88a49632018-04-10 13:04:57 -0600866 except N2VCPrimitiveExecutionFailed as e:
beierlm32862bb2020-04-21 16:36:35 -0400867 self.log.debug("[N2VC] Exception executing primitive: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600868 raise
Adam Israel5e08a0e2018-09-06 19:22:47 -0400869 return uuids
Adam Israel88a49632018-04-10 13:04:57 -0600870
beierlm32862bb2020-04-21 16:36:35 -0400871 async def ExecutePrimitive(
872 self,
873 model_name,
874 application_name,
875 primitive,
876 callback,
877 *callback_args,
878 **params
879 ):
Adam Israelc9df96f2018-05-03 14:49:56 -0400880 """Execute a primitive of a charm for Day 1 or Day 2 configuration.
Adam Israel6817f612018-04-13 08:41:43 -0600881
Adam Israelc9df96f2018-05-03 14:49:56 -0400882 Execute a primitive defined in the VNF descriptor.
883
Adam Israel85a4b212018-11-29 20:30:24 -0500884 :param str model_name: The name or unique id of the network service.
Adam Israelc9df96f2018-05-03 14:49:56 -0400885 :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.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400888 :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
Adam Israelc9df96f2018-05-03 14:49:56 -0400892 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 }
Adam Israel6817f612018-04-13 08:41:43 -0600898 """
tierno1afb30a2018-12-21 13:42:43 +0000899 self.log.debug("Executing primitive={} params={}".format(primitive, params))
Adam Israel6817f612018-04-13 08:41:43 -0600900 uuid = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500901 try:
902 if not self.authenticated:
903 await self.login()
904
Adam Israel5e08a0e2018-09-06 19:22:47 -0400905 model = await self.get_model(model_name)
Adam Israelb5214512018-05-03 10:00:04 -0400906
beierlm32862bb2020-04-21 16:36:35 -0400907 if primitive == "config":
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500908 # config is special, and expecting params to be a dictionary
Adam Israelb0943662018-08-02 15:32:00 -0400909 await self.set_config(
beierlm32862bb2020-04-21 16:36:35 -0400910 model, application_name, params["params"],
Adam Israelb0943662018-08-02 15:32:00 -0400911 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500912 else:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500913 app = await self.get_application(model, application_name)
914 if app:
Adam Israel32a15192019-06-24 11:44:47 -0400915 # Does this primitive exist?
916 actions = await app.get_actions()
917
918 if primitive not in actions.keys():
beierlm32862bb2020-04-21 16:36:35 -0400919 raise PrimitiveDoesNotExist(
920 "Primitive {} does not exist".format(primitive)
921 )
Adam Israel32a15192019-06-24 11:44:47 -0400922
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500923 # Run against the first (and probably only) unit in the app
924 unit = app.units[0]
925 if unit:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500926 action = await unit.run_action(primitive, **params)
Adam Israel6817f612018-04-13 08:41:43 -0600927 uuid = action.id
Adam Israel32a15192019-06-24 11:44:47 -0400928 except PrimitiveDoesNotExist as e:
929 # Catch and raise this exception if it's thrown from the inner block
930 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500931 except Exception as e:
Adam Israel32a15192019-06-24 11:44:47 -0400932 # An unexpected exception was caught
beierlm32862bb2020-04-21 16:36:35 -0400933 self.log.debug("Caught exception while executing primitive: {}".format(e))
Adam Israel7d871fb2018-07-17 12:17:06 -0400934 raise N2VCPrimitiveExecutionFailed(e)
Adam Israel6817f612018-04-13 08:41:43 -0600935 return uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500936
beierlm32862bb2020-04-21 16:36:35 -0400937 async def RemoveCharms(
938 self, model_name, application_name, callback=None, *callback_args
939 ):
Adam Israelc9df96f2018-05-03 14:49:56 -0400940 """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.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400947 :param tuple callback_args: A list of arguments to be passed to the
948 callback function.
Adam Israelc9df96f2018-05-03 14:49:56 -0400949 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500950 try:
951 if not self.authenticated:
952 await self.login()
953
954 model = await self.get_model(model_name)
955 app = await self.get_application(model, application_name)
956 if app:
Adam Israel28a43c02018-04-23 16:04:54 -0400957 # Remove this application from event monitoring
Adam Israel04eee1f2019-04-29 14:59:45 -0400958 await self.Unsubscribe(model_name, application_name)
Adam Israel28a43c02018-04-23 16:04:54 -0400959
beierlm32862bb2020-04-21 16:36:35 -0400960 # self.notify_callback(model_name, application_name, "removing",
961 # callback, *callback_args)
962 self.log.debug("Removing the application {}".format(application_name))
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500963 await app.remove()
Adam Israel28a43c02018-04-23 16:04:54 -0400964
Adam Israel0cd1c022019-09-03 18:26:08 -0400965 # await self.disconnect_model(self.monitors[model_name])
Adam Israel85a4b212018-11-29 20:30:24 -0500966
Adam Israel5e08a0e2018-09-06 19:22:47 -0400967 self.notify_callback(
968 model_name,
969 application_name,
970 "removed",
Adam Israelc4f393e2019-03-19 16:33:30 -0400971 "Removing charm {}".format(application_name),
Adam Israel5e08a0e2018-09-06 19:22:47 -0400972 callback,
973 *callback_args,
974 )
Adam Israel28a43c02018-04-23 16:04:54 -0400975
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500976 except Exception as e:
977 print("Caught exception: {}".format(e))
Adam Israel88a49632018-04-10 13:04:57 -0600978 self.log.debug(e)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500979 raise e
980
Adam Israel6d84dbd2019-03-08 18:33:35 -0500981 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 if not self.authenticated:
992 await self.login()
993
994 models = await self.controller.list_models()
995 if ns_uuid not in models:
Adam Israel19c5cfc2019-10-03 12:35:38 -0400996 # Get the new model
997 await self.get_model(ns_uuid)
Adam Israel7bf2f4d2019-03-15 15:28:47 -0400998
Adam Israel6d84dbd2019-03-08 18:33:35 -0500999 return True
1000
1001 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.
Adam Israelc4f393e2019-03-19 16:33:30 -04001013 if ns_uuid.lower() == "default":
Adam Israel6d84dbd2019-03-08 18:33:35 -05001014 return False
1015
1016 if not self.authenticated:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001017 await self.login()
1018
Adam Israel0cd1c022019-09-03 18:26:08 -04001019 models = await self.controller.list_models()
1020 if ns_uuid in models:
1021 model = await self.controller.get_model(ns_uuid)
1022
1023 for application in model.applications:
1024 app = model.applications[application]
1025
1026 await self.RemoveCharms(ns_uuid, application)
1027
1028 self.log.debug("Unsubscribing Watcher for {}".format(application))
1029 await self.Unsubscribe(ns_uuid, application)
1030
1031 self.log.debug("Waiting for application to terminate")
1032 timeout = 30
1033 try:
1034 await model.block_until(
1035 lambda: all(
beierlm32862bb2020-04-21 16:36:35 -04001036 unit.workload_status in ["terminated"] for unit in app.units
Adam Israel0cd1c022019-09-03 18:26:08 -04001037 ),
beierlm32862bb2020-04-21 16:36:35 -04001038 timeout=timeout,
Adam Israel0cd1c022019-09-03 18:26:08 -04001039 )
beierlm32862bb2020-04-21 16:36:35 -04001040 except Exception:
1041 self.log.debug(
1042 "Timed out waiting for {} to terminate.".format(application)
1043 )
Adam Israel0cd1c022019-09-03 18:26:08 -04001044
1045 for machine in model.machines:
1046 try:
1047 self.log.debug("Destroying machine {}".format(machine))
1048 await model.machines[machine].destroy(force=True)
1049 except JujuAPIError as e:
beierlm32862bb2020-04-21 16:36:35 -04001050 if "does not exist" in str(e):
Adam Israel0cd1c022019-09-03 18:26:08 -04001051 # Our cached model may be stale, because the machine
1052 # has already been removed. It's safe to continue.
1053 continue
1054 else:
1055 self.log.debug("Caught exception: {}".format(e))
1056 raise e
1057
Adam Israel6d84dbd2019-03-08 18:33:35 -05001058 # Disconnect from the Model
1059 if ns_uuid in self.models:
Adam Israel0cd1c022019-09-03 18:26:08 -04001060 self.log.debug("Disconnecting model {}".format(ns_uuid))
1061 # await self.disconnect_model(self.models[ns_uuid])
1062 await self.disconnect_model(ns_uuid)
Adam Israel6d84dbd2019-03-08 18:33:35 -05001063
1064 try:
Adam Israel0cd1c022019-09-03 18:26:08 -04001065 self.log.debug("Destroying model {}".format(ns_uuid))
Adam Israel6d84dbd2019-03-08 18:33:35 -05001066 await self.controller.destroy_models(ns_uuid)
Adam Israelc4f393e2019-03-19 16:33:30 -04001067 except JujuError:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001068 raise NetworkServiceDoesNotExist(
1069 "The Network Service '{}' does not exist".format(ns_uuid)
1070 )
1071
1072 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001073
Adam Israelb5214512018-05-03 10:00:04 -04001074 async def GetMetrics(self, model_name, application_name):
1075 """Get the metrics collected by the VCA.
1076
Adam Israel85a4b212018-11-29 20:30:24 -05001077 :param model_name The name or unique id of the network service
Adam Israelb5214512018-05-03 10:00:04 -04001078 :param application_name The name of the application
1079 """
1080 metrics = {}
1081 model = await self.get_model(model_name)
1082 app = await self.get_application(model, application_name)
1083 if app:
1084 metrics = await app.get_metrics()
1085
1086 return metrics
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001087
Adam Israelfa329072018-09-14 11:26:13 -04001088 async def HasApplication(self, model_name, application_name):
1089 model = await self.get_model(model_name)
1090 app = await self.get_application(model, application_name)
1091 if app:
1092 return True
1093 return False
1094
Adam Israel04eee1f2019-04-29 14:59:45 -04001095 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 self.monitors[ns_name].AddApplication(
beierlm32862bb2020-04-21 16:36:35 -04001105 application_name, callback, *callback_args
Adam Israel04eee1f2019-04-29 14:59:45 -04001106 )
1107
1108 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 """
beierlm32862bb2020-04-21 16:36:35 -04001116 self.monitors[ns_name].RemoveApplication(application_name,)
Adam Israel04eee1f2019-04-29 14:59:45 -04001117
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001118 # Non-public methods
Adam Israel136186e2018-09-14 12:01:12 -04001119 async def add_relation(self, model_name, relation1, relation2):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001120 """
1121 Add a relation between two application endpoints.
1122
Adam Israel85a4b212018-11-29 20:30:24 -05001123 :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>]'
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001126 """
Adam Israel136186e2018-09-14 12:01:12 -04001127
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001128 if not self.authenticated:
1129 await self.login()
1130
Adam Israel136186e2018-09-14 12:01:12 -04001131 m = await self.get_model(model_name)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001132 try:
Adam Israel136186e2018-09-14 12:01:12 -04001133 await m.add_relation(relation1, relation2)
1134 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.
beierlm32862bb2020-04-21 16:36:35 -04001138 if "not found" in e.message:
Adam Israel136186e2018-09-14 12:01:12 -04001139 return
beierlm32862bb2020-04-21 16:36:35 -04001140 if "already exists" in e.message:
Adam Israel136186e2018-09-14 12:01:12 -04001141 return
1142
1143 raise e
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001144
Adam Israelb5214512018-05-03 10:00:04 -04001145 # 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)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001151
1152 def _get_config_from_dict(self, config_primitive, values):
Adam Israel88a49632018-04-10 13:04:57 -06001153 """Transform the yang config primitive to dict.
1154
1155 Expected result:
1156
1157 config = {
1158 'config':
1159 }
1160 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001161 config = {}
1162 for primitive in config_primitive:
beierlm32862bb2020-04-21 16:36:35 -04001163 if primitive["name"] == "config":
Adam Israel88a49632018-04-10 13:04:57 -06001164 # config = self._map_primitive_parameters()
beierlm32862bb2020-04-21 16:36:35 -04001165 for parameter in primitive["parameter"]:
1166 param = str(parameter["name"])
1167 if parameter["value"] == "<rw_mgmt_ip>":
1168 config[param] = str(values[parameter["value"]])
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001169 else:
beierlm32862bb2020-04-21 16:36:35 -04001170 config[param] = str(parameter["value"])
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001171
1172 return config
1173
tierno1afb30a2018-12-21 13:42:43 +00001174 def _map_primitive_parameters(self, parameters, user_values):
Adam Israel88a49632018-04-10 13:04:57 -06001175 params = {}
1176 for parameter in parameters:
beierlm32862bb2020-04-21 16:36:35 -04001177 param = str(parameter["name"])
1178 value = parameter.get("value")
tierno1afb30a2018-12-21 13:42:43 +00001179
beierlm32862bb2020-04-21 16:36:35 -04001180 # map parameters inside a < >; e.g. <rw_mgmt_ip>. with the provided user
1181 # _values.
tierno1afb30a2018-12-21 13:42:43 +00001182 # Must exist at user_values except if there is a default value
1183 if isinstance(value, str) and value.startswith("<") and value.endswith(">"):
beierlm32862bb2020-04-21 16:36:35 -04001184 if parameter["value"][1:-1] in user_values:
1185 value = user_values[parameter["value"][1:-1]]
1186 elif "default-value" in parameter:
1187 value = parameter["default-value"]
tierno1afb30a2018-12-21 13:42:43 +00001188 else:
beierlm32862bb2020-04-21 16:36:35 -04001189 raise KeyError(
1190 "parameter {}='{}' not supplied ".format(param, value)
1191 )
Adam Israel5e08a0e2018-09-06 19:22:47 -04001192
Adam Israelbf793522018-11-20 13:54:13 -05001193 # If there's no value, use the default-value (if set)
beierlm32862bb2020-04-21 16:36:35 -04001194 if value is None and "default-value" in parameter:
1195 value = parameter["default-value"]
Adam Israelbf793522018-11-20 13:54:13 -05001196
Adam Israel5e08a0e2018-09-06 19:22:47 -04001197 # Typecast parameter value, if present
tierno1afb30a2018-12-21 13:42:43 +00001198 paramtype = "string"
1199 try:
beierlm32862bb2020-04-21 16:36:35 -04001200 if "data-type" in parameter:
1201 paramtype = str(parameter["data-type"]).lower()
Adam Israel5e08a0e2018-09-06 19:22:47 -04001202
tierno1afb30a2018-12-21 13:42:43 +00001203 if paramtype == "integer":
1204 value = int(value)
1205 elif paramtype == "boolean":
1206 value = bool(value)
1207 else:
1208 value = str(value)
Adam Israel5e08a0e2018-09-06 19:22:47 -04001209 else:
tierno1afb30a2018-12-21 13:42:43 +00001210 # If there's no data-type, assume the value is a string
1211 value = str(value)
1212 except ValueError:
beierlm32862bb2020-04-21 16:36:35 -04001213 raise ValueError(
1214 "parameter {}='{}' cannot be converted to type {}".format(
1215 param, value, paramtype
1216 )
1217 )
Adam Israel5e08a0e2018-09-06 19:22:47 -04001218
tierno1afb30a2018-12-21 13:42:43 +00001219 params[param] = value
Adam Israel88a49632018-04-10 13:04:57 -06001220 return params
1221
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001222 def _get_config_from_yang(self, config_primitive, values):
1223 """Transform the yang config primitive to dict."""
1224 config = {}
1225 for primitive in config_primitive.values():
beierlm32862bb2020-04-21 16:36:35 -04001226 if primitive["name"] == "config":
1227 for parameter in primitive["parameter"].values():
1228 param = str(parameter["name"])
1229 if parameter["value"] == "<rw_mgmt_ip>":
1230 config[param] = str(values[parameter["value"]])
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001231 else:
beierlm32862bb2020-04-21 16:36:35 -04001232 config[param] = str(parameter["value"])
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001233
1234 return config
1235
1236 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 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001251 appname = ""
1252 for c in "-".join(list(args)):
1253 if c.isdigit():
1254 c = chr(97 + int(c))
1255 elif not c.isalpha():
1256 c = "-"
1257 appname += c
beierlm32862bb2020-04-21 16:36:35 -04001258 return re.sub("-+", "-", appname.lower())
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001259
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001260 # 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 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
beierlm32862bb2020-04-21 16:36:35 -04001283 return name.replace("_", "-").lower()
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001284
1285 async def get_application(self, model, application):
1286 """Get the deployed application."""
1287 if not self.authenticated:
1288 await self.login()
1289
1290 app = None
1291 if application and model:
1292 if model.applications:
1293 if application in model.applications:
1294 app = model.applications[application]
1295
1296 return app
1297
Adam Israel85a4b212018-11-29 20:30:24 -05001298 async def get_model(self, model_name):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001299 """Get a model from the Juju Controller.
1300
1301 Note: Model objects returned must call disconnected() before it goes
1302 out of scope."""
1303 if not self.authenticated:
1304 await self.login()
1305
1306 if model_name not in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001307 # Get the models in the controller
1308 models = await self.controller.list_models()
1309
1310 if model_name not in models:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001311 try:
1312 self.models[model_name] = await self.controller.add_model(
beierlm32862bb2020-04-21 16:36:35 -04001313 model_name, config={"authorized-keys": self.juju_public_key}
Adam Israel6d84dbd2019-03-08 18:33:35 -05001314 )
1315 except JujuError as e:
1316 if "already exists" not in e.message:
1317 raise e
Adam Israel85a4b212018-11-29 20:30:24 -05001318 else:
beierlm32862bb2020-04-21 16:36:35 -04001319 self.models[model_name] = await self.controller.get_model(model_name)
Adam Israel85a4b212018-11-29 20:30:24 -05001320
beierlm32862bb2020-04-21 16:36:35 -04001321 self.refcount["model"] += 1
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001322
Adam Israel28a43c02018-04-23 16:04:54 -04001323 # Create an observer for this model
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001324 await self.create_model_monitor(model_name)
1325
1326 return self.models[model_name]
1327
1328 async def create_model_monitor(self, model_name):
1329 """Create a monitor for the model, if none exists."""
1330 if not self.authenticated:
1331 await self.login()
1332
1333 if model_name not in self.monitors:
Adam Israel28a43c02018-04-23 16:04:54 -04001334 self.monitors[model_name] = VCAMonitor(model_name)
1335 self.models[model_name].add_observer(self.monitors[model_name])
1336
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001337 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001338
1339 async def login(self):
1340 """Login to the Juju controller."""
1341
1342 if self.authenticated:
1343 return
1344
1345 self.connecting = True
1346
1347 self.log.debug("JujuApi: Logging into controller")
1348
Adam Israel5e08a0e2018-09-06 19:22:47 -04001349 self.controller = Controller(loop=self.loop)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001350
1351 if self.secret:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001352 self.log.debug(
Adam Israel19c5cfc2019-10-03 12:35:38 -04001353 "Connecting to controller... ws://{} as {}/{}".format(
beierlm32862bb2020-04-21 16:36:35 -04001354 self.endpoint, self.user, self.secret,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001355 )
1356 )
Adam Israel19c5cfc2019-10-03 12:35:38 -04001357 try:
1358 await self.controller.connect(
1359 endpoint=self.endpoint,
1360 username=self.user,
1361 password=self.secret,
1362 cacert=self.ca_cert,
1363 )
beierlm32862bb2020-04-21 16:36:35 -04001364 self.refcount["controller"] += 1
Adam Israel19c5cfc2019-10-03 12:35:38 -04001365 self.authenticated = True
1366 self.log.debug("JujuApi: Logged into controller")
1367 except Exception as ex:
1368 self.log.debug("Caught exception: {}".format(ex))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001369 else:
1370 # current_controller no longer exists
1371 # self.log.debug("Connecting to current controller...")
1372 # await self.controller.connect_current()
Adam Israel88a49632018-04-10 13:04:57 -06001373 # await self.controller.connect(
1374 # endpoint=self.endpoint,
1375 # username=self.user,
1376 # cacert=cacert,
1377 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001378 self.log.fatal("VCA credentials not configured.")
Adam Israel19c5cfc2019-10-03 12:35:38 -04001379 self.authenticated = False
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001380
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001381 async def logout(self):
1382 """Logout of the Juju controller."""
1383 if not self.authenticated:
Adam Israel6d84dbd2019-03-08 18:33:35 -05001384 return False
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001385
1386 try:
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001387 for model in self.models:
Adam Israel85a4b212018-11-29 20:30:24 -05001388 await self.disconnect_model(model)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001389
1390 if self.controller:
beierlm32862bb2020-04-21 16:36:35 -04001391 self.log.debug("Disconnecting controller {}".format(self.controller))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001392 await self.controller.disconnect()
beierlm32862bb2020-04-21 16:36:35 -04001393 self.refcount["controller"] -= 1
Adam Israel5e08a0e2018-09-06 19:22:47 -04001394 self.controller = None
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001395
1396 self.authenticated = False
Adam Israelfc511ed2018-09-21 14:20:55 +02001397
1398 self.log.debug(self.refcount)
1399
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001400 except Exception as e:
beierlm32862bb2020-04-21 16:36:35 -04001401 self.log.fatal("Fatal error logging out of Juju Controller: {}".format(e))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001402 raise e
Adam Israel6d84dbd2019-03-08 18:33:35 -05001403 return True
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001404
Adam Israel85a4b212018-11-29 20:30:24 -05001405 async def disconnect_model(self, model):
1406 self.log.debug("Disconnecting model {}".format(model))
1407 if model in self.models:
Adam Israel0cd1c022019-09-03 18:26:08 -04001408 try:
1409 await self.models[model].disconnect()
beierlm32862bb2020-04-21 16:36:35 -04001410 self.refcount["model"] -= 1
Adam Israel0cd1c022019-09-03 18:26:08 -04001411 self.models[model] = None
1412 except Exception as e:
1413 self.log.debug("Caught exception: {}".format(e))
1414
beierlm32862bb2020-04-21 16:36:35 -04001415 async def provision_machine(
1416 self, model_name: str, hostname: str, username: str, private_key_path: str
1417 ) -> int:
Adam Israel0cd1c022019-09-03 18:26:08 -04001418 """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
beierlm32862bb2020-04-21 16:36:35 -04001426 :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
Adam Israel0cd1c022019-09-03 18:26:08 -04001430 """
1431 if not self.authenticated:
1432 await self.login()
1433
1434 machine_id = None
1435
1436 if self.api_proxy:
beierlm32862bb2020-04-21 16:36:35 -04001437 self.log.debug(
1438 "Instantiating SSH Provisioner for {}@{} ({})".format(
1439 username, hostname, private_key_path
1440 )
1441 )
Adam Israel0cd1c022019-09-03 18:26:08 -04001442 provisioner = SSHProvisioner(
1443 host=hostname,
1444 user=username,
1445 private_key_path=private_key_path,
1446 log=self.log,
1447 )
1448
1449 params = None
1450 try:
1451 params = provisioner.provision_machine()
1452 except Exception as ex:
1453 self.log.debug("caught exception from provision_machine: {}".format(ex))
1454 return None
1455
1456 if params:
beierlm32862bb2020-04-21 16:36:35 -04001457 params.jobs = ["JobHostUnits"]
Adam Israel0cd1c022019-09-03 18:26:08 -04001458
1459 model = await self.get_model(model_name)
1460
1461 connection = model.connection()
1462
1463 # Submit the request.
1464 self.log.debug("Adding machine to model")
1465 client_facade = client.ClientFacade.from_connection(connection)
1466 results = await client_facade.AddMachines(params=[params])
1467 error = results.machines[0].error
1468 if error:
1469 raise ValueError("Error adding machine: %s" % error.message)
1470
1471 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 self.log.debug("Installing Juju agent")
1476 await provisioner.install_agent(
beierlm32862bb2020-04-21 16:36:35 -04001477 connection, params.nonce, machine_id, self.api_proxy,
Adam Israel0cd1c022019-09-03 18:26:08 -04001478 )
1479 else:
1480 self.log.debug("Missing API Proxy")
1481 return machine_id
Adam Israel85a4b212018-11-29 20:30:24 -05001482
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001483 # 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 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 if not self.authenticated:
1504 await self.login()
1505
beierlm32862bb2020-04-21 16:36:35 -04001506 # m = await self.get_model()
1507 # try:
1508 # m.remove_relation(a, b)
1509 # finally:
1510 # await m.disconnect()
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001511
Adam Israel85a4b212018-11-29 20:30:24 -05001512 async def resolve_error(self, model_name, application=None):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001513 """Resolve units in error state."""
1514 if not self.authenticated:
1515 await self.login()
1516
Adam Israel85a4b212018-11-29 20:30:24 -05001517 model = await self.get_model(model_name)
1518
1519 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001520 if app:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001521 self.log.debug(
beierlm32862bb2020-04-21 16:36:35 -04001522 "JujuApi: Resolving errors for application {}".format(application,)
Adam Israel5e08a0e2018-09-06 19:22:47 -04001523 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001524
beierlm32862bb2020-04-21 16:36:35 -04001525 for _ in app.units:
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001526 app.resolved(retry=True)
1527
Adam Israel85a4b212018-11-29 20:30:24 -05001528 async def run_action(self, model_name, application, action_name, **params):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001529 """Execute an action and return an Action object."""
1530 if not self.authenticated:
1531 await self.login()
beierlm32862bb2020-04-21 16:36:35 -04001532 result = {"status": "", "action": {"tag": None, "results": None}}
Adam Israel85a4b212018-11-29 20:30:24 -05001533
1534 model = await self.get_model(model_name)
1535
1536 app = await self.get_application(model, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001537 if app:
1538 # We currently only have one unit per application
1539 # so use the first unit available.
1540 unit = app.units[0]
1541
Adam Israel5e08a0e2018-09-06 19:22:47 -04001542 self.log.debug(
1543 "JujuApi: Running Action {} against Application {}".format(
beierlm32862bb2020-04-21 16:36:35 -04001544 action_name, application,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001545 )
1546 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001547
1548 action = await unit.run_action(action_name, **params)
1549
1550 # Wait for the action to complete
1551 await action.wait()
1552
beierlm32862bb2020-04-21 16:36:35 -04001553 result["status"] = action.status
1554 result["action"]["tag"] = action.data["id"]
1555 result["action"]["results"] = action.results
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001556
1557 return result
1558
Adam Israelb5214512018-05-03 10:00:04 -04001559 async def set_config(self, model_name, application, config):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001560 """Apply a configuration to the application."""
1561 if not self.authenticated:
1562 await self.login()
1563
Adam Israelb5214512018-05-03 10:00:04 -04001564 app = await self.get_application(model_name, application)
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001565 if app:
beierlm32862bb2020-04-21 16:36:35 -04001566 self.log.debug(
1567 "JujuApi: Setting config for Application {}".format(application,)
1568 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001569 await app.set_config(config)
1570
1571 # Verify the config is set
1572 newconf = await app.get_config()
1573 for key in config:
beierlm32862bb2020-04-21 16:36:35 -04001574 if config[key] != newconf[key]["value"]:
1575 self.log.debug(
1576 (
1577 "JujuApi: Config not set! Key {} Value {} doesn't match {}"
1578 ).format(key, config[key], newconf[key])
1579 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001580
Adam Israelb5214512018-05-03 10:00:04 -04001581 # 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,
beierlm32862bb2020-04-21 16:36:35 -04001594 # )
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001595
beierlm32862bb2020-04-21 16:36:35 -04001596 async def wait_for_application(self, model_name, application_name, timeout=300):
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001597 """Wait for an application to become active."""
1598 if not self.authenticated:
1599 await self.login()
1600
Adam Israel5e08a0e2018-09-06 19:22:47 -04001601 model = await self.get_model(model_name)
1602
1603 app = await self.get_application(model, application_name)
1604 self.log.debug("Application: {}".format(app))
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001605 if app:
1606 self.log.debug(
1607 "JujuApi: Waiting {} seconds for Application {}".format(
beierlm32862bb2020-04-21 16:36:35 -04001608 timeout, application_name,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001609 )
1610 )
1611
Adam Israel5e08a0e2018-09-06 19:22:47 -04001612 await model.block_until(
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001613 lambda: all(
beierlm32862bb2020-04-21 16:36:35 -04001614 unit.agent_status == "idle"
1615 and unit.workload_status in ["active", "unknown"]
1616 for unit in app.units
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001617 ),
beierlm32862bb2020-04-21 16:36:35 -04001618 timeout=timeout,
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001619 )