Skip to content

Commit 384f75e

Browse files
authored
Merge pull request #542 from stackhpc/upstream/master-2026-04-01
Synchronise master with upstream
2 parents b0148ec + a6f4c96 commit 384f75e

File tree

15 files changed

+2406
-17
lines changed

15 files changed

+2406
-17
lines changed

ansible/filter_plugins/nmstate.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright (c) 2026 StackHPC Ltd.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from kayobe.plugins.filter import nmstate
16+
17+
18+
class FilterModule(object):
19+
"""nmstate filters."""
20+
21+
def filters(self):
22+
return nmstate.get_filters()

ansible/network.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@
4343

4444
- name: Configure the network
4545
include_role:
46-
name: "network-{{ ansible_facts.os_family | lower }}"
46+
name: "{{ 'network-nmstate' if network_engine | default('default') == 'nmstate' else 'network-' + ansible_facts.os_family | lower }}"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
network_nmstate_install_packages: true
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/python
2+
# Copyright (c) 2026 StackHPC Ltd.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
# not use this file except in compliance with the License. You may obtain
6+
# a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations
14+
# under the License.
15+
16+
import importlib
17+
18+
from ansible.module_utils.basic import AnsibleModule
19+
20+
DOCUMENTATION = """
21+
---
22+
module: nmstate_apply
23+
version_added: "19.1"
24+
author: "StackHPC"
25+
short_description: Apply network state using nmstate
26+
description:
27+
- "This module allows applying a network state using nmstate library.
28+
Provides idempotency by comparing desired and current states."
29+
options:
30+
state:
31+
description:
32+
- Network state definition in nmstate format
33+
required: True
34+
type: dict
35+
debug:
36+
description:
37+
- Include previous and desired states in output for debugging
38+
required: False
39+
default: False
40+
type: bool
41+
requirements:
42+
- libnmstate
43+
"""
44+
45+
EXAMPLES = """
46+
- name: Apply network state
47+
nmstate_apply:
48+
state:
49+
interfaces:
50+
- name: eth0
51+
type: ethernet
52+
state: up
53+
ipv4:
54+
address:
55+
- ip: 192.168.1.10
56+
prefix-length: 24
57+
dhcp: false
58+
debug: false
59+
"""
60+
61+
RETURN = """
62+
changed:
63+
description: Whether the network state was modified
64+
type: bool
65+
returned: always
66+
state:
67+
description: Current network state after applying desired state
68+
type: dict
69+
returned: always
70+
previous_state:
71+
description: Network state before applying (when debug=true)
72+
type: dict
73+
returned: when debug=True
74+
desired_state:
75+
description: Desired network state that was applied (when debug=true)
76+
type: dict
77+
returned: when debug=True
78+
"""
79+
80+
81+
def run_module():
82+
argument_spec = dict(
83+
state=dict(required=True, type="dict"),
84+
debug=dict(default=False, type="bool"),
85+
)
86+
87+
module = AnsibleModule(
88+
argument_spec=argument_spec,
89+
supports_check_mode=False,
90+
)
91+
92+
try:
93+
libnmstate = importlib.import_module("libnmstate")
94+
except Exception as e:
95+
module.fail_json(
96+
msg=(
97+
"Failed to import libnmstate module. "
98+
"Ensure nmstate Python dependencies are installed "
99+
"(for example python3-libnmstate). "
100+
"Import errors: %s"
101+
) % repr(e)
102+
)
103+
104+
previous_state = libnmstate.show()
105+
desired_state = module.params["state"]
106+
debug = module.params["debug"]
107+
108+
result = {"changed": False}
109+
110+
try:
111+
libnmstate.apply(desired_state)
112+
except Exception as e:
113+
module.fail_json(msg="Failed to apply nmstate state: %s" % repr(e))
114+
115+
current_state = libnmstate.show()
116+
117+
if current_state != previous_state:
118+
result["changed"] = True
119+
if debug:
120+
result["previous_state"] = previous_state
121+
result["desired_state"] = desired_state
122+
123+
result["state"] = current_state
124+
125+
module.exit_json(**result)
126+
127+
128+
def main():
129+
run_module()
130+
131+
132+
if __name__ == "__main__":
133+
main()
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
---
2+
- name: Validate nmstate is supported on this OS
3+
ansible.builtin.assert:
4+
that:
5+
- ansible_facts.os_family == "RedHat"
6+
fail_msg: >-
7+
The nmstate network engine is not supported on {{ ansible_facts.distribution }}
8+
{{ ansible_facts.distribution_version }}. nmstate requires system packages
9+
(nmstate, python3-libnmstate) which are not available in {{ ansible_facts.distribution }}
10+
repositories. The nmstate engine is only supported on Rocky Linux.
11+
For Ubuntu Noble, use the default network engine (network_engine: default)
12+
which provides robust networking via systemd-networkd.
13+
14+
- name: Include nmstate role variables
15+
include_vars: RedHat.yml
16+
17+
- import_role:
18+
name: ahuffman.resolv
19+
when: resolv_is_managed | bool
20+
become: true
21+
22+
- name: Ensure /etc/iproute2 directory exists
23+
ansible.builtin.file:
24+
path: /etc/iproute2
25+
state: directory
26+
owner: root
27+
group: root
28+
mode: '0755'
29+
become: true
30+
when:
31+
- (network_route_tables | default([])) | length > 0
32+
33+
- name: Ensure IP routing tables are defined
34+
blockinfile:
35+
create: true
36+
dest: /etc/iproute2/rt_tables
37+
owner: root
38+
group: root
39+
mode: '0644'
40+
block: |
41+
{% for table in network_route_tables %}
42+
{{ table.id }} {{ table.name }}
43+
{% endfor %}
44+
become: true
45+
when:
46+
- (network_route_tables | default([])) | length > 0
47+
48+
- name: Ensure nmstate packages are installed
49+
package:
50+
name: "{{ network_nmstate_packages }}"
51+
state: present
52+
become: true
53+
when:
54+
- network_nmstate_install_packages | bool
55+
- network_nmstate_packages | length > 0
56+
57+
- name: Ensure NetworkManager is enabled and running
58+
service:
59+
name: NetworkManager
60+
state: started
61+
enabled: true
62+
become: true
63+
64+
- name: Ensure NetworkManager DNS config is present only if required
65+
become: true
66+
community.general.ini_file:
67+
path: /etc/NetworkManager/NetworkManager.conf
68+
section: main
69+
option: "{{ item.option }}"
70+
value: "{{ item.value }}"
71+
state: "{{ 'present' if resolv_is_managed | bool else 'absent' }}"
72+
loop:
73+
- option: dns
74+
value: none
75+
- option: rc-manager
76+
value: unmanaged
77+
when:
78+
- ansible_facts.os_family == "RedHat" and ansible_facts.distribution_major_version | int >= 9
79+
register: dns_config_task
80+
81+
- name: Reload NetworkManager with DNS config
82+
become: true
83+
systemd:
84+
name: NetworkManager
85+
state: reloaded
86+
daemon_reload: true
87+
when: dns_config_task is changed
88+
89+
- name: Generate nmstate desired state
90+
set_fact:
91+
network_nmstate_desired_state: "{{ network_interfaces | nmstate_config }}"
92+
93+
- name: Apply nmstate configuration using module
94+
nmstate_apply:
95+
state: "{{ network_nmstate_desired_state }}"
96+
register: nmstate_apply_result
97+
become: true
98+
vars:
99+
ansible_python_interpreter: "{{ ansible_facts.python.executable }}"
100+
101+
- name: Initialise nmstate firewalld interface-zone map
102+
set_fact:
103+
network_nmstate_zone_map: {}
104+
105+
- name: Build nmstate firewalld interface-zone map
106+
set_fact:
107+
network_nmstate_zone_map: >-
108+
{{ network_nmstate_zone_map
109+
| combine({ (item | net_interface): (item | net_zone) }) }}
110+
loop: "{{ network_interfaces | default([]) }}"
111+
when:
112+
- item | net_zone
113+
- item | net_interface
114+
115+
- name: Build nmstate firewalld interface-zone items
116+
set_fact:
117+
network_nmstate_zone_items: >-
118+
{{ network_nmstate_zone_map
119+
| dict2items(key_name='interface', value_name='zone') }}
120+
121+
- block:
122+
- name: Ensure firewalld package is installed for nmstate zone sync
123+
package:
124+
name: firewalld
125+
state: present
126+
127+
- name: Ensure firewalld service is enabled and running for nmstate zone sync
128+
service:
129+
name: firewalld
130+
state: started
131+
enabled: true
132+
133+
# TODO(gkoper): replace temporary nmcli profile zone sync with native
134+
# zone handling in the nmstate filter/module path.
135+
- name: Gather NetworkManager connection firewalld zones for nmstate interfaces
136+
command:
137+
argv:
138+
- nmcli
139+
- -g
140+
- connection.zone
141+
- connection
142+
- show
143+
- "{{ item.interface }}"
144+
changed_when: false
145+
loop: "{{ network_nmstate_zone_items }}"
146+
register: network_nmstate_nm_zone_result
147+
148+
- name: Ensure NetworkManager connection firewalld zones are set for nmstate interfaces
149+
command:
150+
argv:
151+
- nmcli
152+
- connection
153+
- modify
154+
- "{{ item.item.interface }}"
155+
- connection.zone
156+
- "{{ item.item.zone }}"
157+
loop: "{{ network_nmstate_nm_zone_result.results }}"
158+
when:
159+
- (item.stdout | default('') | trim) != item.item.zone
160+
161+
# Keep permanent firewalld configuration in sync first. Runtime state is
162+
# refreshed separately below from permanent config.
163+
- name: Ensure firewalld zones exist for nmstate interfaces
164+
firewalld:
165+
offline: true
166+
permanent: true
167+
state: present
168+
zone: "{{ item }}"
169+
loop: "{{ network_nmstate_zone_items | map(attribute='zone') | unique | list }}"
170+
register: network_nmstate_zones_result
171+
172+
- name: Ensure permanent firewalld zones are set for nmstate interfaces
173+
firewalld:
174+
interface: "{{ item.interface }}"
175+
offline: true
176+
permanent: true
177+
state: enabled
178+
zone: "{{ item.zone }}"
179+
loop: "{{ network_nmstate_zone_items }}"
180+
register: network_nmstate_perm_result
181+
182+
- name: Reload firewalld runtime from permanent config before nmstate zone sync
183+
ansible.builtin.service:
184+
name: firewalld
185+
state: reloaded
186+
changed_when: false
187+
when:
188+
- network_nmstate_zones_result is changed or network_nmstate_perm_result is changed
189+
190+
# TODO(gkoper): investigate NM profile zone mapping to avoid explicit
191+
# firewalld sync in nmstate path.
192+
- name: Ensure runtime firewalld zones are set for nmstate interfaces
193+
firewalld:
194+
interface: "{{ item.interface }}"
195+
immediate: true
196+
permanent: false
197+
state: enabled
198+
zone: "{{ item.zone }}"
199+
loop: "{{ network_nmstate_zone_items }}"
200+
become: true
201+
when:
202+
- ansible_facts.os_family == "RedHat"
203+
- firewalld_enabled | default(false) | bool
204+
- network_nmstate_zone_items | length > 0
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
network_nmstate_packages:
3+
- nmstate
4+
- python3-libnmstate

0 commit comments

Comments
 (0)