Skip to content

Commit

Permalink
contrib refactor for more client plugins (#117)
Browse files Browse the repository at this point in the history
- launchpad/terraform/sonobuoy contrib all have separated client plugins
- provisioner plugins now use a client plugin for action
- testkit stack fixes. The plugin combo is full stack but still doesn't
  handle upgrades.

- unfotunately this made the cli more complicated so I dropped the
  "contrib" part of the cli path. clis now tend to use the client group
  as a base and add workload/provisioner groups to that.

- version bump to 0.30.0

Signed-off-by: James Nesbitt <[email protected]>
  • Loading branch information
james-nesbitt authored Jul 29, 2021
1 parent d75b3ae commit e1aff94
Show file tree
Hide file tree
Showing 100 changed files with 2,870 additions and 1,787 deletions.
5 changes: 2 additions & 3 deletions mirantis/testing/metta/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"""

import os
from typing import List
from typing import List, Callable
import logging
import importlib

Expand Down Expand Up @@ -100,7 +100,6 @@ def discover(
# Create any environments defined in config

metta_config = config.load("metta")

if metta_config.has(["environments", "from_config"]):
environment_config = metta_config.get(["environments", "from_config"])
label = environment_config["label"] if "label" in environment_config else "metta"
Expand Down Expand Up @@ -355,7 +354,7 @@ def new_environments_from_builder(
raise ValueError(f"Env builder could not load suggested python package:{err}") from err

try:
method = getattr(module, method)
method: Callable = getattr(module, method)
except Exception as err:
raise ValueError(f"Env builder could not execute package method: {err}") from err

Expand Down
1 change: 0 additions & 1 deletion mirantis/testing/metta/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ def discover_project_root(config: Config, start_path: str, marker_files: List[st
# If we found a marker file in a path, then we add that path as
# a config source. If the path contains a ./config then we add
# that as well.

if check_path not in sys.path:
sys.path.append(check_path)

Expand Down
23 changes: 14 additions & 9 deletions mirantis/testing/metta/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,16 +162,17 @@ def __init__(
if not self.config_label:
# this environment does not have a related configuration to program
# itself with, but it could have had bootstraps.
logger.info("New environment created: %s (not from config)", name)
#
# this was the original mechanisms for defining environments, and
# does have usecases left today for simple environment definition,
# but any serious environment usage would be much better served by
# using the configuration options; it lets you define more config
# sources, fixtures, bootstraps etc.

logger.info("New environment created: %s (not from config)", name)
self.bootstrap(bootstraps)
# this was the original mechanisms for defining environments, and does have usecases
# left today for simple environment definition, but any serious environment usage would
# be much better served by using the configuration options; it lets you define more
# config sources, fixtures, bootstraps etc.

else:

logger.info("New environment created: %s", name)

# There is a config dict to add to the environment
Expand Down Expand Up @@ -252,10 +253,10 @@ def set_state(self, state: str = METTA_ENVIRONMENT_STATE_UNUSED_NAME):
""" common config base for all environment states """

# here we check if there is a state that should come from config.
# if the state looks liek a real key then we build a config base for it
# for loading config from that path, otherwsie we leave it as None
# if the state looks like a real key then we build a config base for it
# for loading config from that path, otherwise we leave it as None
# which is used for testing later in this method.
if state in [METTA_ENVIRONMENT_STATE_UNUSED_NAME, state]:
if state in [METTA_ENVIRONMENT_STATE_UNUSED_NAME]:
state_config_base = None
else:
state_config_base = [
Expand Down Expand Up @@ -977,6 +978,10 @@ def add_fixture(
f":{plugin_id}:{instance_id} ({priority})"
)

if labels is None:
labels = {}
labels["environment"] = self.name

plugin_instance = Factory.create(plugin_id, instance_id, *[self, instance_id], **arguments)

fixture = self.fixtures.add(
Expand Down
148 changes: 112 additions & 36 deletions mirantis/testing/metta/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class Fixture:
# pylint: disable=too-many-arguments
def __init__(
self,
plugin: object,
plugin: Any,
plugin_id: str,
instance_id: str,
interfaces: List[str],
Expand All @@ -80,12 +80,12 @@ def __init__(
identifiers that the plugin should support.
"""
self.plugin_id = plugin_id
self.instance_id = instance_id
self.interfaces = interfaces
self.labels = labels
self.priority = priority
self.plugin = plugin
self.plugin_id: str = plugin_id
self.instance_id: str = instance_id
self.interfaces: List[str] = interfaces
self.labels: Dict[str, str] = labels
self.priority: int = priority
self.plugin: Any = plugin

def __eq__(self, other):
"""Compare to another fixture.
Expand All @@ -106,7 +106,29 @@ def __eq__(self, other):
return self.plugin_id == other.plugin_id and self.instance_id == other.instance_id

def info(self, deep: bool = False, children: bool = True) -> Dict[str, Any]:
"""Return some dict metadata about the fixture and plugin."""
"""Return some dict metadata about the fixture and plugin.
Parameters:
-----------
deep (bool) : whether or not the information provided should go deep
into the avaiable sources of data. Info is intended to be cheap
to perform, but there are some scenarios where you want more data
despite the cost, or risk for exception.
children (bool) : if the fixture has children fixtures, then this
flag controls whether or not the children info should also be
included.
Returns:
--------
Dict[str, Any] or primitives that can be used for introspection of the
fixtures.
Raises:
-------
Info() should be safe for operation, but if deep=True then there may be
a risk of a plugin throwing an exception.
"""
fixture_info: Dict[str, Any] = {
"fixture": {
"instance_id": self.instance_id,
Expand Down Expand Up @@ -465,34 +487,84 @@ def filter(
"""
filtered = Fixtures()
for fixture in self._fixtures:
if interfaces:
intersect = False
for required_interface in interfaces:
if required_interface not in fixture.interfaces:
break
# I feel like there is a "right" way to do this?
else:
intersect = True
if not intersect:
continue

if labels:
intersect = False
for required_label in labels:
if required_label not in fixture.labels:
break
# I feel like there is a "right" way to do this?
else:
intersect = True
if not intersect:
continue

if plugin_id and not fixture.plugin_id == plugin_id:
continue
if instance_id and not fixture.instance_id == instance_id:
continue

filtered.add(fixture, replace_existing=True)
try:
if interfaces:
for required_interface in interfaces:
if required_interface not in fixture.interfaces:
raise DoesNotMatchError("does not contain required interface")

if labels:
for required_label in labels:
if required_label not in fixture.labels:
raise DoesNotMatchError("does not contain required label")

if plugin_id and not fixture.plugin_id == plugin_id:
raise DoesNotMatchError("plugin_id does not match")
if instance_id and not fixture.instance_id == instance_id:
raise DoesNotMatchError("instance_id does not match")

filtered.add(fixture, replace_existing=True)

except DoesNotMatchError:
pass

if exception_if_missing and len(filtered) == 0:
raise KeyError(f"Filter found matches [{plugin_id}][{instance_id}]")
return filtered

# I should try to fix the branch count here
# pylint: disable=too-many-branches
def filter_out(
self,
plugin_id: str = "",
instance_id: str = "",
interfaces: List[str] = None,
labels: List[str] = None,
exception_if_missing: bool = False,
) -> "Fixtures":
"""Return the fixtures with fixtures removed.
Parameters:
-----------
Filtering parameters:
plugin_id (str) : registry plugin_id
interfaces (List[str]) : List of interfaces which the fixture must have
labels (List[str]) : List of labels which the fixture must have
instance_id (str) : plugin instance identifier
Returns:
--------
A new Fixtures object with only matching Fixture objects. It could
contain all of the items. If no filters were passed in, or it could
be empty if no matches were found and the passed exception_is_missing
variable is True
Raises:
-------
KeyError if exception_if_missing is True and no matching fixture was found
"""
filtered = Fixtures()
for fixture in self._fixtures:
try:
if interfaces:
for required_interface in interfaces:
if required_interface not in fixture.interfaces:
raise DoesNotMatchError("does not contain required interface")

if labels:
for required_label in labels:
if required_label not in fixture.labels:
raise DoesNotMatchError("does not contain required label")

if plugin_id and not fixture.plugin_id == plugin_id:
raise DoesNotMatchError("plugin_id does not match")
if instance_id and not fixture.instance_id == instance_id:
raise DoesNotMatchError("instance_id does not match")

except DoesNotMatchError:
filtered.add(fixture, replace_existing=True)

if exception_if_missing and len(filtered) == 0:
raise KeyError(f"Filter found matches [{plugin_id}][{instance_id}]")
Expand All @@ -513,3 +585,7 @@ def to_list(self) -> List[Fixture]:
"""
return sorted(self._fixtures, key=lambda i: 1 / i.priority if i.priority > 0 else 0)


class DoesNotMatchError(Exception):
"""An exception to indicate that a value does not match filter expectations."""
4 changes: 2 additions & 2 deletions mirantis/testing/metta/healthcheck.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
Health check plugins
Health check plugins.
Plugins which can be used to determine health of a system.
Expand Down Expand Up @@ -87,7 +87,7 @@ def __init__(self, source: str, status: HealthStatus, message: str):

def __str__(self) -> str:
"""Convert message instance to string."""
return f"[{int(self.time)}] {self.source}: {self.status} => {self.message}"
return f"[{int(self.time)}] {self.status} > {self.source} => {self.message}"


class Health:
Expand Down
2 changes: 1 addition & 1 deletion mirantis/testing/metta_ansible/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def metta_plugin_factory_provisioner_ansibleplaybook(
label: str = ANSIBLE_PROVISIONER_CONFIG_LABEL,
base: Any = LOADED_KEY_ROOT,
):
"""create a metta provisioners plugin"""
"""create a metta provisioner plugin"""
return AnsiblePlaybookProvisionerPlugin(environment, instance_id, label, base)


Expand Down
16 changes: 13 additions & 3 deletions mirantis/testing/metta_ansible/ansiblecli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,23 @@ class AnsibleClient:
def __init__(
self,
inventory_path: str,
ansiblecfg_path: str,
ansiblecfg_path: str = "",
ansiblebinary: str = ANSIBLE_CLIENT_DEFAULT_BINARY,
):
"""Initialize Ansible client.
Parameters:
-----------
inventory_path (str) : string path to the ansible inventory file
ansiblecfg_path (str)
"""
self.inventory_path = inventory_path
"""String path to an ansible invetory path."""
self.ansiblecfg_path = ansiblecfg_path
"""Optional string path to an ansible cfg file."""

if shutil.which(ansiblebinary) is None:
raise ValueError(
Expand All @@ -55,6 +61,7 @@ def __init__(
)

self.ansible_bin = ansiblebinary
"""Ansible binary executable path to run."""

# deep argument is a part of the info() interface for all plugins
# pylint: disable=unused-argument
Expand Down Expand Up @@ -108,7 +115,7 @@ def run(

cmd = [self.ansible_bin]

if with_ansiblecfg:
if with_ansiblecfg and self.ansiblecfg_path:
env["ANSIBLE_CONFIG"] = self.ansiblecfg_path
if with_inventory:
cmd += [f"--inventory={self.inventory_path}"]
Expand Down Expand Up @@ -149,7 +156,9 @@ def __init__(
"""
self.inventory_path = inventory_path
"""String path to an ansible invetory path."""
self.ansiblecfg_path = ansiblecfg_path
"""Optional string path to an ansible cfg file."""

if shutil.which(playbookbinary) is None:
raise ValueError(
Expand All @@ -158,6 +167,7 @@ def __init__(
)

self.ansibleplaybook_bin = playbookbinary
"""Ansible-Playbook binary executable path to run."""

# deep argument is a part of the info() interface for all plugins
# pylint: disable=unused-argument
Expand Down Expand Up @@ -204,7 +214,7 @@ def _run(

cmd = [self.ansibleplaybook_bin]

if with_ansiblecfg:
if with_ansiblecfg and self.ansiblecfg_path:
allenvs["ANSIBLE_CONFIG"] = self.ansiblecfg_path
if extravars_path:
cmd += [f"--extra-vars=@{extravars_path}"]
Expand Down
Loading

0 comments on commit e1aff94

Please sign in to comment.