From e694ad72c119a15fbb37f1ae0727e435fcc4a52c Mon Sep 17 00:00:00 2001 From: Wen Liang Date: Tue, 17 May 2022 11:26:31 -0400 Subject: [PATCH] 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 --- README.md | 119 ++++++++++++- examples/network_state.yml | 51 ++++++ library/network_state.py | 74 ++++++++ tasks/main.yml | 59 ++++++- tests/ensure_provider_tests.py | 3 + tests/playbooks/tests_network_state.yml | 218 ++++++++++++++++++++++++ tests/tests_network_state_nm.yml | 21 +++ 7 files changed, 535 insertions(+), 10 deletions(-) create mode 100644 examples/network_state.yml create mode 100644 library/network_state.py create mode 100644 tests/playbooks/tests_network_state.yml create mode 100644 tests/tests_network_state_nm.yml diff --git a/README.md b/README.md index 04c1c0e..3abebfe 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/network_state.yml b/examples/network_state.yml new file mode 100644 index 0000000..0919c80 --- /dev/null +++ b/examples/network_state.yml @@ -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 +... diff --git a/library/network_state.py b/library/network_state.py new file mode 100644 index 0000000..aac8289 --- /dev/null +++ b/library/network_state.py @@ -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() diff --git a/tasks/main.yml b/tasks/main.yml index 336e387..0626be0 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -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: diff --git a/tests/ensure_provider_tests.py b/tests/ensure_provider_tests.py index a6c014c..9d170e7 100755 --- a/tests/ensure_provider_tests.py +++ b/tests/ensure_provider_tests.py @@ -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": {}, diff --git a/tests/playbooks/tests_network_state.yml b/tests/playbooks/tests_network_state.yml new file mode 100644 index 0000000..ac775d4 --- /dev/null +++ b/tests/playbooks/tests_network_state.yml @@ -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 +... diff --git a/tests/tests_network_state_nm.yml b/tests/tests_network_state_nm.yml new file mode 100644 index 0000000..df6b2c6 --- /dev/null +++ b/tests/tests_network_state_nm.yml @@ -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