Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions ansible/dump-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@
- name: Write host config to file
delegate_to: localhost
copy:
content: "{{ hostvars[inventory_hostname] | to_nice_yaml }}"
content: >-
{{ hostvars[inventory_hostname] | kayobe_config_dump
| to_nice_yaml }}
dest: "{{ dump_path }}/{{ inventory_hostname }}.yml"
when: dump_var_name is not defined

- name: Write host variable to file
delegate_to: localhost
copy:
content: "{{ hostvars[inventory_hostname][dump_var_name] | to_nice_yaml }}"
content: >-
{{ hostvars[inventory_hostname]
| kayobe_config_dump(dump_var_name) | to_nice_yaml }}
dest: "{{ dump_path }}/{{ inventory_hostname }}.yml"
when: dump_var_name is defined

Expand Down
22 changes: 22 additions & 0 deletions ansible/filter_plugins/config_dump.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright (c) 2026 StackHPC Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from kayobe.plugins.filter import config_dump


class FilterModule(object):
"""Configuration dump filters."""

def filters(self):
return config_dump.get_filters()
5 changes: 5 additions & 0 deletions ansible/inventory/group_vars/all/globals
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,8 @@ kayobe_ansible_setup_gather_subset: "{{ omit }}"
# This allows us to install packages and create arbitrary directories that our
# user would not normally have permission to create. Default is true.
kayobe_control_host_become: true

# Global variable to choose whether or not to reboot hosts when required by
# host configuration playbooks, including VGPU and SELinux playbooks. Default
# is false.
kayobe_do_reboot: false
2 changes: 1 addition & 1 deletion ansible/inventory/group_vars/all/vgpu
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ vgpu_driver_url: ""
# Flag to control whether the vGPU playbook should automatically reboot the
# hypervisor. Note: this is necessary for the driver to be loaded correctly.
# Caution should be used when changing this option.
vgpu_do_reboot: true
vgpu_do_reboot: "{{ kayobe_do_reboot | bool }}"

# Time to wait when rebooting the host before failing.
vgpu_reboot_timeout: 600
2 changes: 1 addition & 1 deletion ansible/roles/kolla-ansible/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ kolla_ansible_venv_extra_requirements: []
# tested code. Changes to this limit should be tested. It is possible to only
# install ansible-core by setting kolla_ansible_venv_ansible to None.
kolla_ansible_venv_ansible:
kolla_ansible_venv_ansible_core: 'ansible-core>=2.19,<2.21'
kolla_ansible_venv_ansible_core: 'ansible-core>=2.20,<2.21'

# Path to a requirements.yml file for Ansible collections.
kolla_ansible_requirements_yml: "{{ kolla_ansible_venv }}/share/kolla-ansible/requirements.yml"
Expand Down
13 changes: 5 additions & 8 deletions ansible/roles/kolla-openstack/molecule/default/create.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,12 @@
register: docker_images

- name: Build an Ansible compatible image
docker_image:
community.docker.docker_image_build:
name: "molecule_local/{{ item.item.image }}"
source: build
build:
path: "{{ molecule_ephemeral_directory }}"
dockerfile: "{{ item.item.dockerfile | default(item.invocation.module_args.dest) }}"
network: host
force_source: "{{ item.item.force | default(true) }}"
force_tag: "{{ item.item.force | default(true) }}"
path: "{{ molecule_ephemeral_directory }}"
dockerfile: "{{ item.item.dockerfile | default(item.dest) }}"
network: host
rebuild: "{{ 'always' if item.item.force | default(true) else 'never' }}"
with_items: "{{ platforms.results }}"
when: platforms.changed or docker_images.results | map(attribute='images') | select('equalto', []) | list | count >= 0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,12 @@
register: docker_images

- name: Build an Ansible compatible image
docker_image:
community.docker.docker_image_build:
name: "molecule_local/{{ item.item.image }}"
source: build
build:
path: "{{ molecule_ephemeral_directory }}"
dockerfile: "{{ item.item.dockerfile | default(item.invocation.module_args.dest) }}"
network: host
force_source: "{{ item.item.force | default(true) }}"
force_tag: "{{ item.item.force | default(true) }}"
path: "{{ molecule_ephemeral_directory }}"
dockerfile: "{{ item.item.dockerfile | default(item.dest) }}"
network: host
rebuild: "{{ 'always' if item.item.force | default(true) else 'never' }}"
with_items: "{{ platforms.results }}"
when: platforms.changed or docker_images.results | map(attribute='images') | select('equalto', []) | list | count >= 0

Expand Down
80 changes: 63 additions & 17 deletions ansible/roles/network-nmstate/library/nmstate_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# under the License.

import importlib
import json

from ansible.module_utils.basic import AnsibleModule

Expand All @@ -25,7 +26,8 @@
short_description: Apply network state using nmstate
description:
- "This module allows applying a network state using nmstate library.
Provides idempotency by comparing desired and current states."
Provides idempotency by comparing desired and current states.
Supports check and diff modes."
options:
state:
description:
Expand All @@ -39,7 +41,7 @@
default: False
type: bool
requirements:
- libnmstate
- libnmstate (nmstate 2.x)
"""

EXAMPLES = """
Expand All @@ -66,7 +68,15 @@
state:
description: Current network state after applying desired state
type: dict
returned: always
returned: when not in check mode
differences:
description: Computed differences between the current and desired states
type: dict
returned: when changed
diff:
description: Prepared diff of the computed differences
type: dict
returned: when changed
previous_state:
description: Network state before applying (when debug=true)
type: dict
Expand All @@ -78,6 +88,14 @@
"""


def _is_empty(value):
if isinstance(value, dict):
return all(_is_empty(item) for item in value.values())
if isinstance(value, (list, tuple)):
return all(_is_empty(item) for item in value)
return value is None


def run_module():
argument_spec = dict(
state=dict(required=True, type="dict"),
Expand All @@ -86,7 +104,7 @@ def run_module():

module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=False,
supports_check_mode=True,
)

try:
Expand All @@ -101,26 +119,54 @@ def run_module():
) % repr(e)
)

previous_state = libnmstate.show()
if not hasattr(libnmstate, "generate_differences"):
module.fail_json(
msg=(
"The installed libnmstate does not provide "
"generate_differences(). The nmstate_apply module requires "
"nmstate 2.x (for example python3-libnmstate 2.x)."
)
)

current_state = libnmstate.show()
desired_state = module.params["state"]
debug = module.params["debug"]

result = {"changed": False}

differences = libnmstate.generate_differences(
desired_state, current_state)
changed = not _is_empty(differences)

result = {"changed": changed}
if changed:
# "prepared" is a special Ansible diff key whose content is
# printed as-is, keeping --diff output to the changed properties.
if module._diff:
result["diff"] = {
"prepared": json.dumps(
differences, indent=1, sort_keys=True),
}
result["differences"] = differences

if debug:
result["previous_state"] = current_state
result["desired_state"] = desired_state

if module.check_mode:
module.exit_json(**result)
return

# generate_differences() compares against the runtime state, not
# the config persisted by NetworkManager, so apply unconditionally
# to re-persist the config and prevent drift after a reboot.
try:
libnmstate.apply(desired_state)
except Exception as e:
module.fail_json(msg="Failed to apply nmstate state: %s" % repr(e))

current_state = libnmstate.show()

if current_state != previous_state:
result["changed"] = True
if debug:
result["previous_state"] = previous_state
result["desired_state"] = desired_state
# A failed apply must not claim changed: nmstate verifies the
# applied state and rolls back on failure.
module.fail_json(msg="Failed to apply nmstate state: %s" % repr(e),
differences=differences)

result["state"] = current_state
result["state"] = libnmstate.show()

module.exit_json(**result)

Expand Down
95 changes: 70 additions & 25 deletions ansible/roles/network-nmstate/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
name: "{{ network_nmstate_packages }}"
state: present
become: true
register: network_nmstate_package_result
when:
- network_nmstate_install_packages | bool
- network_nmstate_packages | length > 0
Expand Down Expand Up @@ -97,6 +98,9 @@
become: true
vars:
ansible_python_interpreter: "{{ ansible_facts.python.executable }}"
when: >-
not (ansible_check_mode and
network_nmstate_package_result is changed)

- name: Initialise nmstate firewalld interface-zone map
set_fact:
Expand Down Expand Up @@ -132,31 +136,72 @@

# TODO(gkoper): replace temporary nmcli profile zone sync with native
# zone handling in the nmstate filter/module path.
- name: Gather NetworkManager connection firewalld zones for nmstate interfaces
command:
argv:
- nmcli
- -g
- connection.zone
- connection
- show
- "{{ item.interface }}"
changed_when: false
loop: "{{ network_nmstate_zone_items }}"
register: network_nmstate_nm_zone_result

- name: Ensure NetworkManager connection firewalld zones are set for nmstate interfaces
command:
argv:
- nmcli
- connection
- modify
- "{{ item.item.interface }}"
- connection.zone
- "{{ item.item.zone }}"
loop: "{{ network_nmstate_nm_zone_result.results }}"
when:
- (item.stdout | default('') | trim) != item.item.zone
- name: Sync NetworkManager connection firewalld zones for nmstate interfaces
block:
# Cloud-init can create a profile whose ID is prefixed with
# "cloud-init", while its active device is still the nmstate interface.
# `nmcli connection show` accepts a profile UUID, so get the active
# profile UUID from the device before reading or updating its zone.
- name: Gather NetworkManager connection profiles for nmstate interfaces
command:
argv:
- nmcli
- -g
- GENERAL.CON-UUID
- device
- show
- "{{ zone_item.interface }}"
changed_when: false
loop: "{{ network_nmstate_zone_items }}"
loop_control:
loop_var: zone_item
label: "{{ zone_item.interface }}"
register: network_nmstate_nm_connection_result

- name: Validate NetworkManager connections for nmstate interfaces
ansible.builtin.assert:
that:
- (connection_item.stdout | trim) not in ['', '--']
fail_msg: >-
No active NetworkManager connection found for nmstate interface
{{ connection_item.zone_item.interface }}
loop: "{{ network_nmstate_nm_connection_result.results }}"
loop_control:
loop_var: connection_item
label: "{{ connection_item.zone_item.interface }}"

- name: Gather NetworkManager connection firewalld zones for nmstate interfaces
command:
argv:
- nmcli
- -g
- connection.zone
- connection
- show
- "{{ connection_item.stdout | trim }}"
changed_when: false
loop: "{{ network_nmstate_nm_connection_result.results }}"
loop_control:
loop_var: connection_item
label: "{{ connection_item.zone_item.interface }}"
register: network_nmstate_nm_zone_result

- name: Ensure NetworkManager connection firewalld zones are set for nmstate interfaces
command:
argv:
- nmcli
- connection
- modify
- "{{ zone_result.connection_item.stdout | trim }}"
- connection.zone
- "{{ zone_result.connection_item.zone_item.zone }}"
loop: "{{ network_nmstate_nm_zone_result.results }}"
loop_control:
loop_var: zone_result
label: "{{ zone_result.connection_item.zone_item.interface }}"
when:
- (zone_result.stdout | default('') | trim) != zone_result.connection_item.zone_item.zone
when: not ansible_check_mode

# Keep permanent firewalld configuration in sync first. Runtime state is
# refreshed separately below from permanent config.
Expand Down
2 changes: 1 addition & 1 deletion ansible/roles/selinux/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ selinux_policy: targeted
selinux_state: permissive

# Whether to reboot to apply SELinux config changes.
selinux_do_reboot: false
selinux_do_reboot: "{{ kayobe_do_reboot | bool }}"

# Number of seconds to wait for hosts to become accessible via SSH after being
# rebooted.
Expand Down
Loading