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
This commit is contained in:
Will Szumski 2020-05-01 11:37:51 +01:00
parent 565a0614dc
commit e240a29a92
6 changed files with 293 additions and 0 deletions

View File

@ -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<Hook 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 ``<command>.<target>.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 <https://docs.ansible.com/ansible/latest/user_guide/playbooks_error_handling.html>`_
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

View File

View File

@ -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.

View File

@ -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)

View File

@ -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.

105
setup.cfg
View File

@ -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