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:
parent
ae2ed2215a
commit
3bbf736d8d
22
ansible/filter_plugins/networkd.py
Normal file
22
ansible/filter_plugins/networkd.py
Normal 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()
|
@ -86,3 +86,9 @@ network_patch_suffix_ovs: '-ovs'
|
||||
# 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.
|
||||
network_route_tables: []
|
||||
|
||||
###############################################################################
|
||||
# Systemd-networkd configuration.
|
||||
|
||||
# Prefix for systemd-networkd configuration file names.
|
||||
networkd_prefix: "50-kayobe-"
|
||||
|
16
ansible/roles/network-debian/handlers/main.yml
Normal file
16
ansible/roles/network-debian/handlers/main.yml
Normal 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 }}"
|
@ -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:
|
||||
name: ahuffman.resolv
|
||||
when: resolv_is_managed | bool
|
||||
become: True
|
||||
|
||||
- name: Configure network interfaces (RedHat)
|
||||
import_role:
|
||||
name: MichaelRigart.interfaces
|
||||
vars:
|
||||
interfaces_route_tables: "{{ network_route_tables }}"
|
||||
interfaces_ether_interfaces: >
|
||||
{{ network_interfaces |
|
||||
net_select_ethers |
|
||||
map('net_interface_obj') |
|
||||
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 }}
|
||||
- name: Remove netplan.io packages
|
||||
become: true
|
||||
package:
|
||||
name:
|
||||
- libnetplan0
|
||||
- netplan.io
|
||||
state: absent
|
||||
notify:
|
||||
- Remove netplan systemd-networkd configuration
|
||||
|
||||
# Ensure that interface bouncing is finished before veth pairs are added,
|
||||
# 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
|
||||
- name: Configure systemd-networkd
|
||||
import_role:
|
||||
name: veth
|
||||
name: stackhpc.systemd_networkd
|
||||
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 }}*"
|
||||
|
@ -58,6 +58,8 @@ supported:
|
||||
|
||||
Fully Qualified Domain Name (FQDN) used by API services on this network.
|
||||
``routes``
|
||||
.. note:: ``options`` is not currently supported on Ubuntu.
|
||||
|
||||
List of static IP routes. Each item should be a dict containing the
|
||||
item ``cidr``, and optionally ``gateway``, ``table`` and ``options``.
|
||||
``cidr`` is the CIDR representation of the route's destination. ``gateway``
|
||||
@ -334,11 +336,15 @@ The following attributes are supported:
|
||||
``bond_lacp_rate``
|
||||
For bond interfaces, the lacp_rate to use for the bond.
|
||||
``ethtool_opts``
|
||||
.. note:: ``ethtool_opts`` is not currently supported on Ubuntu.
|
||||
|
||||
Physical network interface options to apply with ``ethtool``. When used on
|
||||
bond and bridge interfaces, settings apply to underlying interfaces. This
|
||||
should be a string of arguments passed to the ``ethtool`` utility, for
|
||||
example ``"-G ${DEVICE} rx 8192 tx 8192"``.
|
||||
``zone``
|
||||
.. note:: ``zone`` is not currently supported on Ubuntu.
|
||||
|
||||
The name of ``firewalld`` zone to be attached to network interface.
|
||||
|
||||
IP Addresses
|
||||
|
@ -240,7 +240,7 @@ Alternatively, this can be added using the following commands::
|
||||
|
||||
sudo ip l add breth1 type bridge
|
||||
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 set eth1 up
|
||||
sudo ip l set eth1 master breth1
|
||||
|
571
kayobe/plugins/filter/networkd.py
Normal file
571
kayobe/plugins/filter/networkd.py
Normal 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,
|
||||
}
|
748
kayobe/tests/unit/plugins/filter/test_networkd.py
Normal file
748
kayobe/tests/unit/plugins/filter/test_networkd.py
Normal 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)
|
@ -16,11 +16,8 @@ controller_extra_network_interfaces:
|
||||
- test_net_eth_vlan
|
||||
- test_net_bridge
|
||||
- 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_vlan
|
||||
{% endif %}
|
||||
|
||||
# dummy2: Ethernet interface.
|
||||
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_vlan: 43
|
||||
|
||||
{% if ansible_os_family != 'Debian' %}
|
||||
# bond0: bond with slaves dummy5, dummy6.
|
||||
test_net_bond_cidr: 192.168.38.0/24
|
||||
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_interface: "{% raw %}{{ test_net_bond_interface }}.{{ test_net_bond_vlan_vlan }}{% endraw %}"
|
||||
test_net_bond_vlan_vlan: 44
|
||||
{% endif %}
|
||||
|
||||
# Define a software RAID device consisting of two loopback devices.
|
||||
controller_mdadm_arrays:
|
||||
|
@ -15,13 +15,6 @@ def _is_dnf():
|
||||
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):
|
||||
interface = host.interface('dummy2')
|
||||
assert interface.exists
|
||||
@ -59,21 +52,21 @@ def test_network_bridge_vlan(host):
|
||||
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):
|
||||
interface = host.interface('bond0')
|
||||
assert interface.exists
|
||||
assert '192.168.38.1' in interface.addresses
|
||||
sys_slaves = host.check_output('cat /sys/class/net/bond0/bonding/slaves')
|
||||
slaves = ['dummy5', 'dummy6']
|
||||
assert sys_slaves == " ".join(slaves)
|
||||
# Ordering is not guaranteed, so compare sets.
|
||||
sys_slaves = set(sys_slaves.split())
|
||||
slaves = set(['dummy5', 'dummy6'])
|
||||
assert sys_slaves == slaves
|
||||
for slave in slaves:
|
||||
interface = host.interface(slave)
|
||||
assert interface.exists
|
||||
assert not interface.addresses
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _supports_bonds(), reason="Bonding no worky on Ubuntu")
|
||||
def test_network_bond_vlan(host):
|
||||
interface = host.interface('bond0.44')
|
||||
assert interface.exists
|
||||
|
@ -1,6 +1,8 @@
|
||||
---
|
||||
- src: ahuffman.resolv
|
||||
version: 1.3.1
|
||||
- src: stackhpc.systemd_networkd
|
||||
version: v1.0.1
|
||||
- src: jriguera.configdrive
|
||||
# There are no versioned releases of this role.
|
||||
version: 8438592c84585c86e62ae07e526d3da53629b377
|
||||
|
@ -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 }}"
|
@ -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 }}"
|
@ -6,4 +6,19 @@
|
||||
- "ip l add {{ bridge_interface }} type bridge"
|
||||
- "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 }}"
|
||||
|
Loading…
Reference in New Issue
Block a user