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
22 changes: 22 additions & 0 deletions ansible/filter_plugins/nmstate.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 nmstate


class FilterModule(object):
"""nmstate filters."""

def filters(self):
return nmstate.get_filters()
2 changes: 1 addition & 1 deletion ansible/network.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@

- name: Configure the network
include_role:
name: "network-{{ ansible_facts.os_family | lower }}"
name: "{{ 'network-nmstate' if network_engine | default('default') == 'nmstate' else 'network-' + ansible_facts.os_family | lower }}"
2 changes: 2 additions & 0 deletions ansible/roles/network-nmstate/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
network_nmstate_install_packages: true
133 changes: 133 additions & 0 deletions ansible/roles/network-nmstate/library/nmstate_apply.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/python
# 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.

import importlib

from ansible.module_utils.basic import AnsibleModule

DOCUMENTATION = """
---
module: nmstate_apply
version_added: "19.1"
author: "StackHPC"
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."
options:
state:
description:
- Network state definition in nmstate format
required: True
type: dict
debug:
description:
- Include previous and desired states in output for debugging
required: False
default: False
type: bool
requirements:
- libnmstate
"""

EXAMPLES = """
- name: Apply network state
nmstate_apply:
state:
interfaces:
- name: eth0
type: ethernet
state: up
ipv4:
address:
- ip: 192.168.1.10
prefix-length: 24
dhcp: false
debug: false
"""

RETURN = """
changed:
description: Whether the network state was modified
type: bool
returned: always
state:
description: Current network state after applying desired state
type: dict
returned: always
previous_state:
description: Network state before applying (when debug=true)
type: dict
returned: when debug=True
desired_state:
description: Desired network state that was applied (when debug=true)
type: dict
returned: when debug=True
"""


def run_module():
argument_spec = dict(
state=dict(required=True, type="dict"),
debug=dict(default=False, type="bool"),
)

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

try:
libnmstate = importlib.import_module("libnmstate")
except Exception as e:
module.fail_json(
msg=(
"Failed to import libnmstate module. "
"Ensure nmstate Python dependencies are installed "
"(for example python3-libnmstate). "
"Import errors: %s"
) % repr(e)
)

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

result = {"changed": False}

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

result["state"] = current_state

module.exit_json(**result)


def main():
run_module()


if __name__ == "__main__":
main()
204 changes: 204 additions & 0 deletions ansible/roles/network-nmstate/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
---
- name: Validate nmstate is supported on this OS
ansible.builtin.assert:
that:
- ansible_facts.os_family == "RedHat"
fail_msg: >-
The nmstate network engine is not supported on {{ ansible_facts.distribution }}
{{ ansible_facts.distribution_version }}. nmstate requires system packages
(nmstate, python3-libnmstate) which are not available in {{ ansible_facts.distribution }}
repositories. The nmstate engine is only supported on Rocky Linux.
For Ubuntu Noble, use the default network engine (network_engine: default)
which provides robust networking via systemd-networkd.

- name: Include nmstate role variables
include_vars: RedHat.yml

- import_role:
name: ahuffman.resolv
when: resolv_is_managed | bool
become: true

- name: Ensure /etc/iproute2 directory exists
ansible.builtin.file:
path: /etc/iproute2
state: directory
owner: root
group: root
mode: '0755'
become: true
when:
- (network_route_tables | default([])) | length > 0

- name: Ensure IP routing tables are defined
blockinfile:
create: true
dest: /etc/iproute2/rt_tables
owner: root
group: root
mode: '0644'
block: |
{% for table in network_route_tables %}
{{ table.id }} {{ table.name }}
{% endfor %}
become: true
when:
- (network_route_tables | default([])) | length > 0

- name: Ensure nmstate packages are installed
package:
name: "{{ network_nmstate_packages }}"
state: present
become: true
when:
- network_nmstate_install_packages | bool
- network_nmstate_packages | length > 0

- name: Ensure NetworkManager is enabled and running
service:
name: NetworkManager
state: started
enabled: true
become: true

- name: Ensure NetworkManager DNS config is present only if required
become: true
community.general.ini_file:
path: /etc/NetworkManager/NetworkManager.conf
section: main
option: "{{ item.option }}"
value: "{{ item.value }}"
state: "{{ 'present' if resolv_is_managed | bool else 'absent' }}"
loop:
- option: dns
value: none
- option: rc-manager
value: unmanaged
when:
- ansible_facts.os_family == "RedHat" and ansible_facts.distribution_major_version | int >= 9
register: dns_config_task

- name: Reload NetworkManager with DNS config
become: true
systemd:
name: NetworkManager
state: reloaded
daemon_reload: true
when: dns_config_task is changed

- name: Generate nmstate desired state
set_fact:
network_nmstate_desired_state: "{{ network_interfaces | nmstate_config }}"

- name: Apply nmstate configuration using module
nmstate_apply:
state: "{{ network_nmstate_desired_state }}"
register: nmstate_apply_result
become: true
vars:
ansible_python_interpreter: "{{ ansible_facts.python.executable }}"

- name: Initialise nmstate firewalld interface-zone map
set_fact:
network_nmstate_zone_map: {}

- name: Build nmstate firewalld interface-zone map
set_fact:
network_nmstate_zone_map: >-
{{ network_nmstate_zone_map
| combine({ (item | net_interface): (item | net_zone) }) }}
loop: "{{ network_interfaces | default([]) }}"
when:
- item | net_zone
- item | net_interface

- name: Build nmstate firewalld interface-zone items
set_fact:
network_nmstate_zone_items: >-
{{ network_nmstate_zone_map
| dict2items(key_name='interface', value_name='zone') }}

- block:
- name: Ensure firewalld package is installed for nmstate zone sync
package:
name: firewalld
state: present

- name: Ensure firewalld service is enabled and running for nmstate zone sync
service:
name: firewalld
state: started
enabled: true

# 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

# Keep permanent firewalld configuration in sync first. Runtime state is
# refreshed separately below from permanent config.
- name: Ensure firewalld zones exist for nmstate interfaces
firewalld:
offline: true
permanent: true
state: present
zone: "{{ item }}"
loop: "{{ network_nmstate_zone_items | map(attribute='zone') | unique | list }}"
register: network_nmstate_zones_result

- name: Ensure permanent firewalld zones are set for nmstate interfaces
firewalld:
interface: "{{ item.interface }}"
offline: true
permanent: true
state: enabled
zone: "{{ item.zone }}"
loop: "{{ network_nmstate_zone_items }}"
register: network_nmstate_perm_result

- name: Reload firewalld runtime from permanent config before nmstate zone sync
ansible.builtin.service:
name: firewalld
state: reloaded
changed_when: false
when:
- network_nmstate_zones_result is changed or network_nmstate_perm_result is changed

# TODO(gkoper): investigate NM profile zone mapping to avoid explicit
# firewalld sync in nmstate path.
- name: Ensure runtime firewalld zones are set for nmstate interfaces
firewalld:
interface: "{{ item.interface }}"
immediate: true
permanent: false
state: enabled
zone: "{{ item.zone }}"
loop: "{{ network_nmstate_zone_items }}"
become: true
when:
- ansible_facts.os_family == "RedHat"
- firewalld_enabled | default(false) | bool
- network_nmstate_zone_items | length > 0
4 changes: 4 additions & 0 deletions ansible/roles/network-nmstate/vars/RedHat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
network_nmstate_packages:
- nmstate
- python3-libnmstate
Loading
Loading