From 03957addc7150fd9478751f9bab1997fdfda82aa Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Wed, 29 Jul 2026 22:51:45 +0200 Subject: [PATCH 1/4] Fix support matrix note about Rocky Linux 9 Change-Id: I263f8f4d388a175838662d72f3efe5b101e22c91 Signed-off-by: Pierre Riteau (cherry picked from commit 9b865932a12251b9e07524159bdb033de82ff302) --- doc/source/support-matrix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/support-matrix.rst b/doc/source/support-matrix.rst index a11503a91..9a5302554 100644 --- a/doc/source/support-matrix.rst +++ b/doc/source/support-matrix.rst @@ -25,7 +25,7 @@ therefore users need to build them by themselves. .. note:: Rocky Linux 9 is no longer supported as a host OS. The 2025.1 Epoxy release - supports both CentOS Stream 9 and 10 to provide a route for migration. + supports both Rocky Linux 9 and 10 to provide a route for migration. Supported container images ~~~~~~~~~~~~~~~~~~~~~~~~~~ From ca7adab6177a6ac195b20d71842b7b8423d49823 Mon Sep 17 00:00:00 2001 From: Will Szumski Date: Fri, 26 Jun 2026 10:07:15 +0100 Subject: [PATCH 2/4] Cleaner split of kolla-ansible install and configure This allows us to install Kayobe without generating the configuration. This is useful when building a Docker image, where you want to install all dependencies at build time but do not require the runtime configuration. The runtime configuration can then be generated when the container is executed. Change-Id: I81c2b2a8e1ccde44854257ce8e34e3517c945a4d Signed-off-by: Will Szumski (cherry picked from commit 7b191d9165ccd710dfc3bbdebc907b27b718c3d3) --- ansible/install.yml | 24 ++++ ansible/kolla-ansible.yml | 5 - ansible/roles/bootstrap/tasks/install.yml | 46 +++++++ ansible/roles/bootstrap/tasks/main.yml | 46 ------- 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 +- doc/source/deployment.rst | 24 ++++ kayobe/cli/commands.py | 46 ++++++- kayobe/tests/unit/cli/test_commands.py | 112 +++++++++++++++++- ...ds-install-only-mode-17f545f434081020.yaml | 8 ++ 14 files changed, 286 insertions(+), 72 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..301b73bd7 --- /dev/null +++ b/ansible/install.yml @@ -0,0 +1,24 @@ +--- +- name: Ensure dependencies are installed + hosts: localhost + gather_facts: true + tags: + - kolla-ansible + - install + - bootstrap + vars: + kolla_ansible_install_epel: "{{ dnf_install_epel }}" + kolla_ansible_custom_requirements_search_paths_static: + - "{{ kayobe_config_path }}" + kolla_ansible_custom_requirements_search_paths: "{{ kolla_ansible_custom_requirements_search_paths_static + kayobe_env_search_paths | default([]) }}" + kolla_ansible_custom_requirements_paths: "{{ kolla_ansible_custom_requirements_search_paths | map('regex_replace', '$', '/kolla/requirements.yml') | list }}" + 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/kolla-ansible.yml b/ansible/kolla-ansible.yml index a02f96135..07514a63d 100644 --- a/ansible/kolla-ansible.yml +++ b/ansible/kolla-ansible.yml @@ -101,11 +101,6 @@ name: kolla-ansible vars: kolla_ansible_control_host_become: "{{ kayobe_control_host_become | bool }}" - kolla_ansible_install_epel: "{{ dnf_install_epel }}" - kolla_ansible_custom_requirements_search_paths_static: - - "{{ kayobe_config_path }}" - kolla_ansible_custom_requirements_search_paths: "{{ kolla_ansible_custom_requirements_search_paths_static + kayobe_env_search_paths | default([]) }}" - kolla_ansible_custom_requirements_paths: "{{ kolla_ansible_custom_requirements_search_paths | map('regex_replace', '$', '/kolla/requirements.yml') | list }}" kolla_external_fqdn_cert: "{{ kolla_config_path }}/certificates/haproxy.pem" kolla_internal_fqdn_cert: "{{ kolla_config_path }}/certificates/haproxy-internal.pem" kolla_ansible_passwords_path: "{{ kayobe_env_config_path }}/kolla/passwords.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..802fb9889 100644 --- a/ansible/roles/bootstrap/tasks/main.yml +++ b/ansible/roles/bootstrap/tasks/main.yml @@ -1,50 +1,4 @@ --- -- 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/doc/source/deployment.rst b/doc/source/deployment.rst index eb75d2127..f9d079291 100644 --- a/doc/source/deployment.rst +++ b/doc/source/deployment.rst @@ -29,6 +29,30 @@ To bootstrap the Ansible control host:: (kayobe) $ kayobe control host bootstrap +The bootstrap process now supports splitting dependency installation from +configuration. This is useful when building a container image for your +execution environment, where packages and Python dependencies are installed at +build time, and runtime-specific configuration is generated later. + +To install dependencies only (without running later bootstrap steps):: + + (kayobe) $ kayobe control host bootstrap --install-only + +To skip installation and run only later bootstrap steps:: + + (kayobe) $ kayobe control host bootstrap --no-install + +These modes are mutually exclusive. + +Environment variables may be used to set defaults: + +- ``KAYOBE_INSTALL_ONLY=true`` enables ``--install-only`` +- ``KAYOBE_NO_INSTALL=true`` enables ``--no-install`` + +For example, a container image build may run ``--install-only``, then at +container runtime execute ``--no-install`` to complete control host bootstrap +once environment-specific configuration is available. + Since the Gazpacho 20.0.0 release it is possible to manage the Ansible control host's configuration in the same way as other hosts. If using this feature, the Ansible control host should be added to the Kayobe inventory in the diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py index 529d9b623..90b15e17c 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..c90087bcb --- /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. From 0ad77191766b363668c156b4fc012ecc208a0af9 Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Thu, 30 Jul 2026 13:15:46 +0200 Subject: [PATCH 3/4] arista-switch: Fix switch interface configuration The arista-switch role was failing to apply switch interface configuration with an error such as: fatal: [arista-switch]: FAILED! => changed: false data: |- description ceph1 % Invalid input ARISTA-SWITCH(config-s-ansible_17)# msg: |- description ceph1 % Invalid input ARISTA-SWITCH(config-s-ansible_17)# This was caused by wrong indentation of the template, as described in the collection documentation [1]: The configuration lines in the source file should be similar to how it will appear if present in the running-configuration (live switch config) of the device including the indentation to ensure idempotency and correct diff. Arista EOS device config has 3 spaces indentation. Also remove the exit statement which appears unneeded. [1] https://docs.ansible.com/projects/ansible/latest/collections/arista/eos/eos_config_module.html Closes-Bug: #2162130 Change-Id: I278897607d7f6375594cfa6341e16caefbc32fd0 Signed-off-by: Pierre Riteau (cherry picked from commit 851aefc2fbbf673726f5e4314326490e822df455) --- ansible/roles/arista-switch/templates/arista-config.j2 | 10 ++++------ releasenotes/notes/bug-2162130-872debd77abe5ff5.yaml | 6 ++++++ 2 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/bug-2162130-872debd77abe5ff5.yaml diff --git a/ansible/roles/arista-switch/templates/arista-config.j2 b/ansible/roles/arista-switch/templates/arista-config.j2 index 2d9254f8a..ddd91b5bb 100644 --- a/ansible/roles/arista-switch/templates/arista-config.j2 +++ b/ansible/roles/arista-switch/templates/arista-config.j2 @@ -4,14 +4,12 @@ {{ line }} {% endfor %} -{% for interface, config in -arista_switch_interface_config.items() %} - interface {{ interface }} +{% for interface, config in arista_switch_interface_config.items() %} +interface {{ interface }} {% if config.description is defined %} - description {{ config.description }} + description {{ config.description }} {% endif %} {% for line in config.config %} - {{ line }} + {{ line }} {% endfor %} - exit {% endfor %} diff --git a/releasenotes/notes/bug-2162130-872debd77abe5ff5.yaml b/releasenotes/notes/bug-2162130-872debd77abe5ff5.yaml new file mode 100644 index 000000000..fb2ec6241 --- /dev/null +++ b/releasenotes/notes/bug-2162130-872debd77abe5ff5.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Resolves an issue that prevented Arista switch interface configuration from + being applied. + `LP#2162130 `__ From 7d598e8865440975ec78315dae957290f893f10a Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Wed, 12 Aug 2026 13:29:11 +0200 Subject: [PATCH 4/4] CI: Pin ansible-lint below 26.8.0 This new release is breaking the linters job. Pin ansible-lint for now. Change-Id: Ifaca67ccc0fe1f7e2f5c0ade3277d86fdbcd7733 Signed-off-by: Pierre Riteau (cherry picked from commit a3993907b45fcc6bcc7b8ffa639da0525c3e7a8a) --- lint-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lint-requirements.txt b/lint-requirements.txt index 222ad5319..a645d87bf 100644 --- a/lint-requirements.txt +++ b/lint-requirements.txt @@ -1,4 +1,4 @@ -ansible-lint>=26.0.0,<27.0.0 # MIT +ansible-lint>=26.0.0,<26.8.0 # MIT bandit>=1.1.0 # Apache-2.0 bashate>=0.2 # Apache-2.0 doc8 # Apache-2.0