1db48432a08c962d6dd4e47d039793f498260a5f
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
29 len(result
.stdout
.decode().strip()) > 0)
32 bootstrapped
= pytest
.mark
.skipif(
33 not is_bootstrapped(),
34 reason
='bootstrapped Juju environment required')
37 class CleanController():
39 Context manager that automatically connects and disconnects from
40 the currently active controller.
42 Note: Unlike CleanModel, this will not create a new controller for you,
43 and an active controller must already be available.
46 self
._controller
= None
48 async def __aenter__(self
):
49 self
._controller
= Controller()
50 await self
._controller
.connect()
51 return self
._controller
53 async def __aexit__(self
, exc_type
, exc
, tb
):
54 await self
._controller
.disconnect()
58 """Format debug messages in a consistent way."""
59 now
= datetime
.datetime
.now()
61 # TODO: Decide on the best way to log. Output from `logging.debug` shows up
62 # when a test fails, but print() will always show up when running tox with
63 # `-s`, which is really useful for debugging single tests without having to
64 # insert a False assert to see the log.
66 "[{}] {}".format(now
.strftime('%Y-%m-%dT%H:%M:%S'), msg
)
69 # "[{}] {}".format(now.strftime('%Y-%m-%dT%H:%M:%S'), msg)
74 return "{}/charms".format(here
)
78 return "{}/charms/layers".format(here
)
81 def collect_metrics(application
):
82 """Invoke Juju's metrics collector.
84 Caveat: this shells out to the `juju collect-metrics` command, rather than
85 making an API call. At the time of writing, that API is not exposed through
90 subprocess
.check_call(['juju', 'collect-metrics', application
])
91 except subprocess
.CalledProcessError
as e
:
92 raise Exception("Unable to collect metrics: {}".format(e
))
95 def has_metrics(charm
):
96 """Check if a charm has metrics defined."""
97 metricsyaml
= "{}/{}/metrics.yaml".format(
101 if os
.path
.exists(metricsyaml
):
106 def get_descriptor(descriptor
):
109 tmp
= yaml
.load(descriptor
)
111 # Remove the envelope
112 root
= list(tmp
.keys())[0]
113 if root
== "nsd:nsd-catalog":
114 desc
= tmp
['nsd:nsd-catalog']['nsd'][0]
115 elif root
== "vnfd:vnfd-catalog":
116 desc
= tmp
['vnfd:vnfd-catalog']['vnfd'][0]
122 def get_n2vc(loop
=None):
123 """Return an instance of N2VC.VNF."""
124 log
= logging
.getLogger()
125 log
.level
= logging
.DEBUG
127 # Extract parameters from the environment in order to run our test
128 vca_host
= os
.getenv('VCA_HOST', '127.0.0.1')
129 vca_port
= os
.getenv('VCA_PORT', 17070)
130 vca_user
= os
.getenv('VCA_USER', 'admin')
131 vca_charms
= os
.getenv('VCA_CHARMS', None)
132 vca_secret
= os
.getenv('VCA_SECRET', None)
134 client
= n2vc
.vnf
.N2VC(
140 artifacts
=vca_charms
,
146 def create_lxd_container(public_key
=None, name
="test_name"):
148 Returns a container object
150 If public_key isn't set, we'll use the Juju ssh key
152 :param public_key: The public key to inject into the container
153 :param name: The name of the test being run
157 # Format name so it's valid
158 name
= name
.replace("_", "-").replace(".", "")
160 client
= get_lxd_client()
161 test_machine
= "test-{}-{}".format(
162 uuid
.uuid4().hex[-4:],
166 private_key_path
, public_key_path
= find_juju_ssh_keys()
169 # create profile w/cloud-init and juju ssh key
172 with
open(public_key_path
, "r") as f
:
173 public_key
= f
.readline()
175 client
.profiles
.create(
178 'user.user-data': '#cloud-config\nssh_authorized_keys:\n- {}'.format(public_key
)},
180 'root': {'path': '/', 'pool': 'default', 'type': 'disk'},
182 'nictype': 'bridged',
188 except Exception as ex
:
189 debug("Error creating lxd profile {}: {}".format(test_machine
, ex
))
195 'name': test_machine
,
200 'protocol': 'simplestreams',
201 'server': 'https://cloud-images.ubuntu.com/releases',
203 'profiles': [test_machine
],
205 container
= client
.containers
.create(config
, wait
=True)
206 container
.start(wait
=True)
207 except Exception as ex
:
208 debug("Error creating lxd container {}: {}".format(test_machine
, ex
))
209 # This is a test-ending failure.
212 def wait_for_network(container
, timeout
=30):
213 """Wait for eth0 to have an ipv4 address."""
214 starttime
= time
.time()
215 while(time
.time() < starttime
+ timeout
):
217 if 'eth0' in container
.state().network
:
218 addresses
= container
.state().network
['eth0']['addresses']
219 if len(addresses
) > 0:
220 if addresses
[0]['family'] == 'inet':
225 wait_for_network(container
)
226 except Exception as ex
:
228 "Error waiting for container {} network: {}".format(
234 # HACK: We need to give sshd a chance to bind to the interface,
235 # and pylxd's container.execute seems to be broken and fails and/or
236 # hangs trying to properly check if the service is up.
237 (exit_code
, stdout
, stderr
) = container
.execute([
239 '-c', '5', # Wait for 5 ECHO_REPLY
240 '8.8.8.8', # Ping Google's public DNS
241 '-W', '15', # Set a 15 second deadline
245 raise Exception("Unable to verify container network")
250 def destroy_lxd_container(container
):
251 """Stop and delete a LXD container.
253 Sometimes we see errors talking to LXD -- ephemerial issues like
254 load or a bug that's killed the API. We'll do our best to clean
255 up here, and we should run a cleanup after all tests are finished
256 to remove any extra containers and profiles belonging to us.
259 if type(container
) is bool:
262 name
= container
.name
263 debug("Destroying container {}".format(name
))
265 client
= get_lxd_client()
267 def wait_for_stop(timeout
=30):
268 """Wait for eth0 to have an ipv4 address."""
269 starttime
= time
.time()
270 while(time
.time() < starttime
+ timeout
):
272 if container
.state
== "Stopped":
275 def wait_for_delete(timeout
=30):
276 starttime
= time
.time()
277 while(time
.time() < starttime
+ timeout
):
279 if client
.containers
.exists(name
) is False:
283 container
.stop(wait
=False)
285 except Exception as ex
:
287 "Error stopping container {}: {}".format(
294 container
.delete(wait
=False)
296 except Exception as ex
:
298 "Error deleting container {}: {}".format(
305 # Delete the profile created for this container
306 profile
= client
.profiles
.get(name
)
309 except Exception as ex
:
311 "Error deleting profile {}: {}".format(
318 def find_lxd_config():
319 """Find the LXD configuration directory."""
321 paths
.append(os
.path
.expanduser("~/.config/lxc"))
322 paths
.append(os
.path
.expanduser("~/snap/lxd/current/.config/lxc"))
325 if os
.path
.exists(path
):
326 crt
= os
.path
.expanduser("{}/client.crt".format(path
))
327 key
= os
.path
.expanduser("{}/client.key".format(path
))
328 if os
.path
.exists(crt
) and os
.path
.exists(key
):
333 def find_juju_ssh_keys():
334 """Find the Juju ssh keys."""
337 paths
.append(os
.path
.expanduser("~/.local/share/juju/ssh/"))
340 if os
.path
.exists(path
):
341 private
= os
.path
.expanduser("{}/juju_id_rsa".format(path
))
342 public
= os
.path
.expanduser("{}/juju_id_rsa.pub".format(path
))
343 if os
.path
.exists(private
) and os
.path
.exists(public
):
344 return (private
, public
)
348 def get_juju_private_key():
349 keys
= find_juju_ssh_keys()
353 def get_lxd_client(host
="127.0.0.1", port
="8443", verify
=False):
354 """ Get the LXD client."""
356 (crt
, key
) = find_lxd_config()
359 client
= pylxd
.Client(
360 endpoint
="https://{}:{}".format(host
, port
),
368 # TODO: This is marked serial but can be run in parallel with work, including:
369 # - Fixing an event loop issue; seems that all tests stop when one test stops?
373 class TestN2VC(object):
375 1. Validator Validation
377 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.
379 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.
383 The six phases of integration testing, for the test itself and each charm?:
385 setup/teardown_class:
386 1. Prepare - Verify the environment and create a new model
387 2. Deploy - Mark the test as ready to execute
388 3. Configure - Configuration to reach Active state
389 4. Test - Execute primitive(s) to verify success
390 5. Collect - Collect any useful artifacts for debugging (charm, logs)
391 6. Destroy - Destroy the model
394 1. Prepare - Building of charm
395 2. Deploy - Deploying charm
396 3. Configure - Configuration to reach Active state
397 4. Test - Execute primitive(s) to verify success
398 5. Collect - Collect any useful artifacts for debugging (charm, logs)
399 6. Destroy - Destroy the charm
403 def setup_class(self
):
404 """ setup any state specific to the execution of the given class (which
405 usually contains tests).
407 # Initialize instance variable(s)
410 # Track internal state for each test run
413 # Parse the test's descriptors
414 self
.nsd
= get_descriptor(self
.NSD_YAML
)
415 self
.vnfd
= get_descriptor(self
.VNFD_YAML
)
417 self
.ns_name
= self
.nsd
['name']
418 self
.vnf_name
= self
.vnfd
['name']
420 # Hard-coded to default for now, but this may change in the future.
421 self
.model
= "default"
424 self
.parse_vnf_descriptor()
425 assert self
.charms
is not {}
427 # Track artifacts, like compiled charms, that will need to be removed
430 # Build the charm(s) needed for this test
431 for charm
in self
.get_charm_names():
432 self
.get_charm(charm
)
434 # A bit of a hack, in order to allow the N2VC callback to run parallel
435 # to pytest. Test(s) should wait for this flag to change to False
438 self
._stopping
= False
441 def teardown_class(self
):
442 """ teardown any state that was previously setup with a call to
445 debug("Running teardown_class...")
447 debug("Destroying LXD containers...")
448 for application
in self
.state
:
449 if self
.state
[application
]['container']:
450 destroy_lxd_container(self
.state
[application
]['container'])
451 debug("Destroying LXD containers...done.")
455 debug("Logging out of N2VC...")
456 yield from self
.n2vc
.logout()
457 debug("Logging out of N2VC...done.")
458 debug("Running teardown_class...done.")
459 except Exception as ex
:
460 debug("Exception in teardown_class: {}".format(ex
))
463 def all_charms_active(self
):
464 """Determine if the all deployed charms are active."""
467 for application
in self
.state
:
468 if 'status' in self
.state
[application
]:
469 debug("status of {} is '{}'".format(
471 self
.state
[application
]['status'],
473 if self
.state
[application
]['status'] == 'active':
476 debug("Active charms: {}/{}".format(
481 if active
== len(self
.charms
):
487 def are_tests_finished(self
):
488 appcount
= len(self
.state
)
490 # If we don't have state yet, keep running.
492 debug("No applications")
496 debug("_stopping is True")
500 for application
in self
.state
:
501 if self
.state
[application
]['done']:
504 debug("{}/{} charms tested".format(appdone
, appcount
))
506 if appcount
== appdone
:
512 async def running(self
, timeout
=600):
513 """Returns if the test is still running.
515 @param timeout The time, in seconds, to wait for the test to complete.
517 if self
.are_tests_finished():
521 await asyncio
.sleep(30)
526 def get_charm(self
, charm
):
527 """Build and return the path to the test charm.
529 Builds one of the charms in tests/charms/layers and returns the path
530 to the compiled charm. The charm will automatically be removed when
531 when the test is complete.
533 Returns: The path to the built charm or None if `charm build` failed.
536 # Make sure the charm snap is installed
538 subprocess
.check_call(['which', 'charm'])
539 except subprocess
.CalledProcessError
as e
:
540 raise Exception("charm snap not installed.")
542 if charm
not in self
.artifacts
:
544 # Note: This builds the charm under N2VC/tests/charms/builds/
545 # Currently, the snap-installed command only has write access
546 # to the $HOME (changing in an upcoming release) so writing to
547 # /tmp isn't possible at the moment.
548 builds
= get_charm_path()
550 if not os
.path
.exists("{}/builds/{}".format(builds
, charm
)):
551 cmd
= "charm build {}/{} -o {}/".format(
556 subprocess
.check_call(shlex
.split(cmd
))
558 self
.artifacts
[charm
] = {
560 'charm': "{}/builds/{}".format(builds
, charm
),
562 except subprocess
.CalledProcessError
as e
:
563 raise Exception("charm build failed: {}.".format(e
))
565 return self
.artifacts
[charm
]['charm']
568 async def deploy(self
, vnf_index
, charm
, params
, loop
):
569 """An inner function to do the deployment of a charm from
574 self
.n2vc
= get_n2vc(loop
=loop
)
576 vnf_name
= self
.n2vc
.FormatApplicationName(
581 debug("Deploying charm at {}".format(self
.artifacts
[charm
]))
583 await self
.n2vc
.DeployCharms(
587 self
.get_charm(charm
),
594 def parse_vnf_descriptor(self
):
595 """Parse the VNF descriptor to make running tests easier.
597 Parse the charm information in the descriptor to make it easy to write
598 tests to run again it.
600 Each charm becomes a dictionary in a list:
603 'vnf-member-index': 1,
606 'initial-config-primitive': {},
607 'config-primitive': {}
610 - is this a proxy charm?
611 - what are the initial-config-primitives (day 1)?
612 - what are the config primitives (day 2)?
617 # You'd think this would be explicit, but it's just an incremental
618 # value that should be consistent.
621 """Get all vdu and/or vdu config in a descriptor."""
622 config
= self
.get_config()
626 # Get the name to be used for the deployed application
627 application_name
= n2vc
.vnf
.N2VC().FormatApplicationName(
630 str(vnf_member_index
),
634 'application-name': application_name
,
636 'vnf-member-index': vnf_member_index
,
637 'vnf-name': self
.vnf_name
,
639 'initial-config-primitive': {},
640 'config-primitive': {},
644 charm
['name'] = juju
['charm']
647 charm
['proxy'] = juju
['proxy']
649 if 'initial-config-primitive' in cfg
:
650 charm
['initial-config-primitive'] = \
651 cfg
['initial-config-primitive']
653 if 'config-primitive' in cfg
:
654 charm
['config-primitive'] = cfg
['config-primitive']
656 charms
[application_name
] = charm
658 # Increment the vnf-member-index
659 vnf_member_index
+= 1
664 def isproxy(self
, application_name
):
666 assert application_name
in self
.charms
667 assert 'proxy' in self
.charms
[application_name
]
668 assert type(self
.charms
[application_name
]['proxy']) is bool
670 # debug(self.charms[application_name])
671 return self
.charms
[application_name
]['proxy']
674 def get_config(self
):
675 """Return an iterable list of config items (vdu and vnf).
677 As far as N2VC is concerned, the config section for vdu and vnf are
678 identical. This joins them together so tests only need to iterate
683 """Get all vdu and/or vdu config in a descriptor."""
684 vnf_config
= self
.vnfd
.get("vnf-configuration")
686 juju
= vnf_config
['juju']
688 configs
.append(vnf_config
)
690 for vdu
in self
.vnfd
['vdu']:
691 vdu_config
= vdu
.get('vdu-configuration')
693 juju
= vdu_config
['juju']
695 configs
.append(vdu_config
)
700 def get_charm_names(self
):
701 """Return a list of charms used by the test descriptor."""
705 # Check if the VDUs in this VNF have a charm
706 for config
in self
.get_config():
707 juju
= config
['juju']
710 if name
not in charms
:
716 def get_phase(self
, application
):
717 return self
.state
[application
]['phase']
720 def set_phase(self
, application
, phase
):
721 self
.state
[application
]['phase'] = phase
724 async def configure_proxy_charm(self
, *args
):
725 (model
, application
, _
, _
) = args
728 if self
.get_phase(application
) == "deploy":
729 self
.set_phase(application
, "configure")
731 debug("Start CreateContainer for {}".format(application
))
732 self
.state
[application
]['container'] = \
733 await self
.CreateContainer(*args
)
734 debug("Done CreateContainer for {}".format(application
))
736 if self
.state
[application
]['container']:
737 debug("Configure {} for container".format(application
))
738 if await self
.configure_ssh_proxy(application
):
739 await asyncio
.sleep(0.1)
742 debug("Failed to configure container for {}".format(application
))
744 debug("skipping CreateContainer for {}: {}".format(
746 self
.get_phase(application
),
749 except Exception as ex
:
750 debug("configure_proxy_charm exception: {}".format(ex
))
752 await asyncio
.sleep(0.1)
757 async def execute_charm_tests(self
, *args
):
758 (model
, application
, _
, _
) = args
760 debug("Executing charm test(s) for {}".format(application
))
762 if self
.state
[application
]['done']:
763 debug("Trying to execute tests against finished charm...aborting")
767 phase
= self
.get_phase(application
)
768 # We enter the test phase when after deploy (for native charms) or
769 # configure, for proxy charms.
770 if phase
in ["deploy", "configure"]:
771 self
.set_phase(application
, "test")
772 if self
.are_tests_finished():
773 raise Exception("Trying to execute init-config on finished test")
775 if await self
.execute_initial_config_primitives(application
):
777 await self
.check_metrics(application
)
779 debug("Done testing {}".format(application
))
780 self
.state
[application
]['done'] = True
782 except Exception as ex
:
783 debug("Exception in execute_charm_tests: {}".format(ex
))
785 await asyncio
.sleep(0.1)
790 async def CreateContainer(self
, *args
):
791 """Create a LXD container for use with a proxy charm.abs
793 1. Get the public key from the charm via `get-ssh-public-key` action
794 2. Create container with said key injected for the ubuntu user
796 Returns a Container object
798 # Create and configure a LXD container for use with a proxy charm.
799 (model
, application
, _
, _
) = args
801 debug("[CreateContainer] {}".format(args
))
805 # Execute 'get-ssh-public-key' primitive and get returned value
806 uuid
= await self
.n2vc
.ExecutePrimitive(
809 "get-ssh-public-key",
813 result
= await self
.n2vc
.GetPrimitiveOutput(model
, uuid
)
814 pubkey
= result
['pubkey']
816 container
= create_lxd_container(
818 name
=os
.path
.basename(__file__
)
822 except Exception as ex
:
823 debug("Error creating container: {}".format(ex
))
829 async def stop(self
):
833 - Stop and delete containers
836 TODO: Clean up duplicate code between teardown_class() and stop()
838 debug("stop() called")
840 if self
.n2vc
and self
._running
and not self
._stopping
:
841 self
._running
= False
842 self
._stopping
= True
844 for application
in self
.charms
:
846 await self
.n2vc
.RemoveCharms(self
.model
, application
)
847 if self
.state
[application
]['container']:
848 debug("Deleting LXD container...")
849 destroy_lxd_container(
850 self
.state
[application
]['container']
852 self
.state
[application
]['container'] = None
853 debug("Deleting LXD container...done.")
855 debug("No container found for {}".format(application
))
856 except Exception as e
:
857 debug("Error while deleting container: {}".format(e
))
861 debug("Logging out of N2VC...")
862 await self
.n2vc
.logout()
864 debug("Logging out of N2VC...Done.")
865 except Exception as ex
:
868 # Let the test know we're finished.
869 debug("Marking test as finished.")
870 # self._running = False
872 debug("Skipping stop()")
875 def get_container_ip(self
, container
):
876 """Return the IPv4 address of container's eth0 interface."""
879 addresses
= container
.state().network
['eth0']['addresses']
880 # The interface may have more than one address, but we only need
881 # the first one for testing purposes.
882 ipaddr
= addresses
[0]['address']
887 async def configure_ssh_proxy(self
, application
, task
=None):
888 """Configure the proxy charm to use the lxd container.
890 Configure the charm to use a LXD container as it's VNF.
892 debug("Configuring ssh proxy for {}".format(application
))
894 mgmtaddr
= self
.get_container_ip(
895 self
.state
[application
]['container'],
899 "Setting ssh-hostname for {} to {}".format(
905 await self
.n2vc
.ExecutePrimitive(
911 'ssh-hostname': mgmtaddr
,
912 'ssh-username': 'ubuntu',
919 async def execute_initial_config_primitives(self
, application
, task
=None):
920 debug("Executing initial_config_primitives for {}".format(application
))
922 init_config
= self
.charms
[application
]
925 The initial-config-primitive is run during deploy but may fail
926 on some steps because proxy charm access isn't configured.
928 Re-run those actions so we can inspect the status.
930 uuids
= await self
.n2vc
.ExecuteInitialPrimitives(
937 ExecutePrimitives will return a list of uuids. We need to check the
938 status of each. The test continues if all Actions succeed, and
939 fails if any of them fail.
941 await self
.wait_for_uuids(application
, uuids
)
942 debug("Primitives for {} finished.".format(application
))
945 except Exception as ex
:
946 debug("execute_initial_config_primitives exception: {}".format(ex
))
951 async def check_metrics(self
, application
, task
=None):
952 """Check and run metrics, if present.
954 Checks to see if metrics are specified by the charm. If so, collects
957 If no metrics, then mark the test as finished.
959 if has_metrics(self
.charms
[application
]['name']):
960 debug("Collecting metrics for {}".format(application
))
962 metrics
= await self
.n2vc
.GetMetrics(
967 return await self
.verify_metrics(application
, metrics
)
970 async def verify_metrics(self
, application
, metrics
):
971 """Verify the charm's metrics.
973 Verify that the charm has sent metrics successfully.
975 Stops the test when finished.
977 debug("Verifying metrics for {}: {}".format(application
, metrics
))
983 # TODO: Ran into a case where it took 9 attempts before metrics
984 # were available; the controller is slow sometimes.
985 await asyncio
.sleep(30)
986 return await self
.check_metrics(application
)
989 async def wait_for_uuids(self
, application
, uuids
):
990 """Wait for primitives to execute.
992 The task will provide a list of uuids representing primitives that are
995 debug("Waiting for uuids for {}: {}".format(application
, uuids
))
999 while waitfor
> finished
:
1001 await asyncio
.sleep(10)
1003 if uuid
not in self
.state
[application
]['actions']:
1004 self
.state
[application
]['actions'][uid
] = "pending"
1006 status
= self
.state
[application
]['actions'][uid
]
1008 # Have we already marked this as done?
1009 if status
in ["pending", "running"]:
1011 debug("Getting status of {} ({})...".format(uid
, status
))
1012 status
= await self
.n2vc
.GetPrimitiveStatus(
1016 debug("...state of {} is {}".format(uid
, status
))
1017 self
.state
[application
]['actions'][uid
] = status
1019 if status
in ['completed', 'failed']:
1022 debug("{}/{} actions complete".format(finished
, waitfor
))
1024 # Wait for the primitive to finish and try again
1025 if waitfor
> finished
:
1026 debug("Waiting 10s for action to finish...")
1027 await asyncio
.sleep(10)
1030 def n2vc_callback(self
, *args
, **kwargs
):
1031 (model
, application
, status
, message
) = args
1032 # debug("callback: {}".format(args))
1034 if application
not in self
.state
:
1035 # Initialize the state of the application
1036 self
.state
[application
] = {
1037 'status': None, # Juju status
1038 'container': None, # lxd container, for proxy charms
1039 'actions': {}, # Actions we've executed
1040 'done': False, # Are we done testing this charm?
1041 'phase': "deploy", # What phase is this application in?
1044 self
.state
[application
]['status'] = status
1046 if status
in ['waiting', 'maintenance', 'unknown']:
1047 # Nothing to do for these
1050 debug("callback: {}".format(args
))
1052 if self
.state
[application
]['done']:
1053 debug("{} is done".format(application
))
1056 if status
in ["blocked"] and self
.isproxy(application
):
1057 if self
.state
[application
]['phase'] == "deploy":
1058 debug("Configuring proxy charm for {}".format(application
))
1059 asyncio
.ensure_future(self
.configure_proxy_charm(*args
))
1061 elif status
in ["active"]:
1062 """When a charm is active, we can assume that it has been properly
1063 configured (not blocked), regardless of if it's a proxy or not.
1065 All primitives should be complete by init_config_primitive
1067 asyncio
.ensure_future(self
.execute_charm_tests(*args
))