blob: 912eab90567efcf375aaf331ab39768cf2b10e34 [file] [log] [blame]
Adam Israel5e08a0e2018-09-06 19:22:47 -04001#!/usr/bin/env python3
2import asyncio
Adam Israelfc511ed2018-09-21 14:20:55 +02003import datetime
Adam Israel5e08a0e2018-09-06 19:22:47 -04004import logging
5import n2vc.vnf
6import pylxd
7import pytest
8import os
9import shlex
Adam Israel5e08a0e2018-09-06 19:22:47 -040010import subprocess
Adam Israel5e08a0e2018-09-06 19:22:47 -040011import time
12import uuid
13import yaml
14
15from juju.controller import Controller
16
17# Disable InsecureRequestWarning w/LXD
18import urllib3
19urllib3.disable_warnings()
20logging.getLogger("urllib3").setLevel(logging.WARNING)
21
22here = os.path.dirname(os.path.realpath(__file__))
23
24
Adam Israel5e08a0e2018-09-06 19:22:47 -040025class CleanController():
26 """
27 Context manager that automatically connects and disconnects from
28 the currently active controller.
29
30 Note: Unlike CleanModel, this will not create a new controller for you,
31 and an active controller must already be available.
32 """
33 def __init__(self):
34 self._controller = None
35
36 async def __aenter__(self):
37 self._controller = Controller()
38 await self._controller.connect()
39 return self._controller
40
41 async def __aexit__(self, exc_type, exc, tb):
42 await self._controller.disconnect()
43
44
Adam Israelfc511ed2018-09-21 14:20:55 +020045def debug(msg):
46 """Format debug messages in a consistent way."""
47 now = datetime.datetime.now()
48
49 # TODO: Decide on the best way to log. Output from `logging.debug` shows up
50 # when a test fails, but print() will always show up when running tox with
51 # `-s`, which is really useful for debugging single tests without having to
52 # insert a False assert to see the log.
53 logging.debug(
54 "[{}] {}".format(now.strftime('%Y-%m-%dT%H:%M:%S'), msg)
55 )
Adam Israel85a4b212018-11-29 20:30:24 -050056 print(
57 "[{}] {}".format(now.strftime('%Y-%m-%dT%H:%M:%S'), msg)
58 )
Adam Israelfc511ed2018-09-21 14:20:55 +020059
60
Adam Israel5e08a0e2018-09-06 19:22:47 -040061def get_charm_path():
62 return "{}/charms".format(here)
63
64
65def get_layer_path():
66 return "{}/charms/layers".format(here)
67
68
Adam Israel5e08a0e2018-09-06 19:22:47 -040069def collect_metrics(application):
70 """Invoke Juju's metrics collector.
71
72 Caveat: this shells out to the `juju collect-metrics` command, rather than
73 making an API call. At the time of writing, that API is not exposed through
74 the client library.
75 """
76
77 try:
78 subprocess.check_call(['juju', 'collect-metrics', application])
79 except subprocess.CalledProcessError as e:
80 raise Exception("Unable to collect metrics: {}".format(e))
81
82
83def has_metrics(charm):
84 """Check if a charm has metrics defined."""
85 metricsyaml = "{}/{}/metrics.yaml".format(
86 get_layer_path(),
87 charm,
88 )
89 if os.path.exists(metricsyaml):
90 return True
91 return False
92
93
94def get_descriptor(descriptor):
95 desc = None
96 try:
97 tmp = yaml.load(descriptor)
98
99 # Remove the envelope
100 root = list(tmp.keys())[0]
101 if root == "nsd:nsd-catalog":
102 desc = tmp['nsd:nsd-catalog']['nsd'][0]
103 elif root == "vnfd:vnfd-catalog":
104 desc = tmp['vnfd:vnfd-catalog']['vnfd'][0]
105 except ValueError:
106 assert False
107 return desc
108
109
110def get_n2vc(loop=None):
111 """Return an instance of N2VC.VNF."""
112 log = logging.getLogger()
113 log.level = logging.DEBUG
114
Adam Israel5e08a0e2018-09-06 19:22:47 -0400115 # Extract parameters from the environment in order to run our test
116 vca_host = os.getenv('VCA_HOST', '127.0.0.1')
117 vca_port = os.getenv('VCA_PORT', 17070)
118 vca_user = os.getenv('VCA_USER', 'admin')
119 vca_charms = os.getenv('VCA_CHARMS', None)
120 vca_secret = os.getenv('VCA_SECRET', None)
121
Adam Israelb2a07f52019-04-25 17:17:05 -0400122 # Get the Juju Public key
123 juju_public_key = get_juju_public_key()
124 if juju_public_key:
125 debug("Reading Juju public key @ {}".format(juju_public_key))
126 with open(juju_public_key, 'r') as f:
127 juju_public_key = f.read()
128 debug("Found public key: {}".format(juju_public_key))
129 else:
130 raise Exception("No Juju Public Key found")
131
132 # Get the ca-cert
133 # os.path.expanduser("~/.config/lxc")
134 # with open("{}/agent.conf".format(AGENT_PATH), "r") as f:
135 # try:
136 # y = yaml.safe_load(f)
137 # self.cacert = y['cacert']
138 # except yaml.YAMLError as exc:
139 # log("Unable to find Juju ca-cert.")
140 # raise exc
141
Adam Israel5e08a0e2018-09-06 19:22:47 -0400142 client = n2vc.vnf.N2VC(
143 log=log,
144 server=vca_host,
145 port=vca_port,
146 user=vca_user,
147 secret=vca_secret,
148 artifacts=vca_charms,
Adam Israelb2a07f52019-04-25 17:17:05 -0400149 loop=loop,
150 juju_public_key=juju_public_key,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400151 )
152 return client
153
154
155def create_lxd_container(public_key=None, name="test_name"):
156 """
157 Returns a container object
158
159 If public_key isn't set, we'll use the Juju ssh key
160
161 :param public_key: The public key to inject into the container
162 :param name: The name of the test being run
163 """
164 container = None
165
166 # Format name so it's valid
167 name = name.replace("_", "-").replace(".", "")
168
169 client = get_lxd_client()
Adam Israelb2a07f52019-04-25 17:17:05 -0400170 if not client:
171 raise Exception("Unable to connect to LXD")
172
Adam Israel5e08a0e2018-09-06 19:22:47 -0400173 test_machine = "test-{}-{}".format(
174 uuid.uuid4().hex[-4:],
175 name,
176 )
177
Adam Israelfa329072018-09-14 11:26:13 -0400178 private_key_path, public_key_path = find_n2vc_ssh_keys()
Adam Israel5e08a0e2018-09-06 19:22:47 -0400179
Adam Israelfc511ed2018-09-21 14:20:55 +0200180 try:
181 # create profile w/cloud-init and juju ssh key
182 if not public_key:
183 public_key = ""
184 with open(public_key_path, "r") as f:
185 public_key = f.readline()
Adam Israel5e08a0e2018-09-06 19:22:47 -0400186
Adam Israelfc511ed2018-09-21 14:20:55 +0200187 client.profiles.create(
188 test_machine,
189 config={
190 'user.user-data': '#cloud-config\nssh_authorized_keys:\n- {}'.format(public_key)},
191 devices={
192 'root': {'path': '/', 'pool': 'default', 'type': 'disk'},
193 'eth0': {
194 'nictype': 'bridged',
195 'parent': 'lxdbr0',
196 'type': 'nic'
197 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400198 }
Adam Israelfc511ed2018-09-21 14:20:55 +0200199 )
200 except Exception as ex:
201 debug("Error creating lxd profile {}: {}".format(test_machine, ex))
202 raise ex
Adam Israel5e08a0e2018-09-06 19:22:47 -0400203
Adam Israelfc511ed2018-09-21 14:20:55 +0200204 try:
205 # create lxc machine
206 config = {
207 'name': test_machine,
208 'source': {
209 'type': 'image',
210 'alias': 'xenial',
211 'mode': 'pull',
212 'protocol': 'simplestreams',
213 'server': 'https://cloud-images.ubuntu.com/releases',
214 },
215 'profiles': [test_machine],
216 }
217 container = client.containers.create(config, wait=True)
218 container.start(wait=True)
219 except Exception as ex:
220 debug("Error creating lxd container {}: {}".format(test_machine, ex))
221 # This is a test-ending failure.
222 raise ex
Adam Israel5e08a0e2018-09-06 19:22:47 -0400223
224 def wait_for_network(container, timeout=30):
225 """Wait for eth0 to have an ipv4 address."""
226 starttime = time.time()
227 while(time.time() < starttime + timeout):
228 time.sleep(1)
229 if 'eth0' in container.state().network:
230 addresses = container.state().network['eth0']['addresses']
231 if len(addresses) > 0:
232 if addresses[0]['family'] == 'inet':
233 return addresses[0]
234 return None
235
Adam Israelfc511ed2018-09-21 14:20:55 +0200236 try:
237 wait_for_network(container)
238 except Exception as ex:
239 debug(
240 "Error waiting for container {} network: {}".format(
241 test_machine,
242 ex,
243 )
244 )
Adam Israel5e08a0e2018-09-06 19:22:47 -0400245
Adam Israelb2c234b2019-04-05 10:17:25 -0400246 try:
247 waitcount = 0
248 while waitcount <= 5:
249 if is_sshd_running(container):
250 break
251 waitcount += 1
252 time.sleep(1)
253 if waitcount >= 5:
254 debug("couldn't detect sshd running")
255 raise Exception("Unable to verify container sshd")
256
257 except Exception as ex:
258 debug(
259 "Error checking sshd status on {}: {}".format(
260 test_machine,
261 ex,
262 )
263 )
264
Adam Israel5e08a0e2018-09-06 19:22:47 -0400265 # HACK: We need to give sshd a chance to bind to the interface,
266 # and pylxd's container.execute seems to be broken and fails and/or
267 # hangs trying to properly check if the service is up.
Adam Israelfc511ed2018-09-21 14:20:55 +0200268 (exit_code, stdout, stderr) = container.execute([
269 'ping',
270 '-c', '5', # Wait for 5 ECHO_REPLY
271 '8.8.8.8', # Ping Google's public DNS
272 '-W', '15', # Set a 15 second deadline
273 ])
274 if exit_code > 0:
275 # The network failed
276 raise Exception("Unable to verify container network")
Adam Israel5e08a0e2018-09-06 19:22:47 -0400277
278 return container
279
280
Adam Israelb2c234b2019-04-05 10:17:25 -0400281def is_sshd_running(container):
282 """Check if sshd is running in the container.
283
284 Check to see if the sshd process is running and listening on port 22.
285
286 :param container: The container to check
287 :return boolean: True if sshd is running.
288 """
289 debug("Container: {}".format(container))
290 try:
291 (rc, stdout, stderr) = container.execute(
292 ["service", "ssh", "status"]
293 )
294 # If the status is a) found and b) running, the exit code will be 0
295 if rc == 0:
296 return True
297 except Exception as ex:
298 debug("Failed to check sshd service status: {}".format(ex))
299
300 return False
301
302
Adam Israel5e08a0e2018-09-06 19:22:47 -0400303def destroy_lxd_container(container):
Adam Israelfc511ed2018-09-21 14:20:55 +0200304 """Stop and delete a LXD container.
305
306 Sometimes we see errors talking to LXD -- ephemerial issues like
307 load or a bug that's killed the API. We'll do our best to clean
308 up here, and we should run a cleanup after all tests are finished
309 to remove any extra containers and profiles belonging to us.
310 """
311
312 if type(container) is bool:
313 return
314
Adam Israel5e08a0e2018-09-06 19:22:47 -0400315 name = container.name
Adam Israelfc511ed2018-09-21 14:20:55 +0200316 debug("Destroying container {}".format(name))
317
Adam Israel5e08a0e2018-09-06 19:22:47 -0400318 client = get_lxd_client()
319
320 def wait_for_stop(timeout=30):
321 """Wait for eth0 to have an ipv4 address."""
322 starttime = time.time()
323 while(time.time() < starttime + timeout):
324 time.sleep(1)
325 if container.state == "Stopped":
326 return
327
328 def wait_for_delete(timeout=30):
329 starttime = time.time()
330 while(time.time() < starttime + timeout):
331 time.sleep(1)
332 if client.containers.exists(name) is False:
333 return
334
Adam Israelfc511ed2018-09-21 14:20:55 +0200335 try:
336 container.stop(wait=False)
337 wait_for_stop()
338 except Exception as ex:
339 debug(
340 "Error stopping container {}: {}".format(
341 name,
342 ex,
343 )
344 )
Adam Israel5e08a0e2018-09-06 19:22:47 -0400345
Adam Israelfc511ed2018-09-21 14:20:55 +0200346 try:
347 container.delete(wait=False)
348 wait_for_delete()
349 except Exception as ex:
350 debug(
351 "Error deleting container {}: {}".format(
352 name,
353 ex,
354 )
355 )
Adam Israel5e08a0e2018-09-06 19:22:47 -0400356
Adam Israelfc511ed2018-09-21 14:20:55 +0200357 try:
358 # Delete the profile created for this container
359 profile = client.profiles.get(name)
360 if profile:
361 profile.delete()
362 except Exception as ex:
363 debug(
364 "Error deleting profile {}: {}".format(
365 name,
366 ex,
367 )
368 )
Adam Israel5e08a0e2018-09-06 19:22:47 -0400369
370
371def find_lxd_config():
372 """Find the LXD configuration directory."""
373 paths = []
374 paths.append(os.path.expanduser("~/.config/lxc"))
375 paths.append(os.path.expanduser("~/snap/lxd/current/.config/lxc"))
376
377 for path in paths:
378 if os.path.exists(path):
379 crt = os.path.expanduser("{}/client.crt".format(path))
380 key = os.path.expanduser("{}/client.key".format(path))
381 if os.path.exists(crt) and os.path.exists(key):
382 return (crt, key)
383 return (None, None)
384
385
Adam Israelfa329072018-09-14 11:26:13 -0400386def find_n2vc_ssh_keys():
387 """Find the N2VC ssh keys."""
388
389 paths = []
390 paths.append(os.path.expanduser("~/.ssh/"))
391
392 for path in paths:
393 if os.path.exists(path):
394 private = os.path.expanduser("{}/id_n2vc_rsa".format(path))
395 public = os.path.expanduser("{}/id_n2vc_rsa.pub".format(path))
396 if os.path.exists(private) and os.path.exists(public):
397 return (private, public)
398 return (None, None)
399
400
Adam Israel5e08a0e2018-09-06 19:22:47 -0400401def find_juju_ssh_keys():
402 """Find the Juju ssh keys."""
403
404 paths = []
Adam Israelb2a07f52019-04-25 17:17:05 -0400405 paths.append(os.path.expanduser("~/.local/share/juju/ssh"))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400406
407 for path in paths:
408 if os.path.exists(path):
409 private = os.path.expanduser("{}/juju_id_rsa".format(path))
410 public = os.path.expanduser("{}/juju_id_rsa.pub".format(path))
411 if os.path.exists(private) and os.path.exists(public):
412 return (private, public)
413 return (None, None)
414
415
416def get_juju_private_key():
417 keys = find_juju_ssh_keys()
418 return keys[0]
419
420
Adam Israelb2a07f52019-04-25 17:17:05 -0400421def get_juju_public_key():
422 """Find the Juju public key."""
423 paths = []
424
425 if 'VCA_PATH' in os.environ:
426 paths.append("{}/ssh".format(os.environ["VCA_PATH"]))
427
428 paths.append(os.path.expanduser("~/.local/share/juju/ssh"))
429 paths.append("/root/.local/share/juju/ssh")
430
431 for path in paths:
432 if os.path.exists(path):
433 public = os.path.expanduser("{}/juju_id_rsa.pub".format(path))
434 if os.path.exists(public):
435 return public
436 return None
437
438
439def get_lxd_client(host=None, port="8443", verify=False):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400440 """ Get the LXD client."""
Adam Israelb2a07f52019-04-25 17:17:05 -0400441
442 if host is None:
443 if 'LXD_HOST' in os.environ:
444 host = os.environ['LXD_HOST']
445 else:
446 host = '127.0.0.1'
447
448 passwd = None
449 if 'LXD_SECRET' in os.environ:
450 passwd = os.environ['LXD_SECRET']
451
452 # debug("Connecting to LXD remote {} w/authentication ({})".format(
453 # host,
454 # passwd
455 # ))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400456 client = None
457 (crt, key) = find_lxd_config()
458
459 if crt and key:
460 client = pylxd.Client(
461 endpoint="https://{}:{}".format(host, port),
462 cert=(crt, key),
463 verify=verify,
464 )
465
Adam Israelb2a07f52019-04-25 17:17:05 -0400466 # If the LXD server has a pasword set, authenticate with it.
467 if not client.trusted and passwd:
468 try:
469 client.authenticate(passwd)
470 if not client.trusted:
471 raise Exception("Unable to authenticate with LXD remote")
472 except pylxd.exceptions.LXDAPIException as ex:
473 if 'Certificate already in trust store' in ex:
474 pass
475
Adam Israel5e08a0e2018-09-06 19:22:47 -0400476 return client
477
Adam Israelfc511ed2018-09-21 14:20:55 +0200478
Adam Israel5e08a0e2018-09-06 19:22:47 -0400479# TODO: This is marked serial but can be run in parallel with work, including:
480# - Fixing an event loop issue; seems that all tests stop when one test stops?
481
482
483@pytest.mark.serial
484class TestN2VC(object):
485 """TODO:
486 1. Validator Validation
487
Adam Israelb2a07f52019-04-25 17:17:05 -0400488 Automatically validate the descriptors we're using here, unless the test
489 author explicitly wants to skip them. Useful to make sure tests aren't
490 being run against invalid descriptors, validating functionality that may
491 fail against a properly written descriptor.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400492
Adam Israelb2a07f52019-04-25 17:17:05 -0400493 We need to have a flag (instance variable) that controls this behavior. It
494 may be necessary to skip validation and run against a descriptor
495 implementing features that have not yet been released in the Information
496 Model.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400497 """
498
Adam Israelfc511ed2018-09-21 14:20:55 +0200499 """
500 The six phases of integration testing, for the test itself and each charm?:
501
502 setup/teardown_class:
503 1. Prepare - Verify the environment and create a new model
504 2. Deploy - Mark the test as ready to execute
505 3. Configure - Configuration to reach Active state
506 4. Test - Execute primitive(s) to verify success
507 5. Collect - Collect any useful artifacts for debugging (charm, logs)
508 6. Destroy - Destroy the model
509
510
511 1. Prepare - Building of charm
512 2. Deploy - Deploying charm
513 3. Configure - Configuration to reach Active state
514 4. Test - Execute primitive(s) to verify success
515 5. Collect - Collect any useful artifacts for debugging (charm, logs)
516 6. Destroy - Destroy the charm
517
518 """
Adam Israel5e08a0e2018-09-06 19:22:47 -0400519 @classmethod
520 def setup_class(self):
521 """ setup any state specific to the execution of the given class (which
522 usually contains tests).
523 """
524 # Initialize instance variable(s)
Adam Israelfc511ed2018-09-21 14:20:55 +0200525 self.n2vc = None
Adam Israel13950822018-09-13 17:14:51 -0400526
527 # Track internal state for each test run
528 self.state = {}
Adam Israel5e08a0e2018-09-06 19:22:47 -0400529
530 # Parse the test's descriptors
531 self.nsd = get_descriptor(self.NSD_YAML)
532 self.vnfd = get_descriptor(self.VNFD_YAML)
533
534 self.ns_name = self.nsd['name']
535 self.vnf_name = self.vnfd['name']
536
537 self.charms = {}
538 self.parse_vnf_descriptor()
539 assert self.charms is not {}
540
541 # Track artifacts, like compiled charms, that will need to be removed
542 self.artifacts = {}
543
544 # Build the charm(s) needed for this test
545 for charm in self.get_charm_names():
Adam Israelb2a07f52019-04-25 17:17:05 -0400546 # debug("Building charm {}".format(charm))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400547 self.get_charm(charm)
548
549 # A bit of a hack, in order to allow the N2VC callback to run parallel
550 # to pytest. Test(s) should wait for this flag to change to False
551 # before returning.
552 self._running = True
Adam Israelfc511ed2018-09-21 14:20:55 +0200553 self._stopping = False
Adam Israel5e08a0e2018-09-06 19:22:47 -0400554
555 @classmethod
556 def teardown_class(self):
557 """ teardown any state that was previously setup with a call to
558 setup_class.
559 """
Adam Israelfc511ed2018-09-21 14:20:55 +0200560 debug("Running teardown_class...")
561 try:
Adam Israelfa329072018-09-14 11:26:13 -0400562
Adam Israelfc511ed2018-09-21 14:20:55 +0200563 debug("Destroying LXD containers...")
564 for application in self.state:
565 if self.state[application]['container']:
566 destroy_lxd_container(self.state[application]['container'])
567 debug("Destroying LXD containers...done.")
Adam Israel5e08a0e2018-09-06 19:22:47 -0400568
Adam Israelfc511ed2018-09-21 14:20:55 +0200569 # Logout of N2VC
570 if self.n2vc:
Adam Israelfa329072018-09-14 11:26:13 -0400571 debug("teardown_class(): Logging out of N2VC...")
Adam Israelfc511ed2018-09-21 14:20:55 +0200572 yield from self.n2vc.logout()
Adam Israelfa329072018-09-14 11:26:13 -0400573 debug("teardown_class(): Logging out of N2VC...done.")
574
Adam Israelfc511ed2018-09-21 14:20:55 +0200575 debug("Running teardown_class...done.")
576 except Exception as ex:
577 debug("Exception in teardown_class: {}".format(ex))
Adam Israel13950822018-09-13 17:14:51 -0400578
579 @classmethod
580 def all_charms_active(self):
581 """Determine if the all deployed charms are active."""
582 active = 0
Adam Israelfc511ed2018-09-21 14:20:55 +0200583
584 for application in self.state:
585 if 'status' in self.state[application]:
586 debug("status of {} is '{}'".format(
587 application,
588 self.state[application]['status'],
589 ))
590 if self.state[application]['status'] == 'active':
591 active += 1
592
593 debug("Active charms: {}/{}".format(
594 active,
595 len(self.charms),
596 ))
Adam Israel13950822018-09-13 17:14:51 -0400597
598 if active == len(self.charms):
Adam Israel13950822018-09-13 17:14:51 -0400599 return True
600
601 return False
Adam Israel5e08a0e2018-09-06 19:22:47 -0400602
603 @classmethod
Adam Israelfc511ed2018-09-21 14:20:55 +0200604 def are_tests_finished(self):
605 appcount = len(self.state)
606
607 # If we don't have state yet, keep running.
608 if appcount == 0:
609 debug("No applications")
610 return False
611
612 if self._stopping:
613 debug("_stopping is True")
614 return True
615
616 appdone = 0
617 for application in self.state:
618 if self.state[application]['done']:
619 appdone += 1
620
621 debug("{}/{} charms tested".format(appdone, appcount))
622
623 if appcount == appdone:
624 return True
625
626 return False
627
628 @classmethod
629 async def running(self, timeout=600):
Adam Israel5e08a0e2018-09-06 19:22:47 -0400630 """Returns if the test is still running.
631
632 @param timeout The time, in seconds, to wait for the test to complete.
633 """
Adam Israelfc511ed2018-09-21 14:20:55 +0200634 if self.are_tests_finished():
635 await self.stop()
636 return False
Adam Israel5e08a0e2018-09-06 19:22:47 -0400637
Adam Israelfc511ed2018-09-21 14:20:55 +0200638 await asyncio.sleep(30)
639
Adam Israel5e08a0e2018-09-06 19:22:47 -0400640 return self._running
641
642 @classmethod
643 def get_charm(self, charm):
644 """Build and return the path to the test charm.
645
646 Builds one of the charms in tests/charms/layers and returns the path
647 to the compiled charm. The charm will automatically be removed when
648 when the test is complete.
649
650 Returns: The path to the built charm or None if `charm build` failed.
651 """
Adam Israel5e08a0e2018-09-06 19:22:47 -0400652 # Make sure the charm snap is installed
Adam Israelb2a07f52019-04-25 17:17:05 -0400653 charm_cmd = None
Adam Israel5e08a0e2018-09-06 19:22:47 -0400654 try:
655 subprocess.check_call(['which', 'charm'])
Adam Israelb2a07f52019-04-25 17:17:05 -0400656 charm_cmd = "charm build"
Adam Israel85a4b212018-11-29 20:30:24 -0500657 except subprocess.CalledProcessError:
Adam Israelb2a07f52019-04-25 17:17:05 -0400658 # charm_cmd = "charm-build"
659 # debug("Using legacy charm-build")
Adam Israel5e08a0e2018-09-06 19:22:47 -0400660 raise Exception("charm snap not installed.")
661
662 if charm not in self.artifacts:
663 try:
Adam Israelfc511ed2018-09-21 14:20:55 +0200664 # Note: This builds the charm under N2VC/tests/charms/builds/
665 # Currently, the snap-installed command only has write access
666 # to the $HOME (changing in an upcoming release) so writing to
667 # /tmp isn't possible at the moment.
Adam Israel5e08a0e2018-09-06 19:22:47 -0400668
Adam Israelb2a07f52019-04-25 17:17:05 -0400669 builds = get_charm_path()
Adam Israelfc511ed2018-09-21 14:20:55 +0200670 if not os.path.exists("{}/builds/{}".format(builds, charm)):
Adam Israelb2a07f52019-04-25 17:17:05 -0400671 cmd = "{} --no-local-layers {}/{} -o {}/".format(
672 charm_cmd,
Adam Israelfc511ed2018-09-21 14:20:55 +0200673 get_layer_path(),
674 charm,
675 builds,
676 )
Adam Israelb2a07f52019-04-25 17:17:05 -0400677 # debug(cmd)
678
679 env = os.environ.copy()
680 env["CHARM_BUILD_DIR"] = builds
681
682 subprocess.check_call(shlex.split(cmd), env=env)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400683
Adam Israel5e08a0e2018-09-06 19:22:47 -0400684 except subprocess.CalledProcessError as e:
Adam Israelc4f393e2019-03-19 16:33:30 -0400685 # charm build will return error code 100 if the charm fails
686 # the auto-run of charm proof, which we can safely ignore for
687 # our CI charms.
688 if e.returncode != 100:
689 raise Exception("charm build failed: {}.".format(e))
690
691 self.artifacts[charm] = {
692 'tmpdir': builds,
693 'charm': "{}/builds/{}".format(builds, charm),
694 }
Adam Israel5e08a0e2018-09-06 19:22:47 -0400695
696 return self.artifacts[charm]['charm']
697
698 @classmethod
699 async def deploy(self, vnf_index, charm, params, loop):
700 """An inner function to do the deployment of a charm from
701 either a vdu or vnf.
702 """
703
Adam Israelfc511ed2018-09-21 14:20:55 +0200704 if not self.n2vc:
705 self.n2vc = get_n2vc(loop=loop)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400706
Adam Israel7bf2f4d2019-03-15 15:28:47 -0400707 debug("Creating model for Network Service {}".format(self.ns_name))
708 await self.n2vc.CreateNetworkService(self.ns_name)
709
Adam Israelfa329072018-09-14 11:26:13 -0400710 application = self.n2vc.FormatApplicationName(
Adam Israel5e08a0e2018-09-06 19:22:47 -0400711 self.ns_name,
712 self.vnf_name,
713 str(vnf_index),
714 )
Adam Israelfa329072018-09-14 11:26:13 -0400715
716 # Initialize the state of the application
717 self.state[application] = {
718 'status': None, # Juju status
719 'container': None, # lxd container, for proxy charms
720 'actions': {}, # Actions we've executed
721 'done': False, # Are we done testing this charm?
722 'phase': "deploy", # What phase is this application in?
723 }
724
Adam Israelfc511ed2018-09-21 14:20:55 +0200725 debug("Deploying charm at {}".format(self.artifacts[charm]))
Adam Israel5e08a0e2018-09-06 19:22:47 -0400726
Adam Israelfa329072018-09-14 11:26:13 -0400727 # If this is a native charm, we need to provision the underlying
728 # machine ala an LXC container.
729 machine_spec = {}
730
731 if not self.isproxy(application):
732 debug("Creating container for native charm")
733 # args = ("default", application, None, None)
734 self.state[application]['container'] = create_lxd_container(
735 name=os.path.basename(__file__)
736 )
737
738 hostname = self.get_container_ip(
739 self.state[application]['container'],
740 )
741
742 machine_spec = {
743 'host': hostname,
744 'user': 'ubuntu',
745 }
746
Adam Israel5e08a0e2018-09-06 19:22:47 -0400747 await self.n2vc.DeployCharms(
748 self.ns_name,
Adam Israelfa329072018-09-14 11:26:13 -0400749 application,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400750 self.vnfd,
751 self.get_charm(charm),
752 params,
Adam Israelfa329072018-09-14 11:26:13 -0400753 machine_spec,
Adam Israelfc511ed2018-09-21 14:20:55 +0200754 self.n2vc_callback,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400755 )
756
757 @classmethod
758 def parse_vnf_descriptor(self):
759 """Parse the VNF descriptor to make running tests easier.
760
761 Parse the charm information in the descriptor to make it easy to write
762 tests to run again it.
763
764 Each charm becomes a dictionary in a list:
765 [
766 'is-proxy': True,
767 'vnf-member-index': 1,
768 'vnf-name': '',
769 'charm-name': '',
Adam Israel5e08a0e2018-09-06 19:22:47 -0400770 'initial-config-primitive': {},
771 'config-primitive': {}
772 ]
773 - charm name
774 - is this a proxy charm?
775 - what are the initial-config-primitives (day 1)?
776 - what are the config primitives (day 2)?
777
778 """
779 charms = {}
780
781 # You'd think this would be explicit, but it's just an incremental
782 # value that should be consistent.
783 vnf_member_index = 0
784
785 """Get all vdu and/or vdu config in a descriptor."""
786 config = self.get_config()
787 for cfg in config:
788 if 'juju' in cfg:
789
790 # Get the name to be used for the deployed application
791 application_name = n2vc.vnf.N2VC().FormatApplicationName(
792 self.ns_name,
793 self.vnf_name,
794 str(vnf_member_index),
795 )
796
797 charm = {
798 'application-name': application_name,
799 'proxy': True,
800 'vnf-member-index': vnf_member_index,
801 'vnf-name': self.vnf_name,
802 'name': None,
803 'initial-config-primitive': {},
804 'config-primitive': {},
805 }
806
807 juju = cfg['juju']
808 charm['name'] = juju['charm']
809
810 if 'proxy' in juju:
811 charm['proxy'] = juju['proxy']
812
813 if 'initial-config-primitive' in cfg:
814 charm['initial-config-primitive'] = \
815 cfg['initial-config-primitive']
816
817 if 'config-primitive' in cfg:
818 charm['config-primitive'] = cfg['config-primitive']
819
820 charms[application_name] = charm
821
822 # Increment the vnf-member-index
823 vnf_member_index += 1
824
825 self.charms = charms
826
827 @classmethod
828 def isproxy(self, application_name):
829
830 assert application_name in self.charms
831 assert 'proxy' in self.charms[application_name]
832 assert type(self.charms[application_name]['proxy']) is bool
833
Adam Israelfc511ed2018-09-21 14:20:55 +0200834 # debug(self.charms[application_name])
Adam Israel5e08a0e2018-09-06 19:22:47 -0400835 return self.charms[application_name]['proxy']
836
837 @classmethod
838 def get_config(self):
839 """Return an iterable list of config items (vdu and vnf).
840
841 As far as N2VC is concerned, the config section for vdu and vnf are
842 identical. This joins them together so tests only need to iterate
843 through one list.
844 """
845 configs = []
846
847 """Get all vdu and/or vdu config in a descriptor."""
848 vnf_config = self.vnfd.get("vnf-configuration")
849 if vnf_config:
850 juju = vnf_config['juju']
851 if juju:
852 configs.append(vnf_config)
853
854 for vdu in self.vnfd['vdu']:
855 vdu_config = vdu.get('vdu-configuration')
856 if vdu_config:
857 juju = vdu_config['juju']
858 if juju:
859 configs.append(vdu_config)
860
861 return configs
862
863 @classmethod
864 def get_charm_names(self):
865 """Return a list of charms used by the test descriptor."""
866
867 charms = {}
868
869 # Check if the VDUs in this VNF have a charm
870 for config in self.get_config():
871 juju = config['juju']
872
873 name = juju['charm']
874 if name not in charms:
875 charms[name] = 1
876
877 return charms.keys()
878
879 @classmethod
Adam Israelfc511ed2018-09-21 14:20:55 +0200880 def get_phase(self, application):
881 return self.state[application]['phase']
882
883 @classmethod
884 def set_phase(self, application, phase):
885 self.state[application]['phase'] = phase
886
887 @classmethod
888 async def configure_proxy_charm(self, *args):
Adam Israelfa329072018-09-14 11:26:13 -0400889 """Configure a container for use via ssh."""
Adam Israelfc511ed2018-09-21 14:20:55 +0200890 (model, application, _, _) = args
891
892 try:
893 if self.get_phase(application) == "deploy":
894 self.set_phase(application, "configure")
895
896 debug("Start CreateContainer for {}".format(application))
897 self.state[application]['container'] = \
898 await self.CreateContainer(*args)
899 debug("Done CreateContainer for {}".format(application))
900
901 if self.state[application]['container']:
902 debug("Configure {} for container".format(application))
903 if await self.configure_ssh_proxy(application):
904 await asyncio.sleep(0.1)
905 return True
906 else:
907 debug("Failed to configure container for {}".format(application))
908 else:
909 debug("skipping CreateContainer for {}: {}".format(
910 application,
911 self.get_phase(application),
912 ))
913
914 except Exception as ex:
915 debug("configure_proxy_charm exception: {}".format(ex))
916 finally:
917 await asyncio.sleep(0.1)
918
919 return False
920
921 @classmethod
922 async def execute_charm_tests(self, *args):
923 (model, application, _, _) = args
924
925 debug("Executing charm test(s) for {}".format(application))
926
927 if self.state[application]['done']:
928 debug("Trying to execute tests against finished charm...aborting")
929 return False
930
931 try:
932 phase = self.get_phase(application)
933 # We enter the test phase when after deploy (for native charms) or
934 # configure, for proxy charms.
935 if phase in ["deploy", "configure"]:
936 self.set_phase(application, "test")
937 if self.are_tests_finished():
938 raise Exception("Trying to execute init-config on finished test")
939
940 if await self.execute_initial_config_primitives(application):
941 # check for metrics
942 await self.check_metrics(application)
943
944 debug("Done testing {}".format(application))
945 self.state[application]['done'] = True
946
947 except Exception as ex:
948 debug("Exception in execute_charm_tests: {}".format(ex))
949 finally:
950 await asyncio.sleep(0.1)
951
952 return True
953
954 @classmethod
Adam Israel5e08a0e2018-09-06 19:22:47 -0400955 async def CreateContainer(self, *args):
956 """Create a LXD container for use with a proxy charm.abs
957
958 1. Get the public key from the charm via `get-ssh-public-key` action
959 2. Create container with said key injected for the ubuntu user
Adam Israelfc511ed2018-09-21 14:20:55 +0200960
961 Returns a Container object
Adam Israel5e08a0e2018-09-06 19:22:47 -0400962 """
Adam Israel13950822018-09-13 17:14:51 -0400963 # Create and configure a LXD container for use with a proxy charm.
964 (model, application, _, _) = args
Adam Israel5e08a0e2018-09-06 19:22:47 -0400965
Adam Israelfc511ed2018-09-21 14:20:55 +0200966 debug("[CreateContainer] {}".format(args))
967 container = None
Adam Israel5e08a0e2018-09-06 19:22:47 -0400968
Adam Israelfc511ed2018-09-21 14:20:55 +0200969 try:
Adam Israel5e08a0e2018-09-06 19:22:47 -0400970 # Execute 'get-ssh-public-key' primitive and get returned value
971 uuid = await self.n2vc.ExecutePrimitive(
Adam Israel13950822018-09-13 17:14:51 -0400972 model,
973 application,
Adam Israel5e08a0e2018-09-06 19:22:47 -0400974 "get-ssh-public-key",
975 None,
976 )
Adam Israelfc511ed2018-09-21 14:20:55 +0200977
Adam Israel13950822018-09-13 17:14:51 -0400978 result = await self.n2vc.GetPrimitiveOutput(model, uuid)
Adam Israel5e08a0e2018-09-06 19:22:47 -0400979 pubkey = result['pubkey']
980
Adam Israelfc511ed2018-09-21 14:20:55 +0200981 container = create_lxd_container(
Adam Israel5e08a0e2018-09-06 19:22:47 -0400982 public_key=pubkey,
983 name=os.path.basename(__file__)
984 )
985
Adam Israelfc511ed2018-09-21 14:20:55 +0200986 return container
987 except Exception as ex:
988 debug("Error creating container: {}".format(ex))
989 pass
990
991 return None
Adam Israel5e08a0e2018-09-06 19:22:47 -0400992
993 @classmethod
Adam Israelfc511ed2018-09-21 14:20:55 +0200994 async def stop(self):
Adam Israel13950822018-09-13 17:14:51 -0400995 """Stop the test.
996
997 - Remove charms
998 - Stop and delete containers
999 - Logout of N2VC
Adam Israelfc511ed2018-09-21 14:20:55 +02001000
1001 TODO: Clean up duplicate code between teardown_class() and stop()
Adam Israel13950822018-09-13 17:14:51 -04001002 """
Adam Israelfc511ed2018-09-21 14:20:55 +02001003 debug("stop() called")
1004
1005 if self.n2vc and self._running and not self._stopping:
1006 self._running = False
1007 self._stopping = True
1008
Adam Israelb2c234b2019-04-05 10:17:25 -04001009 # Destroy the network service
1010 try:
1011 await self.n2vc.DestroyNetworkService(self.ns_name)
1012 except Exception as e:
1013 debug(
1014 "Error Destroying Network Service \"{}\": {}".format(
1015 self.ns_name,
1016 e,
1017 )
1018 )
1019
1020 # Wait for the applications to be removed and delete the containers
Adam Israelfc511ed2018-09-21 14:20:55 +02001021 for application in self.charms:
1022 try:
Adam Israel7bf2f4d2019-03-15 15:28:47 -04001023
Adam Israelfa329072018-09-14 11:26:13 -04001024 while True:
1025 # Wait for the application to be removed
1026 await asyncio.sleep(10)
1027 if not await self.n2vc.HasApplication(
Adam Israel85a4b212018-11-29 20:30:24 -05001028 self.ns_name,
Adam Israelfa329072018-09-14 11:26:13 -04001029 application,
1030 ):
1031 break
1032
1033 # Need to wait for the charm to finish, because native charms
Adam Israelfc511ed2018-09-21 14:20:55 +02001034 if self.state[application]['container']:
1035 debug("Deleting LXD container...")
1036 destroy_lxd_container(
1037 self.state[application]['container']
1038 )
1039 self.state[application]['container'] = None
1040 debug("Deleting LXD container...done.")
1041 else:
1042 debug("No container found for {}".format(application))
1043 except Exception as e:
1044 debug("Error while deleting container: {}".format(e))
1045
1046 # Logout of N2VC
Adam Israel13950822018-09-13 17:14:51 -04001047 try:
Adam Israelfa329072018-09-14 11:26:13 -04001048 debug("stop(): Logging out of N2VC...")
Adam Israelfc511ed2018-09-21 14:20:55 +02001049 await self.n2vc.logout()
1050 self.n2vc = None
Adam Israelfa329072018-09-14 11:26:13 -04001051 debug("stop(): Logging out of N2VC...Done.")
Adam Israelfc511ed2018-09-21 14:20:55 +02001052 except Exception as ex:
1053 debug(ex)
Adam Israel13950822018-09-13 17:14:51 -04001054
Adam Israelfc511ed2018-09-21 14:20:55 +02001055 # Let the test know we're finished.
1056 debug("Marking test as finished.")
1057 # self._running = False
1058 else:
1059 debug("Skipping stop()")
Adam Israel13950822018-09-13 17:14:51 -04001060
1061 @classmethod
1062 def get_container_ip(self, container):
Adam Israel5e08a0e2018-09-06 19:22:47 -04001063 """Return the IPv4 address of container's eth0 interface."""
1064 ipaddr = None
Adam Israel13950822018-09-13 17:14:51 -04001065 if container:
1066 addresses = container.state().network['eth0']['addresses']
Adam Israel5e08a0e2018-09-06 19:22:47 -04001067 # The interface may have more than one address, but we only need
1068 # the first one for testing purposes.
1069 ipaddr = addresses[0]['address']
1070
1071 return ipaddr
1072
1073 @classmethod
Adam Israelfc511ed2018-09-21 14:20:55 +02001074 async def configure_ssh_proxy(self, application, task=None):
1075 """Configure the proxy charm to use the lxd container.
Adam Israel5e08a0e2018-09-06 19:22:47 -04001076
Adam Israelfc511ed2018-09-21 14:20:55 +02001077 Configure the charm to use a LXD container as it's VNF.
Adam Israel5e08a0e2018-09-06 19:22:47 -04001078 """
Adam Israelfc511ed2018-09-21 14:20:55 +02001079 debug("Configuring ssh proxy for {}".format(application))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001080
Adam Israelfc511ed2018-09-21 14:20:55 +02001081 mgmtaddr = self.get_container_ip(
1082 self.state[application]['container'],
1083 )
1084
1085 debug(
1086 "Setting ssh-hostname for {} to {}".format(
1087 application,
1088 mgmtaddr,
1089 )
1090 )
1091
1092 await self.n2vc.ExecutePrimitive(
Adam Israel85a4b212018-11-29 20:30:24 -05001093 self.ns_name,
Adam Israelfc511ed2018-09-21 14:20:55 +02001094 application,
1095 "config",
1096 None,
1097 params={
1098 'ssh-hostname': mgmtaddr,
1099 'ssh-username': 'ubuntu',
Adam Israel13950822018-09-13 17:14:51 -04001100 }
Adam Israelfc511ed2018-09-21 14:20:55 +02001101 )
Adam Israel13950822018-09-13 17:14:51 -04001102
Adam Israelfc511ed2018-09-21 14:20:55 +02001103 return True
Adam Israel5e08a0e2018-09-06 19:22:47 -04001104
Adam Israelfc511ed2018-09-21 14:20:55 +02001105 @classmethod
1106 async def execute_initial_config_primitives(self, application, task=None):
1107 debug("Executing initial_config_primitives for {}".format(application))
1108 try:
Adam Israel5e08a0e2018-09-06 19:22:47 -04001109 init_config = self.charms[application]
1110
1111 """
1112 The initial-config-primitive is run during deploy but may fail
1113 on some steps because proxy charm access isn't configured.
1114
Adam Israelfc511ed2018-09-21 14:20:55 +02001115 Re-run those actions so we can inspect the status.
Adam Israel5e08a0e2018-09-06 19:22:47 -04001116 """
Adam Israelfc511ed2018-09-21 14:20:55 +02001117 uuids = await self.n2vc.ExecuteInitialPrimitives(
Adam Israel85a4b212018-11-29 20:30:24 -05001118 self.ns_name,
Adam Israelfc511ed2018-09-21 14:20:55 +02001119 application,
1120 init_config,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001121 )
1122
1123 """
1124 ExecutePrimitives will return a list of uuids. We need to check the
1125 status of each. The test continues if all Actions succeed, and
1126 fails if any of them fail.
1127 """
Adam Israelfc511ed2018-09-21 14:20:55 +02001128 await self.wait_for_uuids(application, uuids)
1129 debug("Primitives for {} finished.".format(application))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001130
Adam Israelfc511ed2018-09-21 14:20:55 +02001131 return True
1132 except Exception as ex:
1133 debug("execute_initial_config_primitives exception: {}".format(ex))
1134
1135 return False
1136
1137 @classmethod
1138 async def check_metrics(self, application, task=None):
1139 """Check and run metrics, if present.
1140
1141 Checks to see if metrics are specified by the charm. If so, collects
1142 the metrics.
1143
1144 If no metrics, then mark the test as finished.
1145 """
1146 if has_metrics(self.charms[application]['name']):
1147 debug("Collecting metrics for {}".format(application))
1148
1149 metrics = await self.n2vc.GetMetrics(
Adam Israel85a4b212018-11-29 20:30:24 -05001150 self.ns_name,
Adam Israelfc511ed2018-09-21 14:20:55 +02001151 application,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001152 )
1153
Adam Israelfc511ed2018-09-21 14:20:55 +02001154 return await self.verify_metrics(application, metrics)
Adam Israel5e08a0e2018-09-06 19:22:47 -04001155
Adam Israelfc511ed2018-09-21 14:20:55 +02001156 @classmethod
1157 async def verify_metrics(self, application, metrics):
1158 """Verify the charm's metrics.
Adam Israel5e08a0e2018-09-06 19:22:47 -04001159
Adam Israelfc511ed2018-09-21 14:20:55 +02001160 Verify that the charm has sent metrics successfully.
Adam Israel5e08a0e2018-09-06 19:22:47 -04001161
Adam Israelfc511ed2018-09-21 14:20:55 +02001162 Stops the test when finished.
1163 """
1164 debug("Verifying metrics for {}: {}".format(application, metrics))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001165
Adam Israelfc511ed2018-09-21 14:20:55 +02001166 if len(metrics):
1167 return True
Adam Israel5e08a0e2018-09-06 19:22:47 -04001168
Adam Israelfc511ed2018-09-21 14:20:55 +02001169 else:
1170 # TODO: Ran into a case where it took 9 attempts before metrics
1171 # were available; the controller is slow sometimes.
1172 await asyncio.sleep(30)
1173 return await self.check_metrics(application)
Adam Israel5e08a0e2018-09-06 19:22:47 -04001174
Adam Israelfc511ed2018-09-21 14:20:55 +02001175 @classmethod
1176 async def wait_for_uuids(self, application, uuids):
1177 """Wait for primitives to execute.
Adam Israel5e08a0e2018-09-06 19:22:47 -04001178
Adam Israelfc511ed2018-09-21 14:20:55 +02001179 The task will provide a list of uuids representing primitives that are
1180 queued to run.
1181 """
1182 debug("Waiting for uuids for {}: {}".format(application, uuids))
1183 waitfor = len(uuids)
1184 finished = 0
Adam Israel5e08a0e2018-09-06 19:22:47 -04001185
Adam Israelfc511ed2018-09-21 14:20:55 +02001186 while waitfor > finished:
1187 for uid in uuids:
1188 await asyncio.sleep(10)
Adam Israel5e08a0e2018-09-06 19:22:47 -04001189
Adam Israelfc511ed2018-09-21 14:20:55 +02001190 if uuid not in self.state[application]['actions']:
1191 self.state[application]['actions'][uid] = "pending"
Adam Israel5e08a0e2018-09-06 19:22:47 -04001192
Adam Israelfc511ed2018-09-21 14:20:55 +02001193 status = self.state[application]['actions'][uid]
1194
1195 # Have we already marked this as done?
1196 if status in ["pending", "running"]:
1197
1198 debug("Getting status of {} ({})...".format(uid, status))
1199 status = await self.n2vc.GetPrimitiveStatus(
Adam Israel85a4b212018-11-29 20:30:24 -05001200 self.ns_name,
Adam Israelfc511ed2018-09-21 14:20:55 +02001201 uid,
Adam Israel5e08a0e2018-09-06 19:22:47 -04001202 )
Adam Israelfc511ed2018-09-21 14:20:55 +02001203 debug("...state of {} is {}".format(uid, status))
1204 self.state[application]['actions'][uid] = status
Adam Israel5e08a0e2018-09-06 19:22:47 -04001205
Adam Israelfc511ed2018-09-21 14:20:55 +02001206 if status in ['completed', 'failed']:
1207 finished += 1
Adam Israel5e08a0e2018-09-06 19:22:47 -04001208
Adam Israelfc511ed2018-09-21 14:20:55 +02001209 debug("{}/{} actions complete".format(finished, waitfor))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001210
Adam Israelfc511ed2018-09-21 14:20:55 +02001211 # Wait for the primitive to finish and try again
1212 if waitfor > finished:
1213 debug("Waiting 10s for action to finish...")
1214 await asyncio.sleep(10)
Adam Israel13950822018-09-13 17:14:51 -04001215
Adam Israelfc511ed2018-09-21 14:20:55 +02001216 @classmethod
1217 def n2vc_callback(self, *args, **kwargs):
1218 (model, application, status, message) = args
1219 # debug("callback: {}".format(args))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001220
Adam Israelfc511ed2018-09-21 14:20:55 +02001221 if application not in self.state:
1222 # Initialize the state of the application
1223 self.state[application] = {
1224 'status': None, # Juju status
1225 'container': None, # lxd container, for proxy charms
1226 'actions': {}, # Actions we've executed
1227 'done': False, # Are we done testing this charm?
1228 'phase': "deploy", # What phase is this application in?
1229 }
Adam Israel5e08a0e2018-09-06 19:22:47 -04001230
Adam Israelfc511ed2018-09-21 14:20:55 +02001231 self.state[application]['status'] = status
Adam Israel5e08a0e2018-09-06 19:22:47 -04001232
Adam Israelfc511ed2018-09-21 14:20:55 +02001233 if status in ['waiting', 'maintenance', 'unknown']:
1234 # Nothing to do for these
1235 return
Adam Israel5e08a0e2018-09-06 19:22:47 -04001236
Adam Israelfc511ed2018-09-21 14:20:55 +02001237 debug("callback: {}".format(args))
Adam Israel13950822018-09-13 17:14:51 -04001238
Adam Israelfc511ed2018-09-21 14:20:55 +02001239 if self.state[application]['done']:
1240 debug("{} is done".format(application))
1241 return
Adam Israel5e08a0e2018-09-06 19:22:47 -04001242
Adam Israelb2c234b2019-04-05 10:17:25 -04001243 if status in ['error']:
1244 # To test broken charms, if a charm enters an error state we should
1245 # end the test
1246 debug("{} is in an error state, stop the test.".format(application))
1247 # asyncio.ensure_future(self.stop())
1248 self.state[application]['done'] = True
1249 assert False
1250
Adam Israelfc511ed2018-09-21 14:20:55 +02001251 if status in ["blocked"] and self.isproxy(application):
1252 if self.state[application]['phase'] == "deploy":
1253 debug("Configuring proxy charm for {}".format(application))
1254 asyncio.ensure_future(self.configure_proxy_charm(*args))
Adam Israel5e08a0e2018-09-06 19:22:47 -04001255
Adam Israelfc511ed2018-09-21 14:20:55 +02001256 elif status in ["active"]:
1257 """When a charm is active, we can assume that it has been properly
1258 configured (not blocked), regardless of if it's a proxy or not.
Adam Israel13950822018-09-13 17:14:51 -04001259
Adam Israelfc511ed2018-09-21 14:20:55 +02001260 All primitives should be complete by init_config_primitive
1261 """
1262 asyncio.ensure_future(self.execute_charm_tests(*args))