Ubuntu: support systemd-networkd

This change adds support for network configuration via systemd-networkd
on Ubuntu systems.

This is implemented via an Ansible Galaxy role,
stackhpc.systemd_networkd which was forked from
aruhier.systemd_networkd. Several improvements were made in
https://github.com/stackhpc/ansible-role-systemd-networkd/pull/1,
including:

* Add support for removing unexpected config files
* Use become where necessary
* Refactor config generation into a single task to improve performance

The systemd_networkd role does not add much abstraction on top of the
systemd-networkd configuration file format, which provides a lot of
flexibility at the expense of additional code in Kayobe. This code is
implemented as filter plugins, similarly to the existing
MichaelRigart.interfaces role.

This patch includes support for:

* Ethernet interfaces
* bridges
* bonds
* VLANs
* virtual Ethernet pairs (to connect Linux bridges and OVS bridges)
* static IP addresses
* static routes
* MTU

Some network attributes are currently not supported for
systemd-networkd:

* rules
* route options
* ethtool_opts
* zone
* allowed addresses

Story: 2004960
Task: 41881

Change-Id: I248b5bb9ce5a80a07a2a311cb3aca6daca920720
This commit is contained in:
Mark Goddard 2021-02-12 16:42:53 +00:00
parent ae2ed2215a
commit 3bbf736d8d
14 changed files with 1412 additions and 112 deletions

View File

@ -0,0 +1,22 @@
# Copyright (c) 2021 StackHPC Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from kayobe.plugins.filter import networkd
class FilterModule(object):
"""Systemd-networkd filters."""
def filters(self):
return networkd.get_filters()

View File

@ -86,3 +86,9 @@ network_patch_suffix_ovs: '-ovs'
# List of IP routing tables. Each item should be a dict containing 'id' and # List of IP routing tables. Each item should be a dict containing 'id' and
# 'name' items. These tables will be added to /etc/iproute2/rt_tables. # 'name' items. These tables will be added to /etc/iproute2/rt_tables.
network_route_tables: [] network_route_tables: []
###############################################################################
# Systemd-networkd configuration.
# Prefix for systemd-networkd configuration file names.
networkd_prefix: "50-kayobe-"

View File

@ -0,0 +1,16 @@
---
- name: Find netplan systemd-networkd configuration
become: true
find:
path: /run/systemd/network
register: netplan_systemd_networkd_config
listen: Remove netplan systemd-networkd configuration
- name: Remove netplan systemd-networkd configuration
become: true
file:
path: "{{ item.path }}"
state: absent
loop: "{{ netplan_systemd_networkd_config.files }}"
loop_control:
label: "{{ item.path }}"

View File

@ -1,51 +1,29 @@
--- ---
- name: Ensure NetworkManager is disabled
service:
name: NetworkManager
state: stopped
enabled: no
become: True
register: nm_result
failed_when:
- nm_result is failed
# Ugh, Ansible's service module doesn't handle uninstalled services.
- "'Could not find the requested service' not in nm_result.msg"
- import_role: - import_role:
name: ahuffman.resolv name: ahuffman.resolv
when: resolv_is_managed | bool when: resolv_is_managed | bool
become: True become: True
- name: Configure network interfaces (RedHat) - name: Remove netplan.io packages
import_role: become: true
name: MichaelRigart.interfaces package:
vars: name:
interfaces_route_tables: "{{ network_route_tables }}" - libnetplan0
interfaces_ether_interfaces: > - netplan.io
{{ network_interfaces | state: absent
net_select_ethers | notify:
map('net_interface_obj') | - Remove netplan systemd-networkd configuration
list }}
interfaces_bridge_interfaces: >
{{ network_interfaces |
net_select_bridges |
map('net_bridge_obj') |
list }}
interfaces_bond_interfaces: >
{{ network_interfaces |
net_select_bonds |
map('net_bond_obj') |
list }}
# Ensure that interface bouncing is finished before veth pairs are added, - name: Configure systemd-networkd
# since they are only ephemerally configured on Debian.
- name: Flush handlers
meta: flush_handlers
# Configure virtual ethernet patch links to connect the workload provision
# and external network bridges to the Neutron OVS bridge.
- name: Ensure OVS patch links exist
import_role: import_role:
name: veth name: stackhpc.systemd_networkd
vars: vars:
veth_interfaces: "{{ network_interfaces | net_ovs_veths }}" systemd_networkd_link: "{{ network_interfaces | networkd_links }}"
systemd_networkd_netdev: "{{ network_interfaces | networkd_netdevs }}"
systemd_networkd_network: "{{ network_interfaces | networkd_networks }}"
systemd_networkd_apply_config: true
systemd_networkd_enable_resolved: false
systemd_networkd_symlink_resolv_conf: false
systemd_networkd_cleanup: true
systemd_networkd_cleanup_patterns:
- "{{ networkd_prefix }}*"

View File

@ -58,6 +58,8 @@ supported:
Fully Qualified Domain Name (FQDN) used by API services on this network. Fully Qualified Domain Name (FQDN) used by API services on this network.
``routes`` ``routes``
.. note:: ``options`` is not currently supported on Ubuntu.
List of static IP routes. Each item should be a dict containing the List of static IP routes. Each item should be a dict containing the
item ``cidr``, and optionally ``gateway``, ``table`` and ``options``. item ``cidr``, and optionally ``gateway``, ``table`` and ``options``.
``cidr`` is the CIDR representation of the route's destination. ``gateway`` ``cidr`` is the CIDR representation of the route's destination. ``gateway``
@ -334,11 +336,15 @@ The following attributes are supported:
``bond_lacp_rate`` ``bond_lacp_rate``
For bond interfaces, the lacp_rate to use for the bond. For bond interfaces, the lacp_rate to use for the bond.
``ethtool_opts`` ``ethtool_opts``
.. note:: ``ethtool_opts`` is not currently supported on Ubuntu.
Physical network interface options to apply with ``ethtool``. When used on Physical network interface options to apply with ``ethtool``. When used on
bond and bridge interfaces, settings apply to underlying interfaces. This bond and bridge interfaces, settings apply to underlying interfaces. This
should be a string of arguments passed to the ``ethtool`` utility, for should be a string of arguments passed to the ``ethtool`` utility, for
example ``"-G ${DEVICE} rx 8192 tx 8192"``. example ``"-G ${DEVICE} rx 8192 tx 8192"``.
``zone`` ``zone``
.. note:: ``zone`` is not currently supported on Ubuntu.
The name of ``firewalld`` zone to be attached to network interface. The name of ``firewalld`` zone to be attached to network interface.
IP Addresses IP Addresses

View File

@ -240,7 +240,7 @@ Alternatively, this can be added using the following commands::
sudo ip l add breth1 type bridge sudo ip l add breth1 type bridge
sudo ip l set breth1 up sudo ip l set breth1 up
sudo ip a add 192.168.33.5/24 dev breth1 sudo ip a add 192.168.33.5/24 brd 192.168.33.255 dev breth1
sudo ip l add eth1 type dummy sudo ip l add eth1 type dummy
sudo ip l set eth1 up sudo ip l set eth1 up
sudo ip l set eth1 master breth1 sudo ip l set eth1 master breth1

View File

@ -0,0 +1,571 @@
# Copyright (c) 2021 StackHPC Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
This module provides Ansible filters that generate configuration for
systemd-networkd NetDevs, links and networks. The results are compatible with
the stackhpc.ansible_role_systemd_networkd role.
Systemd-networkd uses INI-style configuration files, with the provision for
multiple sections with the same name, and multiple options with the same name
in a given section. This results in a slightly unwieldy data format used by the
role. The top level is a list of dicts with section names as keys. The values
are lists of dicts mapping option names to values.
Example schema (YAML):
- section1:
- option1: value1
- option2: value2
- section2
- option3: value3
"""
import ipaddress
from ansible import errors
import jinja2
from kayobe.plugins.filter import networks
from kayobe.plugins.filter import utils
def _filter_options(config):
"""Filter out None values from a networkd config.
:param config: List of sections to filter.
:returns: a filtered list of sections without empty options.
"""
# Example schema (YAML):
# - section1:
# - option1: value1
# - option2:
# - section2
# - option3:
# We can filter this down to the following:
# - section1:
# - option1: value1
new_config = []
for section_dict in config:
new_section_dict = {}
for section_name, section in section_dict.items():
new_section = []
for option_dict in section:
new_option_dict = {}
for option_name, option in option_dict.items():
if option is not None:
new_option_dict[option_name] = option
if new_option_dict:
new_section.append(new_option_dict)
if new_section:
new_section_dict[section_name] = new_section
if new_section_dict:
new_config.append(new_section_dict)
return new_config
def _ms_to_s(n):
"""Convert from milliseconds to seconds."""
if n is not None:
n = float(n) / 1000
return n
def _vlan_netdev(context, name, inventory_hostname):
"""Return a networkd NetDev configuration for a VLAN interface.
:param context: a Jinja2 Context object.
:param name: name of the network.
:param inventory_hostname: Ansible inventory hostname.
"""
device = networks.net_interface(context, name, inventory_hostname)
mtu = networks.net_mtu(context, name, inventory_hostname)
vlan = networks.net_vlan(context, name, inventory_hostname)
config = [
{
'NetDev': [
{'Name': device},
{'Kind': 'vlan'},
{'MTUBytes': mtu},
],
},
{
'VLAN': [
{'Id': vlan},
]
}
]
return _filter_options(config)
def _bridge_netdev(context, name, inventory_hostname):
"""Return a networkd NetDev configuration for a bridge.
:param context: a Jinja2 Context object.
:param name: name of the network.
:param inventory_hostname: Ansible inventory hostname.
"""
device = networks.net_interface(context, name, inventory_hostname)
mtu = networks.net_mtu(context, name, inventory_hostname)
config = [
{
'NetDev': [
{'Name': device},
{'Kind': 'bridge'},
{'MTUBytes': mtu},
]
}
]
return _filter_options(config)
def _bond_netdev(context, name, inventory_hostname):
"""Return a networkd NetDev configuration for a bond.
:param context: a Jinja2 Context object.
:param name: name of the network.
:param inventory_hostname: Ansible inventory hostname.
"""
device = networks.net_interface(context, name, inventory_hostname)
mtu = networks.net_mtu(context, name, inventory_hostname)
mode = networks.net_bond_mode(context, name, inventory_hostname)
miimon = networks.net_bond_miimon(context, name, inventory_hostname)
updelay = networks.net_bond_updelay(context, name, inventory_hostname)
downdelay = networks.net_bond_downdelay(context, name, inventory_hostname)
xmit_hash_policy = networks.net_bond_xmit_hash_policy(context, name,
inventory_hostname)
lacp_rate = networks.net_bond_lacp_rate(context, name, inventory_hostname)
config = [
{
'NetDev': [
{'Name': device},
{'Kind': 'bond'},
{'MTUBytes': mtu},
]
},
{
'Bond': [
{'Mode': mode},
{'TransmitHashPolicy': xmit_hash_policy},
{'LACPTransmitRate': lacp_rate},
{'MIIMonitorSec': _ms_to_s(miimon)},
{'UpDelaySec': _ms_to_s(updelay)},
{'DownDelaySec': _ms_to_s(downdelay)},
]
}
]
return _filter_options(config)
def _veth_netdev(context, veth, inventory_hostname):
"""Return a networkd NetDev configuration for a veth pair.
:param context: a Jinja2 Context object.
:param veth: a dict describing the virtual Ethernet pair.
:param inventory_hostname: Ansible inventory hostname.
"""
interface = veth['name']
peer = veth['peer']
mtu = veth['mtu']
config = [
{
'NetDev': [
{'Name': interface},
{'Kind': 'veth'},
{'MTUBytes': mtu},
],
},
{
'Peer': [
{'Name': peer},
]
}
]
return _filter_options(config)
def _network(context, name, inventory_hostname, bridge, bond, vlan_interfaces):
"""Return a networkd network for an interface.
:param context: a Jinja2 Context object.
:param name: name of the network.
:param inventory_hostname: Ansible inventory hostname.
:param bridge: Name of a bridge into which the interface is plugged, or
None.
:param bond: Name of a bond of which the interface is a member, or None.
:param vlan_interfaces: List of VLAN subinterfaces of the interface.
"""
# FIXME(mgoddard): Currently does not support: rules, ethtool_opts, zone,
# allowed_addresses.
device = networks.net_interface(context, name, inventory_hostname)
ip = networks.net_ip(context, name, inventory_hostname)
cidr = networks.net_cidr(context, name, inventory_hostname)
gateway = networks.net_gateway(context, name, inventory_hostname)
if ip is None:
gateway = None
else:
if not cidr:
raise errors.AnsibleFilterError(
"No CIDR attribute configured for '%s' network but it has an "
"IP address" %
(name))
ip = "%s/%s" % (ip, ipaddress.ip_network(cidr).prefixlen)
mtu = networks.net_mtu(context, name, inventory_hostname)
routes = networks.net_routes(context, name, inventory_hostname)
bootproto = networks.net_bootproto(context, name, inventory_hostname)
defroute = networks.net_defroute(context, name, inventory_hostname)
if defroute is not None:
defroute = utils.call_bool_filter(context, defroute)
config = [
{
'Match': [
{'Name': device},
]
},
{
'Network': [
{'Address': ip},
{'Broadcast': 'true' if ip else None},
{'Gateway': gateway},
{'DHCP': ('yes' if bootproto and bootproto.lower() == 'dhcp'
else None)},
{'UseGateway': ('false'
if defroute is not None and not defroute
else None)},
{'Bridge': bridge},
{'Bond': bond},
] + [
{'VLAN': vlan_interface}
for vlan_interface in vlan_interfaces
]
},
{
'Link': [
{'MTUBytes': mtu},
]
},
]
if routes:
config += [
{
'Route': [
# FIXME(mgoddard): No support for 'options'.
{'Destination': route['cidr']},
{'Gateway': route.get('gateway')},
]
}
for route in routes or []
]
return _filter_options(config)
def _bridge_port_network(context, name, port, inventory_hostname,
vlan_interfaces):
"""Return a networkd network configuration for a bridge port.
:param context: a Jinja2 Context object.
:param name: name of the network.
:param port: name of the bridge port interface.
:param inventory_hostname: Ansible inventory hostname.
:param vlan_interfaces: List of VLAN subinterfaces of the interface.
"""
bridge = networks.get_and_validate_interface(context, name,
inventory_hostname)
mtu = networks.net_mtu(context, name, inventory_hostname)
config = [
{
'Match': [
{'Name': port},
]
},
{
'Network': [
{'Bridge': bridge},
] + [
{'VLAN': vlan_interface}
for vlan_interface in vlan_interfaces
]
},
{
'Link': [
{'MTUBytes': mtu},
]
}
]
return _filter_options(config)
def _bond_member_network(context, name, member, inventory_hostname,
vlan_interfaces):
"""Return a networkd network configuration for a bond member.
:param context: a Jinja2 Context object.
:param name: name of the network.
:param member: name of the bond member interface.
:param inventory_hostname: Ansible inventory hostname.
:param vlan_interfaces: List of VLAN subinterfaces of the interface.
"""
bond = networks.get_and_validate_interface(context, name,
inventory_hostname)
mtu = networks.net_mtu(context, name, inventory_hostname)
config = [
{
'Match': [
{'Name': member},
]
},
{
'Network': [
{'Bond': bond},
] + [
{'VLAN': vlan_interface}
for vlan_interface in vlan_interfaces
]
},
{
'Link': [
{'MTUBytes': mtu},
]
}
]
return _filter_options(config)
def _veth_network(context, veth, inventory_hostname):
"""Return a networkd network configuration for a veth link.
:param context: a Jinja2 Context object.
:param veth: a dict describing the virtual Ethernet pair.
:param inventory_hostname: Ansible inventory hostname.
"""
interface = veth['name']
bridge = veth['bridge']
config = [
{
'Match': [
{'Name': interface},
]
},
{
'Network': [
{'Bridge': bridge},
]
}
]
return _filter_options(config)
def _veth_peer_network(context, veth, inventory_hostname):
"""Return a networkd network configuration for a veth peer.
:param context: a Jinja2 Context object.
:param veth: a dict describing the virtual Ethernet pair.
:param inventory_hostname: Ansible inventory hostname.
"""
interface = veth['peer']
config = [
{
'Match': [
{'Name': interface},
]
},
{
'Network': [
# NOTE(mgoddard): bring the interface up, even without an IP.
{'ConfigureWithoutCarrier': 'true'},
]
}
]
return _filter_options(config)
@jinja2.contextfilter
def networkd_netdevs(context, names, inventory_hostname=None):
"""Return a dict representation of networkd NetDev configuration.
The format is compatible with the systemd_networkd_netdev variable in the
stackhpc.ansible_role_systemd_networkd role.
:param context: a Jinja2 Context object.
:param names: List of names of networks.
:param inventory_hostname: Ansible inventory hostname.
:returns: a dict representation of networkd NetDev configuration.
"""
# Prefix for configuration file names.
prefix = utils.get_hostvar(context, "networkd_prefix", inventory_hostname)
result = {}
# VLANs.
for name in networks.net_select_vlans(context, names, inventory_hostname):
device = networks.get_and_validate_interface(context, name,
inventory_hostname)
netdev = _vlan_netdev(context, name, inventory_hostname)
result["%s%s" % (prefix, device)] = netdev
# Bridges.
for name in networks.net_select_bridges(context, names,
inventory_hostname):
device = networks.get_and_validate_interface(context, name,
inventory_hostname)
netdev = _bridge_netdev(context, name, inventory_hostname)
result["%s%s" % (prefix, device)] = netdev
# Bonds.
for name in networks.net_select_bonds(context, names, inventory_hostname):
device = networks.get_and_validate_interface(context, name,
inventory_hostname)
netdev = _bond_netdev(context, name, inventory_hostname)
result["%s%s" % (prefix, device)] = netdev
# Virtual Ethernet pairs.
veths = networks.get_ovs_veths(context, names, inventory_hostname)
for veth in veths:
netdev = _veth_netdev(context, veth, inventory_hostname)
device = veth['name']
result["%s%s" % (prefix, device)] = netdev
return result
@jinja2.contextfilter
def networkd_links(context, names, inventory_hostname=None):
"""Return a dict representation of networkd link configuration.
The format is compatible with the systemd_networkd_link variable in the
stackhpc.ansible_role_systemd_networkd role.
:param context: a Jinja2 Context object.
:param names: List of names of networks.
:param inventory_hostname: Ansible inventory hostname.
:returns: a dict representation of networkd link configuration.
"""
# NOTE(mgoddard): We do not currently support link configuration.
return {}
@jinja2.contextfilter
def networkd_networks(context, names, inventory_hostname=None):
"""Return a dict representation of networkd network configuration.
The format is compatible with the systemd_networkd_network variable in the
stackhpc.ansible_role_systemd_networkd role.
:param context: a Jinja2 Context object.
:param names: List of names of networks.
:param inventory_hostname: Ansible inventory hostname.
:returns: a dict representation of networkd network configuration.
"""
# TODO(mgoddard): some attributes are currently not supported for
# systemd-networkd: rules, route options, ethtool_opts, zone,
# allowed addresses
# Build up some useful mappings.
bridge_port_to_bridge = {}
bond_member_to_bond = {}
interface_to_vlans = {}
# List of all interfaces.
interfaces = [
networks.net_interface(context, name, inventory_hostname)
for name in names
]
# Map bridge ports to bridges.
for name in networks.net_select_bridges(context, names,
inventory_hostname):
device = networks.get_and_validate_interface(context, name,
inventory_hostname)
for port in networks.net_bridge_ports(context, name,
inventory_hostname):
bridge_port_to_bridge[port] = device
# Map bond members to bonds.
for name in networks.net_select_bonds(context, names, inventory_hostname):
device = networks.get_and_validate_interface(context, name,
inventory_hostname)
for member in networks.net_bond_slaves(context, name,
inventory_hostname):
bond_member_to_bond[member] = device
# Map interfaces to lists of VLAN subinterfaces.
for name in networks.net_select_vlans(context, names, inventory_hostname):
device = networks.get_and_validate_interface(context, name,
inventory_hostname)
vlan = networks.net_vlan(context, name, inventory_hostname)
parent = networks.get_vlan_parent(device, vlan)
vlan_interfaces = interface_to_vlans.setdefault(parent, [])
vlan_interfaces.append(device)
# Prefix for configuration file names.
prefix = utils.get_hostvar(context, "networkd_prefix", inventory_hostname)
result = {}
# Configured networks.
for name in names:
device = networks.get_and_validate_interface(context, name,
inventory_hostname)
bridge = bridge_port_to_bridge.get(device)
bond = bond_member_to_bond.get(device)
vlan_interfaces = interface_to_vlans.get(device, [])
net = _network(context, name, inventory_hostname, bridge, bond,
vlan_interfaces)
result["%s%s" % (prefix, device)] = net
# Bridge ports that are not in configured networks.
for name in networks.net_select_bridges(context, names,
inventory_hostname):
device = networks.get_and_validate_interface(context, name,
inventory_hostname)
bridge_ports = networks.net_bridge_ports(context, name,
inventory_hostname)
for port in set(bridge_ports) - set(interfaces):
vlan_interfaces = interface_to_vlans.get(port, [])
netdev = _bridge_port_network(context, name, port,
inventory_hostname, vlan_interfaces)
result["%s%s" % (prefix, port)] = netdev
# Bond members that are not in configured networks.
for name in networks.net_select_bonds(context, names, inventory_hostname):
device = networks.get_and_validate_interface(context, name,
inventory_hostname)
bond_members = networks.net_bond_slaves(context, name,
inventory_hostname)
for member in set(bond_members) - set(interfaces):
vlan_interfaces = interface_to_vlans.get(member, [])
netdev = _bond_member_network(context, name, member,
inventory_hostname, vlan_interfaces)
result["%s%s" % (prefix, member)] = netdev
# Virtual Ethernet pairs for Open vSwitch.
veths = networks.get_ovs_veths(context, names, inventory_hostname)
for veth in veths:
net = _veth_network(context, veth, inventory_hostname)
device = veth['name']
result["%s%s" % (prefix, device)] = net
net = _veth_peer_network(context, veth, inventory_hostname)
device = veth['peer']
result["%s%s" % (prefix, device)] = net
return result
def get_filters():
return {
'networkd_netdevs': networkd_netdevs,
'networkd_links': networkd_links,
'networkd_networks': networkd_networks,
}

View File

@ -0,0 +1,748 @@
# Copyright (c) 2021 StackHPC Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import unittest
from ansible import errors
from ansible.plugins.filter.core import to_bool
import jinja2
from kayobe.plugins.filter import networkd
class BaseNetworkdTest(unittest.TestCase):
maxDiff = 2000
variables = {
# Inventory hostname, used to index IP list.
"inventory_hostname": "test-host",
# net1: Ethernet on eth0 with IP 1.2.3.4/24.
"net1_interface": "eth0",
"net1_cidr": "1.2.3.0/24",
"net1_ips": {"test-host": "1.2.3.4"},
# net2: VLAN on eth0.2 with VLAN 2 on interface eth0.
"net2_interface": "eth0.2",
"net2_vlan": 2,
# net3: bridge on br0 with ports eth0 and eth1.
"net3_interface": "br0",
"net3_bridge_ports": ["eth0", "eth1"],
# net4: bond on bond0 with members eth0 and eth1.
"net4_interface": "bond0",
"net4_bond_slaves": ["eth0", "eth1"],
# Prefix for networkd config file names.
"networkd_prefix": "50-kayobe-",
# Veth pair patch link prefix and suffix.
"network_patch_prefix": "p-",
"network_patch_suffix_ovs": "-ovs",
"network_patch_suffix_phy": "-phy",
}
def setUp(self):
# Bandit complains about Jinja2 autoescaping without nosec.
self.env = jinja2.Environment() # nosec
self.env.filters['bool'] = to_bool
self.context = self._make_context(self.variables)
def _make_context(self, parent):
return self.env.context_class(
self.env, parent=parent, name='dummy', blocks={})
def _update_context(self, variables):
updated_vars = copy.deepcopy(self.variables)
updated_vars.update(variables)
self.context = self._make_context(updated_vars)
class TestNetworkdNetDevs(BaseNetworkdTest):
def test_empty(self):
devs = networkd.networkd_netdevs(self.context, [])
self.assertEqual({}, devs)
def test_vlan(self):
devs = networkd.networkd_netdevs(self.context, ["net2"])
expected = {
"50-kayobe-eth0.2": [
{
"NetDev": [
{"Name": "eth0.2"},
{"Kind": "vlan"},
]
},
{
"VLAN": [
{"Id": 2},
]
},
]
}
self.assertEqual(expected, devs)
def test_vlan_all_options(self):
self._update_context({"net2_mtu": 1400})
devs = networkd.networkd_netdevs(self.context, ["net2"])
expected = {
"50-kayobe-eth0.2": [
{
"NetDev": [
{"Name": "eth0.2"},
{"Kind": "vlan"},
{"MTUBytes": 1400},
]
},
{
"VLAN": [
{"Id": 2},
]
},
]
}
self.assertEqual(expected, devs)
def test_vlan_no_interface(self):
self._update_context({"net2_interface": None})
self.assertRaises(errors.AnsibleFilterError,
networkd.networkd_netdevs, self.context, ["net2"])
def test_bridge(self):
devs = networkd.networkd_netdevs(self.context, ["net3"])
expected = {
"50-kayobe-br0": [
{
"NetDev": [
{"Name": "br0"},
{"Kind": "bridge"},
]
},
]
}
self.assertEqual(expected, devs)
def test_bridge_all_options(self):
self._update_context({"net3_mtu": 1400})
devs = networkd.networkd_netdevs(self.context, ["net3"])
expected = {
"50-kayobe-br0": [
{
"NetDev": [
{"Name": "br0"},
{"Kind": "bridge"},
{"MTUBytes": 1400},
]
},
]
}
self.assertEqual(expected, devs)
def test_bridge_no_interface(self):
self._update_context({"net3_interface": None})
self.assertRaises(errors.AnsibleFilterError,
networkd.networkd_netdevs, self.context, ["net3"])
def test_bond(self):
devs = networkd.networkd_netdevs(self.context, ["net4"])
expected = {
"50-kayobe-bond0": [
{
"NetDev": [
{"Name": "bond0"},
{"Kind": "bond"},
]
},
]
}
self.assertEqual(expected, devs)
def test_bond_all_options(self):
self._update_context({
"net4_mtu": 1400,
"net4_bond_mode": "802.3ad",
"net4_bond_miimon": 100,
"net4_bond_updelay": 200,
"net4_bond_downdelay": 300,
"net4_bond_xmit_hash_policy": "layer3+4",
"net4_bond_lacp_rate": 60,
})
devs = networkd.networkd_netdevs(self.context, ["net4"])
expected = {
"50-kayobe-bond0": [
{
"NetDev": [
{"Name": "bond0"},
{"Kind": "bond"},
{"MTUBytes": 1400},
]
},
{
"Bond": [
{"Mode": "802.3ad"},
{"TransmitHashPolicy": "layer3+4"},
{"LACPTransmitRate": 60},
{"MIIMonitorSec": 0.1},
{"UpDelaySec": 0.2},
{"DownDelaySec": 0.3},
]
},
]
}
self.assertEqual(expected, devs)
def test_bond_no_interface(self):
self._update_context({"net4_interface": None})
self.assertRaises(errors.AnsibleFilterError,
networkd.networkd_netdevs, self.context, ["net4"])
def test_veth(self):
self._update_context({"external_net_names": ["net3"]})
devs = networkd.networkd_netdevs(self.context, ["net3"])
expected = {
"50-kayobe-br0": [
{
"NetDev": [
{"Name": "br0"},
{"Kind": "bridge"},
]
},
],
"50-kayobe-p-br0-phy": [
{
"NetDev": [
{"Name": "p-br0-phy"},
{"Kind": "veth"},
]
},
{
"Peer": [
{"Name": "p-br0-ovs"},
]
},
]
}
self.assertEqual(expected, devs)
def test_veth_with_mtu(self):
self._update_context({"external_net_names": ["net3"],
"net3_mtu": 1400})
devs = networkd.networkd_netdevs(self.context, ["net3"])
expected = {
"50-kayobe-br0": [
{
"NetDev": [
{"Name": "br0"},
{"Kind": "bridge"},
{"MTUBytes": 1400},
]
},
],
"50-kayobe-p-br0-phy": [
{
"NetDev": [
{"Name": "p-br0-phy"},
{"Kind": "veth"},
{"MTUBytes": 1400},
]
},
{
"Peer": [
{"Name": "p-br0-ovs"},
]
},
]
}
self.assertEqual(expected, devs)
def test_veth_no_interface(self):
self._update_context({"external_net_names": ["net3"],
"net3_interface": None})
self.assertRaises(errors.AnsibleFilterError,
networkd.networkd_netdevs, self.context, ["net3"])
class TestNetworkdLinks(BaseNetworkdTest):
def test_empty(self):
links = networkd.networkd_links(self.context, ['net1'])
self.assertEqual({}, links)
class TestNetworkdNetworks(BaseNetworkdTest):
def test_empty(self):
nets = networkd.networkd_networks(self.context, [])
self.assertEqual({}, nets)
def test_eth(self):
nets = networkd.networkd_networks(self.context, ["net1"])
expected = {
"50-kayobe-eth0": [
{
"Match": [
{"Name": "eth0"}
]
},
{
"Network": [
{"Address": "1.2.3.4/24"},
{"Broadcast": "true"},
]
},
]
}
self.assertEqual(expected, nets)
def test_eth_all_options(self):
self._update_context({
"net1_gateway": "1.2.3.1",
"net1_mtu": 1400,
"net1_routes": [
{
"cidr": "1.2.4.0/24",
},
{
"cidr": "1.2.5.0/24",
"gateway": "1.2.5.1",
},
{
"cidr": "1.2.6.0/24",
},
],
"net1_bootproto": "dhcp",
"net1_defroute": 'no',
})
nets = networkd.networkd_networks(self.context, ["net1"])
expected = {
"50-kayobe-eth0": [
{
"Match": [
{"Name": "eth0"}
]
},
{
"Network": [
{"Address": "1.2.3.4/24"},
{"Broadcast": "true"},
{"Gateway": "1.2.3.1"},
{"DHCP": "yes"},
{'UseGateway': "false"},
]
},
{
"Link": [
{"MTUBytes": 1400},
]
},
{
"Route": [
{"Destination": "1.2.4.0/24"},
]
},
{
"Route": [
{"Destination": "1.2.5.0/24"},
{"Gateway": "1.2.5.1"},
]
},
{
"Route": [
{"Destination": "1.2.6.0/24"},
]
},
]
}
self.assertEqual(expected, nets)
def test_eth_no_interface(self):
self._update_context({"net1_interface": None})
self.assertRaises(errors.AnsibleFilterError,
networkd.networkd_networks, self.context, ["net1"])
def test_vlan(self):
nets = networkd.networkd_networks(self.context, ["net2"])
expected = {
"50-kayobe-eth0.2": [
{
"Match": [
{"Name": "eth0.2"}
]
},
]
}
self.assertEqual(expected, nets)
def test_vlan_with_parent(self):
nets = networkd.networkd_networks(self.context, ["net1", "net2"])
expected = {
"50-kayobe-eth0": [
{
"Match": [
{"Name": "eth0"}
]
},
{
"Network": [
{"Address": "1.2.3.4/24"},
{"Broadcast": "true"},
{"VLAN": "eth0.2"},
]
},
],
"50-kayobe-eth0.2": [
{
"Match": [
{"Name": "eth0.2"}
]
},
]
}
self.assertEqual(expected, nets)
def test_vlan_no_interface(self):
self._update_context({"net2_interface": None})
self.assertRaises(errors.AnsibleFilterError,
networkd.networkd_networks, self.context, ["net2"])
def test_bridge(self):
nets = networkd.networkd_networks(self.context, ["net3"])
expected = {
"50-kayobe-br0": [
{
"Match": [
{"Name": "br0"}
]
},
],
"50-kayobe-eth0": [
{
"Match": [
{"Name": "eth0"}
]
},
{
"Network": [
{"Bridge": "br0"},
]
},
],
"50-kayobe-eth1": [
{
"Match": [
{"Name": "eth1"}
]
},
{
"Network": [
{"Bridge": "br0"},
]
},
]
}
self.assertEqual(expected, nets)
def test_bridge_with_bridge_port_net(self):
# Test the case where a bridge port interface is a Kayobe network
# (here, eth0 is net1).
self._update_context({
"net1_mtu": 1400,
"net1_ips": None,
})
nets = networkd.networkd_networks(self.context, ["net1", "net3"])
expected = {
"50-kayobe-br0": [
{
"Match": [
{"Name": "br0"}
]
},
],
"50-kayobe-eth0": [
{
"Match": [
{"Name": "eth0"}
]
},
{
"Network": [
{"Bridge": "br0"},
]
},
{
"Link": [
{"MTUBytes": 1400},
]
},
],
"50-kayobe-eth1": [
{
"Match": [
{"Name": "eth1"}
]
},
{
"Network": [
{"Bridge": "br0"},
]
},
]
}
self.assertEqual(expected, nets)
def test_bridge_no_interface(self):
self._update_context({"net3_interface": None})
self.assertRaises(errors.AnsibleFilterError,
networkd.networkd_networks, self.context, ["net3"])
def test_bond(self):
nets = networkd.networkd_networks(self.context, ["net4"])
expected = {
"50-kayobe-bond0": [
{
"Match": [
{"Name": "bond0"}
]
},
],
"50-kayobe-eth0": [
{
"Match": [
{"Name": "eth0"}
]
},
{
"Network": [
{"Bond": "bond0"},
]
},
],
"50-kayobe-eth1": [
{
"Match": [
{"Name": "eth1"}
]
},
{
"Network": [
{"Bond": "bond0"},
]
},
]
}
self.assertEqual(expected, nets)
def test_bond_with_bond_member_net(self):
# Test the case where a bond member interface is a Kayobe network
# (here, eth0 is net1).
self._update_context({
"net1_mtu": 1400,
"net1_ips": None,
})
nets = networkd.networkd_networks(self.context, ["net1", "net4"])
expected = {
"50-kayobe-bond0": [
{
"Match": [
{"Name": "bond0"}
]
},
],
"50-kayobe-eth0": [
{
"Match": [
{"Name": "eth0"}
]
},
{
"Network": [
{"Bond": "bond0"},
]
},
{
"Link": [
{"MTUBytes": 1400},
]
},
],
"50-kayobe-eth1": [
{
"Match": [
{"Name": "eth1"}
]
},
{
"Network": [
{"Bond": "bond0"},
]
},
]
}
self.assertEqual(expected, nets)
def test_bond_no_interface(self):
self._update_context({"net4_interface": None})
self.assertRaises(errors.AnsibleFilterError,
networkd.networkd_networks, self.context, ["net4"])
def test_veth(self):
self._update_context({"external_net_names": ["net3"],
"net3_bridge_ports": []})
nets = networkd.networkd_networks(self.context, ["net3"])
expected = {
"50-kayobe-br0": [
{
"Match": [
{"Name": "br0"}
]
},
],
"50-kayobe-p-br0-phy": [
{
"Match": [
{"Name": "p-br0-phy"}
]
},
{
"Network": [
{"Bridge": "br0"},
]
},
],
"50-kayobe-p-br0-ovs": [
{
"Match": [
{"Name": "p-br0-ovs"}
]
},
{
"Network": [
{"ConfigureWithoutCarrier": "true"},
]
},
],
}
self.assertEqual(expected, nets)
def test_veth_on_vlan(self):
# Test the case where a VLAN interface is one of the networks that
# needs patching to OVS. The parent interface is a bridge, and the veth
# pair should be plugged into it.
self._update_context({
"provision_wl_net_name": "net5",
"net3_bridge_ports": [],
"net5_interface": "br0.42",
"net5_vlan": 42})
nets = networkd.networkd_networks(self.context, ["net3", "net5"])
expected = {
"50-kayobe-br0": [
{
"Match": [
{"Name": "br0"}
]
},
{
"Network": [
{"VLAN": "br0.42"}
]
}
],
"50-kayobe-br0.42": [
{
"Match": [
{"Name": "br0.42"}
]
},
],
"50-kayobe-p-br0-phy": [
{
"Match": [
{"Name": "p-br0-phy"}
]
},
{
"Network": [
{"Bridge": "br0"},
]
},
],
"50-kayobe-p-br0-ovs": [
{
"Match": [
{"Name": "p-br0-ovs"}
]
},
{
"Network": [
{"ConfigureWithoutCarrier": "true"},
]
},
],
}
self.assertEqual(expected, nets)
def test_veth_no_interface(self):
self._update_context({"external_net_names": ["net3"],
"net3_interface": None})
self.assertRaises(errors.AnsibleFilterError,
networkd.networkd_networks, self.context, ["net3"])
def test_no_veth_without_bridge(self):
self._update_context({"external_net_names": ["net1"]})
nets = networkd.networkd_networks(self.context, ["net1"])
expected = {
"50-kayobe-eth0": [
{
"Match": [
{"Name": "eth0"}
]
},
{
"Network": [
{"Address": "1.2.3.4/24"},
{"Broadcast": "true"},
]
},
]
}
self.assertEqual(expected, nets)
def test_no_veth_on_vlan_without_bridge(self):
# Test the case where a VLAN interface is one of the networks that
# needs patching to OVS. The parent interface is a bridge, and the veth
# pair should be plugged into it.
self._update_context({"provision_wl_net": "net2"})
nets = networkd.networkd_networks(self.context, ["net1", "net2"])
expected = {
"50-kayobe-eth0": [
{
"Match": [
{"Name": "eth0"}
]
},
{
"Network": [
{"Address": "1.2.3.4/24"},
{"Broadcast": "true"},
{"VLAN": "eth0.2"},
]
},
],
"50-kayobe-eth0.2": [
{
"Match": [
{"Name": "eth0.2"}
]
},
]
}
self.assertEqual(expected, nets)

View File

@ -16,11 +16,8 @@ controller_extra_network_interfaces:
- test_net_eth_vlan - test_net_eth_vlan
- test_net_bridge - test_net_bridge
- test_net_bridge_vlan - test_net_bridge_vlan
{# Bond configuration does not seem to work with dummy interfaces on Ubuntu #}
{% if ansible_os_family != 'Debian' %}
- test_net_bond - test_net_bond
- test_net_bond_vlan - test_net_bond_vlan
{% endif %}
# dummy2: Ethernet interface. # dummy2: Ethernet interface.
test_net_eth_cidr: 192.168.34.0/24 test_net_eth_cidr: 192.168.34.0/24
@ -44,7 +41,6 @@ test_net_bridge_vlan_cidr: 192.168.37.0/24
test_net_bridge_vlan_interface: "{% raw %}{{ test_net_bridge_interface }}.{{ test_net_bridge_vlan_vlan }}{% endraw %}" test_net_bridge_vlan_interface: "{% raw %}{{ test_net_bridge_interface }}.{{ test_net_bridge_vlan_vlan }}{% endraw %}"
test_net_bridge_vlan_vlan: 43 test_net_bridge_vlan_vlan: 43
{% if ansible_os_family != 'Debian' %}
# bond0: bond with slaves dummy5, dummy6. # bond0: bond with slaves dummy5, dummy6.
test_net_bond_cidr: 192.168.38.0/24 test_net_bond_cidr: 192.168.38.0/24
test_net_bond_interface: bond0 test_net_bond_interface: bond0
@ -54,7 +50,6 @@ test_net_bond_bond_slaves: [dummy5, dummy6]
test_net_bond_vlan_cidr: 192.168.39.0/24 test_net_bond_vlan_cidr: 192.168.39.0/24
test_net_bond_vlan_interface: "{% raw %}{{ test_net_bond_interface }}.{{ test_net_bond_vlan_vlan }}{% endraw %}" test_net_bond_vlan_interface: "{% raw %}{{ test_net_bond_interface }}.{{ test_net_bond_vlan_vlan }}{% endraw %}"
test_net_bond_vlan_vlan: 44 test_net_bond_vlan_vlan: 44
{% endif %}
# Define a software RAID device consisting of two loopback devices. # Define a software RAID device consisting of two loopback devices.
controller_mdadm_arrays: controller_mdadm_arrays:

View File

@ -15,13 +15,6 @@ def _is_dnf():
return info[0] == 'CentOS Linux' and info[1].startswith('8') return info[0] == 'CentOS Linux' and info[1].startswith('8')
def _supports_bonds():
# Bond configuration does not currently work on Ubuntu when using dummy
# devices as slaves.
info = distro.linux_distribution()
return info[0] != 'Ubuntu'
def test_network_ethernet(host): def test_network_ethernet(host):
interface = host.interface('dummy2') interface = host.interface('dummy2')
assert interface.exists assert interface.exists
@ -59,21 +52,21 @@ def test_network_bridge_vlan(host):
assert host.file('/sys/class/net/br0.43/lower_br0').exists assert host.file('/sys/class/net/br0.43/lower_br0').exists
@pytest.mark.skipif(not _supports_bonds(), reason="Bonding no worky on Ubuntu")
def test_network_bond(host): def test_network_bond(host):
interface = host.interface('bond0') interface = host.interface('bond0')
assert interface.exists assert interface.exists
assert '192.168.38.1' in interface.addresses assert '192.168.38.1' in interface.addresses
sys_slaves = host.check_output('cat /sys/class/net/bond0/bonding/slaves') sys_slaves = host.check_output('cat /sys/class/net/bond0/bonding/slaves')
slaves = ['dummy5', 'dummy6'] # Ordering is not guaranteed, so compare sets.
assert sys_slaves == " ".join(slaves) sys_slaves = set(sys_slaves.split())
slaves = set(['dummy5', 'dummy6'])
assert sys_slaves == slaves
for slave in slaves: for slave in slaves:
interface = host.interface(slave) interface = host.interface(slave)
assert interface.exists assert interface.exists
assert not interface.addresses assert not interface.addresses
@pytest.mark.skipif(not _supports_bonds(), reason="Bonding no worky on Ubuntu")
def test_network_bond_vlan(host): def test_network_bond_vlan(host):
interface = host.interface('bond0.44') interface = host.interface('bond0.44')
assert interface.exists assert interface.exists

View File

@ -1,6 +1,8 @@
--- ---
- src: ahuffman.resolv - src: ahuffman.resolv
version: 1.3.1 version: 1.3.1
- src: stackhpc.systemd_networkd
version: v1.0.1
- src: jriguera.configdrive - src: jriguera.configdrive
# There are no versioned releases of this role. # There are no versioned releases of this role.
version: 8438592c84585c86e62ae07e526d3da53629b377 version: 8438592c84585c86e62ae07e526d3da53629b377

View File

@ -1,38 +0,0 @@
---
- name: Ensure interfaces.d directory exists
file:
path: /etc/network/interfaces.d
state: directory
become: true
- name: Ensure interfaces.d directory is sourced
lineinfile:
path: /etc/network/interfaces
line: source /etc/network/interfaces.d/*
become: true
- name: Ensure all-in-one network dummy interface exists
become: true
copy:
content: |
auto {{ bridge_port_interface }}
iface {{ bridge_port_interface }} inet manual
dest: /etc/network/interfaces.d/ifcfg-{{ bridge_port_interface }}
- name: Ensure all-in-one network bridge interface exists
become: true
copy:
content: |
auto {{ bridge_interface }}
iface {{ bridge_interface }} inet static
address {{ bridge_ip }}
netmask {{ (bridge_ip ~ '/' ~ bridge_prefix) | ipaddr('netmask') }}
bridge_ports {{ bridge_port_interface }}
dest: /etc/network/interfaces.d/ifcfg-{{ bridge_interface }}
- name: Ensure all-in-one network bridge interfaces are up
become: true
command: "{{ item }}"
with_items:
- "ifup {{ bridge_interface }}"
- "ifup {{ bridge_port_interface }}"

View File

@ -1,14 +0,0 @@
---
- name: Ensure all-in-one network bridge interface exists (RedHat)
command: "{{ item }}"
become: true
with_items:
- "ip l set {{ bridge_interface }} up"
- "ip a add {{ bridge_ip }}/{{ bridge_prefix }} dev {{ bridge_interface }}"
# NOTE(mgoddard): CentOS 8 removes interfaces from their bridge during
# ifdown, and removes the bridge if there are no interfaces left. When
# Kayobe bounces veth links plugged into the bridge, it causes the
# bridge which has the IP we are using for SSH to be removed. Use a
# dummy interface.
- "ip l set {{ bridge_port_interface }} up"
- "ip l set {{ bridge_port_interface }} master {{ bridge_interface }}"

View File

@ -6,4 +6,19 @@
- "ip l add {{ bridge_interface }} type bridge" - "ip l add {{ bridge_interface }} type bridge"
- "ip l add {{ bridge_port_interface }} type dummy" - "ip l add {{ bridge_port_interface }} type dummy"
- include_tasks: "{{ ansible_os_family }}.yml" - name: Ensure all-in-one network bridge interface exists
vars:
bridge_cidr: "{{ bridge_ip }}/{{ bridge_prefix }}"
bridge_broadcast: "{{ bridge_cidr | ipaddr('broadcast') }}"
command: "{{ item }}"
become: true
with_items:
- "ip l set {{ bridge_interface }} up"
- "ip a add {{ bridge_cidr }} brd {{ bridge_broadcast }} dev {{ bridge_interface }}"
# NOTE(mgoddard): CentOS 8 removes interfaces from their bridge during
# ifdown, and removes the bridge if there are no interfaces left. When
# Kayobe bounces veth links plugged into the bridge, it causes the
# bridge which has the IP we are using for SSH to be removed. Use a
# dummy interface.
- "ip l set {{ bridge_port_interface }} up"
- "ip l set {{ bridge_port_interface }} master {{ bridge_interface }}"