This commit is contained in:
2020-08-17 12:06:41 -04:00
parent 9fa09f26bd
commit 6eb48873e6
455 changed files with 45184 additions and 14 deletions

View File

@@ -0,0 +1,11 @@
/.cache
/.coverage
*.pyc
/.pytest_cache
/tests/.coverage
/tests/htmlcov*
/tests/__pycache__/
/tests/remote-coveragedata-*
/tests/tmp_merge_coveragerc
/tests/total-*coveragedata
/.tox

View File

@@ -0,0 +1,27 @@
---
dist: xenial
language: python
matrix:
include:
- python: 2.6
dist: trusty
- python: 2.7
- python: 3.5
env: aptpkgs=python3-selinux
- python: 3.6
- python: 3.7
- python: 3.7-dev
- python: 3.8-dev
# - python: nightly
services:
- docker
before_install:
- if [ -n "${aptpkgs}" ]; then sudo apt-get install -y python3-selinux; fi
install:
- pip install tox tox-travis
script:
- tox

View File

@@ -0,0 +1,28 @@
BSD-3-Clause License
Copyright (c) 2017-2018 Red Hat, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,682 @@
linux-system-roles/network
==========================
[![Coverage Status](https://coveralls.io/repos/github/linux-system-roles/network/badge.svg)](https://coveralls.io/github/linux-system-roles/network)
[![Travis Build Status](https://travis-ci.org/linux-system-roles/network.svg?branch=master)](https://travis-ci.org/linux-system-roles/network)
[![Code Style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
Overview
--------
The `network` role enables users to configure network on the target machines.
This role can be used to configure:
- Ethernet interfaces
- Bridge interfaces
- Bonded interfaces
- VLAN interfaces
- MacVLAN interfaces
- Infiniband interfaces
- IP configuration
Introduction
------------
The `network` role supports two providers: `nm` and `initscripts`. `nm` is
used by default in RHEL7 and `initscripts` in RHEL6. These providers can be
configured per host via the [`network_provider`](#provider) variable. In
absence of explicit configuration, it is autodetected based on the
distribution. However, note that either `nm` or `initscripts` is not tied to a certain
distribution. The `network` role works everywhere the required API is available.
This means that `nm` requires at least NetworkManager's API version 1.2 available.
For `initscripts`, the legacy network service is required as used in Fedora or RHEL.
For each host a list of networking profiles can be configured via the
`network_connections` variable.
- For `initscripts`, profiles correspond to ifcfg files in the `/etc/sysconfig/network-scripts/ifcfg-*` directory.
- For `NetworkManager`, profiles correspond to connection profiles as handled by
NetworkManager. Fedora and RHEL use the `ifcfg-rh-plugin` for NetworkManager,
which also writes or reads configuration files to `/etc/sysconfig/network-scripts/ifcfg-*`
for compatibility.
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.
**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.
Variables
---------
The `network` role is configured via variables starting with `network_` as the name prefix.
List of variables:
* `network_provider` - The `network_provider` variable allows to set a specific
provider (`nm` or `initscripts`) . Setting it to `{{ network_provider_os_default }}`,
the provider is set depending on the operating system. This is usually `nm`
except for RHEL 6 or CentOS 6 systems.
* `network_connections` - The connection profiles are configured as `network_connections`,
which is a list of dictionaries that include specific options.
Examples of Variables
---------------------
Setting the variables
```yaml
network_provider: nm
network_connections:
- name: eth0
#...
```
Options
-------
The `network_connections` variable is a list of dictionaries that include the following options.
List of options:
### `name` (required)
The `name` option identifies the connection profile. It is not the name of the
networking interface for which the profile applies, though we can associate
the profile with an interface and give them the same name.
Note that you can have multiple profiles for the same device, but only
one profile can be active on the device each time.
For NetworkManager, a connection can only be active at one device each time.
* For `NetworkManager`, the `name` option corresponds to the
[`connection.id`](https://developer.gnome.org/NetworkManager/stable/nm-settings.html#nm-settings.property.connection.id)
property option.
Although NetworkManager supports multiple connections with the same `connection.id`,
the `network` role cannot handle a duplicate `name`. Specifying a `name` multiple
times refers to the same connection profile.
* For `initscripts`, the `name` option determines the ifcfg file name `/etc/sysconfig/network-scripts/ifcfg-$NAME`.
Note that the `name` does not specify the `DEVICE` but a filename. As a consequence,
`'/'` is not a valid character for the `name`.
You can also use the same connection profile multiple times. Therefore, it is possible to create a profile and activate it separately.
### `state`
The `state` option identifies what is the runtime state of each connection profile. The `state` option (optional) can be set to the following values:
* `up` - the connection profile is activated
* `down` - the connection profile is deactivated
#### `state: up`
- For `NetworkManager`, this corresponds to `nmcli connection id {{name}} up`.
- For `initscripts`, this corresponds to `ifup {{name}}`.
When the `state` option is set to `up`, you can also specify the `wait` option (optional):
* `wait: 0` - initiates only the activation, but does not wait until the device is fully connected.
The connection will be completed in the background, for example after a DHCP lease was received.
* `wait: <seconds>` is a timeout that enables you to decide how long you give the device to
activate. The default is using a suitable timeout. Note that the `wait` option is
only supported by NetworkManager.
Note that `state: up` always re-activates the profile and possibly changes the
networking configuration, even if the profile was already active before. As
a consequence, `state: up` always changes the system.
#### `state: down`
- For `NetworkManager`, it corresponds to `nmcli connection id {{name}} down`.
- For `initscripts`, it corresponds to call `ifdown {{name}}`.
You can deactivate a connection profile, even if is currently not active. As a consequence, `state: down` always changes the system.
Note that if the `state` option is unset, the connection profiles runtime state will not be changed.
### `persistent_state`
The `persistent_state` option identifies if a connection profile is persistent (saved on disk). The `persistent_state` option can be set to the following values:
#### `persistent_state: present` (default)
Note that if `persistent_state` is `present` and the connection profile contains
the `type` option, the profile will be created or updated. If the connection profile is
incomplete (no `type` option), the behavior is undefined. Also, the `present` value
does not directly result in a change in the network configuration. If the `state` option
is not set to `up`, the profile is only created or modified, not activated.
For NetworkManager, the new connection profile is created with the `autoconnect`
option enabled by default. Therefore, NetworkManager can activate the new
profile on a currently disconnected device. ([rh#1401515](https://bugzilla.redhat.com/show_bug.cgi?id=1401515)).
#### `persistent_state: absent`
The `absent` value ensures that the profile is not present on the
target host. If a profile with the given `name` exists, it will be deleted. In this case:
- `NetworkManager` deletes all connection profiles with the corresponding `connection.id`.
Deleting a profile usually does not change the current networking configuration, unless
the profile was currently activated on a device. Deleting the currently
active connection profile disconnects the device. That makes the device eligible
to autoconnect another connection (for more details, see [rh#1401515](https://bugzilla.redhat.com/show_bug.cgi?id=1401515)).
- `initscripts` deletes the ifcfg file in most cases with no impact on the runtime state of the system unless some component is watching the sysconfig directory.
**Note**: For profiles that only contain a `state` option, the `network` role only activates
or deactivates the connection without changing its configuration.
### `type`
The `type` option can be set to the following values:
- `ethernet`
- `bridge`
- `bond`
- `team`
- `vlan`
- `macvlan`
- `infiniband`
#### `type: ethernet`
If the type is `ethernet`, then there can be an extra `ethernet` dictionary with the following
items (options): `autoneg`, `speed` and `duplex`, which correspond to the
settings of the `ethtool` utility with the same name.
* `autoneg`: `yes` (default) or `no` [if auto-negotiation is enabled or disabled]
* `speed`: speed in Mbit/s
* `duplex`: `half` or `full`
Note that the `speed` and `duplex` link settings are required when autonegotiation is disabled (autoneg:no).
#### `type: bridge`, `type: bond`, `type: team`
The `bridge`, `bond`, `team` device types work similar. Note that `team` is not supported in RHEL6 kernels.
For slaves, the `slave_type` and `master` properties must be set. Note that slaves should not have `ip` settings.
The `master` refers to the `name` of a profile in the Ansible
playbook. It is neither an interface-name nor a connection-id of
NetworkManager.
- For NetworkManager, `master` will be converted to the `connection.uuid`
of the corresponding profile.
- For initscripts, the master is looked up as the `DEVICE` from the corresponding
ifcfg file.
As `master` refers to other profiles of the same or another play,
the order of the `connections` list matters. Also, `--check` ignores
the value of the `master` and assumes it will be present during a real
run. That means, in presence of an invalid `master`, `--check` may
signal success but the actual play run fails.
#### `type: vlan`
Similar to `master`, the `parent` references the connection profile in the ansible
role.
#### `type: macvlan`
Similar to `master` and `vlan`, the `parent` references the connection profile in the ansible
role.
### `autoconnect`
By default, profiles are created with autoconnect enabled.
- For `NetworkManager`, this corresponds to the `connection.autoconnect` property.
- For `initscripts`, this corresponds to the `ONBOOT` property.
### `mac`
The `mac` address is optional and restricts the profile to be usable only on
devices with the given MAC address. `mac` is only allowed for `type`
`ethernet` or `infiniband` to match a non-virtual device with the
profile.
- For `NetworkManager`, `mac` is the permanent MAC address, `ethernet.mac-address`.
- For `initscripts`, `mac` is the currently configured MAC address of the device (`HWADDR`).
### `interface_name`
For the `ethernet` and `infiniband` types, the `interface_name` option restricts the profile to
the given interface by name. This argument is optional and by default the
profile name is used unless a mac address is specified using the `mac` key.
Specifying an empty string (`""`) means that the profile is not
restricted to a network interface.
**Note:** With [persistent interface naming](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Networking_Guide/ch-Consistent_Network_Device_Naming.html),
the interface is predictable based on the hardware configuration.
Otherwise, the `mac` address might be an option.
For virtual interface types such as bridges, the `interface_name` is the name of the created
interface. In case of a missing `interface_name`, the `name` of the profile name is used.
**Note:** The `name` (the profile name) and the `interface_name` (the device name) may be
different or the profile may not be tied to an interface at all.
### `zone`
The `zone` option sets the firewalld zone for the interface.
Slaves to the bridge, bond or team devices cannot specify a zone.
### `ip`
The IP configuration supports the following options:
* `address`
Manual addressing can be specified via a list of addresses under the `address` option.
* `dhcp4` and `auto6`
Also, manual addressing can be specified by setting either `dhcp4` or `auto6`.
The `dhcp4` key is for DHCPv4 and `auto6` for StateLess Address Auto Configuration
(SLAAC). Note that the `dhcp4` and `auto6` keys can be omitted and the default key
depends on the presence of manual addresses.
* `dhcp4_send_hostname`
If `dhcp4` is enabled, it can be configured whether the DHCPv4 request includes
the hostname via the `dhcp4_send_hostname` option. Note that `dhcp4_send_hostname`
is only supported by the `nm` provider and corresponds to
[`ipv4.dhcp-send-hostname`](https://developer.gnome.org/NetworkManager/stable/nm-settings.html#nm-settings.property.ipv4.dhcp-send-hostname)
property.
* `dns` and `dns_search`
Manual DNS configuration can be specified via a list of addresses
given in the `dns` option and a list of domains to search given in the
`dns_search` option.
* `route_metric4` and `route_metric6`
- For `NetworkManager`, `route_metric4` and `route_metric6` corresponds to the
[`ipv4.route-metric`](https://developer.gnome.org/NetworkManager/stable/nm-settings.html#nm-settings.property.ipv4.route-metric) and
[`ipv6.route-metric`](https://developer.gnome.org/NetworkManager/stable/nm-settings.html#nm-settings.property.ipv6.route-metric)
properties, respectively. If specified, it determines the route metric for DHCP
assigned routes and the default route, and thus the priority for multiple interfaces.
* `route`
Static route configuration can be specified via a list of routes given in the `route`
option. The default value is an empty list. Each route is a dictionary with the following
entries: `network`, `prefix`, `gateway` and `metric`. `network` and `prefix` specify
the destination network.
Note that Classless inter-domain routing (CIDR) notation or network mask notation are not supported yet.
* `route_append_only`
The `route_append_only` option allows only to add new routes to the
existing routes on the system.
If the `route_append_only` boolean option is set to `yes`, the specified routes are appended to the existing routes.
If `route_append_only` is set to `no` (default), the current routes are replaced.
Note that setting `route_append_only` to `yes` without setting `route` has the effect of preserving the current static routes.
* `rule_append_only`
The `rule_append_only` boolean option allows to preserve the current routing rules.
Note that specifying routing rules is not supported yet.
**Note:** When `route_append_only` or `rule_append_only` is not specified, the `network` role deletes the current routes or routing rules.
**Note:** Slaves to the bridge, bond or team devices cannot specify `ip` settings.
### `ethtool`
The ethtool settings allow to enable or disable varios features. The names
correspond to the names used by the `ethtool` utility. Depending on the actual
kernel and device, changing some features might not be supported.
```yaml
ethtool:
features:
esp-hw-offload: yes|no # optional
esp-tx-csum-hw-offload: yes|no # optional
fcoe-mtu: yes|no # optional
gro: yes|no # optional
gso: yes|no # optional
highdma: yes|no # optional
hw-tc-offload: yes|no # optional
l2-fwd-offload: yes|no # optional
loopback: yes|no # optional
lro: yes|no # optional
ntuple: yes|no # optional
rx: yes|no # optional
rx-all: yes|no # optional
rx-fcs: yes|no # optional
rx-gro-hw: yes|no # optional
rx-udp_tunnel-port-offload: yes|no # optional
rx-vlan-filter: yes|no # optional
rx-vlan-stag-filter: yes|no # optional
rx-vlan-stag-hw-parse: yes|no # optional
rxhash: yes|no # optional
rxvlan: yes|no # optional
sg: yes|no # optional
tls-hw-record: yes|no # optional
tls-hw-tx-offload: yes|no # optional
tso: yes|no # optional
tx: yes|no # optional
tx-checksum-fcoe-crc: yes|no # optional
tx-checksum-ip-generic: yes|no # optional
tx-checksum-ipv4: yes|no # optional
tx-checksum-ipv6: yes|no # optional
tx-checksum-sctp: yes|no # optional
tx-esp-segmentation: yes|no # optional
tx-fcoe-segmentation: yes|no # optional
tx-gre-csum-segmentation: yes|no # optional
tx-gre-segmentation: yes|no # optional
tx-gso-partial: yes|no # optional
tx-gso-robust: yes|no # optional
tx-ipxip4-segmentation: yes|no # optional
tx-ipxip6-segmentation: yes|no # optional
tx-nocache-copy: yes|no # optional
tx-scatter-gather: yes|no # optional
tx-scatter-gather-fraglist: yes|no # optional
tx-sctp-segmentation: yes|no # optional
tx-tcp-ecn-segmentation: yes|no # optional
tx-tcp-mangleid-segmentation: yes|no # optional
tx-tcp-segmentation: yes|no # optional
tx-tcp6-segmentation: yes|no # optional
tx-udp-segmentation: yes|no # optional
tx-udp_tnl-csum-segmentation: yes|no # optional
tx-udp_tnl-segmentation: yes|no # optional
tx-vlan-stag-hw-insert: yes|no # optional
txvlan: yes|no # optional
```
Examples of Options
-------------------
Setting the same connection profile multiple times:
```yaml
network_connections:
- name: Wired0
type: ethernet
interface_name: eth0
ip:
dhcp4: yes
- name: Wired0
state: up
```
Activating a preexisting connection profile:
```yaml
network_connections:
- name: eth0
state: up
```
Deactivating a preexisting connection profile:
```yaml
network_connections:
- name: eth0
state: down
```
Creating a persistent connection profile:
```yaml
network_connections:
- name: eth0
#persistent_state: present # default
type: ethernet
autoconnect: yes
mac: 00:00:5e:00:53:5d
ip:
dhcp4: yes
```
Deleting a connection profile named `eth0` (if it exists):
```yaml
network_connections:
- name: eth0
persistent_state: absent
```
Configuring the Ethernet link settings:
```yaml
network_connections:
- name: eth0
type: ethernet
ethernet:
autoneg: no
speed: 1000
duplex: full
```
Creating a bridge connection:
```yaml
network_connections:
- name: br0
type: bridge
#interface_name: br0 # defaults to the connection name
```
Configuring a bridge connection:
```yaml
network_connections:
- name: internal-br0
interface_name: br0
type: bridge
ip:
dhcp4: no
auto6: no
```
Setting `master` and `slave_type`:
```yaml
network_connections:
- name: br0-bond0
type: bond
interface_name: bond0
master: internal-br0
slave_type: bridge
- name: br0-bond0-eth1
type: ethernet
interface_name: eth1
master: br0-bond0
slave_type: bond
```
Configuring VLANs:
```yaml
network_connections:
- name: eth1-profile
autoconnet: no
type: ethernet
interface_name: eth1
ip:
dhcp4: no
auto6: no
- name: eth1.6
autoconnect: no
type: vlan
parent: eth1-profile
vlan:
id: 6
ip:
address:
- 192.0.2.5/24
auto6: no
```
Configuring MACVLAN:
```yaml
network_connections:
- name: eth0-profile
type: ethernet
interface_name: eth0
ip:
address:
- 192.168.0.1/24
- name: veth0
type: macvlan
parent: eth0-profile
macvlan:
mode: bridge
promiscuous: yes
tap: no
ip:
address:
- 192.168.1.1/24
```
Setting the IP configuration:
```yaml
network_connections:
- name: eth0
type: ethernet
ip:
route_metric4: 100
dhcp4: no
#dhcp4_send_hostname: no
gateway4: 192.0.2.1
dns:
- 192.0.2.2
- 198.51.100.5
dns_search:
- example.com
- subdomain.example.com
route_metric6: -1
auto6: no
gateway6: 2001:db8::1
address:
- 192.0.2.3/24
- 198.51.100.3/26
- 2001:db8::80/7
route:
- network: 198.51.100.128
prefix: 26
gateway: 198.51.100.1
metric: 2
- network: 198.51.100.64
prefix: 26
gateway: 198.51.100.6
metric: 4
route_append_only: no
rule_append_only: yes
```
### Invalid and Wrong Configuration
The `network` role rejects invalid configurations. It is recommended to test the role
with `--check` first. There is no protection against wrong (but valid) configuration.
Double-check your configuration before applying it.
Compatibility
-------------
The `network` role supports the same configuration scheme for both providers (`nm`
and `initscripts`). That means, you can use the same playbook with NetworkManager
and initscripts. However, note that not every option is handled exactly the same
by every provider. Do a test run first with `--check`.
It is not supported to create a configuration for one provider, and expect another
provider to handle them. For example, creating profiles with the `initscripts` provider,
and later enabling NetworkManager is not guaranteed to work automatically. Possibly,
you have to adjust the configuration so that it can be used by another provider.
For example, configuring a RHEL6 host with initscripts and upgrading to
RHEL7 while continuing to use initscripts in RHEL7 is an acceptable scenario. What
is not guaranteed is to upgrade to RHEL7, disable initscripts and expect NetworkManager
to take over the configuration automatically.
Depending on NetworkManager's configuration, connections may be stored as ifcfg files
as well, but it is not guaranteed that plain initscripts can handle these ifcfg files
after disabling the NetworkManager service.
Limitations
-----------
As Ansible usually works via the network, for example via SSH, there are some limitations to be considered:
The `network` role does not support bootstraping networking configuration. One
option may be [ansible-pull](https://docs.ansible.com/ansible/playbooks_intro.html#ansible-pull).
Another option maybe be to initially auto-configure the host during installation
(ISO based, kickstart, etc.), so that the host is connected to a management LAN
or VLAN. It strongly depends on your environment.
For `initscripts` provider, deploying a profile merely means to create the ifcfg
files. Nothing happens automatically until the play issues `ifup` or `ifdown`
via the `up` or `down` [states](#state) -- unless there are other
components that rely on the ifcfg files and react on changes.
The `initscripts` provider requires the different profiles to be in the right
order when they depend on each other. For example the bonding master device
needs to be specified before the slave devices.
When removing a profile for NetworkManager it also takes the connection
down and possibly removes virtual interfaces. With the `initscripts` provider
removing a profile does not change its current runtime state (this is a future
feature for NetworkManager as well).
For NetworkManager, modifying a connection with autoconnect enabled
may result in the activation of a new profile on a previously disconnected
interface. Also, deleting a NetworkManager connection that is currently active
results in removing the interface. Therefore, the order of the steps should be
followed, and carefully handling of [autoconnect](#autoconnect) property may be
necessary. This should be improved in NetworkManager RFE [rh#1401515](https://bugzilla.redhat.com/show_bug.cgi?id=1401515).
It seems difficult to change networking of the target host in a way that breaks
the current SSH connection of ansible. If you want to do that, ansible-pull might
be a solution. Alternatively, a combination of `async`/`poll` with changing
the `ansible_host` midway of the play.
**TODO** The current role does not yet support to easily split the
play in a pre-configure step, and a second step to activate the new configuration.
In general, to successfully run the play, determine which configuration is
active in the first place, and then carefully configure a sequence of steps to change to
the new configuration. The actual solution depends strongly on your environment.
### Handling potential problems
When something goes wrong while configuring networking remotely, you might need
to get physical access to the machine to recover.
**TODO** NetworkManager supports a
[checkpoint/rollback](https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html#gdbus-method-org-freedesktop-NetworkManager.CheckpointCreate)
feature. At the beginning of the play we could create a checkpoint and if we lose
connectivity due to an error, NetworkManager would automatically rollback after
timeout. The limitations is that this would only work with NetworkManager, and
it is not clear that rollback will result in a working configuration.

View File

@@ -0,0 +1,82 @@
# SPDX-License-Identifier: BSD-3-Clause
---
network_connections: []
# Use initscripts for RHEL/CentOS < 7, nm otherwise
network_provider_os_default: "{{
'initscripts' if ansible_distribution in ['RedHat', 'CentOS'] and
ansible_distribution_major_version is version('7', '<')
else 'nm' }}"
# If NetworkManager.service is running, assume that 'nm' is currently in-use,
# otherwise initscripts
network_provider_current: "{{
'nm' if 'NetworkManager.service' in ansible_facts.services and
ansible_facts.services['NetworkManager.service']['state'] == 'running'
else 'initscripts'
}}"
# Default to the auto-detected value
network_provider: "{{ network_provider_current }}"
# The python-gobject-base package depends on the python version and
# distribution:
# - python-gobject-base on RHEL7 (no python2-gobject-base :-/)
# - python-gobject-base or python2-gobject-base on Fedora 27
# - python3-gobject-base on Fedora 28+
network_service_name_default_nm: NetworkManager
network_packages_default_nm:
- ethtool
- NetworkManager
- "python{{ ansible_python['version']['major'] | replace('2', '') }}-gobject-base"
network_service_name_default_initscripts: network
# initscripts requires bridge-utils to manage bridges, install it when the
# 'bridge' type is used in network_connections
_network_packages_default_initscripts_bridge: ["{% if ['bridge'] in network_connections|json_query('[*][type]') and
(
(ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('7', '<=')) or
(ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('28', '<='))
)
%}bridge-utils{% endif %}"]
_network_packages_default_initscripts_network_scripts: ["{%
if (ansible_distribution in ['RedHat', 'CentOS'] and ansible_distribution_major_version is version('7', '<=')) or
(ansible_distribution == 'Fedora' and ansible_distribution_major_version is version('28', '<='))
%}initscripts{% else %}network-scripts{% endif %}"]
# convert _network_packages_default_initscripts_bridge to an empty list if it
# contains only the empty string and add it to the default package list
# |select() filters the list to include only values that evaluate to true
# (the empty string is false)
# |list() converts the generator that |select() creates to a list
network_packages_default_initscripts: "{{ ['ethtool']
+ _network_packages_default_initscripts_bridge|select()|list()
+ _network_packages_default_initscripts_network_scripts|select()|list()
}}"
# The user can explicitly set host variables "network_provider",
# "network_service_name" and "network_packages".
#
# Usually, the user only wants to select the "network_provider"
# (or not set it at all and let it be autodetected via the
# internal variable "{{ network_provider_current }}". Hence,
# depending on the "network_provider", a different set of
# service-name and packages is chosen.
#
# That is done via the internal "_network_provider_setup" dictionary.
# If the user doesn't explicitly set "network_service_name" or
# "network_packages" (which he usually wouldn't), then the defaults
# from "network_service_name_default_*" and "network_packages_default_*"
# apply. These values are hard-coded in this file, but they also could
# be overwritten as host variables or via vars/*.yml.
_network_provider_setup:
nm:
service_name: "{{ network_service_name_default_nm }}"
packages: "{{ network_packages_default_nm }}"
initscripts:
service_name: "{{ network_service_name_default_initscripts }}"
packages: "{{ network_packages_default_initscripts }}"
network_packages: "{{
_network_provider_setup[network_provider]['packages'] }}"
network_service_name: "{{
_network_provider_setup[network_provider]['service_name'] }}"

View File

@@ -0,0 +1,38 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
# Create a bond profile, which is the parent of VLAN.
- name: prod2
state: up
type: bond
interface_name: bond2
ip:
dhcp4: no
auto6: no
bond:
mode: active-backup
miimon: 110
# enslave an ethernet to the bond
- name: prod2-slave1
state: up
type: ethernet
interface_name: "{{ network_interface_name2 }}"
master: prod2
# on top of it, create a VLAN with ID 100 and static
# addressing
- name: prod2.100
state: up
type: vlan
parent: prod2
vlan_id: 100
ip:
address:
- "192.0.2.{{ network_iphost }}/24"
roles:
- linux-system-roles.network

View File

@@ -0,0 +1,36 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
# Create a bridge profile, which is the parent of VLAN.
- name: prod2
state: up
type: bridge
interface_name: bridge2
ip:
dhcp4: no
auto6: no
# enslave an ethernet to the bridge
- name: prod2-slave1
state: up
type: ethernet
interface_name: "{{ network_interface_name2 }}"
master: prod2
slave_type: bridge
# on top of it, create a VLAN with ID 100 and static
# addressing
- name: prod2.100
state: up
type: vlan
parent: prod2
vlan_id: 100
ip:
address:
- "192.0.2.{{ network_iphost }}/24"
roles:
- linux-system-roles.network

View File

@@ -0,0 +1 @@
../tests/down-profile.yml

View File

@@ -0,0 +1,18 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
# Create one ethernet profile and activate it.
# The profile uses automatic IP addressing
# and is tied to the interface by MAC address.
- name: prod1
state: up
type: ethernet
autoconnect: yes
mac: "{{ network_mac1 }}"
mtu: 1450
roles:
- linux-system-roles.network

View File

@@ -0,0 +1,29 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
# Create a profile for the underlying device of the VLAN.
- name: prod2
type: ethernet
autoconnect: no
state: up
interface_name: "{{ network_interface_name2 }}"
ip:
dhcp4: no
auto6: no
# on top of it, create a VLAN with ID 100 and static
# addressing
- name: prod2.100
state: up
type: vlan
parent: prod2
vlan_id: 100
ip:
address:
- "192.0.2.{{ network_iphost }}/24"
roles:
- linux-system-roles.network

View File

@@ -0,0 +1,14 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
tasks:
- include_role:
name: linux-system-roles.network
vars:
network_connections:
- name: "{{ network_interface_name1 }}"
state: up
type: ethernet
ip:
dhcp4: "no"
auto6: "no"

View File

@@ -0,0 +1,19 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
tasks:
- include_role:
name: linux-system-roles.network
vars:
network_connections:
- name: "{{ network_interface_name1 }}"
state: up
type: ethernet
ip:
dhcp4: "no"
auto6: "no"
ethtool:
features:
gro: "no"
gso: "yes"
tx-sctp-segmentation: "no"

View File

@@ -0,0 +1,26 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
- name: ib0
type: infiniband
interface_name: ib0
# Create a simple infiniband profile
- name: ib0-10
interface_name: ib0.000a
type: infiniband
autoconnect: yes
infiniband_p_key: 10
parent: ib0
state: up
ip:
dhcp4: no
auto6: no
address:
- 198.51.100.133/30
roles:
- linux-system-roles.network

View File

@@ -0,0 +1,5 @@
# SPDX-License-Identifier: BSD-3-Clause
# an inventory for the examples.
[network-test]
v-rhel6 ansible_user=root network_iphost=196 network_mac1=00:00:5e:00:53:00 network_interface_name1=eth0 network_interface_name2=eth1
v-rhel7 ansible_user=root network_iphost=97 network_mac1=00:00:5e:00:53:01 network_interface_name1=eth0 network_interface_name2=eth1

View File

@@ -0,0 +1,29 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: network-test
vars:
network_connections:
- name: eth0
type: ethernet
state: up
interface_name: eth0
ip:
address:
- 192.168.0.1/24
# Create a virtual ethernet card bound to eth0
- name: veth0
type: macvlan
state: up
parent: eth0
macvlan:
mode: bridge
promiscuous: True
tap: False
ip:
address:
- 192.168.1.1/24
roles:
- linux-system-roles.network

View File

@@ -0,0 +1 @@
../tests/remove-profile.yml

View File

@@ -0,0 +1 @@
../tests/roles/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
install_date: Wed Jul 1 18:41:54 2020
version: 1.1.0

View File

@@ -0,0 +1,27 @@
# SPDX-License-Identifier: BSD-3-Clause
---
galaxy_info:
author: Thomas Haller <thaller@redhat.com>, Till Maas <till@redhat.com>
description: Configure networking
company: Red Hat, Inc.
license: BSD-3-Clause
min_ansible_version: 2.5
galaxy_tags:
- centos
- fedora
- network
- networking
- redhat
- rhel
- system
platforms:
- name: Fedora
versions:
- 28
- 29
- 30
- name: EL
versions:
- 6
- 7
- 8

View File

@@ -0,0 +1,7 @@
#!/usr/bin/python3 -tt
# vim: fileencoding=utf8
# SPDX-License-Identifier: BSD-3-Clause
class MyError(Exception):
pass

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
# SPDX-License-Identifier: BSD-3-Clause
""" Support for NetworkManager aka the NM provider """
# pylint: disable=import-error, no-name-in-module
from ansible.module_utils.network_lsr.utils import Util
ETHTOOL_FEATURE_PREFIX = "ETHTOOL_OPTNAME_FEATURE_"
def get_nm_ethtool_feature(name):
"""
Translate ethtool feature into Network Manager name
:param name: Name of the feature
:type name: str
:returns: Name of the feature to be used by `NM.SettingEthtool.set_feature()`
:rtype: str
"""
name = ETHTOOL_FEATURE_PREFIX + name.upper().replace("-", "_")
feature = getattr(Util.NM(), name, None)
return feature

View File

@@ -0,0 +1,338 @@
#!/usr/bin/python3 -tt
# SPDX-License-Identifier: BSD-3-Clause
# vim: fileencoding=utf8
import os
import socket
import sys
import uuid
# pylint: disable=import-error, no-name-in-module
from ansible.module_utils.network_lsr import MyError
class Util:
PY3 = sys.version_info[0] == 3
STRING_TYPE = str if PY3 else basestring # noqa:F821
@staticmethod
def first(iterable, default=None, pred=None):
for v in iterable:
if pred is None or pred(v):
return v
return default
@staticmethod
def check_output(argv):
# subprocess.check_output is python 2.7.
with open("/dev/null", "wb") as DEVNULL:
import subprocess
env = os.environ.copy()
env["LANG"] = "C"
p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=DEVNULL, env=env)
# FIXME: Can we assume this to always be UTF-8?
out = p.communicate()[0].decode("UTF-8")
if p.returncode != 0:
raise MyError("failure calling %s: exit with %s" % (argv, p.returncode))
return out
@classmethod
def create_uuid(cls):
return str(uuid.uuid4())
@classmethod
def NM(cls):
n = getattr(cls, "_NM", None)
if n is None:
# Installing pygobject in a tox virtualenv does not work out of the
# box
# pylint: disable=import-error
import gi
gi.require_version("NM", "1.0")
from gi.repository import NM, GLib, Gio, GObject
cls._NM = NM
cls._GLib = GLib
cls._Gio = Gio
cls._GObject = GObject
n = NM
return n
@classmethod
def GLib(cls):
cls.NM()
return cls._GLib
@classmethod
def Gio(cls):
cls.NM()
return cls._Gio
@classmethod
def GObject(cls):
cls.NM()
return cls._GObject
@classmethod
def Timestamp(cls):
return cls.GLib().get_monotonic_time()
@classmethod
def GMainLoop(cls):
gmainloop = getattr(cls, "_GMainLoop", None)
if gmainloop is None:
gmainloop = cls.GLib().MainLoop()
cls._GMainLoop = gmainloop
return gmainloop
@classmethod
def GMainLoop_run(cls, timeout=None):
if timeout is None:
cls.GMainLoop().run()
return True
GLib = cls.GLib()
timeout_reached = []
loop = cls.GMainLoop()
def _timeout_cb(unused):
timeout_reached.append(1)
loop.quit()
return False
timeout_id = GLib.timeout_add(int(timeout * 1000), _timeout_cb, None)
loop.run()
if not timeout_reached:
GLib.source_remove(timeout_id)
return not timeout_reached
@classmethod
def GMainLoop_iterate(cls, may_block=False):
return cls.GMainLoop().get_context().iteration(may_block)
@classmethod
def GMainLoop_iterate_all(cls):
c = 0
while cls.GMainLoop_iterate():
c += 1
return c
@classmethod
def call_async_method(cls, object_, action, args, mainloop_timeout=10):
""" Asynchronously call a NetworkManager method """
cancellable = cls.create_cancellable()
async_action = action + "_async"
# NM does not use a uniform naming for the async methods,
# for checkpoints it is:
# NMClient.checkpoint_create() and NMClient.checkpoint_create_finish(),
# but for reapply it is:
# NMDevice.reapply_async() and NMDevice.reapply_finish()
# NMDevice.reapply() is a synchronous version
# Therefore check if there is a method if an `async` suffix and use the
# one without the suffix otherwise
if not hasattr(object_, async_action):
async_action = action
finish = action + "_finish"
user_data = {}
fullargs = []
fullargs += args
fullargs += (cancellable, cls.create_callback(finish), user_data)
getattr(object_, async_action)(*fullargs)
if not cls.GMainLoop_run(mainloop_timeout):
cancellable.cancel()
raise MyError("failure to call %s.%s(): timeout" % object_, async_action)
success = user_data.get("success", None)
if success is not None:
return success
raise MyError(
"failure to %s checkpoint: %s: %r"
% (action, user_data.get("error", "unknown error"), user_data)
)
@classmethod
def create_cancellable(cls):
return cls.Gio().Cancellable.new()
@classmethod
def create_callback(cls, finish_method):
"""
Create a callback that will return the result of the finish method and
quit the GMainLoop
:param finish_method str: Name of the finish method to call from the
source object in the callback
"""
def callback(source_object, res, user_data):
success = None
try:
success = getattr(source_object, finish_method)(res)
except Exception as e:
if cls.error_is_cancelled(e):
return
user_data["error"] = str(e)
user_data["success"] = success
cls.GMainLoop().quit()
return callback
@classmethod
def error_is_cancelled(cls, e):
GLib = cls.GLib()
if isinstance(e, GLib.GError):
if (
e.domain == "g-io-error-quark"
and e.code == cls.Gio().IOErrorEnum.CANCELLED
):
return True
return False
@staticmethod
def ifname_valid(ifname):
# see dev_valid_name() in kernel's net/core/dev.c
if not ifname:
return False
if ifname in [".", ".."]:
return False
if len(ifname) >= 16:
return False
if any([c == "/" or c == ":" or c.isspace() for c in ifname]):
return False
# FIXME: encoding issues regarding python unicode string
return True
@staticmethod
def mac_aton(mac_str, force_len=None):
# we also accept None and '' for convenience.
# - None yiels None
# - '' yields []
if mac_str is None:
return mac_str
i = 0
b = []
for c in mac_str:
if i == 2:
if c != ":":
raise MyError("not a valid MAC address: '%s'" % (mac_str))
i = 0
continue
try:
if i == 0:
n = int(c, 16) * 16
i = 1
else:
assert i == 1
n = n + int(c, 16)
i = 2
b.append(n)
except Exception:
raise MyError("not a valid MAC address: '%s'" % (mac_str))
if i == 1:
raise MyError("not a valid MAC address: '%s'" % (mac_str))
if force_len is not None:
if force_len != len(b):
raise MyError(
"not a valid MAC address of length %s: '%s'" % (force_len, mac_str)
)
return b
@staticmethod
def mac_ntoa(mac):
if mac is None:
return None
return ":".join(["%02x" % c for c in mac])
@staticmethod
def mac_norm(mac_str, force_len=None):
return Util.mac_ntoa(Util.mac_aton(mac_str, force_len))
@staticmethod
def boolean(arg):
if arg is None or isinstance(arg, bool):
return arg
arg0 = arg
if isinstance(arg, Util.STRING_TYPE):
arg = arg.lower()
if arg in ["y", "yes", "on", "1", "true", 1, True]:
return True
if arg in ["n", "no", "off", "0", "false", 0, False]:
return False
raise MyError("value '%s' is not a boolean" % (arg0))
@staticmethod
def parse_ip(addr, family=None):
if addr is None:
return (None, None)
if family is not None:
Util.addr_family_check(family)
a = socket.inet_pton(family, addr)
else:
a = None
family = None
try:
a = socket.inet_pton(socket.AF_INET, addr)
family = socket.AF_INET
except Exception:
a = socket.inet_pton(socket.AF_INET6, addr)
family = socket.AF_INET6
return (socket.inet_ntop(family, a), family)
@staticmethod
def addr_family_check(family):
if family != socket.AF_INET and family != socket.AF_INET6:
raise MyError("invalid address family %s" % (family))
@staticmethod
def addr_family_to_v(family):
if family is None:
return ""
if family == socket.AF_INET:
return "v4"
if family == socket.AF_INET6:
return "v6"
raise MyError("invalid address family '%s'" % (family))
@staticmethod
def addr_family_default_prefix(family):
Util.addr_family_check(family)
if family == socket.AF_INET:
return 24
else:
return 64
@staticmethod
def addr_family_valid_prefix(family, prefix):
Util.addr_family_check(family)
if family == socket.AF_INET:
m = 32
else:
m = 128
return prefix >= 0 and prefix <= m
@staticmethod
def parse_address(address, family=None):
try:
parts = address.split()
addr_parts = parts[0].split("/")
if len(addr_parts) != 2:
raise MyError("expect two addr-parts: ADDR/PLEN")
a, family = Util.parse_ip(addr_parts[0], family)
prefix = int(addr_parts[1])
if not Util.addr_family_valid_prefix(family, prefix):
raise MyError("invalid prefix %s" % (prefix))
if len(parts) > 1:
raise MyError("too many parts")
return {"address": a, "family": family, "prefix": prefix}
except Exception:
raise MyError("invalid address '%s'" % (address))

View File

@@ -0,0 +1,14 @@
# Molecule managed
{% if item.registry is defined %}
FROM {{ item.registry.url }}/{{ item.image }}
{% else %}
FROM {{ item.image }}
{% endif %}
RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates && apt-get clean; \
elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python2-dnf bash && dnf clean all; \
elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml && zypper clean -a; \
elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \
elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates && xbps-remove -O; fi

View File

@@ -0,0 +1,38 @@
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
options:
config-file: molecule/default/yamllint.yml
platforms:
- name: centos-6
image: linuxsystemroles/centos-6
privileged: true
- name: centos-7
image: linuxsystemroles/centos-7
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
privileged: true
provisioner:
name: ansible
log: true
lint:
name: ansible-lint
playbooks:
converge: ../../tests/tests_default.yml
scenario:
name: default
test_sequence:
- destroy
- create
- converge
- idempotence
- check
- destroy
verifier:
name: testinfra
lint:
name: flake8

View File

@@ -0,0 +1,12 @@
---
extends: default
rules:
braces:
max-spaces-inside: 1
level: error
brackets:
max-spaces-inside: 1
level: error
line-length: disable
truthy: disable
document-start: disable

View File

@@ -0,0 +1,545 @@
# This file was generated using `pylint --generate-rcfile > pylintrc` command.
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()) + '/library'); sys.path.append(os.path.dirname(find_pylintrc()) + '/module_utils'); sys.path.append(os.path.dirname(find_pylintrc()) + '/tests')"
# Use multiple processes to speed up Pylint.
jobs=1
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Specify a configuration file.
#rcfile=
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=
#disable=print-statement,
# parameter-unpacking,
# unpacking-in-except,
# old-raise-syntax,
# backtick,
# long-suffix,
# old-ne-operator,
# old-octal-literal,
# import-star-module-level,
# non-ascii-bytes-literal,
# raw-checker-failed,
# bad-inline-option,
# locally-disabled,
# locally-enabled,
# file-ignored,
# suppressed-message,
# useless-suppression,
# deprecated-pragma,
# apply-builtin,
# basestring-builtin,
# buffer-builtin,
# cmp-builtin,
# coerce-builtin,
# execfile-builtin,
# file-builtin,
# long-builtin,
# raw_input-builtin,
# reduce-builtin,
# standarderror-builtin,
# unicode-builtin,
# xrange-builtin,
# coerce-method,
# delslice-method,
# getslice-method,
# setslice-method,
# no-absolute-import,
# old-division,
# dict-iter-method,
# dict-view-method,
# next-method-called,
# metaclass-assignment,
# indexing-exception,
# raising-string,
# reload-builtin,
# oct-method,
# hex-method,
# nonzero-method,
# cmp-method,
# input-builtin,
# round-builtin,
# intern-builtin,
# unichr-builtin,
# map-builtin-not-iterating,
# zip-builtin-not-iterating,
# range-builtin-not-iterating,
# filter-builtin-not-iterating,
# using-cmp-argument,
# eq-without-hash,
# div-method,
# idiv-method,
# rdiv-method,
# exception-message-attribute,
# invalid-str-codec,
# sys-max-int,
# bad-python3-import,
# deprecated-string-function,
# deprecated-str-translate-call,
# deprecated-itertools-function,
# deprecated-types-field,
# next-method-defined,
# dict-items-not-iterating,
# dict-keys-not-iterating,
# dict-values-not-iterating
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[REPORTS]
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio).You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages
reports=no
# Activate the evaluation score.
score=yes
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=optparse.Values,sys.exit
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module
max-module-lines=1000
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,
dict-separator
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
# Minimum lines number of a similarity.
min-similarity-lines=4
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins
[BASIC]
# Naming style matching correct argument names
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style
#argument-rgx=
# Naming style matching correct attribute names
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Naming style matching correct class attribute names
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style
#class-attribute-rgx=
# Naming style matching correct class names
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-style
#class-rgx=
# Naming style matching correct constant names
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style
#function-rgx=
# Good variable names which should always be accepted, separated by a comma
good-names=i,
j,
k,
ex,
Run,
_
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# Naming style matching correct inline iteration names
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style
#inlinevar-rgx=
# Naming style matching correct method names
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style
#method-rgx=
# Naming style matching correct module names
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty
# Naming style matching correct variable names
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style
#variable-rgx=
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
[IMPORTS]
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,
TERMIOS,
Bastion,
rexec
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of statements in function / method body
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

View File

@@ -0,0 +1,58 @@
# SPDX-License-Identifier: BSD-3-Clause
# get service facts, used in defaults/main.yml
---
- name: Check which services are running
service_facts:
no_log: true
# needed for ansible_facts.packages
- name: Check which packages are installed
package_facts:
no_log: true
- name: Print network provider
debug:
msg: "Using network provider: {{ network_provider }}"
# 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
- name: Install packages
package:
name: "{{ network_packages }}"
state: present
when:
- not network_packages is subset(ansible_facts.packages.keys())
- name: Enable and start NetworkManager
service:
name: "{{ network_service_name }}"
state: started
enabled: true
when:
- network_provider == "nm"
- name: Enable network service
service:
name: "{{ network_service_name }}"
enabled: true
when:
- network_provider == "initscripts"
- name: Ensure initscripts network file dependency is present
copy:
dest: /etc/sysconfig/network
content: "# Created by network system role"
force: false
when:
- network_provider == "initscripts"
- name: Configure networking connection profiles
network_connections:
provider: "{{ network_provider | mandatory }}"
ignore_errors: "{{ network_ignore_errors | default(omit) }}"
force_state_change: "{{ network_force_state_change | default(omit) }}"
connections: "{{ network_connections | default([]) }}"
- name: Re-test connectivity
ping:

View File

@@ -0,0 +1,2 @@
/*.retry
/inventory

View File

@@ -0,0 +1 @@
roles/linux-system-roles.network/library/network_connections.py

View File

@@ -0,0 +1,16 @@
#! /bin/bash
# SPDX-License-Identifier: BSD-3-Clause
if [ "$#" -lt 1 ]
then
echo "USAGE: ${0} coverage_data_file..."
echo "Show Statistics for each coverage data file"
exit 1
fi
for coverage_file in "${@}"
do
echo "coverage statistics for: ${coverage_file}:"
COVERAGE_FILE="${coverage_file}" coverage report
echo
done

View File

@@ -0,0 +1,10 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- name: Set {{ profile }} down
hosts: all
vars:
network_connections:
- name: "{{ profile }}"
state: down
roles:
- linux-system-roles.network

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-3-Clause
""" Check that there is a playbook to run all role tests with the non-default
provider as well """
# vim: fileencoding=utf8
import glob
import os
import sys
import yaml
OTHER_PROVIDER_SUFFIX = "_other_provider.yml"
IGNORE = [
"tests_helpers-and-asserts.yml",
"tests_states.yml",
"tests_unit.yml",
"tests_vlan_mtu_initscripts.yml",
"tests_vlan_mtu_nm.yml",
"tests_ethtool_features_initscripts.yml",
"tests_ethtool_features_nm.yml",
]
OTHER_PLAYBOOK = """
# SPDX-License-Identifier: BSD-3-Clause
---
- name: Run playbook '{tests_playbook}' with non-default provider
hosts: all
vars:
network_provider_current:
tasks:
# required for the code to set network_provider_current
- name: Get service facts
service_facts:
- name: Set network provider
set_fact:
network_provider: '{{{{ "initscripts" if network_provider_current == "nm"
else "nm" }}}}'
- import_playbook: "{tests_playbook}"
when:
- ansible_distribution_major_version != '6'
""" # noqa: E501 # ignore that the line is too long
def get_current_provider_code():
with open("../defaults/main.yml") as defaults:
yaml_defaults = yaml.safe_load(defaults)
current_provider = yaml_defaults["network_provider_current"]
return current_provider
def generate_nominal_other_playbook(tests_playbook):
nominal_other_testfile_data = OTHER_PLAYBOOK.format(tests_playbook=tests_playbook)
nominal = yaml.safe_load(nominal_other_testfile_data)
nominal[0]["vars"]["network_provider_current"] = get_current_provider_code()
return yaml.dump(nominal, default_flow_style=False, explicit_start=True, width=80)
def main():
testsfiles = glob.glob("tests_*.yml")
missing = []
returncode = 0
# Generate files when specified
generate = bool(len(sys.argv) > 1 and sys.argv[1] == "generate")
if not testsfiles:
print("ERROR: No tests found")
returncode = 1
for filename in testsfiles:
if filename.endswith(OTHER_PROVIDER_SUFFIX):
continue
if filename in IGNORE:
continue
fileroot = os.path.splitext(filename)[0]
other_testfile = fileroot + OTHER_PROVIDER_SUFFIX
nominal_other_testfile_data = generate_nominal_other_playbook(filename)
if generate:
with open(other_testfile, "w") as ofile:
ofile.write(nominal_other_testfile_data)
if other_testfile not in testsfiles and not generate:
missing.append(filename)
else:
with open(other_testfile) as ifile:
testdata = ifile.read()
if testdata != nominal_other_testfile_data:
print(
"ERROR: Playbook does not match nominal value " + other_testfile
)
returncode = 1
if missing:
print("ERROR: No tests for other provider found for:\n" + ", \n".join(missing))
print("Try to generate them with '{} generate'".format(sys.argv[0]))
returncode = 1
return returncode
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,68 @@
#! /bin/bash
# SPDX-License-Identifier: BSD-3-Clause
if [ -n "${DEBUG}" ]
then
set -x
fi
set -e
if [ "$#" -lt 2 ]
then
echo "USAGE: ${0} host playbook"
echo "Get coverage info from host for playbook"
exit 1
fi
host="${1}"
shift
playbook="${1}"
coverage_data="remote-coveragedata-${host}-${playbook%.yml}"
coverage="/root/.local/bin/coverage"
echo "Getting coverage for ${playbook} on ${host}" >&2
call_ansible() {
local module="${1}"
shift
local args="${1}"
shift
ansible -m "${module}" -i "${host}", -a "${args}" all "${@}"
}
remote_coverage_dir="$(mktemp -d /tmp/remote_coverage-XXXXXX)"
trap "rm -rf '${remote_coverage_dir}'" EXIT
ansible-playbook -i "${host}", get-coverage.yml -e "test_playbook=${playbook} destdir=${remote_coverage_dir}"
#COVERAGE_FILE=remote-coverage coverage combine remote-coverage/tests_*/*/root/.coverage
./merge-coverage.sh coverage "${coverage_data}"-tmp $(find "${remote_coverage_dir}" -type f | tr , _)
# When https://github.com/nedbat/coveragepy/pull/49 is merged, this can be simplified:
if false
then
cat > tmp_merge_coveragerc <<EOF
[paths]
source =
.
/tmp/ansible_*/
EOF
else
cat > tmp_merge_coveragerc <<EOF
[paths]
source =
.
EOF
for file in $(COVERAGE_FILE="${coverage_data}"-tmp coverage report | grep -o "/tmp/ansible_[^/]*" | sort -u)
do
echo " ${file}" >> tmp_merge_coveragerc
done
fi
COVERAGE_FILE="${coverage_data}" coverage combine --rcfile tmp_merge_coveragerc "${coverage_data}"-tmp
rm tmp_merge_coveragerc
COVERAGE_FILE="${coverage_data}" coverage report ||:
COVERAGE_FILE="${coverage_data}" coverage html --directory "htmlcov-${coverage_data}" ||:
echo "Coverage collected in: ${coverage_data}"

View File

@@ -0,0 +1,66 @@
# SPDX-License-Identifier: BSD-3-Clause
---
# This expects the variable test_playbook to be set from the outside
- name: Prepare for coverage extraction
hosts: all
tasks:
# Use set_fact to set variables to make them available in all plays
# 'vars:' Would only set variables for the current play
- name: set facts
set_fact:
coverage_module: network_connections
coverage: /root/.local/bin/coverage
destdir: "remote_coverage/{{ test_playbook }}"
# This uses variables from the other set_fact task, therefore it needs to
# be its own task
- name: set more facts
set_fact:
coverage_file: ansible-coverage-{{ coverage_module }}-{{ test_playbook|replace('.yml', '') }}
- name: debug info
debug:
msg: Getting coverage for '{{ coverage_module }}' with '{{ test_playbook }}'
# combine data in case old data is left there
- command: "{{ coverage }} combine"
environment:
COVERAGE_FILE: "{{ coverage_file }}"
ignore_errors: yes
- name: remove old data
file:
state: absent
path: "{{ coverage_file }}"
- name: remove old data
shell: rm -f .coverage.*
- name: copy coveragerc
copy:
content: "[run]\ndisable_warnings = no-data-collected\n"
dest: .coveragerc
- name: install latest pip
pip:
name: coverage
extra_args: --user --upgrade
- import_playbook: "{{ test_playbook }}"
vars:
ansible_python_interpreter: "{{ coverage }} run -p --include *ansible_module_{{ coverage_module }}.py"
- name: Gather coverage data
hosts: all
tasks:
- shell: "{{ coverage }} combine .coverage.*"
environment:
COVERAGE_FILE: "{{ coverage_file }}"
- name: Get coverage data
hosts: all
tasks:
- fetch:
src: "{{ coverage_file }}"
dest: "{{ destdir }}"
flat: no

View File

@@ -0,0 +1,34 @@
#! /bin/bash
# SPDX-License-Identifier: BSD-3-Clause
set -e
coverage_data=total-coveragedata
testhost="${1}"
if [ "$#" -lt 1 ]
then
echo "USAGE: ${0} host"
echo "Get local and all remote coverage data for host"
exit 1
fi
rm -f remote-coveragedata* "${coveragedata}"
# collect pytest coverage
tox -e py26,py27,py36,py37 -- --cov-append
for test_playbook in tests_*.yml
do
./get-coverage.sh "${testhost}" "${test_playbook}"
done
./merge-coverage.sh coverage "total-remote-coveragedata" remote-coveragedata-*
./covstats .coverage remote-coveragedata-* "total-remote-coveragedata"
./merge-coverage.sh coverage "${coverage_data}" .coverage remote-coveragedata-*
echo "Total coverage:"
COVERAGE_FILE="${coverage_data}" coverage report ||:
COVERAGE_FILE="${coverage_data}" coverage html --directory "htmlcov-${coverage_data}" ||:
echo "Open HTML report with:"
echo "xdg-open htmlcov-${coverage_data}/index.html"

View File

@@ -0,0 +1,35 @@
#! /bin/bash
# SPDX-License-Identifier: BSD-3-Clause
if [ -n "${DEBUG}" ]
then
set -x
fi
set -e
if [ "$#" -lt 3 ]
then
echo "USAGE: ${0} path_to_coverage_binary output_file input_files..."
echo "Merges all input_files into output file without removing input_files"
exit 1
fi
# path to coverage binary
coverage="${1}"
shift
# read by coverage binary
export COVERAGE_FILE="${1}"
shift
tempdir="$(mktemp -d /tmp/coverage_merge-XXXXXX)"
trap "rm -rf '${tempdir}'" EXIT
cp --backup=numbered -- "${@}" "${tempdir}"
# FIXME: Would not work if coverage files are not hidden but they are by
# default
shopt -s dotglob
"${coverage}" combine "${tempdir}/"*
echo "Merged data into ${COVERAGE_FILE}"
./covstats "${COVERAGE_FILE}"

View File

@@ -0,0 +1 @@
../roles/

View File

@@ -0,0 +1 @@
../tasks/

View File

@@ -0,0 +1,111 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
vars:
interface: lsrfeat1
type: veth
tasks:
- name: "INIT: Ethtool feeatures tests"
debug:
msg: "##################################################"
- include_tasks: tasks/show-interfaces.yml
- include_tasks: tasks/manage-test-interface.yml
vars:
state: present
- include_tasks: tasks/assert-device_present.yml
- name: Install ethtool (test dependency)
package:
name: ethtool
state: present
- block:
- name: "TEST: I can create a profile without changing the ethtool features."
debug:
msg: "##################################################"
- name: Get current device features
command: "ethtool --show-features {{ interface }}"
register: original_ethtool_features
- import_role:
name: linux-system-roles.network
vars:
network_connections:
- name: "{{ interface }}"
state: up
type: ethernet
ip:
dhcp4: "no"
auto6: "no"
- name: Get current device features
command: "ethtool --show-features {{ interface }}"
register: ethtool_features
- name: "ASSERT: The profile does not change the ethtool features"
assert:
that:
- original_ethtool_features.stdout == ethtool_features.stdout
- name: "TEST: I can disable gro and tx-tcp-segmentation and enable gso."
debug:
msg: "##################################################"
- import_role:
name: linux-system-roles.network
vars:
network_connections:
- name: "{{ interface }}"
state: up
type: ethernet
ip:
dhcp4: "no"
auto6: "no"
ethtool:
features:
gro: "no"
gso: "yes"
tx-tcp-segmentation: "no"
- name: Get current device features
command: "ethtool --show-features {{ interface }}"
register: ethtool_features
- name:
debug:
var: ethtool_features.stdout_lines
- name: Assert device features
assert:
that:
- "'generic-receive-offload: off' in ethtool_features.stdout_lines"
- "'generic-segmentation-offload: on' in ethtool_features.stdout_lines"
- "'tx-tcp-segmentation: off' in ethtool_features.stdout_lines | map('trim')"
- name: "TEST: I can reset features to their original value."
debug:
msg: "##################################################"
- import_role:
name: linux-system-roles.network
vars:
network_connections:
- name: "{{ interface }}"
state: up
type: ethernet
ip:
dhcp4: "no"
auto6: "no"
- name: Get current device features
command: "ethtool --show-features {{ interface }}"
register: ethtool_features
# Resetting the ethtools only works with NetworkManager
- name: "ASSERT: The profile does not change the ethtool features"
assert:
that:
- original_ethtool_features.stdout == ethtool_features.stdout
when:
network_provider == 'nm'
always:
- block:
- import_role:
name: linux-system-roles.network
vars:
network_connections:
- name: "{{ interface }}"
persistent_state: absent
state: down
ignore_errors: true
- include_tasks: tasks/manage-test-interface.yml
vars:
state: absent
tags:
- "tests::cleanup"

View File

@@ -0,0 +1,49 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
vars:
interface: statebr
profile: "{{ interface }}"
network_provider: nm
tasks:
- debug:
msg: Inside states tests
- include_tasks: tasks/show-interfaces.yml
- include_tasks: tasks/assert-device_absent.yml
# create test profile
- include_role:
name: linux-system-roles.network
vars:
network_connections:
- name: statebr
state: up
type: bridge
ip:
dhcp4: false
auto6: false
- include_tasks: tasks/assert-device_present.yml
- include_tasks: tasks/assert-profile_present.yml
# test case (remove profile but keep it up)
# I can remove a profile but keep the configuration active.
- include_role:
name: linux-system-roles.network
vars:
network_connections:
- name: statebr
persistent_state: absent
- include_tasks: tasks/assert-device_present.yml
- include_tasks: tasks/assert-profile_absent.yml
# test case
# I can set a profile down that is up and absent.
- name: Set down
include_role:
name: linux-system-roles.network
vars:
network_connections:
- name: statebr
state: down
- include_tasks: tasks/assert-device_absent.yml
- include_tasks: tasks/assert-profile_absent.yml

View File

@@ -0,0 +1,66 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
vars:
type: veth
interface: lsr101
vlan_interface: lsr101.90
tasks:
- include_tasks: tasks/show-interfaces.yml
- include_tasks: tasks/manage-test-interface.yml
vars:
state: present
- include_tasks: tasks/assert-device_present.yml
- name: "TEST: I can configure the MTU for a vlan interface without autoconnect."
debug:
msg: "##################################################"
- import_role:
name: linux-system-roles.network
vars:
network_connections:
- name: "{{ interface }}"
type: ethernet
state: up
mtu: 1492
autoconnect: false
ip:
dhcp4: false
auto6: false
- name: "{{ vlan_interface }}"
parent: "{{ interface }}"
type: vlan
vlan_id: 90
mtu: 1280
state: up
autoconnect: false
ip:
dhcp4: false
auto6: false
- include_tasks: tasks/assert-device_present.yml
vars:
interface: "{{ vlan_interface }}"
- include_tasks: tasks/assert-profile_present.yml
vars:
profile: "{{ item }}"
loop:
- "{{ interface }}"
- "{{ vlan_interface }}"
- name: "TEARDOWN: remove profiles."
debug:
msg: "##################################################"
- import_role:
name: linux-system-roles.network
vars:
network_connections:
- name: "{{ interface }}"
persistent_state: absent
state: down
- name: "{{ vlan_interface }}"
persistent_state: absent
state: down
ignore_errors: true
- include_tasks: tasks/manage-test-interface.yml
vars:
state: absent

View File

@@ -0,0 +1,10 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- name: Remove {{ profile }}
hosts: all
vars:
network_connections:
- name: "{{ profile }}"
persistent_state: absent
roles:
- linux-system-roles.network

View File

@@ -0,0 +1 @@
../../../defaults/

View File

@@ -0,0 +1 @@
../../../library/

View File

@@ -0,0 +1 @@
../../../meta/

View File

@@ -0,0 +1 @@
../../../module_utils/

View File

@@ -0,0 +1 @@
../../../tasks/

View File

@@ -0,0 +1,6 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- name: Run the tasklist {{ task }}
hosts: all
tasks:
- include_tasks: "{{ task }}"

View File

@@ -0,0 +1,7 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- include: get-interface_stat.yml
- name: "assert that interface {{ interface }} is absent"
assert:
that: not interface_stat.stat.exists
msg: "{{ interface }} exists"

View File

@@ -0,0 +1,7 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- include: get-interface_stat.yml
- name: "assert that interface {{ interface }} is present"
assert:
that: interface_stat.stat.exists
msg: "{{ interface }} does not exist"

View File

@@ -0,0 +1,7 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- include: get-profile_stat.yml
- name: "assert that profile '{{ profile }}' is absent"
assert:
that: not profile_stat.stat.exists
msg: "profile {{ profile_path }} does exist"

View File

@@ -0,0 +1,7 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- include: get-profile_stat.yml
- name: "assert that profile '{{ profile }}' is present"
assert:
that: profile_stat.stat.exists
msg: "profile {{ profile_path }} does not exist"

View File

@@ -0,0 +1,20 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- include_tasks: show-interfaces.yml
- include_tasks: manage-test-interface.yml
vars:
state: absent
- include_tasks: show-interfaces.yml
- include_tasks: assert-device_absent.yml
- include_tasks: manage-test-interface.yml
vars:
state: present
- include_tasks: show-interfaces.yml
- include_tasks: assert-device_present.yml
- include_tasks: manage-test-interface.yml
vars:
state: absent
- include_tasks: show-interfaces.yml
- include_tasks: assert-device_absent.yml

View File

@@ -0,0 +1,8 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- command: ls -1
args:
chdir: /sys/class/net
register: _current_interfaces
- set_fact:
current_interfaces: "{{ _current_interfaces.stdout_lines }}"

View File

@@ -0,0 +1,9 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- name: "Get stat for interface {{ interface }}"
stat:
get_attributes: false
get_checksum: false
get_mime: false
path: "/sys/class/net/{{ interface }}"
register: interface_stat

View File

@@ -0,0 +1,26 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- name: "Get stat for network-scripts"
stat:
get_attributes: false
get_checksum: false
get_mime: false
path: "/etc/sysconfig/network-scripts"
register: network_scripts_stat
- name: Set profile path (network-scripts)
set_fact:
profile_path: /etc/sysconfig/network-scripts/ifcfg-{{ profile }}
when:
- network_scripts_stat.stat.exists
- name: Set profile path (NetworkManager system-connections)
set_fact:
profile_path: /etc/NetworkManager/system-connections/{{ profile }}
when:
- not network_scripts_stat.stat.exists
- name: stat profile file
stat:
get_attributes: false
get_checksum: false
get_mime: false
path: "{{ profile_path }}"
register: profile_stat

View File

@@ -0,0 +1,50 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- fail:
msg: "state needs to be present or absent, not '{{ state }}'"
when: state not in ["present", "absent"]
- fail:
msg: "type needs to be dummy, tap or veth, not '{{ type }}'"
when: type not in ["dummy", "tap", "veth"]
# - include: get-current_interfaces.yml
- include: show-interfaces.yml
- name: Install iproute
package:
name: iproute
state: present
# veth
- name: Create veth interface {{ interface }}
shell: ip link add {{ interface }} type veth peer name peer{{ interface }}
when: "type == 'veth' and state == 'present' and
interface not in current_interfaces"
- name: Delete veth interface {{ interface }}
shell: ip link del {{ interface }} type veth
when: "type == 'veth' and state == 'absent' and
interface in current_interfaces"
# dummy
- name: Create dummy interface {{ interface }}
shell: ip link add "{{ interface }}" type dummy
when: "type == 'dummy' and state == 'present' and
interface not in current_interfaces"
- name: Delete dummy interface {{ interface }}
shell: ip link del "{{ interface }}" type dummy
when: "type == 'dummy' and state == 'absent' and
interface in current_interfaces"
# tap
- name: Create tap interface {{ interface }}
shell: ip tuntap add dev {{ interface }} mode tap
when: "type == 'tap' and state == 'present'
and interface not in current_interfaces"
- name: Delete tap interface {{ interface }}
shell: ip tuntap del dev {{ interface }} mode tap
when: "type == 'tap' and state == 'absent' and
interface in current_interfaces"

View File

@@ -0,0 +1,5 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- include: get-current_interfaces.yml
- debug:
msg: "current_interfaces: {{ current_interfaces }}"

View File

@@ -0,0 +1,55 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- name: Test configuring bridges
hosts: all
vars:
interface: LSR-TST-br31
tasks:
- name: "set interface={{ interface }}"
set_fact:
interface: "{{ interface }}"
- include_tasks: tasks/show-interfaces.yml
- include_tasks: tasks/assert-device_absent.yml
- name: Add test bridge
hosts: all
vars:
network_connections:
- name: "{{ interface }}"
interface_name: "{{ interface }}"
state: up
type: bridge
ip:
dhcp4: no
auto6: yes
roles:
- linux-system-roles.network
- import_playbook: run-tasks.yml
vars:
task: tasks/assert-device_present.yml
- import_playbook: run-tasks.yml
vars:
profile: "{{ interface }}"
task: tasks/assert-profile_present.yml
- import_playbook: down-profile.yml
vars:
profile: "{{ interface }}"
# FIXME: assert profile/device down
- import_playbook: remove-profile.yml
vars:
profile: "{{ interface }}"
- import_playbook: run-tasks.yml
vars:
profile: "{{ interface }}"
task: tasks/assert-profile_absent.yml
# FIXME: Devices might still be left when profile is absent
#- import_playbook: run-tasks.yml
# vars:
# task: tasks/assert-device_absent.yml

View File

@@ -0,0 +1,17 @@
---
- hosts: all
name: Run playbook 'tests_bridge.yml' with non-default provider
tasks:
- name: Get service facts
service_facts: null
- name: Set network provider
set_fact:
network_provider: '{{ "initscripts" if network_provider_current == "nm" else
"nm" }}'
vars:
network_provider_current: '{{ ''nm'' if ''NetworkManager.service'' in ansible_facts.services
and ansible_facts.services[''NetworkManager.service''][''state''] == ''running''
else ''initscripts'' }}'
- import_playbook: tests_bridge.yml
when:
- ansible_distribution_major_version != '6'

View File

@@ -0,0 +1,6 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- name: Test executing the role with default parameters
hosts: all
roles:
- linux-system-roles.network

View File

@@ -0,0 +1,17 @@
---
- hosts: all
name: Run playbook 'tests_default.yml' with non-default provider
tasks:
- name: Get service facts
service_facts: null
- name: Set network provider
set_fact:
network_provider: '{{ "initscripts" if network_provider_current == "nm" else
"nm" }}'
vars:
network_provider_current: '{{ ''nm'' if ''NetworkManager.service'' in ansible_facts.services
and ansible_facts.services[''NetworkManager.service''][''state''] == ''running''
else ''initscripts'' }}'
- import_playbook: tests_default.yml
when:
- ansible_distribution_major_version != '6'

View File

@@ -0,0 +1,62 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
tasks:
- debug:
msg: Inside ethernet tests
- debug:
var: network_provider
- name: Test configuring ethernet devices
hosts: all
vars:
type: veth
interface: lsr27
tasks:
- name: "set type={{ type }} and interface={{ interface }}"
set_fact:
type: "{{ type }}"
interface: "{{ interface }}"
- include_tasks: tasks/show-interfaces.yml
- include_tasks: tasks/manage-test-interface.yml
vars:
state: present
- include_tasks: tasks/assert-device_present.yml
- name: Test static interface up
hosts: all
vars:
network_connections:
- name: "{{ interface }}"
interface_name: "{{ interface }}"
state: up
type: ethernet
autoconnect: yes
ip:
address: 192.0.2.1/24
roles:
- linux-system-roles.network
- hosts: all
tasks:
- debug:
var: network_provider
# FIXME: assert profile present
# FIXME: assert profile/device up + IP address
- import_playbook: down-profile.yml
vars:
profile: "{{ interface }}"
# FIXME: assert profile/device down
- import_playbook: remove-profile.yml
vars:
profile: "{{ interface }}"
# FIXME: assert profile away
- name: Remove interfaces
hosts: all
tasks:
- include_tasks: tasks/manage-test-interface.yml
vars:
state: absent
- include_tasks: tasks/assert-device_absent.yml

View File

@@ -0,0 +1,17 @@
---
- hosts: all
name: Run playbook 'tests_ethernet.yml' with non-default provider
tasks:
- name: Get service facts
service_facts: null
- name: Set network provider
set_fact:
network_provider: '{{ "initscripts" if network_provider_current == "nm" else
"nm" }}'
vars:
network_provider_current: '{{ ''nm'' if ''NetworkManager.service'' in ansible_facts.services
and ansible_facts.services[''NetworkManager.service''][''state''] == ''running''
else ''initscripts'' }}'
- import_playbook: tests_ethernet.yml
when:
- ansible_distribution_major_version != '6'

View File

@@ -0,0 +1,13 @@
---
# set network provider and gather facts
- hosts: all
tasks:
- name: Set network provider to 'initscripts'
set_fact:
network_provider: initscripts
# workaround for: https://github.com/ansible/ansible/issues/27973
# There is no way in Ansible to abort a playbook hosts with specific OS
# releases Therefore we include the playbook with the tests only if the hosts
# would support it.
- import_playbook: playbooks/tests_ethtool_features.yml

View File

@@ -0,0 +1,28 @@
---
# set network provider and gather facts
- hosts: all
tasks:
- name: Set network provider to 'nm'
set_fact:
network_provider: nm
- name: Install NetworkManager
package:
name: NetworkManager
state: present
- name: Get NetworkManager version
command: rpm -q --qf "%{version}" NetworkManager
args:
warn: "no"
when: true
register: NetworkManager_version
# workaround for: https://github.com/ansible/ansible/issues/27973
# There is no way in Ansible to abort a playbook hosts with specific OS
# releases Therefore we include the playbook with the tests only if the hosts
# would support it.
# The test should run with NetworkManager, therefore it cannot run on RHEL 6 or CentOS 6.
- import_playbook: playbooks/tests_ethtool_features.yml
when:
- ansible_distribution_major_version != '6'
# NetworkManager 1.20.0 introduced ethtool settings support
- NetworkManager_version.stdout is version('1.20.0', '>=')

View File

@@ -0,0 +1,27 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- name: Check that creating and removing test devices and assertions work
hosts: all
tasks:
- name: test veth interface management
include_tasks: tasks/create-and-remove-interface.yml
vars:
type: veth
interface: veth1298
- name: test veth interface management
include_tasks: tasks/create-and-remove-interface.yml
vars:
type: dummy
interface: dummy1298
# FIXME: when: does not seem to work with include_tasks, therefore this cannot be safely tested for now
# - name: test tap interfaces
# include_tasks: tasks/create-and-remove-interface.yml
# vars:
# - type: tap
# - interface: tap1298
# when: ansible_distribution_major_version > 6
# # ip tuntap does not exist on RHEL6
# # FIXME: Maybe use some other tool to manage devices, openvpn can do this,
# # but it is in EPEL

View File

@@ -0,0 +1,11 @@
---
# empty playbook to gather facts for import_playbook when clause
- hosts: all
# workaround for: https://github.com/ansible/ansible/issues/27973
# There is no way in Ansible to abort a playbook hosts with specific OS
# releases Therefore we include the playbook with the tests only if the hosts
# would support it.
# The test requires NetworkManager, therefore it cannot run on RHEL 6 or CentOS 6.
- import_playbook: playbooks/tests_states.yml
when: ansible_distribution_major_version != '6'

View File

@@ -0,0 +1,89 @@
# SPDX-License-Identifier: BSD-3-Clause
---
- hosts: all
name: Setup for test running
tasks:
- name: Install EPEL on enterprise Linux for python2-mock
command: yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-{{ ansible_distribution_major_version }}.noarch.rpm
args:
warn: false
creates: /etc/yum.repos.d/epel.repo
when:
- ansible_distribution in ['RedHat', 'CentOS']
- ansible_distribution_major_version in ['6', '7']
- name: Install dependencies
package:
name: "{{ item }}"
state: present
# Ignore error because some package names might not be available
ignore_errors: true
loop:
- NetworkManager-libnm
- python2-gobject-base
- python3-gobject-base
- python-gobject-base
- python2-mock
- hosts: all
name: execute python unit tests
tasks:
- name: Copy python modules
copy:
src: "{{ item }}"
dest: /tmp/test-unit-1/
local_follow: false
loop:
- ../library/network_connections.py
- unit/test_network_connections.py
- ../module_utils/network_lsr
- name: Create helpers directory
file:
state: directory
dest: /tmp/test-unit-1/helpers
- name: Copy helpers
copy:
src: "{{ item }}"
dest: /tmp/test-unit-1/helpers
mode: 0755
with_fileglob:
- unit/helpers/*
- name: Check if python2 is available
command: python2 --version
ignore_errors: true
register: python2_available
when: true
- name: Run python2 unit tests
command: python2 /tmp/test-unit-1/test_network_connections.py --verbose
when: python2_available is succeeded
register: python2_result
- name: Check if python3 is available
command: python3 --version
ignore_errors: true
register: python3_available
when: true
- name: Run python3 unit tests
command: python3 /tmp/test-unit-1/test_network_connections.py --verbose
when: python3_available is succeeded
register: python3_result
- name: Show python2 unit test results
debug:
var: python2_result.stderr_lines
when: python2_result is succeeded
- name: Show python3 unit test results
debug:
var: python3_result.stderr_lines
when: python3_result is succeeded
- name: Ensure that at least one python unit test ran
fail:
msg: Tests did not run with python2 or python3
when: not (python2_available is succeeded or python3_available is succeeded)

View File

@@ -0,0 +1,13 @@
---
# set network provider and gather facts
- hosts: all
tasks:
- name: Set network provider to 'initscripts'
set_fact:
network_provider: initscripts
# workaround for: https://github.com/ansible/ansible/issues/27973
# There is no way in Ansible to abort a playbook hosts with specific OS
# releases Therefore we include the playbook with the tests only if the hosts
# would support it.
- import_playbook: playbooks/tests_vlan_mtu.yml

View File

@@ -0,0 +1,15 @@
---
# set network provider and gather facts
- hosts: all
tasks:
- name: Set network provider to 'nm'
set_fact:
network_provider: nm
# workaround for: https://github.com/ansible/ansible/issues/27973
# There is no way in Ansible to abort a playbook hosts with specific OS
# releases Therefore we include the playbook with the tests only if the hosts
# would support it.
# The test requires NetworkManager, therefore it cannot run on RHEL 6 or CentOS 6.
- import_playbook: playbooks/tests_vlan_mtu.yml
when: ansible_distribution_major_version != '6'

View File

@@ -0,0 +1,6 @@
#! /bin/bash
if [ "${1}" == "-P" ] && [ "${2}" != "" ]
then
echo "Permanent address: 23:00:00:00:00:00"
fi

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python
""" Tests for network_connections Ansible module """
# SPDX-License-Identifier: BSD-3-Clause
import os
import sys
TESTS_BASEDIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(1, os.path.join(TESTS_BASEDIR, "../..", "library"))
sys.path.insert(1, os.path.join(TESTS_BASEDIR, "../..", "module_utils"))
try:
from unittest import mock
except ImportError: # py2
import mock
sys.modules["ansible"] = mock.Mock()
sys.modules["ansible.module_utils.basic"] = mock.Mock()
sys.modules["ansible.module_utils"] = mock.Mock()
sys.modules["ansible.module_utils.network_lsr"] = __import__("network_lsr")
with mock.patch.dict("sys.modules", {"gi": mock.Mock(), "gi.repository": mock.Mock()}):
# pylint: disable=import-error, wrong-import-position
from network_lsr import nm_provider
def test_get_nm_ethtool_feature():
""" Test get_nm_ethtool_feature() """
with mock.patch.object(nm_provider.Util, "NM") as nm_mock:
nm_feature = nm_provider.get_nm_ethtool_feature("esp-hw-offload")
assert nm_feature == nm_mock.return_value.ETHTOOL_OPTNAME_FEATURE_ESP_HW_OFFLOAD

View File

@@ -0,0 +1,181 @@
[tox]
envlist = black, flake8, pylint, py{26,27,36,37}, ensure_non_running_provider
skipsdist = true
skip_missing_interpreters = True
[testenv]
basepython = python3
deps =
py{26,27,36,37,38}: pytest-cov
py{27,36,37,38}: pytest>=3.5.1
py{26,27}: mock
py26: pytest
molecule_{lint,syntax,test}: docker
molecule_{lint,syntax,test}: jmespath
molecule_{lint,syntax,test}: molecule
# The selinux pypi shim does not work with Ubuntu (as used by Travis), yet.
# Therefore use a fork with Ubuntu support. This can be changed once the
# update is available on PyPi.
# molecule_{lint,syntax,test}: selinux
molecule_{lint,syntax,test}: git+https://github.com/tyll/selinux-pypi-shim@fulllocation
[base]
passenv = *
setenv =
PYTHONPATH = {toxinidir}/library:{toxinidir}/module_utils
LC_ALL = C
changedir = {toxinidir}/tests
covtarget = {toxinidir}/library --cov {toxinidir}/module_utils
pytesttarget = .
[testenv:black]
deps = black
commands = black --check --diff --include "^[^.].*\.py$" .
[testenv:py26]
install_command = pip install {opts} {packages}
list_dependencies_command = pip freeze
basepython = python2.6
passenv = {[base]passenv}
setenv =
{[base]setenv}
changedir = {[base]changedir}
commands =
pytest \
--durations=5 \
--cov={[base]covtarget} \
--cov-report=html:htmlcov-py26 --cov-report=term \
{posargs} \
{[base]pytesttarget}
[testenv:py27]
basepython = python2.7
passenv = {[base]passenv}
setenv =
{[base]setenv}
changedir = {[base]changedir}
commands =
pytest \
--durations=5 \
--cov={[base]covtarget} \
--cov-report=html:htmlcov-py27 --cov-report=term \
{posargs} \
{[base]pytesttarget}
[testenv:py36]
basepython = python3.6
passenv = {[base]passenv}
setenv =
{[base]setenv}
changedir = {[base]changedir}
commands =
pytest \
--durations=5 \
--cov={[base]covtarget} \
--cov-report=html:htmlcov-py36 --cov-report=term \
{posargs} \
{[base]pytesttarget}
[testenv:py37]
basepython = python3.7
passenv = {[base]passenv}
setenv =
{[base]setenv}
changedir = {[base]changedir}
commands =
pytest \
--durations=5 \
--cov={[base]covtarget} \
--cov-report=html:htmlcov-py37 --cov-report=term \
{posargs} \
{[base]pytesttarget}
[testenv:py38]
passenv = {[base]passenv}
setenv =
{[base]setenv}
changedir = {[base]changedir}
basepython = python3.8
commands =
pytest \
--durations=5 \
--cov={[base]covtarget} \
--cov-report=html:htmlcov-py38 --cov-report=term \
{posargs} \
{[base]pytesttarget}
[testenv:pylint]
basepython = python2.7
setenv =
{[base]setenv}
deps =
pylint>=1.8.4
ansible
commands =
pylint \
--errors-only \
{posargs} \
library/network_connections.py \
module_utils/network_lsr \
tests/unit/test_network_connections.py
[testenv:flake8]
basepython = python2.7
deps =
flake8>=3.5
whitelist_externals = flake8
commands=
flake8 --statistics {posargs} \
.
[testenv:coveralls]
basepython = python2.7
passenv = TRAVIS TRAVIS_*
deps =
coveralls
changedir = {[base]changedir}
commands =
coveralls
[testenv:ensure_non_running_provider]
deps =
PyYAML
changedir = {toxinidir}/tests
commands = {toxinidir}/tests/ensure_non_running_provider.py
[testenv:molecule_lint]
commands_pre =
molecule --version
ansible --version
commands = molecule {posargs} lint
[testenv:molecule_syntax]
commands = molecule {posargs} syntax
[testenv:molecule_test]
commands = molecule {posargs} test
[pytest]
addopts = -rxs
[flake8]
show_source = True
max-line-length = 88
ignore = E402,W503
[pylint]
max-line-length = 88
disable = wrong-import-position
[pycodestyle]
max-line-length = 88
[travis]
python =
2.6: py26
2.7: py27,coveralls,flake8,pylint
3.5: molecule_lint,molecule_syntax,molecule_test
3.6: py36,black,ensure_non_running_provider
3.7: py37
3.8: py38