From e240a29a929668623252cc4df0f7369bc922c4a9 Mon Sep 17 00:00:00 2001 From: Will Szumski Date: Fri, 1 May 2020 11:37:51 +0100 Subject: [PATCH] Add ability to run playbooks before and after a kayobe command Sometimes there is a need to develop site specific playbooks. Currently, it is necessary to manually invoke these at the right point during the deployment. Adding the ability to automatically run these custom playbooks will reduce the chance of running these playbooks at the wrong point or forgetting to run them at all. Change-Id: I1ae0f1f94665925326c8b1869dd75038f6f1b87d Story: 2001663 Task: 12606 --- doc/source/custom-ansible-playbooks.rst | 81 ++++++++++++++ etc/kayobe/hooks/.gitkeep | 0 kayobe/cli/commands.py | 73 ++++++++++++ kayobe/tests/unit/cli/test_commands.py | 28 +++++ .../add-command-hooks-827aa0732b7399de.yaml | 6 + setup.cfg | 105 ++++++++++++++++++ 6 files changed, 293 insertions(+) create mode 100644 etc/kayobe/hooks/.gitkeep create mode 100644 releasenotes/notes/add-command-hooks-827aa0732b7399de.yaml diff --git a/doc/source/custom-ansible-playbooks.rst b/doc/source/custom-ansible-playbooks.rst index e1a759759..d409a8276 100644 --- a/doc/source/custom-ansible-playbooks.rst +++ b/doc/source/custom-ansible-playbooks.rst @@ -120,3 +120,84 @@ We should first install the Galaxy role dependencies, to download the Then, to run the ``foo.yml`` playbook:: (kayobe) $ kayobe playbook run $KAYOBE_CONFIG_PATH/ansible/foo.yml + +Hooks +===== + +.. warning:: + Hooks are an experimental feature and the design could change in the future. + You may have to update your config if there are any changes to the design. + This warning will be removed when the design has been stabilised. + +Hooks allow you to automatically execute custom playbooks at certain points during +the execution of a kayobe command. The point at which a hook is run is referred to +as a ``target``. Please see the :ref:`list of available targets`. + +Hooks are created by symlinking an existing playbook into the the relevant directory under +``$KAYOBE_CONFIG_PATH/hooks``. Kayobe will search the hooks directory for sub-directories +matching ``..d``, where ``command`` is the name of a kayobe command +with any spaces replaced with dashes, and ``target`` is one of the supported targets for +the command. + +For example, when using the command:: + + (kayobe) $ kayobe control host bootstrap + +kayobe will search the paths: + +- ``$KAYOBE_CONFIG_PATH/hooks/control-host-bootstrap/pre.d`` +- ``$KAYOBE_CONFIG_PATH/hooks/control-host-bootstrap/post.d`` + +Any playbooks listed under the ``pre.d`` directory will be run before kayobe executes +its own playbooks and any playbooks under ``post.d`` will be run after. You can affect +the order of the playbooks by prefixing the symlink with a sequence number. The sequence +number must be separated from the hook name with a dash. Playbooks with smaller sequence +numbers are run before playbooks with larger ones. Any ties are broken by alphabetical +ordering. + +For example to run the playbook ``foo.yml`` after ``kayobe overcloud host configure``, +you could do the following:: + + (kayobe) $ mkdir -p $KAYOBE_CONFIG_PATH/hooks/overcloud-host-configure/post.d + (kayobe) $ ln -s $KAYOBE_CONFIG_PATH/ansible/foo.yml \ + $KAYOBE_CONFIG_PATH/hooks/overcloud-host-configure/post.d/10-foo.yml + +The sequence number for the ``foo.yml`` playbook is ``10``. + +Failure handling +---------------- + +If the exit status of any playbook, including built-in playbooks and custom hooks, +is non-zero, kayobe will not run any subsequent hooks or built-in kayobe playbooks. +Ansible provides several methods for preventing a task from producing a failure. Please +see the `Ansible documentation `_ +for more details. Below is an example showing how you can use the ``ignore_errors`` option +to prevent a task from causing the playbook to report a failure:: + + --- + - name: Failure example + hosts: localhost + tasks: + - name: Deliberately fail + fail: + ignore_errors: true + +A failure in the ``Deliberately fail`` task would not prevent subsequent tasks, hooks, +and playbooks from running. + +.. _Hook Targets: + +Targets +------- +The following targets are available for all commands: + +.. list-table:: all commands + :widths: 10 500 + :header-rows: 1 + + * - Target + - Description + * - pre + - Runs before a kayobe command has start executing + * - post + - Runs after a kayobe command has finished executing diff --git a/etc/kayobe/hooks/.gitkeep b/etc/kayobe/hooks/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py index d43c2d844..5ceac6581 100644 --- a/kayobe/cli/commands.py +++ b/kayobe/cli/commands.py @@ -12,16 +12,22 @@ # License for the specific language governing permissions and limitations # under the License. +import glob import json +import os import sys from cliff.command import Command +from cliff.hooks import CommandHook from kayobe import ansible from kayobe import kolla_ansible from kayobe import utils from kayobe import vault +# This is set to an arbitrary large number to simplify the sorting logic +DEFAULT_SEQUENCE_NUMBER = sys.maxsize + def _build_playbook_list(*playbooks): """Return a list of names of playbook files given their basenames.""" @@ -144,6 +150,73 @@ class KollaAnsibleMixin(object): return kolla_ansible.run_seed(*args, **kwargs) +def _split_hook_sequence_number(hook): + parts = hook.split("-", 1) + if len(parts) < 2: + return (DEFAULT_SEQUENCE_NUMBER, hook) + try: + return (int(parts[0]), parts[1]) + except ValueError: + return (DEFAULT_SEQUENCE_NUMBER, hook) + + +class HookDispatcher(CommandHook): + """Runs custom playbooks before and after a command""" + +# Order of calls: get_epilog, get_parser, before, after + + def __init__(self, *args, **kwargs): + self.command = kwargs["command"] + self.logger = self.command.app.LOG + cmd = self.command.cmd_name + # Replace white space with dashes for consistency with ansible + # playbooks. Example cmd: kayobe control host bootstrap + self.name = "-".join(cmd.split()) + + def get_epilog(self): + pass + + def get_parser(self, prog_name): + pass + + def _find_hooks(self, config_path, target): + name = self.name + path = os.path.join(config_path, "hooks", name, "%s.d" % target) + self.logger.debug("Discovering hooks in: %s" % path) + if not os.path.exists: + return [] + hooks = glob.glob(os.path.join(path, "*.yml")) + self.logger.debug("Discovered the following hooks: %s" % hooks) + return hooks + + def hooks(self, config_path, target): + hooks = self._find_hooks(config_path, target) + # Hooks can be prefixed with a sequence number to adjust running order, + # e.g 10-my-custom-playbook.yml. Sort by sequence number. + hooks = sorted(hooks, key=_split_hook_sequence_number) + # Resolve symlinks so that we can reference roles. + hooks = [os.path.realpath(hook) for hook in hooks] + return hooks + + def run_hooks(self, parsed_args, target): + config_path = parsed_args.config_path + hooks = self.hooks(config_path, target) + if hooks: + self.logger.debug("Running hooks: %s" % hooks) + self.command.run_kayobe_playbooks(parsed_args, hooks) + + def before(self, parsed_args): + self.run_hooks(parsed_args, "pre") + return parsed_args + + def after(self, parsed_args, return_code): + if return_code == 0: + self.run_hooks(parsed_args, "post") + else: + self.logger.debug("Not running hooks due to non-zero return code") + return return_code + + class ControlHostBootstrap(KayobeAnsibleMixin, KollaAnsibleMixin, VaultMixin, Command): """Bootstrap the Kayobe control environment. diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py index a67d6ee26..e29d1da46 100644 --- a/kayobe/tests/unit/cli/test_commands.py +++ b/kayobe/tests/unit/cli/test_commands.py @@ -1969,3 +1969,31 @@ class TestCase(unittest.TestCase): ), ] self.assertEqual(expected_calls, mock_run.call_args_list) + + +class TestHookDispatcher(unittest.TestCase): + + @mock.patch('kayobe.cli.commands.os.path') + def test_hook_ordering(self, mock_path): + mock_command = mock.MagicMock() + dispatcher = commands.HookDispatcher(command=mock_command) + dispatcher._find_hooks = mock.MagicMock() + dispatcher._find_hooks.return_value = [ + "10-hook.yml", + "5-hook.yml", + "z-test-alphabetical.yml", + "10-before-hook.yml", + "5-multiple-dashes-in-name.yml", + "no-prefix.yml" + ] + expected_result = [ + "5-hook.yml", + "5-multiple-dashes-in-name.yml", + "10-before-hook.yml", + "10-hook.yml", + "no-prefix.yml", + "z-test-alphabetical.yml", + ] + mock_path.realpath.side_effect = lambda x: x + actual = dispatcher.hooks("config/path", "pre") + self.assertListEqual(actual, expected_result) diff --git a/releasenotes/notes/add-command-hooks-827aa0732b7399de.yaml b/releasenotes/notes/add-command-hooks-827aa0732b7399de.yaml new file mode 100644 index 000000000..4e958de85 --- /dev/null +++ b/releasenotes/notes/add-command-hooks-827aa0732b7399de.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds an experimental mechanism to automatically run custom playbooks + before and after kayobe commands. Please see the ``Custom Ansible Playbooks`` + section in the documentation for more details. diff --git a/setup.cfg b/setup.cfg index 60d9a5cbd..f74400bf7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -89,3 +89,108 @@ kayobe.cli= seed_service_upgrade = kayobe.cli.commands:SeedServiceUpgrade seed_vm_deprovision = kayobe.cli.commands:SeedVMDeprovision seed_vm_provision = kayobe.cli.commands:SeedVMProvision + +kayobe.cli.baremetal_compute_inspect = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.baremetal_compute_manage = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.baremetal_compute_provide = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.baremetal_compute_rename = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.baremetal_compute_update_deployment_image = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.baremetal_compute_serial_console_enable = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.baremetal_compute_serial_console_disable = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.control_host_bootstrap = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.control_host_upgrade = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.configuration_dump = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.kolla_ansible_run = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.network_connectivity_check = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_bios_raid_configure = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_container_image_build = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_container_image_pull = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_database_backup = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_database_recover = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_deployment_image_build = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_deprovision = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_hardware_inspect = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_host_configure = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_host_package_update = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_host_command_run = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_host_upgrade = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_introspection_data_save = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_inventory_discover = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_post_configure = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_provision = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_service_configuration_save = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_service_configuration_generate = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_service_deploy = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_service_deploy_containers = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_service_destroy = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_service_reconfigure = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_service_stop = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_service_upgrade = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.overcloud_swift_rings_generate = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.physical_network_configure = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.playbook_run = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_container_image_build = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_deployment_image_build = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_host_configure = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_host_package_update = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_host_command_run = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_host_upgrade = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_hypervisor_host_configure = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_hypervisor_host_command_run = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_hypervisor_host_upgrade = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_service_deploy = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_service_upgrade = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_vm_deprovision = + hooks = kayobe.cli.commands:HookDispatcher +kayobe.cli.seed_vm_provision = + hooks = kayobe.cli.commands:HookDispatcher