From 2bc568debb2a6f7d3e0b28e1568b5f27eded27d3 Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Tue, 9 Mar 2021 16:06:36 +0100 Subject: [PATCH] Add a command to create a new Kayobe environment Change-Id: I2f33bda5f2f84bbbb0f40240d2575a0b69ddceea Story: 2002009 Task: 40038 --- kayobe/cli/commands.py | 32 +++++++++++ kayobe/environment.py | 64 +++++++++++++++++++++ kayobe/tests/unit/test_environment.py | 83 +++++++++++++++++++++++++++ kayobe/utils.py | 14 +++++ setup.cfg | 1 + 5 files changed, 194 insertions(+) create mode 100644 kayobe/environment.py create mode 100644 kayobe/tests/unit/test_environment.py diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py index 94fb42eca..060704f7f 100644 --- a/kayobe/cli/commands.py +++ b/kayobe/cli/commands.py @@ -21,6 +21,7 @@ from cliff.command import Command from cliff.hooks import CommandHook from kayobe import ansible +from kayobe import environment from kayobe import kolla_ansible from kayobe import utils from kayobe import vault @@ -1687,3 +1688,34 @@ class BaremetalComputeUpdateDeploymentImage(KayobeAnsibleMixin, VaultMixin, ) self.run_kayobe_playbooks(parsed_args, playbooks, extra_vars=extra_vars) + + +class EnvironmentCreate(KayobeAnsibleMixin, VaultMixin, Command): + """Create a new Kayobe environment.""" + + def get_parser(self, prog_name): + parser = super(EnvironmentCreate, self).get_parser(prog_name) + group = parser.add_argument_group("Kayobe Environments") + environment.add_args(group) + return parser + + def take_action(self, parsed_args): + self.app.LOG.debug("Creating new Kayobe environment") + if not parsed_args.environment: + self.app.LOG.error("An environment must be specified") + sys.exit(1) + + source_config_path = parsed_args.source_config_path + if source_config_path: + result = utils.is_readable_dir(source_config_path) + if not result["result"]: + self.app.LOG.error("Kayobe configuration %s is invalid: %s", + source_config_path, result["message"]) + sys.exit(1) + + try: + environment.create_kayobe_environment(parsed_args) + except Exception as e: + self.app.LOG.error("Failed to create environment %s: %s", + parsed_args.environment, repr(e)) + sys.exit(1) diff --git a/kayobe/environment.py b/kayobe/environment.py new file mode 100644 index 000000000..218fe0f6c --- /dev/null +++ b/kayobe/environment.py @@ -0,0 +1,64 @@ +# Copyright (c) 2021 StackHPC Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import os +import os.path +import sys + +from kayobe import utils + + +LOG = logging.getLogger(__name__) + + +def add_args(parser): + """Add arguments required for managing Kayobe environments to a parser.""" + parser.add_argument("--source-config-path", + help="Kayobe configuration to import") + + +def create_kayobe_environment(parsed_args): + """Create a new Kayobe environment.""" + if not parsed_args.environment: + LOG.error("You must specify an environment to create") + sys.exit(1) + + # Ensure environments directory exists and is readable inside config path + kc_environments = os.path.join(parsed_args.config_path, "environments") + result = utils.is_readable_dir(kc_environments) + if not result["result"]: + if result["message"] == "Path does not exist": + os.mkdir(kc_environments) + else: + LOG.error("Kayobe global environments directory %s is invalid: %s", + kc_environments, result["message"]) + sys.exit(1) + + env_path = os.path.join(kc_environments, parsed_args.environment) + result = utils.is_readable_dir(env_path) + if result["result"]: + LOG.error("Kayobe environment directory %s already exists", env_path) + sys.exit(1) + else: + if result["message"] == "Path does not exist": + os.mkdir(env_path) + else: + LOG.error("Kayobe environment directory %s is invalid: %s", + env_path, result["message"]) + sys.exit(1) + + source_config_path = parsed_args.source_config_path + if source_config_path: + utils.copy_dir(source_config_path, env_path) diff --git a/kayobe/tests/unit/test_environment.py b/kayobe/tests/unit/test_environment.py new file mode 100644 index 000000000..c03515349 --- /dev/null +++ b/kayobe/tests/unit/test_environment.py @@ -0,0 +1,83 @@ +# Copyright (c) 2021 StackHPC Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +import os +import unittest +from unittest import mock + +from kayobe import ansible +from kayobe import environment +from kayobe import utils + + +class TestCase(unittest.TestCase): + + @mock.patch.object(utils, "is_readable_dir") + def test_unreadable_environments_directory(self, mock_readable_dir): + mock_readable_dir.return_value = { + "result": False, + "message": "Directory is not readable" + } + parser = argparse.ArgumentParser() + args = [ + "--config-path", "/path/to/config", + "--environment", "foo", + ] + ansible.add_args(parser) + environment.add_args(parser) + parsed_args = parser.parse_args(args) + self.assertRaises(SystemExit, + environment.create_kayobe_environment, parsed_args) + + @mock.patch.object(utils, "is_readable_dir") + def test_environment_exists(self, mock_readable_dir): + mock_readable_dir.side_effect = [{"result": True}, {"result": True}] + parser = argparse.ArgumentParser() + args = [ + "--config-path", "/path/to/config", + "--environment", "foo", + ] + ansible.add_args(parser) + environment.add_args(parser) + parsed_args = parser.parse_args(args) + self.assertRaises(SystemExit, + environment.create_kayobe_environment, parsed_args) + + @mock.patch.object(utils, "copy_dir") + @mock.patch.object(os, "mkdir") + @mock.patch.object(utils, "is_readable_dir") + def test_create_kayobe_environment(self, mock_readable_dir, mock_mkdir, + mock_copy_dir): + mock_readable_dir.return_value = { + "result": False, + "message": "Path does not exist" + } + parser = argparse.ArgumentParser() + args = [ + "--config-path", "/path/to/config", + "--source-config-path", "/path/to/foo", + "--environment", "foo", + ] + ansible.add_args(parser) + environment.add_args(parser) + parsed_args = parser.parse_args(args) + environment.create_kayobe_environment(parsed_args) + expected_calls = [ + mock.call("/path/to/config/environments"), + mock.call("/path/to/config/environments/foo"), + ] + self.assertEqual(expected_calls, mock_mkdir.call_args_list) + mock_copy_dir.assert_called_once_with( + "/path/to/foo", "/path/to/config/environments/foo") diff --git a/kayobe/utils.py b/kayobe/utils.py index 40828ac5e..389450d61 100644 --- a/kayobe/utils.py +++ b/kayobe/utils.py @@ -17,6 +17,7 @@ import glob import itertools import logging import os +import shutil import subprocess import sys @@ -222,3 +223,16 @@ def intersect_limits(args_limit, cli_limit): separator = ':&' limits = [l for l in [args_limit, cli_limit] if l] return separator.join(limits) + + +def copy_dir(src, dest): + if not os.path.isdir(dest): + shutil.copytree(src, dest) + else: + for file in os.listdir(src): + src_path = os.path.join(src, file) + dest_path = os.path.join(dest, file) + if os.path.isdir(src_path): + copy_dir(src_path, dest_path) + else: + shutil.copy2(src_path, dest_path) diff --git a/setup.cfg b/setup.cfg index 83110f6ca..f8b651c6b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,6 +48,7 @@ kayobe.cli= control_host_bootstrap = kayobe.cli.commands:ControlHostBootstrap control_host_upgrade = kayobe.cli.commands:ControlHostUpgrade configuration_dump = kayobe.cli.commands:ConfigurationDump + environment_create = kayobe.cli.commands:EnvironmentCreate kolla_ansible_run = kayobe.cli.commands:KollaAnsibleRun network_connectivity_check = kayobe.cli.commands:NetworkConnectivityCheck overcloud_bios_raid_configure = kayobe.cli.commands:OvercloudBIOSRAIDConfigure