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