From 7862ee3f5a859970a2b810a0388019950e9d48b0 Mon Sep 17 00:00:00 2001 From: Will Szumski Date: Fri, 26 Jun 2026 10:07:15 +0100 Subject: [PATCH] Cleaner split of kolla-ansible install and configure This allows us to install kayobe without generating the config. This is useful in a docker image build where will later mount the config in. Change-Id: I81c2b2a8e1ccde44854257ce8e34e3517c945a4d Signed-off-by: Will Szumski --- ansible/install.yml | 18 +++ ansible/roles/bootstrap/tasks/install.yml | 46 +++++++ ansible/roles/bootstrap/tasks/main.yml | 47 -------- ansible/roles/kolla-ansible/tasks/install.yml | 2 +- ansible/roles/kolla-ansible/tasks/main.yml | 6 - .../kolla-ansible/tests/test-defaults.yml | 12 +- .../roles/kolla-ansible/tests/test-extras.yml | 12 +- .../tests/test-globals-merge.yml | 11 +- .../kolla-ansible/tests/test-requirements.yml | 4 +- kayobe/cli/commands.py | 48 +++++++- kayobe/tests/unit/cli/test_commands.py | 112 +++++++++++++++++- ...ds-install-only-mode-17f545f434081020.yaml | 8 ++ 12 files changed, 257 insertions(+), 69 deletions(-) create mode 100644 ansible/install.yml create mode 100644 ansible/roles/bootstrap/tasks/install.yml create mode 100644 releasenotes/notes/adds-install-only-mode-17f545f434081020.yaml diff --git a/ansible/install.yml b/ansible/install.yml new file mode 100644 index 000000000..1e382d4f5 --- /dev/null +++ b/ansible/install.yml @@ -0,0 +1,18 @@ +--- + +- name: Ensure depnedencies are installed + hosts: localhost + gather_facts: true + tags: + - kolla-ansible + - install + tasks: + - name: Install Kayobe dependencies + import_role: + name: bootstrap + tasks_from: install.yml + + - name: Install kolla-ansible + import_role: + name: kolla-ansible + tasks_from: install.yml diff --git a/ansible/roles/bootstrap/tasks/install.yml b/ansible/roles/bootstrap/tasks/install.yml new file mode 100644 index 000000000..402960d31 --- /dev/null +++ b/ansible/roles/bootstrap/tasks/install.yml @@ -0,0 +1,46 @@ +--- +- block: + - name: Testing privilege escalation + raw: "true" + become: true + failed_when: false + changed_when: false + register: privilege_escalation_result + + - name: Assert that we can escalate privileges + assert: + that: + - privilege_escalation_result is success + - '"password is required" not in privilege_escalation_result.stderr' + fail_msg: >- + Could not escalate privileges. You can either: set kayobe_control_host_become: true, + set ansible_become_password, or set up passwordless sudo. + when: kayobe_control_host_become | bool + +- name: Include OS family-specific variables + include_vars: "{{ ansible_facts.os_family }}.yml" + +- name: Gather the package facts + ansible.builtin.package_facts: + manager: auto + +- block: + - name: Assert that all packages are installed if not using privilege escalation + assert: + that: missing_packages is falsy + fail_msg: >- + The following packages are missing from your system: {{ missing_packages | join(', ') }} and + privilege escalation is disabled. Please get your system administator to install these packages + or enable kayobe_control_host_become. + when: not kayobe_control_host_become | bool + + - name: Ensure required packages are installed + package: + name: "{{ bootstrap_package_dependencies }}" + state: present + cache_valid_time: "{{ apt_cache_valid_time if ansible_facts.os_family == 'Debian' else omit }}" + update_cache: "{{ True if ansible_facts.os_family == 'Debian' else omit }}" + become: True + when: missing_packages is truthy + vars: + missing_packages: "{{ bootstrap_package_dependencies | difference(ansible_facts.packages.keys()) }}" diff --git a/ansible/roles/bootstrap/tasks/main.yml b/ansible/roles/bootstrap/tasks/main.yml index ee673a204..71bc73729 100644 --- a/ansible/roles/bootstrap/tasks/main.yml +++ b/ansible/roles/bootstrap/tasks/main.yml @@ -1,50 +1,3 @@ ---- -- block: - - name: Testing privilege escalation - raw: "true" - become: true - failed_when: false - changed_when: false - register: privilege_escalation_result - - - name: Assert that we can escalate privileges - assert: - that: - - privilege_escalation_result is success - - '"password is required" not in privilege_escalation_result.stderr' - fail_msg: >- - Could not escalate privileges. You can either: set kayobe_control_host_become: true, - set ansible_become_password, or set up passwordless sudo. - when: kayobe_control_host_become | bool - -- name: Include OS family-specific variables - include_vars: "{{ ansible_facts.os_family }}.yml" - -- name: Gather the package facts - ansible.builtin.package_facts: - manager: auto - -- block: - - name: Assert that all packages are installed if not using privilege escalation - assert: - that: missing_packages is falsy - fail_msg: >- - The following packages are missing from your system: {{ missing_packages | join(', ') }} and - privilege escalation is disabled. Please get your system administator to install these packages - or enable kayobe_control_host_become. - when: not kayobe_control_host_become | bool - - - name: Ensure required packages are installed - package: - name: "{{ bootstrap_package_dependencies }}" - state: present - cache_valid_time: "{{ apt_cache_valid_time if ansible_facts.os_family == 'Debian' else omit }}" - update_cache: "{{ True if ansible_facts.os_family == 'Debian' else omit }}" - become: True - when: missing_packages is truthy - vars: - missing_packages: "{{ bootstrap_package_dependencies | difference(ansible_facts.packages.keys()) }}" - - name: Check whether an SSH key exists stat: path: "{{ bootstrap_ssh_private_key_path }}" diff --git a/ansible/roles/kolla-ansible/tasks/install.yml b/ansible/roles/kolla-ansible/tasks/install.yml index 9408c3793..baaddd09d 100644 --- a/ansible/roles/kolla-ansible/tasks/install.yml +++ b/ansible/roles/kolla-ansible/tasks/install.yml @@ -44,7 +44,7 @@ package: name: "{{ packages }}" state: present - cache_valid_time: "{{ apt_cache_valid_time if ansible_facts.os_family == 'Debian' else omit }}" + cache_valid_time: "{{ apt_cache_valid_time | default(3600) if ansible_facts.os_family == 'Debian' else omit }}" update_cache: "{{ True if ansible_facts.os_family == 'Debian' else omit }}" become: True when: missing_packages is truthy diff --git a/ansible/roles/kolla-ansible/tasks/main.yml b/ansible/roles/kolla-ansible/tasks/main.yml index 031f38a97..1fc412a10 100644 --- a/ansible/roles/kolla-ansible/tasks/main.yml +++ b/ansible/roles/kolla-ansible/tasks/main.yml @@ -1,10 +1,4 @@ --- -# NOTE: Use import_tasks here, since tags are not applied to tasks included via -# include_tasks. -- import_tasks: install.yml - tags: - - install - - import_tasks: config.yml tags: - config diff --git a/ansible/roles/kolla-ansible/tests/test-defaults.yml b/ansible/roles/kolla-ansible/tests/test-defaults.yml index f5832c0d1..9e67db5b5 100644 --- a/ansible/roles/kolla-ansible/tests/test-defaults.yml +++ b/ansible/roles/kolla-ansible/tests/test-defaults.yml @@ -9,6 +9,17 @@ register: tempfile_result - block: + - name: Install dependencies + include_role: + name: "{{ playbook_dir }}/.." + tasks_from: install.yml + vars: + kolla_ansible_source_path: "{{ temp_path }}/src" + kolla_ansible_ctl_install_type: "source" + kolla_ansible_source_url: "http://github.com/openstack/kolla-ansible" + kolla_ansible_source_version: "{{ openstack_branch }}" + kolla_ansible_venv: "{{ temp_path }}/venv" + - name: Test the kolla-ansible role with default values include_role: name: "{{ playbook_dir }}/.." @@ -41,7 +52,6 @@ kolla_openstack_logging_debug: False kolla_globals_paths_extra: - "{{ tempfile_result.path ~ '/etc/kayobe/' }}" - apt_cache_valid_time: 3600 - name: Verify kolla-ansible installation shell: ". {{ temp_path }}/venv/bin/activate && kolla-ansible -h" diff --git a/ansible/roles/kolla-ansible/tests/test-extras.yml b/ansible/roles/kolla-ansible/tests/test-extras.yml index 83c3513ab..6b10ae794 100644 --- a/ansible/roles/kolla-ansible/tests/test-extras.yml +++ b/ansible/roles/kolla-ansible/tests/test-extras.yml @@ -81,6 +81,17 @@ bogus backend certificate - block: + - name: Install dependencies + include_role: + name: "{{ playbook_dir }}/.." + tasks_from: install.yml + vars: + kolla_ansible_source_path: "{{ temp_path }}/src" + kolla_ansible_ctl_install_type: "source" + kolla_ansible_source_url: "http://github.com/openstack/kolla-ansible" + kolla_ansible_source_version: "{{ openstack_branch }}" + kolla_ansible_venv: "{{ temp_path }}/venv" + - name: Test the kolla-ansible role with default values include_role: name: ../../kolla-ansible @@ -195,7 +206,6 @@ custom-password-1: "custom-password-1" custom-password-2: "custom-password-2" kolla_nova_compute_ironic_host: "controller1" - apt_cache_valid_time: 3600 - name: Verify kolla-ansible installation shell: ". {{ temp_path }}/venv/bin/activate && kolla-ansible -h" diff --git a/ansible/roles/kolla-ansible/tests/test-globals-merge.yml b/ansible/roles/kolla-ansible/tests/test-globals-merge.yml index 7613815cb..2529de994 100644 --- a/ansible/roles/kolla-ansible/tests/test-globals-merge.yml +++ b/ansible/roles/kolla-ansible/tests/test-globals-merge.yml @@ -9,6 +9,16 @@ register: tempfile_result - block: + - name: Install dependencies + include_role: + name: "{{ playbook_dir }}/.." + tasks_from: install.yml + vars: + kolla_ansible_source_path: "{{ temp_path }}/src" + kolla_ansible_ctl_install_type: "source" + kolla_ansible_source_url: "http://github.com/openstack/kolla-ansible" + kolla_ansible_source_version: "{{ openstack_branch }}" + kolla_ansible_venv: "{{ temp_path }}/venv" - name: Ensure directories exists file: @@ -74,7 +84,6 @@ - "{{ tempfile_result.path ~ '/etc/kayobe/' }}" - "{{ tempfile_result.path ~ '/etc/kayobe/environments/level1/' }}" - "{{ tempfile_result.path ~ '/etc/kayobe/environments/level2/' }}" - apt_cache_valid_time: 3600 - name: Verify kolla-ansible installation shell: ". {{ temp_path }}/venv/bin/activate && kolla-ansible -h" diff --git a/ansible/roles/kolla-ansible/tests/test-requirements.yml b/ansible/roles/kolla-ansible/tests/test-requirements.yml index 874a2ff9e..203b126e9 100644 --- a/ansible/roles/kolla-ansible/tests/test-requirements.yml +++ b/ansible/roles/kolla-ansible/tests/test-requirements.yml @@ -11,7 +11,8 @@ - block: - name: Test the kolla-ansible role with extra Python requirements include_role: - name: ../../kolla-ansible + name: "{{ playbook_dir }}/.." + tasks_from: install.yml vars: kolla_ansible_source_path: "{{ temp_path }}/src" kolla_ansible_ctl_install_type: "source" @@ -39,7 +40,6 @@ kolla_openstack_logging_debug: False kolla_globals_paths_extra: - "{{ tempfile_result.path ~ '/etc/kayobe/' }}" - apt_cache_valid_time: 3600 - name: List Python packages installed in virtualenv command: "{{ temp_path }}/venv/bin/pip list" diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py index 529d9b623..8e6bba4d7 100644 --- a/kayobe/cli/commands.py +++ b/kayobe/cli/commands.py @@ -18,6 +18,7 @@ import os import re import sys +import yaml from cliff.command import Command from cliff.hooks import CommandHook @@ -301,13 +302,48 @@ def get_parser(self, prog_name): group = parser.add_argument_group("Host Bootstrap") group.add_argument("--add-known-hosts", action='store_true', help="add SSH known hosts entries for each host") + install_group = group.add_mutually_exclusive_group() + install_group.add_argument( + "--no-install", + action='store_true', + default=yaml.safe_load(os.getenv("KAYOBE_NO_INSTALL", "false")), + help=("skip dependency installation and run later bootstrap " + "steps only (default from KAYOBE_NO_INSTALL env var)"), + ) + install_group.add_argument( + "--install-only", + action='store_true', + default=yaml.safe_load(os.getenv("KAYOBE_INSTALL_ONLY", "false")), + help=("only install dependencies " + "(default from KAYOBE_INSTALL_ONLY env var)"), + ) return parser def take_action(self, parsed_args): self.app.LOG.debug("Bootstrapping Kayobe Ansible control host") - self.handle_kolla_tags_limits_deprecation(parsed_args) - ansible.install_galaxy_roles(parsed_args) - ansible.install_galaxy_collections(parsed_args) + + if parsed_args.install_only and parsed_args.no_install: + self.app.LOG.error("--install-only and --no-install are " + "mutually exclusive") + sys.exit(1) + + if parsed_args.no_install: + self.app.LOG.debug("Skipping dependency and Galaxy installation " + "due to " + "--no-install") + else: + ansible.install_galaxy_roles(parsed_args) + ansible.install_galaxy_collections(parsed_args) + + playbooks = _build_playbook_list("install") + self.run_kayobe_playbooks(parsed_args, playbooks, + ignore_limit=True) + + if parsed_args.install_only: + self.app.LOG.debug("Skipping reset of bootstrap due to " + "--install-only") + return + playbooks = _build_playbook_list("bootstrap") self.run_kayobe_playbooks(parsed_args, playbooks, ignore_limit=True) @@ -455,11 +491,11 @@ def take_action(self, parsed_args): # Use force to upgrade roles and collections. ansible.install_galaxy_roles(parsed_args, force=True) ansible.install_galaxy_collections(parsed_args, force=True) + playbooks = _build_playbook_list("install") + self.run_kayobe_playbooks(parsed_args, playbooks, ignore_limit=True) + playbooks = _build_playbook_list("bootstrap") self.run_kayobe_playbooks(parsed_args, playbooks, ignore_limit=True) - playbooks = _build_playbook_list("kolla-ansible") - self.run_kayobe_playbooks(parsed_args, playbooks, tags="install", - ignore_limit=True, check=False) class ControlHostServiceDeploy(KayobeAnsibleMixin, VaultMixin, Command): diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py index 171cf4678..8cda090b0 100644 --- a/kayobe/tests/unit/cli/test_commands.py +++ b/kayobe/tests/unit/cli/test_commands.py @@ -38,6 +38,54 @@ class TestCase(unittest.TestCase): maxDiff = None + @mock.patch.dict(os.environ, {}, clear=True) + def test_control_host_bootstrap_install_only_default_false(self): + command = commands.ControlHostBootstrap(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args([]) + self.assertFalse(parsed_args.install_only) + + @mock.patch.dict(os.environ, {"KAYOBE_INSTALL_ONLY": "false"}, + clear=True) + def test_control_host_bootstrap_install_only_default_false_from_env(self): + command = commands.ControlHostBootstrap(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args([]) + self.assertFalse(parsed_args.install_only) + + @mock.patch.dict(os.environ, {}, clear=True) + def test_control_host_bootstrap_no_install_default_false(self): + command = commands.ControlHostBootstrap(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args([]) + self.assertFalse(parsed_args.no_install) + + @mock.patch.dict(os.environ, {"KAYOBE_NO_INSTALL": "false"}, + clear=True) + def test_control_host_bootstrap_no_install_default_false_from_env(self): + command = commands.ControlHostBootstrap(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args([]) + self.assertFalse(parsed_args.no_install) + + @mock.patch.dict(os.environ, {"KAYOBE_NO_INSTALL": "true"}, + clear=True) + def test_control_host_bootstrap_no_install_default_true_from_env(self): + command = commands.ControlHostBootstrap(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args([]) + self.assertTrue(parsed_args.no_install) + + def test_control_host_bootstrap_install_only_no_install_mutually_exclusive( + self): + command = commands.ControlHostBootstrap(TestApp(), []) + parser = command.get_parser("test") + self.assertRaises( + SystemExit, + parser.parse_args, + ["--install-only", "--no-install"], + ) + @mock.patch.object(ansible, "install_galaxy_roles", autospec=True) @mock.patch.object(ansible, "install_galaxy_collections", autospec=True) @mock.patch.object(ansible, "passwords_yml_exists", autospec=True) @@ -54,6 +102,44 @@ def test_control_host_bootstrap(self, mock_run, mock_passwords, self.assertEqual(0, result) mock_install_roles.assert_called_once_with(parsed_args) mock_install_collections.assert_called_once_with(parsed_args) + expected_calls = [ + mock.call( + mock.ANY, + [utils.get_data_files_path("ansible", "install.yml")], + ignore_limit=True, + ), + mock.call( + mock.ANY, + [utils.get_data_files_path("ansible", "bootstrap.yml")], + ignore_limit=True, + ), + mock.call( + mock.ANY, + [utils.get_data_files_path("ansible", "kolla-ansible.yml")], + tags="install", + ignore_limit=True, + check=False, + ), + ] + self.assertListEqual(expected_calls, mock_run.call_args_list) + + @mock.patch.object(ansible, "install_galaxy_roles", autospec=True) + @mock.patch.object(ansible, "install_galaxy_collections", autospec=True) + @mock.patch.object(ansible, "passwords_yml_exists", autospec=True) + @mock.patch.object(commands.KayobeAnsibleMixin, + "run_kayobe_playbooks") + def test_control_host_bootstrap_no_install(self, mock_run, + mock_passwords, + mock_install_collections, + mock_install_roles): + mock_passwords.return_value = False + command = commands.ControlHostBootstrap(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args(["--no-install"]) + result = command.run(parsed_args) + self.assertEqual(0, result) + mock_install_roles.assert_not_called() + mock_install_collections.assert_not_called() expected_calls = [ mock.call( mock.ANY, @@ -70,6 +156,21 @@ def test_control_host_bootstrap(self, mock_run, mock_passwords, ] self.assertListEqual(expected_calls, mock_run.call_args_list) + @mock.patch.dict(os.environ, {"KAYOBE_INSTALL_ONLY": "true", + "KAYOBE_NO_INSTALL": "true"}, + clear=True) + @mock.patch.object(ansible, "install_galaxy_roles", autospec=True) + @mock.patch.object(ansible, "install_galaxy_collections", autospec=True) + def test_control_host_bootstrap_install_only_no_install_env_conflict( + self, mock_install_collections, mock_install_roles): + command = commands.ControlHostBootstrap(TestApp(), []) + parser = command.get_parser("test") + parsed_args = parser.parse_args([]) + + self.assertRaises(SystemExit, command.run, parsed_args) + mock_install_roles.assert_not_called() + mock_install_collections.assert_not_called() + @mock.patch.object(ansible, "install_galaxy_roles", autospec=True) @mock.patch.object(ansible, "install_galaxy_collections", autospec=True) @mock.patch.object(ansible, "passwords_yml_exists", autospec=True) @@ -89,6 +190,11 @@ def test_control_host_bootstrap_with_passwords( mock_install_roles.assert_called_once_with(parsed_args) mock_install_collections.assert_called_once_with(parsed_args) expected_calls = [ + mock.call( + mock.ANY, + [utils.get_data_files_path("ansible", "install.yml")], + ignore_limit=True, + ), mock.call( mock.ANY, [utils.get_data_files_path("ansible", "bootstrap.yml")], @@ -244,15 +350,13 @@ def test_control_host_upgrade(self, mock_run, mock_prune, expected_calls = [ mock.call( mock.ANY, - [utils.get_data_files_path("ansible", "bootstrap.yml")], + [utils.get_data_files_path("ansible", "install.yml")], ignore_limit=True, ), mock.call( mock.ANY, - [utils.get_data_files_path("ansible", "kolla-ansible.yml")], - tags="install", + [utils.get_data_files_path("ansible", "bootstrap.yml")], ignore_limit=True, - check=False, ), ] self.assertListEqual(expected_calls, mock_run.call_args_list) diff --git a/releasenotes/notes/adds-install-only-mode-17f545f434081020.yaml b/releasenotes/notes/adds-install-only-mode-17f545f434081020.yaml new file mode 100644 index 000000000..b8c41d33b --- /dev/null +++ b/releasenotes/notes/adds-install-only-mode-17f545f434081020.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Adds the ``install-only`` and ``no-install`` command line options to + ``kayobe control host bootstrap``. In ``install-only`` mode, only the + dependencies are installed, but the rest of the host configuration is + skipped. In ``no-install`` mode, install is skipped, but the rest of the + tasks are executed.