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/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/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/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/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/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/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/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/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/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/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/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/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 ~~~~~~~~~~~~~~~~~~~~~~~~~~ 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. 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/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/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/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/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 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. 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. 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 `__ 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 `__ 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 `__