From 404cfd7dd5c6044ca63365da5bf430288c752ba3 Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Sat, 25 Jul 2026 11:55:50 +0200 Subject: [PATCH 1/8] Bump hacking to 8.y.z releases Change-Id: I4c352ef16cfa9a423c6dae4a32b75d7c77e6720d Signed-off-by: Pierre Riteau --- lint-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lint-requirements.txt b/lint-requirements.txt index 222ad5319..4c7397768 100644 --- a/lint-requirements.txt +++ b/lint-requirements.txt @@ -2,5 +2,5 @@ ansible-lint>=26.0.0,<27.0.0 # MIT bandit>=1.1.0 # Apache-2.0 bashate>=0.2 # Apache-2.0 doc8 # Apache-2.0 -hacking>=7.0.0,<7.1.0 # Apache-2.0 +hacking>=8.0.0,<9.0.0 # Apache-2.0 yamllint # GPLv3 From ad463df1d5f51b0b493a3ec29e2fdb9fba86113e Mon Sep 17 00:00:00 2001 From: Leonie Chamberlin-Medd Date: Mon, 27 Jul 2026 15:46:09 +0100 Subject: [PATCH 2/8] Support whitespace in Dell switch port description Updates the Dell OS config template files to allow for whitespace in the descriptions. Closes-Bug:#2161899 Change-Id: I70e39d10c592639d0ae20080459babd6cdf5980a Signed-off-by: Leonie Chamberlin-Medd --- ansible/roles/dell-switch/templates/dellos10-config.j2 | 2 +- ansible/roles/dell-switch/templates/dellos6-config.j2 | 2 +- ansible/roles/dell-switch/templates/dellos9-config.j2 | 4 ++-- ...support-whitespace-dellos-switches-1b6a8526850d957f.yaml | 6 ++++++ 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/support-whitespace-dellos-switches-1b6a8526850d957f.yaml diff --git a/ansible/roles/dell-switch/templates/dellos10-config.j2 b/ansible/roles/dell-switch/templates/dellos10-config.j2 index 94c9dc068..7904f89c6 100644 --- a/ansible/roles/dell-switch/templates/dellos10-config.j2 +++ b/ansible/roles/dell-switch/templates/dellos10-config.j2 @@ -7,7 +7,7 @@ {% for interface, config in dell_switch_interface_config.items() %} interface {{ interface }} {% if config.description is defined %} -description {{ config.description }} +description "{{ config.description }}" {% endif %} {% for line in config.config %} {{ line }} diff --git a/ansible/roles/dell-switch/templates/dellos6-config.j2 b/ansible/roles/dell-switch/templates/dellos6-config.j2 index 94c9dc068..7904f89c6 100644 --- a/ansible/roles/dell-switch/templates/dellos6-config.j2 +++ b/ansible/roles/dell-switch/templates/dellos6-config.j2 @@ -7,7 +7,7 @@ {% for interface, config in dell_switch_interface_config.items() %} interface {{ interface }} {% if config.description is defined %} -description {{ config.description }} +description "{{ config.description }}" {% endif %} {% for line in config.config %} {{ line }} diff --git a/ansible/roles/dell-switch/templates/dellos9-config.j2 b/ansible/roles/dell-switch/templates/dellos9-config.j2 index 9b3cbc1fd..6108e9a0d 100644 --- a/ansible/roles/dell-switch/templates/dellos9-config.j2 +++ b/ansible/roles/dell-switch/templates/dellos9-config.j2 @@ -12,7 +12,7 @@ dell_switch_interface_config.items() %} {% if 'vlan' not in interface %} interface {{ interface }} {% if config.description is defined %} - description {{ config.description }} + description "{{ config.description }}" {% endif %} {% for line in config.config %} {{ line }} @@ -25,7 +25,7 @@ dell_switch_interface_config.items() %} {% if 'vlan' in interface %} interface {{ interface }} {% if config.description is defined %} - description {{ config.description }} + description "{{ config.description }}" {% endif %} {% for line in config.config %} {{ line }} diff --git a/releasenotes/notes/support-whitespace-dellos-switches-1b6a8526850d957f.yaml b/releasenotes/notes/support-whitespace-dellos-switches-1b6a8526850d957f.yaml new file mode 100644 index 000000000..f594a31a5 --- /dev/null +++ b/releasenotes/notes/support-whitespace-dellos-switches-1b6a8526850d957f.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixes a bug found when trying to assign a multi-word description + to a Dell OS switch with ``kayobe physical network configure``. + `LP#2161899 `__ From 9b865932a12251b9e07524159bdb033de82ff302 Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Wed, 29 Jul 2026 22:51:45 +0200 Subject: [PATCH 3/8] Fix support matrix note about Rocky Linux 9 Change-Id: I263f8f4d388a175838662d72f3efe5b101e22c91 Signed-off-by: Pierre Riteau --- 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 8194ead0bb03d428c3638a54791bccc9fec5cdfa Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Thu, 30 Jul 2026 10:03:27 +0200 Subject: [PATCH 4/8] Fix stale DNF configuration comments Change-Id: I1b12599dc2ed97954b28cfabe05811103056d7e4 Signed-off-by: Pierre Riteau --- ansible/inventory/group_vars/all/dnf | 9 +++++++-- doc/source/configuration/reference/hosts.rst | 8 ++++---- etc/kayobe/dnf.yml | 16 ++++++++-------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/ansible/inventory/group_vars/all/dnf b/ansible/inventory/group_vars/all/dnf index d9f2507ce..01bcd8375 100644 --- a/ansible/inventory/group_vars/all/dnf +++ b/ansible/inventory/group_vars/all/dnf @@ -1,10 +1,15 @@ --- -# Yum configuration. Dict mapping Yum config option names to their values. +# DNF configuration. + +############################################################################### +# DNF repository configuration. + +# DNF configuration. Dict mapping DNF config option names to their values. # dnf_config: # proxy: http://proxy.example.com dnf_config: {} -# Whether or not to use a local Yum mirror. Default value is 'false'. +# Whether or not to use a local DNF mirror. Default value is 'false'. dnf_use_local_mirror: false # Mirror FQDN for DNF CentOS repos. Default value is 'mirror.stream.centos.org'. diff --git a/doc/source/configuration/reference/hosts.rst b/doc/source/configuration/reference/hosts.rst index e74aebcc0..292934d9e 100644 --- a/doc/source/configuration/reference/hosts.rst +++ b/doc/source/configuration/reference/hosts.rst @@ -239,10 +239,10 @@ CentOS/Rocky and EPEL mirrors can be enabled by setting ``dnf_use_local_mirror`` to ``true``. CentOS repository mirrors are configured via the following variables: -* ``dnf_centos_mirror_host`` (default ``mirror.centos.org``) is the mirror - hostname. -* ``dnf_centos_mirror_directory`` (default ``centos``) is a directory on the - mirror in which repositories may be accessed. +* ``dnf_centos_mirror_host`` (default ``mirror.stream.centos.org``) is the + mirror hostname. +* ``dnf_centos_mirror_directory`` (default ``''``) is a directory on the mirror + in which repositories may be accessed. Rocky repository mirrors are configured via the following variables: diff --git a/etc/kayobe/dnf.yml b/etc/kayobe/dnf.yml index f418d1a13..09c8d4001 100644 --- a/etc/kayobe/dnf.yml +++ b/etc/kayobe/dnf.yml @@ -4,31 +4,31 @@ ############################################################################### # DNF repository configuration. -# Yum configuration. Dict mapping Yum config option names to their values. +# DNF configuration. Dict mapping DNF config option names to their values. # dnf_config: # proxy: http://proxy.example.com #dnf_config: -# Whether or not to use a local Yum mirror. Default value is 'false'. +# Whether or not to use a local DNF mirror. Default value is 'false'. #dnf_use_local_mirror: -# Mirror FQDN for Yum CentOS repos. Default value is 'mirror.centos.org'. +# Mirror FQDN for DNF CentOS repos. Default value is 'mirror.stream.centos.org'. #dnf_centos_mirror_host: -# Mirror directory for Yum CentOS repos. Default value is 'centos'. +# Mirror directory for DNF CentOS repos. Default value is ''. #dnf_centos_mirror_directory: -# Mirror FQDN for Yum Rocky repos. Default value is 'dl.rockylinux.org'. +# Mirror FQDN for DNF Rocky repos. Default value is 'dl.rockylinux.org'. #dnf_rocky_mirror_host: -# Mirror directory for Yum Rocky repos. Default value is 'pub/rocky'. +# Mirror directory for DNF Rocky repos. Default value is 'pub/rocky'. #dnf_rocky_mirror_directory: -# Mirror FQDN for Yum EPEL repos. Default value is +# Mirror FQDN for DNF EPEL repos. Default value is # 'download.fedoraproject.org'. #dnf_epel_mirror_host: -# Mirror directory for Yum EPEL repos. Default value is 'pub/epel'. +# Mirror directory for DNF EPEL repos. Default value is 'pub/epel'. #dnf_epel_mirror_directory: # A dict of custom repositories. From 851aefc2fbbf673726f5e4314326490e822df455 Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Thu, 30 Jul 2026 13:15:46 +0200 Subject: [PATCH 5/8] 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 --- 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 8aae85bb9016579f1240a056e6525ffdc5b19df1 Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Tue, 4 Aug 2026 11:11:55 +0200 Subject: [PATCH 6/8] Fix Bifrost extra_kernel_params validation Ironic recently added parsing of extra_kernel_params options [1]. Kayobe was generating "ipa-inspection-benchmarks=" when extra-hardware was disabled, which failed to validate. Update ipa_kernel_options_default to avoid producing kernel parameters with no value. [1] https://review.opendev.org/c/openstack/ironic/+/992306 Closes-Bug: #2162755 Change-Id: I35827eeb132fc7cd75a85aa0b33367d0a56dd33f Signed-off-by: Pierre Riteau --- ansible/inventory/group_vars/all/ipa | 4 ++-- doc/source/configuration/reference/ironic-python-agent.rst | 7 ++++--- releasenotes/notes/bug-2162755-9852bd6b0606d9d8.yaml | 6 ++++++ 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/bug-2162755-9852bd6b0606d9d8.yaml diff --git a/ansible/inventory/group_vars/all/ipa b/ansible/inventory/group_vars/all/ipa index 0ace49f8f..7b98c3366 100644 --- a/ansible/inventory/group_vars/all/ipa +++ b/ansible/inventory/group_vars/all/ipa @@ -178,8 +178,8 @@ ipa_benchmarks: > # List of default kernel parameters for Ironic python agent. ipa_kernel_options_default: > {{ ['ipa-collect-lldp=' ~ ('1' if ipa_collect_lldp | bool else '0')] + - ['ipa-inspection-collectors=' ~ ipa_collectors | join(',')] + - ['ipa-inspection-benchmarks=' ~ ipa_benchmarks | join(',')] }} + (['ipa-inspection-collectors=' ~ ipa_collectors | join(',')] if ipa_collectors else []) + + (['ipa-inspection-benchmarks=' ~ ipa_benchmarks | join(',')] if ipa_benchmarks else []) }} # List of additional kernel parameters for Ironic python agent. ipa_kernel_options_extra: [] diff --git a/doc/source/configuration/reference/ironic-python-agent.rst b/doc/source/configuration/reference/ironic-python-agent.rst index 6492bb04e..3ece11e96 100644 --- a/doc/source/configuration/reference/ironic-python-agent.rst +++ b/doc/source/configuration/reference/ironic-python-agent.rst @@ -327,9 +327,10 @@ inspection. ``ipa_benchmarks_default`` and ``ipa_benchmarks_extra``. ``ipa_kernel_options_default`` List of default kernel parameters for Ironic python agent. Default includes - ``ipa-collect-lldp``, ``ipa-inspection-collectors`` and - ``ipa-inspection-benchmarks``, with arguments taken from - ``ipa_collect_lldp``, ``ipa_collectors`` and ``ipa_benchmarks``. + ``ipa-collect-lldp``, ``ipa-inspection-collectors`` (if ``ipa_collectors`` + is not empty) and ``ipa-inspection-benchmarks`` (if ``ipa_benchmarks`` is + not empty), with arguments taken from ``ipa_collect_lldp``, + ``ipa_collectors`` and ``ipa_benchmarks``. ``ipa_kernel_options_extra`` List of additional kernel parameters for Ironic python agent. Default is none. diff --git a/releasenotes/notes/bug-2162755-9852bd6b0606d9d8.yaml b/releasenotes/notes/bug-2162755-9852bd6b0606d9d8.yaml new file mode 100644 index 000000000..19a316df3 --- /dev/null +++ b/releasenotes/notes/bug-2162755-9852bd6b0606d9d8.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixes Bifrost deployment by ensuring that extra kernel parameters are + successfully parsed by Ironic. + `LP#2162755 `__ From 20d4d7765a148b42fbaf865472944247933638b1 Mon Sep 17 00:00:00 2001 From: Matt Crees Date: Thu, 14 Mar 2024 14:26:09 +0000 Subject: [PATCH 7/8] Add support for QoS egress/ingress settings in systemd-networkd Change-Id: I6b1e536c98ce0e8fc1fadc14883fa4115f23bfc0 Co-Authored-By: Rafal Lewandowski Signed-off-by: Rafal Lewandowski Signed-off-by: Pierre Riteau --- doc/source/configuration/reference/network.rst | 6 ++++++ kayobe/plugins/filter/networkd.py | 14 ++++++++++++++ kayobe/plugins/filter/networks.py | 10 ++++++++++ kayobe/tests/unit/plugins/filter/test_networkd.py | 14 +++++++++++++- .../notes/add_qos_maps-c458d8709fdc1f65.yaml | 7 +++++++ 5 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add_qos_maps-c458d8709fdc1f65.yaml diff --git a/doc/source/configuration/reference/network.rst b/doc/source/configuration/reference/network.rst index 86e2cef80..4c0fe56e7 100644 --- a/doc/source/configuration/reference/network.rst +++ b/doc/source/configuration/reference/network.rst @@ -621,6 +621,9 @@ The following attributes are supported: ``ingress_qos_map`` is only supported with ``network_engine: nmstate`` on VLAN interfaces. + ``network_engine: default`` on Debian-family distributions + (which uses systemd-networkd). It is not currently supported + with the ``default`` engine on RedHat-family distributions. VLAN ingress QoS map configuration. This maps VLAN header Priority Code Point (PCP) to Linux internal packet priority for incoming packets. @@ -631,6 +634,9 @@ The following attributes are supported: ``egress_qos_map`` is only supported with ``network_engine: nmstate`` on VLAN interfaces. + ``network_engine: default`` on Debian-family distributions + (which uses systemd-networkd). It is not currently supported + with the ``default`` engine on RedHat-family distributions. VLAN egress QoS map configuration. This maps Linux internal packet priority to VLAN header Priority Code Point (PCP) for outgoing packets. diff --git a/kayobe/plugins/filter/networkd.py b/kayobe/plugins/filter/networkd.py index b0bfd9e35..e56cf6ef2 100644 --- a/kayobe/plugins/filter/networkd.py +++ b/kayobe/plugins/filter/networkd.py @@ -81,6 +81,12 @@ def _ms_to_s(n): return n +def _format_vlan_qos_map(qos_map): + if qos_map is None: + return None + return ' '.join(f"{e['from']}-{e['to']}" for e in qos_map) + + def _vlan_netdev(context, name, inventory_hostname): """Return a networkd NetDev configuration for a VLAN interface. @@ -91,6 +97,12 @@ def _vlan_netdev(context, name, inventory_hostname): device = networks.net_interface(context, name, inventory_hostname) mtu = networks.net_mtu(context, name, inventory_hostname) vlan = networks.net_vlan(context, name, inventory_hostname) + ingress_qos_map = networks.net_ingress_qos_map( + context, name, inventory_hostname + ) + egress_qos_map = networks.net_egress_qos_map( + context, name, inventory_hostname + ) config = [ { 'NetDev': [ @@ -102,6 +114,8 @@ def _vlan_netdev(context, name, inventory_hostname): { 'VLAN': [ {'Id': vlan}, + {'IngressQOSMaps': _format_vlan_qos_map(ingress_qos_map)}, + {'EgressQOSMaps': _format_vlan_qos_map(egress_qos_map)}, ] } ] diff --git a/kayobe/plugins/filter/networks.py b/kayobe/plugins/filter/networks.py index 50fe5caf8..bd0b09003 100644 --- a/kayobe/plugins/filter/networks.py +++ b/kayobe/plugins/filter/networks.py @@ -278,6 +278,16 @@ def net_vlan(context, name, inventory_hostname=None): return net_attr(context, name, 'vlan', inventory_hostname) +@jinja2.pass_context +def net_ingress_qos_map(context, name, inventory_hostname=None): + return net_attr(context, name, 'ingress_qos_map', inventory_hostname) + + +@jinja2.pass_context +def net_egress_qos_map(context, name, inventory_hostname=None): + return net_attr(context, name, 'egress_qos_map', inventory_hostname) + + @jinja2.pass_context def net_mtu(context, name, inventory_hostname=None): mtu = net_attr(context, name, 'mtu', inventory_hostname) diff --git a/kayobe/tests/unit/plugins/filter/test_networkd.py b/kayobe/tests/unit/plugins/filter/test_networkd.py index c1d42bd09..abc733432 100644 --- a/kayobe/tests/unit/plugins/filter/test_networkd.py +++ b/kayobe/tests/unit/plugins/filter/test_networkd.py @@ -118,7 +118,17 @@ def test_vlan(self): self.assertEqual(expected, devs) def test_vlan_all_options(self): - self._update_context({"net2_mtu": 1400}) + self._update_context({ + "net2_mtu": 1400, + "net2_egress_qos_map": [ + {"from": 129, "to": 7}, + {"from": 130, "to": 6}, + ], + "net2_ingress_qos_map": [ + {"from": 3, "to": 12}, + {"from": 7, "to": 254}, + ], + }) devs = networkd.networkd_netdevs(self.context, ["net2"]) expected = { "50-kayobe-eth0.2": [ @@ -132,6 +142,8 @@ def test_vlan_all_options(self): { "VLAN": [ {"Id": 2}, + {"IngressQOSMaps": "3-12 7-254"}, + {"EgressQOSMaps": "129-7 130-6"}, ] }, ] diff --git a/releasenotes/notes/add_qos_maps-c458d8709fdc1f65.yaml b/releasenotes/notes/add_qos_maps-c458d8709fdc1f65.yaml new file mode 100644 index 000000000..b271c7fa6 --- /dev/null +++ b/releasenotes/notes/add_qos_maps-c458d8709fdc1f65.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds support for QoS egress/ingress settings in systemd-networkd for + Debian-family distributions. Use ``ingress_qos_map`` and ``egress_qos_map`` + variables to configure QoS maps for incoming and outgoing packets + respectively. From 7b191d9165ccd710dfc3bbdebc907b27b718c3d3 Mon Sep 17 00:00:00 2001 From: Will Szumski Date: Fri, 26 Jun 2026 10:07:15 +0100 Subject: [PATCH 8/8] 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 --- 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 ea7a577dd..d6f4a9c47 100644 --- a/kayobe/cli/commands.py +++ b/kayobe/cli/commands.py @@ -17,6 +17,7 @@ import os import re import sys +import yaml from cliff.command import Command from cliff.hooks import CommandHook @@ -273,12 +274,47 @@ 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") - 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) @@ -426,11 +462,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.