Support the nmstate network state configuration

The users want to apply the nmstate network state configuration to the
interface directly through the role, which necessitates the less
complexity of the network configuration and allows the partial
configuration on the network.

To warrant that the users are capable to apply the nmstate network state
configuration, add the support for the `network_state` variable.

Signed-off-by: Wen Liang <liangwen12year@gmail.com>
This commit is contained in:
Wen Liang 2022-05-17 11:26:31 -04:00 committed by Fernando Fernández Mancera
parent 6dfd6485ed
commit e694ad72c1
7 changed files with 535 additions and 10 deletions

119
README.md
View file

@ -43,18 +43,25 @@ For each host a list of networking profiles can be configured via the
- For `nm`, profiles correspond to connection profiles as handled by
NetworkManager.
Note that the `network` role primarily operates on networking profiles
(connections) and not on devices, but it uses the profile name by default as
the interface name. It is also possible to create generic profiles, by creating
for example a profile with a certain IP configuration without activating the
profile. To apply the configuration to the actual networking interface, use the
`nmcli` commands on the target system.
For each host the network state configuration can also be applied to the interface
directly via the `network_state` variable, and only the `nm` provider supports using
the `network_state` variable.
Note that the `network` role both operates on the connection profiles of the devices
(via the `network_connections` variable) and on devices directly (via the
`network_state` variable). When configuring the connection profiles through the role,
it uses the profile name by default as the interface name. It is also possible to
create generic profiles, by creating for example a profile with a certain IP
configuration without activating the profile. To apply the configuration to the actual
networking interface, use the `nmcli` commands on the target system.
**Warning**: The `network` role updates or creates all connection profiles on
the target system as specified in the `network_connections` variable. Therefore,
the `network` role removes options from the specified profiles if the options are
only present on the system but not in the `network_connections` variable.
Exceptions are mentioned below.
Exceptions are mentioned below. However, the partial networking configuration can be
achieved via specifying the network state configuration in the `network_state`
variable.
Variables
---------
@ -77,6 +84,9 @@ the name prefix. List of variables:
NetworkManager-wifi is not installed, NetworkManager must be restarted prior
to the connection being configured. Setting this to `no` will prevent the
role from restarting network service.
- `network_state` - The network state settings can be configured in the managed
host, and the format and the syntax of the configuration should be consistent
with the [nmstate state examples](https://nmstate.io/examples.html) (YAML).
Examples of Variables
---------------------
@ -91,6 +101,20 @@ network_connections:
network_allow_restart: yes
```
```yaml
network_provider: nm
network_state:
interfaces:
- name: eth0
#...
routes:
config:
#...
dns-resolver:
config:
#...
```
Options
-------
@ -1173,6 +1197,87 @@ network_connections:
key_mgmt: "owe"
```
Examples of Applying the Network State Configuration
-------------------
Configuring the IP addresses:
```yaml
network_state:
interfaces:
- name: ethtest0
type: ethernet
state: up
ipv4:
enabled: true
address:
- ip: 192.168.122.250
prefix-length: 24
dhcp: false
ipv6:
enabled: true
address:
- ip: 2001:db8::1:1
prefix-length: 64
autoconf: false
dhcp: false
- name: ethtest1
type: ethernet
state: up
ipv4:
enabled: true
address:
- ip: 192.168.100.192
prefix-length: 24
auto-dns: false
dhcp: false
ipv6:
enabled: true
address:
- ip: 2001:db8::2:1
prefix-length: 64
autoconf: false
dhcp: false
```
Configuring the route:
```yaml
network_state:
interfaces:
- name: eth1
type: ethernet
state: up
ipv4:
enabled: true
address:
- ip: 192.0.2.251
prefix-length: 24
dhcp: false
routes:
config:
- destination: 198.51.100.0/24
metric: 150
next-hop-address: 192.0.2.251
next-hop-interface: eth1
table-id: 254
```
Configuring the DNS search and server:
```yaml
network_state:
dns-resolver:
config:
search:
- example.com
- example.org
server:
- 2001:4860:4860::8888
- 8.8.8.8
```
### Invalid and Wrong Configuration
The `network` role rejects invalid configurations. It is recommended to test the role

View file

@ -0,0 +1,51 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
vars:
network_state:
interfaces:
- name: ethtest0
type: ethernet
state: up
ipv4:
enabled: true
address:
- ip: 192.168.122.250
prefix-length: 24
dhcp: false
ipv6:
enabled: true
address:
- ip: 2001:db8::1:1
prefix-length: 64
autoconf: false
dhcp: false
- name: ethtest1
type: ethernet
state: up
ipv4:
enabled: true
auto-dns: false
dhcp: true
ipv6:
enabled: true
auto-dns: false
dhcp: true
routes:
config:
- destination: 192.0.2.100/30
metric: 150
next-hop-address: 192.168.122.250
next-hop-interface: ethtest0
table-id: 254
dns-resolver:
config:
search:
- example.com
- example.org
server:
- 2001:4860:4860::8888
- 8.8.8.8
roles:
- linux-system-roles.network
...

74
library/network_state.py Normal file
View file

@ -0,0 +1,74 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r"""
---
module: network_state
version_added: "2.9"
short_description: module for network role to apply network state configuration
description:
- This module allows to apply the network state configuration through nmstate,
https://github.com/nmstate/nmstate
options:
desired_state:
description: Nmstate state definition
required: true
type: dict
author: "Wen Liang (@liangwen12year)"
"""
RETURN = r"""
state:
description: Network state after running the module
type: dict
returned: always
"""
from ansible.module_utils.basic import AnsibleModule
import libnmstate # pylint: disable=import-error
class NetworkState:
def __init__(self, module, module_name):
self.module = module
self.params = module.params
self.result = dict(changed=False)
self.module_name = module_name
self.previous_state = libnmstate.show()
def run(self):
desired_state = self.params["desired_state"]
libnmstate.apply(desired_state)
current_state = libnmstate.show()
if current_state != self.previous_state:
self.result["changed"] = True
self.result["state"] = current_state
self.module.exit_json(**self.result)
def run_module():
module_args = dict(
desired_state=dict(type="dict", required=True),
)
module = AnsibleModule(
argument_spec=module_args,
)
network_state_module = NetworkState(module, "network_state")
network_state_module.run()
def main():
run_module()
if __name__ == "__main__":
main()

View file

@ -8,6 +8,22 @@
debug:
msg: "Using network provider: {{ network_provider }}"
- name: Abort applying the network state configuration if using the
`network_state` variable with the initscripts provider
fail:
msg: Only the `nm` provider supports using the `network_state` variable
when:
- network_state is defined
- network_provider == "initscripts"
- name: Abort applying the network state configuration if the system version
of the managed host is below 8
fail:
msg: The `network_state` variable uses nmstate backend which is only
supported since RHEL-8
when:
- network_state is defined
- ansible_distribution_major_version | int < 8
# Depending on the plugins, checking installed packages might be slow
# for example subscription manager might slow this down
# Therefore install packages only when rpm does not find them
@ -19,6 +35,31 @@
- not network_packages is subset(ansible_facts.packages.keys())
register: __network_package_install
- name: Install NetworkManager and nmstate when using network_state variable
package:
name:
- NetworkManager
- nmstate
state: present
when:
- network_state is defined
- ansible_distribution == 'Fedora' and
ansible_distribution_major_version | int > 27 or
ansible_distribution != 'Fedora' and
ansible_distribution_major_version | int > 7
- name: Install python3-libnmstate when using network_state variable
package:
name:
- python3-libnmstate
state: present
when:
- network_state is defined
- ansible_distribution == 'Fedora' and
ansible_distribution_major_version | int > 34 or
ansible_distribution != 'Fedora' and
ansible_distribution_major_version | int > 8
# If network packages changed and wireless or team connections are specified,
# NetworkManager must be restarted
- name: Restart NetworkManager due to wireless or team interfaces
@ -41,7 +82,7 @@
state: started
enabled: true
when:
- network_provider == "nm"
- network_provider == "nm" or network_state is defined
no_log: true
# If any 802.1x connections are used, the wpa_supplicant
@ -84,14 +125,26 @@
__lsr_ansible_managed: "{{ lookup('template', 'get_ansible_managed.j2') }}"
register: __network_connections_result
- name: Show stderr messages
- name: Configure networking state
network_state:
desired_state: "{{ network_state | default([]) }}"
register: __network_state_result
when: network_state is defined
- name: Show stderr messages for the network_connections
debug:
var: __network_connections_result.stderr_lines
- name: Show debug messages
- name: Show debug messages for the network_connections
debug:
var: __network_connections_result
verbosity: 1
- name: Show debug messages for the network_state
debug:
var: __network_state_result
verbosity: 1
when: network_state is defined
- name: Re-test connectivity
ping:

View file

@ -83,6 +83,9 @@ ibution_major_version | int < 9",
MINIMUM_VERSION: "'1.26.0'",
"comment": "# NetworkManager 1.26.0 added support for match.path setting",
},
"playbooks/tests_network_state.yml": {
EXTRA_RUN_CONDITION: "ansible_distribution_major_version | int > 7",
},
"playbooks/tests_reapply.yml": {},
"playbooks/tests_route_table.yml": {},
"playbooks/tests_routing_rules.yml": {},

View file

@ -0,0 +1,218 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
- name: Test configuring ethernet devices
hosts: all
vars:
type: veth
interface0: ethtest0
interface1: ethtest1
tasks:
- name: "set type={{ type }} and interface={{ interface0 }}"
set_fact:
type: "{{ type }}"
interface: "{{ interface0 }}"
- include_tasks: tasks/show_interfaces.yml
- include_tasks: tasks/manage_test_interface.yml
vars:
state: present
- include_tasks: tasks/assert_device_present.yml
- name: "set type={{ type }} and interface={{ interface1 }}"
set_fact:
type: "{{ type }}"
interface: "{{ interface1 }}"
- include_tasks: tasks/show_interfaces.yml
- include_tasks: tasks/manage_test_interface.yml
vars:
state: present
- include_tasks: tasks/assert_device_present.yml
- name: Configure the IP addresses
import_role:
name: linux-system-roles.network
vars:
network_state:
interfaces:
- name: ethtest0
type: ethernet
state: up
ipv4:
enabled: true
address:
- ip: 192.168.122.250
prefix-length: 24
dhcp: false
ipv6:
enabled: true
address:
- ip: 2001:db8::1:1
prefix-length: 64
autoconf: false
dhcp: false
- name: ethtest1
type: ethernet
state: up
ipv4:
enabled: true
auto-dns: false
address:
- ip: 192.168.122.88
prefix-length: 24
dhcp: false
ipv6:
enabled: true
auto-dns: false
dhcp: true
- name: Get the ethtest0 state configuration
command: nmstatectl show ethtest0
register: ethtest0_state
ignore_errors: yes # noqa ignore-errors
changed_when: false
- name: Get the ethtest1 state configuration
command: nmstatectl show ethtest1
register: ethtest1_state
ignore_errors: yes # noqa ignore-errors
changed_when: false
- name: Assert that the ethtest0 state configuration contains the specified
settings
assert:
that:
- ethtest0_state.stdout is search("192.168.122.250")
- ethtest0_state.stdout is search("2001:db8::1:1")
msg: the ethtest0 state configuration does not contain the specified
settings
- name: Assert that the ethtest1 state configuration contains the specified
settings
assert:
that:
- ethtest1_state.stdout is search("192.168.122.88")
msg: the ethtest1 state configuration does not contain the specified
settings
- name: Configure the route
import_role:
name: linux-system-roles.network
vars:
network_state:
routes:
config:
- destination: 192.0.2.100/30
metric: 150
next-hop-address: 192.168.122.250
next-hop-interface: ethtest0
table-id: 254
- name: Get the route configuration
command: nmstatectl show
register: route
ignore_errors: yes # noqa ignore-errors
changed_when: false
- name: Assert that the route configuration contains the specified route
assert:
that:
- route.stdout is search("destination:(\s+)192.0.2.100/30")
- route.stdout is search("metric:(\s+)150")
- route.stdout is search("next-hop-address:(\s+)192.168.122.250")
- route.stdout is search("next-hop-interface:(\s+)ethtest0")
- route.stdout is search("table-id:(\s+)254")
msg: the route configuration does not contain the specified route
- name: Set the DNS processing mode and the resolv.conf management mode
lineinfile:
path: /etc/NetworkManager/NetworkManager.conf
line: "rc-manager=unmanaged\ndns=systemd-resolved"
insertafter: \[main\]
- name: Restart the NetworkManager
service:
name: NetworkManager
state: restarted
- name: Install the systemd-resolved
package:
name: systemd-resolved
state: present
when:
- ansible_distribution_major_version | int > 8
- name: Enable the systemd-resolved service
service:
name: systemd-resolved
enabled: true
- name: Configure the DNS
import_role:
name: linux-system-roles.network
vars:
network_state:
dns-resolver:
config:
search:
- example.com
- example.org
server:
- 2001:4860:4860::8888
- name: Get the DNS configuration from nmstatectl
command: nmstatectl show
register: nmstatectl
ignore_errors: yes # noqa ignore-errors
changed_when: false
- name: Get the DNS configuration from the file `/etc/resolv.conf`
command: cat /etc/resolv.conf
register: resolvconf
ignore_errors: yes # noqa ignore-errors
changed_when: false
- name: Check if `/etc/resolv.conf` is generated by NM
command: grep "Generated by NetworkManager" /etc/resolv.conf
register: generateByNM
ignore_errors: yes # noqa ignore-errors
changed_when: false
- name: Assert that the nmstatectl contains the specified DNS configuration
assert:
that:
- nmstatectl.stdout is search("example.com")
- nmstatectl.stdout is search("example.org")
- nmstatectl.stdout is search("2001:4860:4860::8888")
msg: the nmstatectl does not contain the specified DNS configuration
- name: Assert that the file `/etc/resolv.conf` does not contain the
specified DNS configuration
assert:
that:
- resolvconf.stdout is not search("example.com") and
resolvconf.stdout is not search("example.org") and
resolvconf.stdout is not search("2001:4860:4860::8888") or
generateByNM.stdout | length == 0
msg: the file `/etc/resolv.conf` contains the specified DNS
configuration
- command: resolvectl
name: "** TEST check resolvectl"
register: result
until: "'example.com' in result.stdout"
retries: 20
delay: 2
changed_when: false
- include_tasks: tasks/delete_interface.yml
- include_tasks: tasks/assert_device_absent.yml
- name: "set interface={{ interface0 }}"
set_fact:
type: "{{ type }}"
interface: "{{ interface0 }}"
- include_tasks: tasks/delete_interface.yml
- include_tasks: tasks/assert_device_absent.yml
...

View file

@ -0,0 +1,21 @@
# SPDX-License-Identifier: BSD-3-Clause
# This file was generated by ensure_provider_tests.py
---
# set network provider and gather facts
- hosts: all
name: Run playbook 'playbooks/tests_network_state.yml' with nm as provider
tasks:
- include_tasks: tasks/el_repo_setup.yml
- name: Set network provider to 'nm'
set_fact:
network_provider: nm
tags:
- always
# The test requires or should run with NetworkManager, therefore it cannot run
# on RHEL/CentOS 6
- import_playbook: playbooks/tests_network_state.yml
when:
- ansible_distribution_major_version != '6'
- ansible_distribution_major_version | int > 7