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