예제 #1
0
    def prepare(self, fixtures: Fixtures = None):
        """Find the dependent fixtures."""
        if fixtures is None:
            fixtures = self._environment.fixtures()

        self._ansible_client = fixtures.get_plugin(
            plugin_id=METTA_ANSIBLE_ANSIBLECLI_CORECLIENT_PLUGIN_ID)
예제 #2
0
    def prepare(self, fixtures: Fixtures = None):
        """Create a workload instance from a set of fixtures.

        Parameters:
        -----------
        fixtures (Fixtures) : a set of fixtures that this workload will use to
            retrieve a docker client plugin.

        """
        if fixtures is None:
            fixtures = self._environment.fixtures()

        self._docker_client = fixtures.get_plugin(
            interfaces=[METTA_PLUGIN_ID_DOCKER_CLIENT])
예제 #3
0
    def prepare(self, fixtures: Fixtures = None):
        """Find the dependent fixtures."""
        if fixtures is None:
            fixtures = self._environment.fixtures()

        self._ansibleplaybook_client = fixtures.get_plugin(
            plugin_id=METTA_ANSIBLE_ANSIBLECLI_PLAYBOOKCLIENT_PLUGIN_ID)

        # Loaded configerus config for the plugin. Ready for .get().
        plugin_config = self._environment.config().load(self._config_label)

        playbook_contents: str = plugin_config.get(
            [self._config_base, ANSIBLE_WORKLOAD_CONFIG_PLAYBOOK_KEY],
            default={})
        playbook_path: str = plugin_config.get(
            [self._config_base, ANSIBLE_WORKLOAD_CONFIG_PLAYBOOK_PATH_KEY],
            default="",
        )
        vars_values: str = plugin_config.get(
            [self._config_base, ANSIBLE_WORKLOAD_CONFIG_PLAYBOOK_VARS_KEY],
            default={})
        vars_path: str = plugin_config.get(
            [
                self._config_base,
                ANSIBLE_WORKLOAD_CONFIG_PLAYBOOK_VARS_PATH_KEY
            ],
            default="",
        )

        if playbook_contents:
            os.makedirs(os.path.dirname(os.path.realpath(playbook_path)),
                        exist_ok=True)
            with open(playbook_path, "w",
                      encoding="utf8") as playbook_fileobject:
                yaml.safe_dump(playbook_contents, playbook_fileobject)
        else:
            if playbook_path and os.path.exists(playbook_path):
                os.remove(playbook_path)
        if vars_values:
            os.makedirs(os.path.dirname(os.path.realpath(vars_path)),
                        exist_ok=True)
            with open(vars_path, "w", encoding="utf8") as vars_fileobject:
                yaml.safe_dump(vars_values, vars_fileobject)
        else:
            if vars_path and os.path.exists(vars_path):
                os.remove(vars_path)
예제 #4
0
    def prepare(self, fixtures: Fixtures = None):
        """Get the kubeapi client from a set of fixtures.

        Parameters:
        -----------
        fixtures (Fixtures) : a set of fixtures that this workload will use to
            retrieve a kubernetes client plugin.

        """
        if fixtures is None:
            fixtures = self._environment.fixtures()

        try:
            self.client = fixtures.get_plugin(
                plugin_id=METTA_PLUGIN_ID_KUBERNETES_CLIENT)
        except KeyError as err:
            raise NotImplementedError(
                "Workload could not find the needed client: "
                f"{METTA_PLUGIN_ID_KUBERNETES_CLIENT}") from err
예제 #5
0
class TerraformProvisionerPlugin:
    """Terraform provisioner plugin.

    Provisioner plugin that allows control of and interaction with a terraform
    cluster.

    ## Requirements

    1. this plugin uses subprocess to call a terraform binary, so you have to
        install terraform in the environment

    ## Usage

    ### Plan

    The plan must exists somewhere on disk, and be accessible.

    You must specify the path and related configuration in config, which are
    read in the .prepare() execution.

    ### Vars/State

    This plugin reads TF vars from config and writes them to a vars file.  We
    could run without relying on vars file, but having a vars file allows cli
    interaction with the cluster if this plugin messes up.

    You can override where Terraform vars/state files are written to allow
    sharing of a plan across test suites.

    Parameters:
    -----------
    environment (Environment) : All metta plugins receive the environment
        object in which they were created.
    instance_id (str) : all metta plugins receive their own string identity.

    label (str) : Configerus load label for finding plugin config.
    base (str) : Configerus get base key in which the plugin should look for
        all config.

    """
    def __init__(
        self,
        environment: Environment,
        instance_id: str,
        label: str = TERRAFORM_PROVISIONER_CONFIG_LABEL,
        base: Any = LOADED_KEY_ROOT,
    ):
        """Run the super constructor but also set class properties.

        Interpret provided config and configure the object with all of the
        needed pieces for executing terraform commands

        """
        self._environment: Environment = environment
        """ Environemnt in which this plugin exists """
        self._instance_id: str = instance_id
        """ Unique id for this plugin instance """

        self._config_label = label
        """ configerus load label that should contain all of the config """
        self._config_base = base
        """ configerus get key that should contain all tf config """

        self.fixtures: Fixtures = Fixtures()
        """Children fixtures, typically just the client plugin."""

        # Make the client fixture in the constructor.  The TF client fixture is
        # quite state safe, and should only need to be created once, unlike
        # other provisioner clients which may be vulnerable to state change.
        self.make_fixtures()

    # deep argument is an info() standard across plugins
    # pylint: disable=unused-argument
    def info(self, deep: bool = False):
        """Get info about the provisioner plugin.

        Returns:
        --------
        Dict of keyed introspective information about the plugin.

        """
        terraform_config: Loaded = self._environment.config().load(
            self._config_label)

        info = {
            "config": {
                "label":
                self._config_label,
                "base":
                self._config_base,
                "tfvars":
                terraform_config.get(
                    [
                        self._config_base,
                        TERRAFORM_PROVISIONER_CONFIG_TFVARS_KEY
                    ],
                    default="NONE",
                ),
                "chart_path":
                terraform_config.get(
                    [
                        self._config_base,
                        TERRAFORM_PROVISIONER_CONFIG_CHART_PATH_KEY
                    ],
                    default="MISSING",
                ),
                "state_path":
                terraform_config.get(
                    [
                        self._config_base,
                        TERRAFORM_PROVISIONER_CONFIG_STATE_PATH_KEY
                    ],
                    default="MISSING",
                ),
                "tfvars_path":
                terraform_config.get(
                    [
                        self._config_base,
                        TERRAFORM_PROVISIONER_CONFIG_TFVARS_PATH_KEY
                    ],
                    default="MISSING",
                ),
            },
            "client": {
                "instance_id": self.client_instance_id(),
            },
        }

        return info

    def prepare(self):
        """Run terraform init."""
        logger.info("Running Terraform INIT")
        self._get_client_plugin().init()

    def apply(self, lock: bool = True):
        """Create all terraform resources described in the plan."""
        logger.info("Running Terraform APPLY")
        self._get_client_plugin().apply(lock=lock)

    def destroy(self, lock: bool = True):
        """Remove all terraform resources in state."""
        logger.info("Running Terraform DESTROY")
        self._get_client_plugin().destroy(lock=lock)
        # accessing parent property for clearing out existing output fixtures
        # pylint: disable=attribute-defined-outside-init
        self.fixtures = Fixtures()

    def make_fixtures(self):
        """Make the client plugin for terraform interaction."""
        try:
            terraform_config = self._environment.config().load(
                self._config_label,
                force_reload=True,
                validator=TERRAFORM_VALIDATE_TARGET)
            """ get a configerus LoadedConfig for the label """
        except ValidationError as err:
            raise ValueError("Terraform config failed validation") from err

        try:
            chart_path = terraform_config.get([
                self._config_base, TERRAFORM_PROVISIONER_CONFIG_CHART_PATH_KEY
            ])
            """ subprocess commands for terraform will be run in this path """
        except Exception as err:
            raise ValueError(
                "Plugin config did not give us a working/plan path:"
                f" {terraform_config.get()}") from err

        state_path = terraform_config.get(
            [self._config_base, TERRAFORM_PROVISIONER_CONFIG_STATE_PATH_KEY],
            default=os.path.join(chart_path,
                                 TERRAFORM_PROVISIONER_DEFAULT_STATE_SUBPATH),
        )
        """ terraform state path """

        tfvars = terraform_config.get(
            [self._config_base, TERRAFORM_PROVISIONER_CONFIG_TFVARS_KEY],
            default={},
        )
        """ List of vars to pass to terraform.  Will be written to a file """

        tfvars_path = terraform_config.get(
            [self._config_base, TERRAFORM_PROVISIONER_CONFIG_TFVARS_PATH_KEY],
            default=os.path.join(chart_path,
                                 TERRAFORM_PROVISIONER_DEFAULT_TFVARS_FILE),
        )
        """ vars file which will be written before running terraform """

        logger.debug("Creating Terraform client")

        fixture = self._environment.new_fixture(
            plugin_id=METTA_TERRAFORM_CLIENT_PLUGIN_ID,
            instance_id=self.client_instance_id(),
            priority=70,
            arguments={
                "chart_path": chart_path,
                "state_path": state_path,
                "tfvars": tfvars,
                "tfvars_path": tfvars_path,
            },
            labels={
                "parent_plugin_id": METTA_TERRAFORM_PROVISIONER_PLUGIN_ID,
                "parent_instance_id": self._instance_id,
            },
            replace_existing=True,
        )
        # keep this fixture attached to the workload to make it retrievable.
        self.fixtures.add(fixture, replace_existing=True)

    def client_instance_id(self) -> str:
        """Construct an instanceid for the child client plugin."""
        return f"{self._instance_id}-{METTA_TERRAFORM_CLIENT_PLUGIN_ID}"

    def _get_client_plugin(self) -> TerraformClientPlugin:
        """Retrieve the client plugin if we can."""
        return self.fixtures.get_plugin(
            plugin_id=METTA_TERRAFORM_CLIENT_PLUGIN_ID)
예제 #6
0
class TestkitProvisionerPlugin:
    """Testkit provisioner plugin.

    Provisioner plugin that allows control of and interaction with a testkit
    cluster.

    ## Requirements

    1. this plugin uses subprocess to call a testkit binary, so you have to
       install testkit in the environment

    ## Usage

    @TODO

    """
    def __init__(
        self,
        environment,
        instance_id,
        label: str = TESTKIT_PROVISIONER_CONFIG_LABEL,
        base: Any = TESTKIT_PROVISIONER_CONFIG_BASE,
    ):
        """Initialize Testkit provisioner.

        Parameters:
        -----------
        environment (Environment) : metta environment object that this plugin
            is attached.
        instance_id (str) : label for this plugin instances.

        label (str) : config load label for plugin configuration.
        base (str) : config base for loaded config for plugin configuration.

        """
        self._environment: Environment = environment
        """ Environemnt in which this plugin exists """
        self._instance_id: str = instance_id
        """ Unique id for this plugin instance """

        self._config_label = label
        """ configerus load label that should contain all of the config """
        self._config_base = base

        self.fixtures = Fixtures()
        """This object makes and keeps track of fixtures for MKE/MSR clients."""

        try:
            self._write_config_file()
            self.make_fixtures()
            # pylint: disable= broad-except
        except Exception as err:
            # there are many reasons this can fail, and we just want to
            # see if we can get fixtures early.
            # No need to ask forgiveness for this one.
            logger.debug("Could not make initial fixtures: %s", err)

    # the deep argument is a standard for the info hook
    # pylint: disable=unused-argument
    def info(self, deep: bool = False) -> Dict[str, Any]:
        """Get info about a provisioner plugin."""
        testkit_config = self._environment.config().load(self._config_label)

        return {
            "plugin": {
                "config": {
                    "config_label": self._config_label,
                    "config_base": self._config_base,
                },
                "config_file":
                testkit_config.get(
                    [self._config_base, TESTKIT_CONFIG_KEY_CONFIGFILE],
                    default="MISSING",
                ),
                "working_dir":
                testkit_config.get(
                    [self._config_base, TESTKIT_CONFIG_KEY_SYSTEMNAME],
                    default="MISSING"),
                "systems":
                testkit_config.get(
                    [self._config_base, TESTKIT_CONFIG_KEY_SYSTEMNAME],
                    default="MISSING",
                ),
            },
            "client": self._get_client_plugin().info(deep=deep),
        }

    def prepare(self):
        """Prepare any needed resources.

        We don't create the testkit file here so that it is created as late as
        possible.  This allows more options for dynamic config sources in the
        testkit config.

        """
        # Make sure that we are running on up to date config
        self._write_config_file()
        self.make_fixtures()

    def apply(self):
        """Create the testkit yaml file and run testkit to create a cluster."""
        # Make sure that we are running on up to date config
        self._write_config_file()
        self.make_fixtures()

        testkit_config = self._environment.config().load(self._config_label,
                                                         force_reload=True)
        """ load the plugin configuration so we can retrieve options """
        opts = testkit_config.get(
            [self._config_base, TESTKIT_CONFIG_KEY_CREATE_OPTIONS], default={})
        """ retrieve testkit client options from config """
        opt_list = []
        for key, value in opts.items():
            if isinstance(value, str):
                opt_list.append(f'--{key}="{value}"')
            else:
                opt_list.append(f"--{key}={value}")

        # run the testkit client command to provisioner the cluster
        self._get_client_plugin().create(opts=opt_list)

    def destroy(self):
        """Destroy any created resources."""
        # run the testkit client command to provisioner the cluster
        self._get_client_plugin().destroy()
        self._rm_config_file()

    def _write_config_file(self):
        """Write the config file for testkit."""
        try:
            # load all of the testkit configuration, force a reload to get up to date contents
            testkit_config = self._environment.config().load(
                self._config_label, force_reload=True)
            config = testkit_config.get(
                [self._config_base, TESTKIT_CONFIG_KEY_CONFIG],
                validator=TESTKIT_CONFIG_VALIDATE_TARGET,
            )
            """ config source of launchpad yaml """
        except KeyError as err:
            raise ValueError(
                "Could not find launchpad configuration from config.") from err
        except ValidationError as err:
            raise ValueError("Launchpad config failed validation") from err

        config_file = testkit_config.get(
            [self._config_base, TESTKIT_CONFIG_KEY_CONFIGFILE],
            default=TESTKIT_CONFIG_DEFAULT_CONFIGFILE,
        )
        """ config_file value from plugin configuration """

        # write the configto our yaml file target (creating the path)
        os.makedirs(os.path.dirname(os.path.realpath(config_file)),
                    exist_ok=True)
        with open(os.path.realpath(config_file), "w", encoding="utf8") as file:
            yaml.dump(config, file)

    def _rm_config_file(self):
        """Remove the written config file."""
        testkit_config = self._environment.config().load(self._config_label)
        config_file = testkit_config.get(
            [self._config_base, TESTKIT_CONFIG_KEY_CONFIGFILE],
            default=TESTKIT_CONFIG_DEFAULT_CONFIGFILE,
        )
        if os.path.isfile(config_file):
            os.remove(config_file)

    def make_fixtures(self):
        """Make related fixtures from a testkit installation.

        Creates:
        --------

        Testkit client : a client for interaction with the teskit cli

        """
        testkit_config = self._environment.config().load(self._config_label,
                                                         force_reload=True)
        """ load the plugin configuration so we can retrieve options """

        try:
            testkit_config = self._environment.config().load(
                self._config_label, force_reload=True)
            """ loaded plugin configuration label """
        except KeyError as err:
            raise ValueError(
                "Testkit plugin configuration did not have any config"
            ) from err

        system_name = testkit_config.get(
            [self._config_base, TESTKIT_CONFIG_KEY_SYSTEMNAME])
        """ hat will testkit call the system """

        # instances = testkit_config.get([self._config_base, TESTKIT_CONFIG_KEY_INSTANCES])
        # """ what instances to create """

        config_file = testkit_config.get(
            [self._config_base, TESTKIT_CONFIG_KEY_CONFIGFILE],
            default=TESTKIT_CONFIG_DEFAULT_CONFIGFILE,
        )
        """ config_file value from plugin configuration """

        systems = testkit_config.get(
            [self._config_base, TESTKIT_CONFIG_KEY_SYSTEMS],
            default={},
        )

        fixture = self._environment.new_fixture(
            plugin_id=METTA_TESTKIT_CLIENT_PLUGIN_ID,
            instance_id=self.client_instance_id(),
            priority=70,
            arguments={
                "config_file": config_file,
                "system_name": system_name,
                "systems": systems,
            },
            labels={
                "parent_plugin_id": METTA_TESTKIT_PROVISIONER_PLUGIN_ID,
                "parent_instance_id": self._instance_id,
            },
            replace_existing=True,
        )
        # keep this fixture attached to the workload to make it retrievable.
        self.fixtures.add(fixture, replace_existing=True)

    def client_instance_id(self) -> str:
        """Construct an instanceid for the child client plugin."""
        return f"{self._instance_id}-{METTA_TESTKIT_CLIENT_PLUGIN_ID}"

    def _get_client_plugin(self) -> TestkitClientPlugin:
        """Retrieve the client plugin if we can."""
        try:
            return self.fixtures.get_plugin(
                instance_id=self.client_instance_id())
        except KeyError as err:
            raise RuntimeError(
                "Testkit provisioner cannot find its client plugin, and "
                "cannot process any client actions.  Was a client created?"
            ) from err
예제 #7
0
class TestkitClientPlugin:
    """Testkit client plugin.

    client plugin that allows control of and interaction with a testkit
    cluster.

    ## Requirements

    1. this plugin uses subprocess to call a testkit binary, so you have to
       install testkit in the environment

    ## Usage

    @TODO

    """

    # pylint: disable=too-many-arguments
    def __init__(
        self,
        environment,
        instance_id,
        system_name: str,
        config_file: str,
        systems: Dict[str, Dict[str, str]] = None,
    ):
        """Initialize Testkit provisioner.

        Parameters:
        -----------
        environment (Environment) : metta environment object that this plugin
            is attached.
        instance_id (str) : label for this plugin instances.
        config_file (str) : string path to the testkit config file.
        systems (Dict[str, Dict[str, str]]) : A dictionary of systems which this
            client is expected to provide using testkit.

            This is something which should be determinable using the config/file
            client directly, but sits outside of information encapsulated in
            the tool/conf.

            What we are talking about here is information to answer questions:

                Did testkit install MKE? if so, what accesspoint and U/P can I
                use to build an MKE client to access it.

            This is not an ideal approach but rather a necessity.

        """
        self._environment: Environment = environment
        """ Environemnt in which this plugin exists """
        self._instance_id: str = instance_id
        """ Unique id for this plugin instance """

        self._system_name: str = system_name
        """ What will testkit call the system, used client ops """

        self._testkit = TestkitClient(config_file=config_file)
        """ testkit client object """

        self._systems = systems
        """What systems will testkit install, so what fixtures are needed"""

        self.fixtures = Fixtures()
        """This object makes and keeps track of fixtures for MKE/MSR clients."""
        try:
            self.make_fixtures()
            # pylint: disable= broad-except
        except Exception as err:
            # there are many reasons this can fail, and we just want to
            # see if we can get fixtures early.
            # No need to ask forgiveness for this one.
            logger.debug("Could not make initial fixtures: %s", err)

    # the deep argument is a standard for the info hook
    # pylint: disable=unused-argument
    def info(self, deep: bool = False) -> Dict[str, Any]:
        """Get info about a provisioner plugin."""
        info = {
            "plugin": {
                "system_name": self._system_name,
            },
            "client": self._testkit.info(deep=deep),
        }
        if deep:
            try:
                info["hosts"] = self.hosts()
            # pylint: disable=broad-except
            except Exception:
                pass

        return info

    def version(self):
        """Return testkit client version."""
        return self._testkit.version()

    def create(self, opts: List[str]):
        """Run the testkit create command."""
        self._testkit.create(opts=opts)
        self.make_fixtures()

        mke_plugin = self._get_mke_client_plugin()
        mke_plugin.api_get_bundle(force=True)
        mke_plugin.make_fixtures()

    def destroy(self):
        """Remove a system from testkit."""
        return self._testkit.system_rm(system_name=self._system_name)

    def hosts(self):
        """List testkit system machines."""
        return self._testkit.machine_ls(system_name=self._system_name)

    def exec(self, host: str, cmd: str):
        """List testkit system machines."""
        return self._testkit.machine_ssh(machine=host, cmd=cmd)

    def system_ls(self):
        """List all of the systems testkit can see using our config."""
        return self._testkit.system_ls()

    # pylint: disable=too-many-branches
    def make_fixtures(self):
        """Make related fixtures from a testkit installation.

        Creates:
        --------

        MKE client : if we have manager nodes, then we create an MKE client
            which will then create docker and kubernestes clients if they are
            appropriate.

        MSR Client : if we have an MSR node, then the related client is
            created.

        """
        if self._systems is None:
            return

        testkit_hosts = self._testkit.machine_ls(system_name=self._system_name)
        """ list of all of the testkit hosts. """

        manager_hosts = []
        worker_hosts = []
        mke_hosts = []
        msr_hosts = []
        for host in testkit_hosts:
            host["address"] = host["public_ip"]
            if host["swarm_manager"] == "yes":
                manager_hosts.append(host)
            else:
                worker_hosts.append(host)

            if host["ucp_controller"] == "yes":
                mke_hosts.append(host)

        if len(msr_hosts) == 0 and len(worker_hosts) > 0:
            # Testkit installs MSR on the first work node, but the api is
            # accessible using port 444 in order to not conflict.
            first_worker = worker_hosts[0]
            first_worker_ip = first_worker["public_ip"]
            first_worker["msr_accesspoint"] = f"{first_worker_ip}:444"
            msr_hosts.append(first_worker)

        if len(mke_hosts
               ) > 0 and METTA_MIRANTIS_CLIENT_MKE_PLUGIN_ID in self._systems:
            instance_id = f"{self._instance_id}-{METTA_MIRANTIS_CLIENT_MKE_PLUGIN_ID}"
            arguments = self._systems[METTA_MIRANTIS_CLIENT_MKE_PLUGIN_ID]
            arguments["hosts"] = mke_hosts

            if "accesspoint" in arguments and arguments["accesspoint"]:
                arguments["accesspoint"] = clean_accesspoint(
                    arguments["accesspoint"])

            logger.debug(
                "Launchpad client is creating an MKE client plugin: %s",
                instance_id)
            fixture = self._environment.new_fixture(
                plugin_id=METTA_MIRANTIS_CLIENT_MKE_PLUGIN_ID,
                instance_id=instance_id,
                priority=70,
                arguments=arguments,
                labels={
                    "parent_plugin_id": METTA_TESTKIT_CLIENT_PLUGIN_ID,
                    "parent_instance_id": self._instance_id,
                },
                replace_existing=True,
            )
            self.fixtures.add(fixture, replace_existing=True)

            # We got an MKE client, so let's activate it.

        else:
            logger.debug(
                "No MKE master hosts found, not creating an MKE client.")

        if len(msr_hosts
               ) > 0 and METTA_MIRANTIS_CLIENT_MSR_PLUGIN_ID in self._systems:
            instance_id = f"{self._instance_id}-{METTA_MIRANTIS_CLIENT_MSR_PLUGIN_ID}"
            arguments = self._systems[METTA_MIRANTIS_CLIENT_MSR_PLUGIN_ID]
            arguments["hosts"] = msr_hosts

            if "accesspoint" in arguments and arguments["accesspoint"]:
                arguments["accesspoint"] = clean_accesspoint(
                    arguments["accesspoint"])

            logger.debug(
                "Launchpad client is creating an MSR client plugin: %s",
                instance_id)
            fixture = self._environment.new_fixture(
                plugin_id=METTA_MIRANTIS_CLIENT_MSR_PLUGIN_ID,
                instance_id=instance_id,
                priority=70,
                arguments=arguments,
                labels={
                    "parent_plugin_id": METTA_TESTKIT_CLIENT_PLUGIN_ID,
                    "parent_instance_id": self._instance_id,
                },
                replace_existing=True,
            )
            self.fixtures.add(fixture, replace_existing=True)

        else:
            logger.debug(
                "No MSR master hosts found, not creating an MSR client.")

    def _get_mke_client_plugin(self) -> MKEAPIClientPlugin:
        """Retrieve the MKE client plugin if we can."""
        try:
            return self.fixtures.get_plugin(
                plugin_id=METTA_MIRANTIS_CLIENT_MKE_PLUGIN_ID)
        except KeyError as err:
            raise RuntimeError(
                "Launchpad client cannot find its MKE client plugin, and "
                "cannot process any client actions.  Was a client created?"
            ) from err
예제 #8
0
    def prepare(self, fixtures: Fixtures = None):
        """Create a workload instance from a set of fixtures.

        Parameters:
        -----------
        fixtures (Fixtures) : a set of fixtures that this workload will use to
            retrieve a kubernetes api client plugin.

        """
        if fixtures is None:
            fixtures = self._environment.fixtures()

        # Retrieve and Validate the config overall using jsonschema
        try:
            # get a configerus LoadedConfig for the sonobuoy label
            loaded = self._environment.config().load(
                self._config_label, validator=SONOBUOY_VALIDATE_TARGET)
        except ValidationError as err:
            raise ValueError("Invalid sonobuoy config received") from err

        # We need to discover all of the plugins to run.
        #
        # For plugins with inline definitions, we need to create file
        # definitions to pass to sonobuoy.
        resources_path: str = loaded.get([self._config_base, "resources.path"],
                                         default="./")
        resources_prefix: str = loaded.get(
            [self._config_base, "resources.prefix"], default="sonobuoy-")
        resources_plugin_prefix: str = f"{resources_prefix}plugin"

        # If we have config then write then to a file
        config_path: str = ""
        sonobuoy_config: Dict[str,
                              Any] = loaded.get([self._config_base, "config"],
                                                default=[])
        if sonobuoy_config:
            config_path = resources_path + resources_prefix + "config.json"
            with open(config_path, "w", encoding="utf-8") as config_file:
                json.dump(sonobuoy_config, config_file)

        # if we have plugins then prepare them
        plugins: List[Plugin] = []
        for plugin_id in loaded.get(
            [self._config_base, SONOBUOY_CONFIG_KEY_PLUGINS],
                default={}).keys():

            plugin_envs = loaded.get(
                [
                    self._config_base,
                    SONOBUOY_CONFIG_KEY_PLUGINS,
                    plugin_id,
                    SONOBUOY_CONFIG_KEY_PLUGINENVS,
                ],
                default=plugin_id,
            )

            # plugin_def gives us a plugin definition which defines how we pass
            # to sonobuoy using the -p flag.
            #
            # If a plugin def is missing then plugin_id is used.
            #
            # It can be one of three types:
            # 1. a core plugin id like 'e2e'
            # 2. a path to a plugin yml file which defines a plugin.
            # 3. an object which defines the plugin conf which will be written
            #    to a yaml file.
            plugin_def = loaded.get(
                [
                    self._config_base,
                    SONOBUOY_CONFIG_KEY_PLUGINS,
                    plugin_id,
                    SONOBUOY_CONFIG_KEY_PLUGINDEF,
                ],
                default="",
            )
            plugin_path = loaded.get(
                [
                    self._config_base,
                    SONOBUOY_CONFIG_KEY_PLUGINS,
                    plugin_id,
                    SONOBUOY_CONFIG_KEY_PLUGINPATH,
                ],
                default="",
            )

            if plugin_def:
                # here we received a plugin definition which we must write to
                # a file.
                if not plugin_path:
                    plugin_path = (resources_path + resources_plugin_prefix +
                                   "-" + plugin_id + ".yml")

                with open(plugin_path, "w", encoding="utf-8") as plugin_file:
                    yaml.dump(plugin_def, plugin_file, encoding="utf-8")
                plugin_def = plugin_path

                plugins.append(
                    Plugin(plugin_id=plugin_id,
                           plugin_def=plugin_path,
                           envs=plugin_envs))
                continue

            if plugin_path:
                plugins.append(
                    Plugin(plugin_id=plugin_id,
                           plugin_def=plugin_path,
                           envs=plugin_envs))
                continue

            plugins.append(
                Plugin(plugin_id=plugin_id,
                       plugin_def=plugin_id,
                       envs=plugin_envs))

        # String path to where to keep the results.
        # maybe get this from config?
        results_path: str = loaded.get(
            [self._config_base, SONOBUOY_CONFIG_KEY_RESULTSPATH],
            default=SONOBUOY_DEFAULT_RESULTS_PATH,
        )

        kubeclient: KubernetesApiClientPlugin = fixtures.get_plugin(
            plugin_id=METTA_PLUGIN_ID_KUBERNETES_CLIENT, )

        client_fixture = self._environment.new_fixture(
            plugin_id=METTA_SONOBUOY_CLIENT_PLUGIN_ID,
            instance_id=self.client_instance_id(),
            priority=70,
            arguments={
                "kubeclient": kubeclient,
                "plugins": plugins,
                "config_path": config_path,
                "results_path": results_path,
            },
            labels={
                "container": "plugin",
                "environment": self._environment.instance_id(),
                "parent_plugin_id": METTA_SONOBUOY_WORKLOAD_PLUGIN_ID,
                "parent_instance_id": self._instance_id,
            },
            replace_existing=True,
        )
        # keep this fixture attached to the workload to make it retrievable.
        self.fixtures.add(client_fixture, replace_existing=True)
예제 #9
0
class LaunchpadProvisionerPlugin:
    """Launchpad provisioner class.

    Use this to provision a system using Mirantis launchpad

    """
    def __init__(
        self,
        environment: Environment,
        instance_id: str,
        label: str = METTA_LAUNCHPAD_CONFIG_LABEL,
        base: Any = LOADED_KEY_ROOT,
    ):
        """Configure a new Launchpad provisioner plugin instance."""
        self._environment: Environment = environment
        """ Environemnt in which this plugin exists """
        self._instance_id: str = instance_id
        """ Unique id for this plugin instance """

        self._config_label = label
        """ configerus load label that should contain all of the config """
        self._config_base = base
        """ configerus get key that should contain all tf config """

        self.fixtures = Fixtures()
        """ keep a collection of fixtures that this provisioner creates """

        # attempt to be declarative and make the client plugin in case the
        # terraform chart has already been run.
        try:
            # Make the child client plugin.
            self.make_fixtures()

        # dont' block the construction on an exception
        # pylint: disable=broad-except
        except Exception:
            pass

    def info(self, deep: bool = False) -> Dict[str, Any]:
        """Get info about the plugin.

        Returns:
        --------
        Dict of introspective information about this plugin_info

        """
        # Loaded plugin configuration
        launchpad_config_loaded = self._environment.config().load(
            self._config_label)

        return {
            "plugin": {
                "config_label": self._config_label,
                "config_base": self._config_base,
            },
            "config": {
                "working_dir":
                launchpad_config_loaded.get(
                    [self._config_base, METTA_LAUNCHPAD_CLI_WORKING_DIR_KEY],
                    default="MISSING"),
                "root_path":
                launchpad_config_loaded.get(
                    [self._config_base, METTA_LAUNCHPAD_CONFIG_ROOT_PATH_KEY],
                    default="NONE"),
                "config_file":
                launchpad_config_loaded.get(
                    [self._config_base, METTA_LAUNCHPAD_CLI_CONFIG_FILE_KEY],
                    default="MISSING"),
                "cli_options":
                launchpad_config_loaded.get(
                    [self._config_base, METTA_LAUNCHPAD_CLI_OPTIONS_KEY],
                    default="NONE"),
            },
            "client": self._get_client_plugin().info(deep=deep),
        }

    # pylint: disable=no-self-use
    def prepare(self):
        """Prepare the provisioning cluster for install.

        We ignore this.

        """
        logger.info(
            "Running Launchpad Prepare().  Launchpad has no prepare stage.")

    def apply(self, debug: bool = False):
        """Bring a cluster up.

        Not that we re-write the yaml file as it may depend on config which was not
        available when this object was first constructed.

        Raises:
        -------
        ValueError if the object has been configured (prepare) with config that
            doesn't work, or if the backend doesn't give valid yml

        Exception if launchpad fails.

        """
        logger.info("Using launchpad to install products onto backend cluster")
        self._write_launchpad_yml()
        self.make_fixtures(
        )  # we wouldn't need this if we could update the systems
        self._get_client_plugin().apply(debug=debug)

    def destroy(self):
        """Ask the client to remove installed resources."""
        if self._has_launchpad_yml():
            logger.info(
                "Using launchpad to remove installed products from the backend cluster"
            )
            self._get_client_plugin().reset()
            self._rm_launchpad_yml()

    # ----- CLUSTER INTERACTION -----

    def _has_launchpad_yml(self) -> bool:
        """Check if the launchpad yml file exists."""
        # Loaded configerus config for the plugin. Ready for .get().
        plugin_config = self._environment.config().load(self._config_label)

        config_file: str = plugin_config.get(
            [self._config_base, METTA_LAUNCHPAD_CLI_CONFIG_FILE_KEY],
            default=METTA_LAUNCHPAD_CLI_CONFIG_FILE_DEFAULT,
        )
        return bool(config_file) and os.path.exists(config_file)

    def _write_launchpad_yml(self):
        """Write config contents to a yaml file for launchpad."""
        self._rm_launchpad_yml()

        # load and validation all of the launchpad configuration.
        launchpad_loaded = self._environment.config().load(
            self._config_label,
            validator=METTA_LAUNCHPAD_PROVISIONER_VALIDATE_TARGET,
            force_reload=True,
        )

        # load all of the launchpad configuration, force a reload to get up to date contents
        config_contents: Dict[str, Any] = launchpad_loaded.get(
            [self._config_base, METTA_LAUNCHPAD_CONFIG_KEY],
            validator=METTA_LAUNCHPAD_CONFIG_VALIDATE_TARGET,
        )

        # decide on a path for the runtime launchpad.yml file
        config_path: str = os.path.realpath(
            launchpad_loaded.get(
                [self._config_base, METTA_LAUNCHPAD_CLI_CONFIG_FILE_KEY],
                default=METTA_LAUNCHPAD_CLI_CONFIG_FILE_DEFAULT,
            ))

        # Our launchpad config differs slightly from the schema that launchpad
        # consumes, so we need a small conversion
        config_contents = self._convert_launchpad_config_to_file_format(
            config_contents)

        # write the launchpad output to our yaml file target (after creating the path)
        logger.debug(
            "Updating launchpad yaml file: %s =>/n%s",
            config_path,
            yaml.dump(config_contents),
        )
        with open(config_path, "w", encoding="utf8") as config_file_object:
            yaml.dump(config_contents, config_file_object)

    def _rm_launchpad_yml(self):
        """Update config and write the cfg and inventory files."""
        # Loaded configerus config for the plugin. Ready for .get().
        plugin_config = self._environment.config().load(self._config_label)

        config_file: str = plugin_config.get(
            [self._config_base, METTA_LAUNCHPAD_CLI_CONFIG_FILE_KEY],
            default=METTA_LAUNCHPAD_CLI_CONFIG_FILE_DEFAULT,
        )
        if config_file and os.path.exists(config_file):
            logger.debug("Launchpad provisioner removing created files.")
            os.remove(config_file)

    def make_fixtures(self):
        """Make the client plugin for terraform interaction."""
        try:
            # load and validation all of the launchpad configuration.
            launchpad_config_loaded = self._environment.config().load(
                self._config_label,
                validator=METTA_LAUNCHPAD_PROVISIONER_VALIDATE_TARGET,
                force_reload=True,
            )
        except ValidationError as err:
            raise ValueError("Launchpad config failed validation.") from err

        # if launchpad needs to be run in a certain path, set it with this config
        working_dir: str = launchpad_config_loaded.get(
            [self._config_base, METTA_LAUNCHPAD_CLI_WORKING_DIR_KEY],
            default=METTA_LAUNCHPADCLIENT_WORKING_DIR_DEFAULT,
        )

        # decide on a path for the runtime launchpad.yml file
        config_file: str = launchpad_config_loaded.get(
            [self._config_base, METTA_LAUNCHPAD_CLI_CONFIG_FILE_KEY],
            default=METTA_LAUNCHPAD_CLI_CONFIG_FILE_DEFAULT,
        )
        # List of launchpad cli options to pass to the client for all operations.
        cli_options: Dict[str, Any] = launchpad_config_loaded.get(
            [self._config_base, METTA_LAUNCHPAD_CLI_OPTIONS_KEY], default={})
        # List of systems that the client should configure for children plugins.
        systems: Dict[str, Dict[str, Any]] = launchpad_config_loaded.get(
            [self._config_base, METTA_LAUNCHPAD_CLIENT_SYSTEMS_KEY],
            default={})

        fixture = self._environment.new_fixture(
            plugin_id=METTA_LAUNCHPAD_CLIENT_PLUGIN_ID,
            instance_id=self.client_instance_id(),
            priority=70,
            arguments={
                "config_file": config_file,
                "working_dir": working_dir,
                "cli_options": cli_options,
                "systems": systems,
            },
            labels={
                "parent_plugin_id": METTA_LAUNCHPAD_PROVISIONER_PLUGIN_ID,
                "parent_instance_id": self._instance_id,
            },
            replace_existing=True,
        )
        # keep this fixture attached to the workload to make it retrievable.
        self.fixtures.add(fixture, replace_existing=True)

    def client_instance_id(self) -> str:
        """Construct an instanceid for the child client plugin."""
        return f"{self._instance_id}-{METTA_LAUNCHPAD_CLIENT_PLUGIN_ID}"

    def _get_client_plugin(self) -> LaunchpadClientPlugin:
        """Retrieve the client plugin if we can."""
        try:
            return self.fixtures.get_plugin(
                plugin_id=METTA_LAUNCHPAD_CLIENT_PLUGIN_ID)
        except KeyError as err:
            raise RuntimeError(
                "Launchpad provisioner cannot find its client plugin, and "
                "cannot process any client actions.  Was a client created?"
            ) from err

    def _convert_launchpad_config_to_file_format(self, config):
        """Convert our launchpad config to the schema that launchpad uses."""
        # 1 discover the hosts counts
        hosts = []
        managers = []
        workers = []
        msrs = []
        for host in config["spec"]["hosts"]:
            hosts.append(host)
            if host["role"] == "manager":
                managers.append(host)
            if host["role"] == "worker":
                workers.append(host)
            if host["role"] == "msr":
                msrs.append(host)

        # convert install flags and update flags to lists from dicts
        def dtol(dic):
            """Convert dict flags to lists."""
            items: List[str] = []
            for key, value in dic.items():
                if value is True:
                    items.append(f"--{key}")
                else:
                    items.append(f"--{key}={value}")
            return items

        try:
            config["spec"]["mke"]["installFlags"] = dtol(
                config["spec"]["mke"]["installFlags"])
        except KeyError:
            pass
        try:
            config["spec"]["mke"]["upgradeFlags"] = dtol(
                config["spec"]["mke"]["upgradeFlags"])
        except KeyError:
            pass
        try:
            config["spec"]["msr"]["installFlags"] = dtol(
                config["spec"]["msr"]["installFlags"])
        except KeyError:
            pass
        try:
            config["spec"]["msr"]["upgradeFlags"] = dtol(
                config["spec"]["msr"]["upgradeFlags"])
        except KeyError:
            pass

        # If no msrs, then drop the msr block and force the type.
        if len(msrs) == 0:
            config["kind"] = "mke"
            config["spec"].pop("msr")

        return config