diff --git a/ansible/host-command-run.yml b/ansible/host-command-run.yml new file mode 100644 index 000000000..580cd7466 --- /dev/null +++ b/ansible/host-command-run.yml @@ -0,0 +1,6 @@ +--- +- name: Run a command + hosts: seed-hypervisor:seed:overcloud + tasks: + - name: Run a command + shell: "{{ host_command_to_run }}" diff --git a/doc/source/administration/overcloud.rst b/doc/source/administration/overcloud.rst index 3a02aaa18..2a3336ce2 100644 --- a/doc/source/administration/overcloud.rst +++ b/doc/source/administration/overcloud.rst @@ -21,6 +21,20 @@ To only install updates that have been marked security related:: Note that these commands do not affect packages installed in containers, only those installed on the host. +Running Commands +================ + +It is possible to run a command on the overcloud hosts:: + + (kayobe) $ kayobe overcloud host command run --command "" + +For example:: + + (kayobe) $ kayobe overcloud host command run --command "service docker restart" + +To execute the command with root privileges, add the ``--become`` argument. +Adding the ``--verbose`` argument allows the output of the command to be seen. + Reconfiguring Containerised Services ==================================== diff --git a/doc/source/administration/seed.rst b/doc/source/administration/seed.rst index 9d99e3bb7..41ad8e8ee 100644 --- a/doc/source/administration/seed.rst +++ b/doc/source/administration/seed.rst @@ -122,3 +122,21 @@ Finally, start the Ironic and Ironic Inspector services again:: docker exec -it bifrost_deploy \ systemctl start ironic-api ironic-conductor ironic-inspector + +Running Commands +================ + +It is possible to run a command on the seed host:: + + (kayobe) $ kayobe seed host command run --command "" + +For example:: + + (kayobe) $ kayobe seed host command run --command "service docker restart" + +Commands can also be run on the seed hypervisor host, if one is in use:: + + (kayobe) $ kayobe seed hypervisor host command run --command "" + +To execute the command with root privileges, add the ``--become`` argument. +Adding the ``--verbose`` argument allows the output of the command to be seen. diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py index 4596af695..9f91dd269 100644 --- a/kayobe/cli/commands.py +++ b/kayobe/cli/commands.py @@ -326,6 +326,27 @@ class SeedHypervisorHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, limit="seed-hypervisor") +class SeedHypervisorHostCommandRun(KayobeAnsibleMixin, VaultMixin, Command): + """Run command on the seed hypervisor host.""" + + def get_parser(self, prog_name): + parser = super(SeedHypervisorHostCommandRun, self).get_parser( + prog_name) + group = parser.add_argument_group("Host Command Run") + group.add_argument("--command", required=True, + help="Command to run (required).") + return parser + + def take_action(self, parsed_args): + self.app.LOG.debug("Run command on seed hypervisor host") + extra_vars = { + "host_command_to_run": utils.escape_jinja(parsed_args.command)} + playbooks = _build_playbook_list("host-command-run") + self.run_kayobe_playbooks(parsed_args, playbooks, + limit="seed-hypervisor", + extra_vars=extra_vars) + + class SeedHypervisorHostUpgrade(KayobeAnsibleMixin, VaultMixin, Command): """Upgrade the seed hypervisor host services. @@ -503,6 +524,25 @@ class SeedHostPackageUpdate(KayobeAnsibleMixin, VaultMixin, Command): extra_vars=extra_vars) +class SeedHostCommandRun(KayobeAnsibleMixin, VaultMixin, Command): + """Run command on the seed host.""" + + def get_parser(self, prog_name): + parser = super(SeedHostCommandRun, self).get_parser(prog_name) + group = parser.add_argument_group("Host Command Run") + group.add_argument("--command", required=True, + help="Command to run (required).") + return parser + + def take_action(self, parsed_args): + self.app.LOG.debug("Run command on seed host") + extra_vars = { + "host_command_to_run": utils.escape_jinja(parsed_args.command)} + playbooks = _build_playbook_list("host-command-run") + self.run_kayobe_playbooks(parsed_args, playbooks, limit="seed", + extra_vars=extra_vars) + + class SeedHostUpgrade(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin, Command): """Upgrade the seed host services. @@ -877,6 +917,25 @@ class OvercloudHostPackageUpdate(KayobeAnsibleMixin, VaultMixin, Command): extra_vars=extra_vars) +class OvercloudHostCommandRun(KayobeAnsibleMixin, VaultMixin, Command): + """Run command on the overcloud host.""" + + def get_parser(self, prog_name): + parser = super(OvercloudHostCommandRun, self).get_parser(prog_name) + group = parser.add_argument_group("Host Command Run") + group.add_argument("--command", required=True, + help="Command to run (required).") + return parser + + def take_action(self, parsed_args): + self.app.LOG.debug("Run command on overcloud host") + extra_vars = { + "host_command_to_run": utils.escape_jinja(parsed_args.command)} + playbooks = _build_playbook_list("host-command-run") + self.run_kayobe_playbooks(parsed_args, playbooks, limit="overcloud", + extra_vars=extra_vars) + + class OvercloudHostUpgrade(KayobeAnsibleMixin, VaultMixin, Command): """Upgrade the overcloud host services. diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py index c3d45635c..8f89257a5 100644 --- a/kayobe/tests/unit/cli/test_commands.py +++ b/kayobe/tests/unit/cli/test_commands.py @@ -273,6 +273,30 @@ class TestCase(unittest.TestCase): ] self.assertEqual(expected_calls, mock_run.call_args_list) + @mock.patch.object(commands.KayobeAnsibleMixin, + "run_kayobe_playbooks") + def test_seed_hypervisor_host_command_run(self, mock_run): + command = commands.SeedHypervisorHostCommandRun(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args(["--command", "ls -a"]) + + result = command.run(parsed_args) + self.assertEqual(0, result) + + expected_calls = [ + mock.call( + mock.ANY, + [ + utils.get_data_files_path("ansible", + "host-command-run.yml"), + ], + limit="seed-hypervisor", + extra_vars={ + "host_command_to_run": utils.escape_jinja("ls -a")}, + ), + ] + self.assertEqual(expected_calls, mock_run.call_args_list) + @mock.patch.object(commands.KayobeAnsibleMixin, "run_kayobe_playbooks") def test_seed_hypervisor_host_upgrade(self, mock_run): @@ -485,6 +509,30 @@ class TestCase(unittest.TestCase): ] self.assertEqual(expected_calls, mock_kolla_run.call_args_list) + @mock.patch.object(commands.KayobeAnsibleMixin, + "run_kayobe_playbooks") + def test_seed_host_command_run(self, mock_run): + command = commands.SeedHostCommandRun(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args(["--command", "ls -a"]) + + result = command.run(parsed_args) + self.assertEqual(0, result) + + expected_calls = [ + mock.call( + mock.ANY, + [ + utils.get_data_files_path("ansible", + "host-command-run.yml"), + ], + limit="seed", + extra_vars={ + "host_command_to_run": utils.escape_jinja("ls -a")}, + ), + ] + self.assertEqual(expected_calls, mock_run.call_args_list) + @mock.patch.object(commands.KayobeAnsibleMixin, "run_kayobe_playbooks") def test_seed_host_package_update_all(self, mock_run): @@ -1059,6 +1107,30 @@ class TestCase(unittest.TestCase): ] self.assertEqual(expected_calls, mock_kolla_run.call_args_list) + @mock.patch.object(commands.KayobeAnsibleMixin, + "run_kayobe_playbooks") + def test_overcloud_host_command_run(self, mock_run): + command = commands.OvercloudHostCommandRun(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args(["--command", "ls -a"]) + + result = command.run(parsed_args) + self.assertEqual(0, result) + + expected_calls = [ + mock.call( + mock.ANY, + [ + utils.get_data_files_path("ansible", + "host-command-run.yml"), + ], + limit="overcloud", + extra_vars={ + "host_command_to_run": utils.escape_jinja("ls -a")}, + ), + ] + self.assertEqual(expected_calls, mock_run.call_args_list) + @mock.patch.object(commands.KayobeAnsibleMixin, "run_kayobe_playbooks") def test_overcloud_host_package_update_all(self, mock_run): diff --git a/kayobe/tests/unit/test_utils.py b/kayobe/tests/unit/test_utils.py index 4f5029ba1..47f998b40 100644 --- a/kayobe/tests/unit/test_utils.py +++ b/kayobe/tests/unit/test_utils.py @@ -123,3 +123,8 @@ key2: value2 def test_quote_and_escape_non_string(self): self.assertEqual(True, utils.quote_and_escape(True)) + + def test_escape_jinja(self): + value = "string to escape" + expected = "{{'c3RyaW5nIHRvIGVzY2FwZQ==' | b64decode }}" + self.assertEqual(expected, utils.escape_jinja(value)) diff --git a/kayobe/utils.py b/kayobe/utils.py index 20302b824..4bed03d8e 100644 --- a/kayobe/utils.py +++ b/kayobe/utils.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import base64 import glob import logging import os @@ -165,3 +166,18 @@ def quote_and_escape(value): if not isinstance(value, six.string_types): return value return "'" + value.replace("'", "'\\''") + "'" + + +def escape_jinja(string): + """Escapes a string so that jinja template variables are not expanded + + :param string: the string to escape + :return: the escaped string + """ + # We base64 encode the string to avoid the need to escape characters. + # This is because ansible has some parsing quirks that makes it fairly + # hard to escape stuff in generic way. + # See: https://github.com/ansible/ansible/issues/10464 + + b64_value = base64.b64encode(string.encode()) + return ''.join(('{{', "'", b64_value.decode(), "' | b64decode ", '}}')) diff --git a/releasenotes/notes/host-run-command-eb98cb077d546551.yaml b/releasenotes/notes/host-run-command-eb98cb077d546551.yaml new file mode 100644 index 000000000..8771e6829 --- /dev/null +++ b/releasenotes/notes/host-run-command-eb98cb077d546551.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add commands to run commands on seed hypervisor, seed and overcloud hosts: + + ``kayobe seed hypervisor host command run --command `` + ``kayobe seed host command run --command `` + ``kayobe overcloud host command run --command `` diff --git a/setup.cfg b/setup.cfg index cc1c4bf75..872582e1c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,6 +58,7 @@ kayobe.cli= overcloud_hardware_inspect = kayobe.cli.commands:OvercloudHardwareInspect overcloud_host_configure = kayobe.cli.commands:OvercloudHostConfigure overcloud_host_package_update = kayobe.cli.commands:OvercloudHostPackageUpdate + overcloud_host_command_run = kayobe.cli.commands:OvercloudHostCommandRun overcloud_host_upgrade = kayobe.cli.commands:OvercloudHostUpgrade overcloud_introspection_data_save = kayobe.cli.commands:OvercloudIntrospectionDataSave overcloud_inventory_discover = kayobe.cli.commands:OvercloudInventoryDiscover @@ -75,8 +76,10 @@ kayobe.cli= seed_deployment_image_build = kayobe.cli.commands:SeedDeploymentImageBuild seed_host_configure = kayobe.cli.commands:SeedHostConfigure seed_host_package_update = kayobe.cli.commands:SeedHostPackageUpdate + seed_host_command_run = kayobe.cli.commands:SeedHostCommandRun seed_host_upgrade = kayobe.cli.commands:SeedHostUpgrade seed_hypervisor_host_configure = kayobe.cli.commands:SeedHypervisorHostConfigure + seed_hypervisor_host_command_run = kayobe.cli.commands:SeedHypervisorHostCommandRun seed_hypervisor_host_upgrade = kayobe.cli.commands:SeedHypervisorHostUpgrade seed_service_deploy = kayobe.cli.commands:SeedServiceDeploy seed_service_upgrade = kayobe.cli.commands:SeedServiceUpgrade