15 from juju
.controller
import Controller
17 # Disable InsecureRequestWarning w/LXD
19 urllib3
.disable_warnings()
20 logging
.getLogger("urllib3").setLevel(logging
.WARNING
)
22 here
= os
.path
.dirname(os
.path
.realpath(__file__
))
25 def is_bootstrapped():
26 result
= subprocess
.run(['juju', 'switch'], stdout
=subprocess
.PIPE
)
28 result
.returncode
== 0 and len(result
.stdout
.decode().strip()) > 0)
31 bootstrapped
= pytest
.mark
.skipif(
32 not is_bootstrapped(),
33 reason
='bootstrapped Juju environment required')
36 class CleanController():
38 Context manager that automatically connects and disconnects from
39 the currently active controller.
41 Note: Unlike CleanModel, this will not create a new controller for you,
42 and an active controller must already be available.
45 self
._controller
= None
47 async def __aenter__(self
):
48 self
._controller
= Controller()
49 await self
._controller
.connect()
50 return self
._controller
52 async def __aexit__(self
, exc_type
, exc
, tb
):
53 await self
._controller
.disconnect()
57 """Format debug messages in a consistent way."""
58 now
= datetime
.datetime
.now()
60 # TODO: Decide on the best way to log. Output from `logging.debug` shows up
61 # when a test fails, but print() will always show up when running tox with
62 # `-s`, which is really useful for debugging single tests without having to
63 # insert a False assert to see the log.
65 "[{}] {}".format(now
.strftime('%Y-%m-%dT%H:%M:%S'), msg
)
68 "[{}] {}".format(now
.strftime('%Y-%m-%dT%H:%M:%S'), msg
)
73 return "{}/charms".format(here
)
77 return "{}/charms/layers".format(here
)
80 def collect_metrics(application
):
81 """Invoke Juju's metrics collector.
83 Caveat: this shells out to the `juju collect-metrics` command, rather than
84 making an API call. At the time of writing, that API is not exposed through
89 subprocess
.check_call(['juju', 'collect-metrics', application
])
90 except subprocess
.CalledProcessError
as e
:
91 raise Exception("Unable to collect metrics: {}".format(e
))
94 def has_metrics(charm
):
95 """Check if a charm has metrics defined."""
96 metricsyaml
= "{}/{}/metrics.yaml".format(
100 if os
.path
.exists(metricsyaml
):
105 def get_descriptor(descriptor
):
108 tmp
= yaml
.load(descriptor
)
110 # Remove the envelope
111 root
= list(tmp
.keys())[0]
112 if root
== "nsd:nsd-catalog":
113 desc
= tmp
['nsd:nsd-catalog']['nsd'][0]
114 elif root
== "vnfd:vnfd-catalog":
115 desc
= tmp
['vnfd:vnfd-catalog']['vnfd'][0]
121 def get_n2vc(loop
=None):
122 """Return an instance of N2VC.VNF."""
123 log
= logging
.getLogger()
124 log
.level
= logging
.DEBUG
126 # Extract parameters from the environment in order to run our test
127 vca_host
= os
.getenv('VCA_HOST', '127.0.0.1')
128 vca_port
= os
.getenv('VCA_PORT', 17070)
129 vca_user
= os
.getenv('VCA_USER', 'admin')
130 vca_charms
= os
.getenv('VCA_CHARMS', None)
131 vca_secret
= os
.getenv('VCA_SECRET', None)
133 client
= n2vc
.vnf
.N2VC(
139 artifacts
=vca_charms
,
145 def create_lxd_container(public_key
=None, name
="test_name"):
147 Returns a container object
149 If public_key isn't set, we'll use the Juju ssh key
151 :param public_key: The public key to inject into the container
152 :param name: The name of the test being run
156 # Format name so it's valid
157 name
= name
.replace("_", "-").replace(".", "")
159 client
= get_lxd_client()
160 test_machine
= "test-{}-{}".format(
161 uuid
.uuid4().hex[-4:],
165 private_key_path
, public_key_path
= find_n2vc_ssh_keys()
168 # create profile w/cloud-init and juju ssh key
171 with
open(public_key_path
, "r") as f
:
172 public_key
= f
.readline()
174 client
.profiles
.create(
177 'user.user-data': '#cloud-config\nssh_authorized_keys:\n- {}'.format(public_key
)},
179 'root': {'path': '/', 'pool': 'default', 'type': 'disk'},
181 'nictype': 'bridged',
187 except Exception as ex
:
188 debug("Error creating lxd profile {}: {}".format(test_machine
, ex
))
194 'name': test_machine
,
199 'protocol': 'simplestreams',
200 'server': 'https://cloud-images.ubuntu.com/releases',
202 'profiles': [test_machine
],
204 container
= client
.containers
.create(config
, wait
=True)
205 container
.start(wait
=True)
206 except Exception as ex
:
207 debug("Error creating lxd container {}: {}".format(test_machine
, ex
))
208 # This is a test-ending failure.
211 def wait_for_network(container
, timeout
=30):
212 """Wait for eth0 to have an ipv4 address."""
213 starttime
= time
.time()
214 while(time
.time() < starttime
+ timeout
):
216 if 'eth0' in container
.state().network
:
217 addresses
= container
.state().network
['eth0']['addresses']
218 if len(addresses
) > 0:
219 if addresses
[0]['family'] == 'inet':
224 wait_for_network(container
)
225 except Exception as ex
:
227 "Error waiting for container {} network: {}".format(
235 while waitcount
<= 5:
236 if is_sshd_running(container
):
241 debug("couldn't detect sshd running")
242 raise Exception("Unable to verify container sshd")
244 except Exception as ex
:
246 "Error checking sshd status on {}: {}".format(
252 # HACK: We need to give sshd a chance to bind to the interface,
253 # and pylxd's container.execute seems to be broken and fails and/or
254 # hangs trying to properly check if the service is up.
255 (exit_code
, stdout
, stderr
) = container
.execute([
257 '-c', '5', # Wait for 5 ECHO_REPLY
258 '8.8.8.8', # Ping Google's public DNS
259 '-W', '15', # Set a 15 second deadline
263 raise Exception("Unable to verify container network")
268 def is_sshd_running(container
):
269 """Check if sshd is running in the container.
271 Check to see if the sshd process is running and listening on port 22.
273 :param container: The container to check
274 :return boolean: True if sshd is running.
276 debug("Container: {}".format(container
))
278 (rc
, stdout
, stderr
) = container
.execute(
279 ["service", "ssh", "status"]
281 # If the status is a) found and b) running, the exit code will be 0
284 except Exception as ex
:
285 debug("Failed to check sshd service status: {}".format(ex
))
290 def destroy_lxd_container(container
):
291 """Stop and delete a LXD container.
293 Sometimes we see errors talking to LXD -- ephemerial issues like
294 load or a bug that's killed the API. We'll do our best to clean
295 up here, and we should run a cleanup after all tests are finished
296 to remove any extra containers and profiles belonging to us.
299 if type(container
) is bool:
302 name
= container
.name
303 debug("Destroying container {}".format(name
))
305 client
= get_lxd_client()
307 def wait_for_stop(timeout
=30):
308 """Wait for eth0 to have an ipv4 address."""
309 starttime
= time
.time()
310 while(time
.time() < starttime
+ timeout
):
312 if container
.state
== "Stopped":
315 def wait_for_delete(timeout
=30):
316 starttime
= time
.time()
317 while(time
.time() < starttime
+ timeout
):
319 if client
.containers
.exists(name
) is False:
323 container
.stop(wait
=False)
325 except Exception as ex
:
327 "Error stopping container {}: {}".format(
334 container
.delete(wait
=False)
336 except Exception as ex
:
338 "Error deleting container {}: {}".format(
345 # Delete the profile created for this container
346 profile
= client
.profiles
.get(name
)
349 except Exception as ex
:
351 "Error deleting profile {}: {}".format(
358 def find_lxd_config():
359 """Find the LXD configuration directory."""
361 paths
.append(os
.path
.expanduser("~/.config/lxc"))
362 paths
.append(os
.path
.expanduser("~/snap/lxd/current/.config/lxc"))
365 if os
.path
.exists(path
):
366 crt
= os
.path
.expanduser("{}/client.crt".format(path
))
367 key
= os
.path
.expanduser("{}/client.key".format(path
))
368 if os
.path
.exists(crt
) and os
.path
.exists(key
):
373 def find_n2vc_ssh_keys():
374 """Find the N2VC ssh keys."""
377 paths
.append(os
.path
.expanduser("~/.ssh/"))
380 if os
.path
.exists(path
):
381 private
= os
.path
.expanduser("{}/id_n2vc_rsa".format(path
))
382 public
= os
.path
.expanduser("{}/id_n2vc_rsa.pub".format(path
))
383 if os
.path
.exists(private
) and os
.path
.exists(public
):
384 return (private
, public
)
388 def find_juju_ssh_keys():
389 """Find the Juju ssh keys."""
392 paths
.append(os
.path
.expanduser("~/.local/share/juju/ssh/"))
395 if os
.path
.exists(path
):
396 private
= os
.path
.expanduser("{}/juju_id_rsa".format(path
))
397 public
= os
.path
.expanduser("{}/juju_id_rsa.pub".format(path
))
398 if os
.path
.exists(private
) and os
.path
.exists(public
):
399 return (private
, public
)
403 def get_juju_private_key():
404 keys
= find_juju_ssh_keys()
408 def get_lxd_client(host
="127.0.0.1", port
="8443", verify
=False):
409 """ Get the LXD client."""
411 (crt
, key
) = find_lxd_config()
414 client
= pylxd
.Client(
415 endpoint
="https://{}:{}".format(host
, port
),
423 # TODO: This is marked serial but can be run in parallel with work, including:
424 # - Fixing an event loop issue; seems that all tests stop when one test stops?
428 class TestN2VC(object):
430 1. Validator Validation
432 Automatically validate the descriptors we're using here, unless the test author explicitly wants to skip them. Useful to make sure tests aren't being run against invalid descriptors, validating functionality that may fail against a properly written descriptor.
434 We need to have a flag (instance variable) that controls this behavior. It may be necessary to skip validation and run against a descriptor implementing features that have not yet been released in the Information Model.
438 The six phases of integration testing, for the test itself and each charm?:
440 setup/teardown_class:
441 1. Prepare - Verify the environment and create a new model
442 2. Deploy - Mark the test as ready to execute
443 3. Configure - Configuration to reach Active state
444 4. Test - Execute primitive(s) to verify success
445 5. Collect - Collect any useful artifacts for debugging (charm, logs)
446 6. Destroy - Destroy the model
449 1. Prepare - Building of charm
450 2. Deploy - Deploying charm
451 3. Configure - Configuration to reach Active state
452 4. Test - Execute primitive(s) to verify success
453 5. Collect - Collect any useful artifacts for debugging (charm, logs)
454 6. Destroy - Destroy the charm
458 def setup_class(self
):
459 """ setup any state specific to the execution of the given class (which
460 usually contains tests).
462 # Initialize instance variable(s)
465 # Track internal state for each test run
468 # Parse the test's descriptors
469 self
.nsd
= get_descriptor(self
.NSD_YAML
)
470 self
.vnfd
= get_descriptor(self
.VNFD_YAML
)
472 self
.ns_name
= self
.nsd
['name']
473 self
.vnf_name
= self
.vnfd
['name']
476 self
.parse_vnf_descriptor()
477 assert self
.charms
is not {}
479 # Track artifacts, like compiled charms, that will need to be removed
482 # Build the charm(s) needed for this test
483 for charm
in self
.get_charm_names():
484 self
.get_charm(charm
)
486 # A bit of a hack, in order to allow the N2VC callback to run parallel
487 # to pytest. Test(s) should wait for this flag to change to False
490 self
._stopping
= False
493 def teardown_class(self
):
494 """ teardown any state that was previously setup with a call to
497 debug("Running teardown_class...")
500 debug("Destroying LXD containers...")
501 for application
in self
.state
:
502 if self
.state
[application
]['container']:
503 destroy_lxd_container(self
.state
[application
]['container'])
504 debug("Destroying LXD containers...done.")
508 debug("teardown_class(): Logging out of N2VC...")
509 yield from self
.n2vc
.logout()
510 debug("teardown_class(): Logging out of N2VC...done.")
512 debug("Running teardown_class...done.")
513 except Exception as ex
:
514 debug("Exception in teardown_class: {}".format(ex
))
517 def all_charms_active(self
):
518 """Determine if the all deployed charms are active."""
521 for application
in self
.state
:
522 if 'status' in self
.state
[application
]:
523 debug("status of {} is '{}'".format(
525 self
.state
[application
]['status'],
527 if self
.state
[application
]['status'] == 'active':
530 debug("Active charms: {}/{}".format(
535 if active
== len(self
.charms
):
541 def are_tests_finished(self
):
542 appcount
= len(self
.state
)
544 # If we don't have state yet, keep running.
546 debug("No applications")
550 debug("_stopping is True")
554 for application
in self
.state
:
555 if self
.state
[application
]['done']:
558 debug("{}/{} charms tested".format(appdone
, appcount
))
560 if appcount
== appdone
:
566 async def running(self
, timeout
=600):
567 """Returns if the test is still running.
569 @param timeout The time, in seconds, to wait for the test to complete.
571 if self
.are_tests_finished():
575 await asyncio
.sleep(30)
580 def get_charm(self
, charm
):
581 """Build and return the path to the test charm.
583 Builds one of the charms in tests/charms/layers and returns the path
584 to the compiled charm. The charm will automatically be removed when
585 when the test is complete.
587 Returns: The path to the built charm or None if `charm build` failed.
590 # Make sure the charm snap is installed
592 subprocess
.check_call(['which', 'charm'])
593 except subprocess
.CalledProcessError
:
594 raise Exception("charm snap not installed.")
596 if charm
not in self
.artifacts
:
598 # Note: This builds the charm under N2VC/tests/charms/builds/
599 # Currently, the snap-installed command only has write access
600 # to the $HOME (changing in an upcoming release) so writing to
601 # /tmp isn't possible at the moment.
602 builds
= get_charm_path()
604 if not os
.path
.exists("{}/builds/{}".format(builds
, charm
)):
605 cmd
= "charm build --no-local-layers {}/{} -o {}/".format(
610 subprocess
.check_call(shlex
.split(cmd
))
612 except subprocess
.CalledProcessError
as e
:
613 # charm build will return error code 100 if the charm fails
614 # the auto-run of charm proof, which we can safely ignore for
616 if e
.returncode
!= 100:
617 raise Exception("charm build failed: {}.".format(e
))
619 self
.artifacts
[charm
] = {
621 'charm': "{}/builds/{}".format(builds
, charm
),
624 return self
.artifacts
[charm
]['charm']
627 async def deploy(self
, vnf_index
, charm
, params
, loop
):
628 """An inner function to do the deployment of a charm from
633 self
.n2vc
= get_n2vc(loop
=loop
)
635 debug("Creating model for Network Service {}".format(self
.ns_name
))
636 await self
.n2vc
.CreateNetworkService(self
.ns_name
)
638 application
= self
.n2vc
.FormatApplicationName(
644 # Initialize the state of the application
645 self
.state
[application
] = {
646 'status': None, # Juju status
647 'container': None, # lxd container, for proxy charms
648 'actions': {}, # Actions we've executed
649 'done': False, # Are we done testing this charm?
650 'phase': "deploy", # What phase is this application in?
653 debug("Deploying charm at {}".format(self
.artifacts
[charm
]))
655 # If this is a native charm, we need to provision the underlying
656 # machine ala an LXC container.
659 if not self
.isproxy(application
):
660 debug("Creating container for native charm")
661 # args = ("default", application, None, None)
662 self
.state
[application
]['container'] = create_lxd_container(
663 name
=os
.path
.basename(__file__
)
666 hostname
= self
.get_container_ip(
667 self
.state
[application
]['container'],
675 await self
.n2vc
.DeployCharms(
679 self
.get_charm(charm
),
686 def parse_vnf_descriptor(self
):
687 """Parse the VNF descriptor to make running tests easier.
689 Parse the charm information in the descriptor to make it easy to write
690 tests to run again it.
692 Each charm becomes a dictionary in a list:
695 'vnf-member-index': 1,
698 'initial-config-primitive': {},
699 'config-primitive': {}
702 - is this a proxy charm?
703 - what are the initial-config-primitives (day 1)?
704 - what are the config primitives (day 2)?
709 # You'd think this would be explicit, but it's just an incremental
710 # value that should be consistent.
713 """Get all vdu and/or vdu config in a descriptor."""
714 config
= self
.get_config()
718 # Get the name to be used for the deployed application
719 application_name
= n2vc
.vnf
.N2VC().FormatApplicationName(
722 str(vnf_member_index
),
726 'application-name': application_name
,
728 'vnf-member-index': vnf_member_index
,
729 'vnf-name': self
.vnf_name
,
731 'initial-config-primitive': {},
732 'config-primitive': {},
736 charm
['name'] = juju
['charm']
739 charm
['proxy'] = juju
['proxy']
741 if 'initial-config-primitive' in cfg
:
742 charm
['initial-config-primitive'] = \
743 cfg
['initial-config-primitive']
745 if 'config-primitive' in cfg
:
746 charm
['config-primitive'] = cfg
['config-primitive']
748 charms
[application_name
] = charm
750 # Increment the vnf-member-index
751 vnf_member_index
+= 1
756 def isproxy(self
, application_name
):
758 assert application_name
in self
.charms
759 assert 'proxy' in self
.charms
[application_name
]
760 assert type(self
.charms
[application_name
]['proxy']) is bool
762 # debug(self.charms[application_name])
763 return self
.charms
[application_name
]['proxy']
766 def get_config(self
):
767 """Return an iterable list of config items (vdu and vnf).
769 As far as N2VC is concerned, the config section for vdu and vnf are
770 identical. This joins them together so tests only need to iterate
775 """Get all vdu and/or vdu config in a descriptor."""
776 vnf_config
= self
.vnfd
.get("vnf-configuration")
778 juju
= vnf_config
['juju']
780 configs
.append(vnf_config
)
782 for vdu
in self
.vnfd
['vdu']:
783 vdu_config
= vdu
.get('vdu-configuration')
785 juju
= vdu_config
['juju']
787 configs
.append(vdu_config
)
792 def get_charm_names(self
):
793 """Return a list of charms used by the test descriptor."""
797 # Check if the VDUs in this VNF have a charm
798 for config
in self
.get_config():
799 juju
= config
['juju']
802 if name
not in charms
:
808 def get_phase(self
, application
):
809 return self
.state
[application
]['phase']
812 def set_phase(self
, application
, phase
):
813 self
.state
[application
]['phase'] = phase
816 async def configure_proxy_charm(self
, *args
):
817 """Configure a container for use via ssh."""
818 (model
, application
, _
, _
) = args
821 if self
.get_phase(application
) == "deploy":
822 self
.set_phase(application
, "configure")
824 debug("Start CreateContainer for {}".format(application
))
825 self
.state
[application
]['container'] = \
826 await self
.CreateContainer(*args
)
827 debug("Done CreateContainer for {}".format(application
))
829 if self
.state
[application
]['container']:
830 debug("Configure {} for container".format(application
))
831 if await self
.configure_ssh_proxy(application
):
832 await asyncio
.sleep(0.1)
835 debug("Failed to configure container for {}".format(application
))
837 debug("skipping CreateContainer for {}: {}".format(
839 self
.get_phase(application
),
842 except Exception as ex
:
843 debug("configure_proxy_charm exception: {}".format(ex
))
845 await asyncio
.sleep(0.1)
850 async def execute_charm_tests(self
, *args
):
851 (model
, application
, _
, _
) = args
853 debug("Executing charm test(s) for {}".format(application
))
855 if self
.state
[application
]['done']:
856 debug("Trying to execute tests against finished charm...aborting")
860 phase
= self
.get_phase(application
)
861 # We enter the test phase when after deploy (for native charms) or
862 # configure, for proxy charms.
863 if phase
in ["deploy", "configure"]:
864 self
.set_phase(application
, "test")
865 if self
.are_tests_finished():
866 raise Exception("Trying to execute init-config on finished test")
868 if await self
.execute_initial_config_primitives(application
):
870 await self
.check_metrics(application
)
872 debug("Done testing {}".format(application
))
873 self
.state
[application
]['done'] = True
875 except Exception as ex
:
876 debug("Exception in execute_charm_tests: {}".format(ex
))
878 await asyncio
.sleep(0.1)
883 async def CreateContainer(self
, *args
):
884 """Create a LXD container for use with a proxy charm.abs
886 1. Get the public key from the charm via `get-ssh-public-key` action
887 2. Create container with said key injected for the ubuntu user
889 Returns a Container object
891 # Create and configure a LXD container for use with a proxy charm.
892 (model
, application
, _
, _
) = args
894 debug("[CreateContainer] {}".format(args
))
898 # Execute 'get-ssh-public-key' primitive and get returned value
899 uuid
= await self
.n2vc
.ExecutePrimitive(
902 "get-ssh-public-key",
906 result
= await self
.n2vc
.GetPrimitiveOutput(model
, uuid
)
907 pubkey
= result
['pubkey']
909 container
= create_lxd_container(
911 name
=os
.path
.basename(__file__
)
915 except Exception as ex
:
916 debug("Error creating container: {}".format(ex
))
922 async def stop(self
):
926 - Stop and delete containers
929 TODO: Clean up duplicate code between teardown_class() and stop()
931 debug("stop() called")
933 if self
.n2vc
and self
._running
and not self
._stopping
:
934 self
._running
= False
935 self
._stopping
= True
937 # Destroy the network service
939 await self
.n2vc
.DestroyNetworkService(self
.ns_name
)
940 except Exception as e
:
942 "Error Destroying Network Service \"{}\": {}".format(
948 # Wait for the applications to be removed and delete the containers
949 for application
in self
.charms
:
953 # Wait for the application to be removed
954 await asyncio
.sleep(10)
955 if not await self
.n2vc
.HasApplication(
961 # Need to wait for the charm to finish, because native charms
962 if self
.state
[application
]['container']:
963 debug("Deleting LXD container...")
964 destroy_lxd_container(
965 self
.state
[application
]['container']
967 self
.state
[application
]['container'] = None
968 debug("Deleting LXD container...done.")
970 debug("No container found for {}".format(application
))
971 except Exception as e
:
972 debug("Error while deleting container: {}".format(e
))
976 debug("stop(): Logging out of N2VC...")
977 await self
.n2vc
.logout()
979 debug("stop(): Logging out of N2VC...Done.")
980 except Exception as ex
:
983 # Let the test know we're finished.
984 debug("Marking test as finished.")
985 # self._running = False
987 debug("Skipping stop()")
990 def get_container_ip(self
, container
):
991 """Return the IPv4 address of container's eth0 interface."""
994 addresses
= container
.state().network
['eth0']['addresses']
995 # The interface may have more than one address, but we only need
996 # the first one for testing purposes.
997 ipaddr
= addresses
[0]['address']
1002 async def configure_ssh_proxy(self
, application
, task
=None):
1003 """Configure the proxy charm to use the lxd container.
1005 Configure the charm to use a LXD container as it's VNF.
1007 debug("Configuring ssh proxy for {}".format(application
))
1009 mgmtaddr
= self
.get_container_ip(
1010 self
.state
[application
]['container'],
1014 "Setting ssh-hostname for {} to {}".format(
1020 await self
.n2vc
.ExecutePrimitive(
1026 'ssh-hostname': mgmtaddr
,
1027 'ssh-username': 'ubuntu',
1034 async def execute_initial_config_primitives(self
, application
, task
=None):
1035 debug("Executing initial_config_primitives for {}".format(application
))
1037 init_config
= self
.charms
[application
]
1040 The initial-config-primitive is run during deploy but may fail
1041 on some steps because proxy charm access isn't configured.
1043 Re-run those actions so we can inspect the status.
1045 uuids
= await self
.n2vc
.ExecuteInitialPrimitives(
1052 ExecutePrimitives will return a list of uuids. We need to check the
1053 status of each. The test continues if all Actions succeed, and
1054 fails if any of them fail.
1056 await self
.wait_for_uuids(application
, uuids
)
1057 debug("Primitives for {} finished.".format(application
))
1060 except Exception as ex
:
1061 debug("execute_initial_config_primitives exception: {}".format(ex
))
1066 async def check_metrics(self
, application
, task
=None):
1067 """Check and run metrics, if present.
1069 Checks to see if metrics are specified by the charm. If so, collects
1072 If no metrics, then mark the test as finished.
1074 if has_metrics(self
.charms
[application
]['name']):
1075 debug("Collecting metrics for {}".format(application
))
1077 metrics
= await self
.n2vc
.GetMetrics(
1082 return await self
.verify_metrics(application
, metrics
)
1085 async def verify_metrics(self
, application
, metrics
):
1086 """Verify the charm's metrics.
1088 Verify that the charm has sent metrics successfully.
1090 Stops the test when finished.
1092 debug("Verifying metrics for {}: {}".format(application
, metrics
))
1098 # TODO: Ran into a case where it took 9 attempts before metrics
1099 # were available; the controller is slow sometimes.
1100 await asyncio
.sleep(30)
1101 return await self
.check_metrics(application
)
1104 async def wait_for_uuids(self
, application
, uuids
):
1105 """Wait for primitives to execute.
1107 The task will provide a list of uuids representing primitives that are
1110 debug("Waiting for uuids for {}: {}".format(application
, uuids
))
1111 waitfor
= len(uuids
)
1114 while waitfor
> finished
:
1116 await asyncio
.sleep(10)
1118 if uuid
not in self
.state
[application
]['actions']:
1119 self
.state
[application
]['actions'][uid
] = "pending"
1121 status
= self
.state
[application
]['actions'][uid
]
1123 # Have we already marked this as done?
1124 if status
in ["pending", "running"]:
1126 debug("Getting status of {} ({})...".format(uid
, status
))
1127 status
= await self
.n2vc
.GetPrimitiveStatus(
1131 debug("...state of {} is {}".format(uid
, status
))
1132 self
.state
[application
]['actions'][uid
] = status
1134 if status
in ['completed', 'failed']:
1137 debug("{}/{} actions complete".format(finished
, waitfor
))
1139 # Wait for the primitive to finish and try again
1140 if waitfor
> finished
:
1141 debug("Waiting 10s for action to finish...")
1142 await asyncio
.sleep(10)
1145 def n2vc_callback(self
, *args
, **kwargs
):
1146 (model
, application
, status
, message
) = args
1147 # debug("callback: {}".format(args))
1149 if application
not in self
.state
:
1150 # Initialize the state of the application
1151 self
.state
[application
] = {
1152 'status': None, # Juju status
1153 'container': None, # lxd container, for proxy charms
1154 'actions': {}, # Actions we've executed
1155 'done': False, # Are we done testing this charm?
1156 'phase': "deploy", # What phase is this application in?
1159 self
.state
[application
]['status'] = status
1161 if status
in ['waiting', 'maintenance', 'unknown']:
1162 # Nothing to do for these
1165 debug("callback: {}".format(args
))
1167 if self
.state
[application
]['done']:
1168 debug("{} is done".format(application
))
1171 if status
in ['error']:
1172 # To test broken charms, if a charm enters an error state we should
1174 debug("{} is in an error state, stop the test.".format(application
))
1175 # asyncio.ensure_future(self.stop())
1176 self
.state
[application
]['done'] = True
1179 if status
in ["blocked"] and self
.isproxy(application
):
1180 if self
.state
[application
]['phase'] == "deploy":
1181 debug("Configuring proxy charm for {}".format(application
))
1182 asyncio
.ensure_future(self
.configure_proxy_charm(*args
))
1184 elif status
in ["active"]:
1185 """When a charm is active, we can assume that it has been properly
1186 configured (not blocked), regardless of if it's a proxy or not.
1188 All primitives should be complete by init_config_primitive
1190 asyncio
.ensure_future(self
.execute_charm_tests(*args
))