Support configuring VLANs with systemd-networkd syntax
This allows operators to configure arbitrarily named VLAN interfaces using systemd-networkd. Story: 2010266 Task: 46178 Change-Id: I666d7011bde0050ebc509b427c1d4f5a66b6231a
This commit is contained in:
parent
4a3f88694e
commit
6d7b8812ae
@ -343,6 +343,9 @@ The following attributes are supported:
|
|||||||
|
|
||||||
``interface``
|
``interface``
|
||||||
The name of the network interface attached to the network.
|
The name of the network interface attached to the network.
|
||||||
|
``parent``
|
||||||
|
The name of the parent interface, when configuring a VLAN interface using
|
||||||
|
``systemd-networkd`` syntax.
|
||||||
``bootproto``
|
``bootproto``
|
||||||
Boot protocol for the interface. Valid values are ``static`` and ``dhcp``.
|
Boot protocol for the interface. Valid values are ``static`` and ``dhcp``.
|
||||||
The default is ``static``. When set to ``dhcp``, an external DHCP server
|
The default is ``static``. When set to ``dhcp``, an external DHCP server
|
||||||
@ -473,8 +476,9 @@ Configuring VLAN Interfaces
|
|||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
A VLAN interface may be configured by setting the ``interface`` attribute of a
|
A VLAN interface may be configured by setting the ``interface`` attribute of a
|
||||||
network to the name of the VLAN interface. The interface name must be of the
|
network to the name of the VLAN interface. The interface name must normally be
|
||||||
form ``<parent interface>.<VLAN ID>``.
|
of the form ``<parent interface>.<VLAN ID>`` to ensure compatibility with all
|
||||||
|
supported host operating systems.
|
||||||
|
|
||||||
To configure a network called ``example`` with a VLAN interface with a parent
|
To configure a network called ``example`` with a VLAN interface with a parent
|
||||||
interface of ``eth2`` for VLAN ``123``:
|
interface of ``eth2`` for VLAN ``123``:
|
||||||
@ -491,6 +495,16 @@ To keep the configuration DRY, reference the network's ``vlan`` attribute:
|
|||||||
|
|
||||||
example_interface: "eth2.{{ example_vlan }}"
|
example_interface: "eth2.{{ example_vlan }}"
|
||||||
|
|
||||||
|
Alternatively, when using Ubuntu as a host operating system, VLAN interfaces
|
||||||
|
can be named arbitrarily using syntax supported by ``systemd-networkd``. In
|
||||||
|
this case, a ``parent`` attribute must specify the underlying interface:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
:caption: ``inventory/group_vars/<group>/network-interfaces``
|
||||||
|
|
||||||
|
example_interface: "myvlan{{ example_vlan }}"
|
||||||
|
example_parent: "eth2"
|
||||||
|
|
||||||
Ethernet interfaces, bridges, and bond master interfaces may all be parents to
|
Ethernet interfaces, bridges, and bond master interfaces may all be parents to
|
||||||
a VLAN interface.
|
a VLAN interface.
|
||||||
|
|
||||||
|
@ -123,7 +123,11 @@ class ActionModule(ActionBase):
|
|||||||
# tagged interface may be shared between these networks.
|
# tagged interface may be shared between these networks.
|
||||||
vlan = self._templar.template("{{ '%s' | net_vlan }}" %
|
vlan = self._templar.template("{{ '%s' | net_vlan }}" %
|
||||||
net_name)
|
net_name)
|
||||||
if vlan and iface.endswith(".%s" % vlan):
|
parent = self._templar.template("{{ '%s' | net_parent }}" %
|
||||||
|
net_name)
|
||||||
|
if vlan and parent:
|
||||||
|
iface = parent
|
||||||
|
elif vlan and iface.endswith(".%s" % vlan):
|
||||||
iface = iface.replace(".%s" % vlan, "")
|
iface = iface.replace(".%s" % vlan, "")
|
||||||
return iface
|
return iface
|
||||||
elif required:
|
elif required:
|
||||||
|
@ -612,7 +612,8 @@ def networkd_networks(context, names, inventory_hostname=None):
|
|||||||
inventory_hostname)
|
inventory_hostname)
|
||||||
vlan = networks.net_vlan(context, name, inventory_hostname)
|
vlan = networks.net_vlan(context, name, inventory_hostname)
|
||||||
mtu = networks.net_mtu(context, name, inventory_hostname)
|
mtu = networks.net_mtu(context, name, inventory_hostname)
|
||||||
parent = networks.get_vlan_parent(device, vlan)
|
parent = networks.get_vlan_parent(
|
||||||
|
context, name, device, vlan, inventory_hostname)
|
||||||
vlan_interfaces = interface_to_vlans.setdefault(parent, [])
|
vlan_interfaces = interface_to_vlans.setdefault(parent, [])
|
||||||
vlan_interfaces.append({"device": device, "mtu": mtu})
|
vlan_interfaces.append({"device": device, "mtu": mtu})
|
||||||
|
|
||||||
|
@ -106,7 +106,8 @@ def get_ovs_veths(context, names, inventory_hostname):
|
|||||||
# tagged interface may be shared between these networks.
|
# tagged interface may be shared between these networks.
|
||||||
vlan = net_vlan(context, name, inventory_hostname)
|
vlan = net_vlan(context, name, inventory_hostname)
|
||||||
if vlan:
|
if vlan:
|
||||||
parent_or_device = get_vlan_parent(device, vlan)
|
parent_or_device = get_vlan_parent(
|
||||||
|
context, name, device, vlan, inventory_hostname)
|
||||||
else:
|
else:
|
||||||
parent_or_device = device
|
parent_or_device = device
|
||||||
if parent_or_device in bridge_interfaces:
|
if parent_or_device in bridge_interfaces:
|
||||||
@ -131,14 +132,21 @@ def get_ovs_veths(context, names, inventory_hostname):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_vlan_parent(device, vlan):
|
def get_vlan_parent(context, name, device, vlan, inventory_hostname):
|
||||||
"""Return the parent interface of a VLAN subinterface.
|
"""Return the parent interface of a VLAN subinterface.
|
||||||
|
|
||||||
|
:param context: a Jinja2 Context object.
|
||||||
|
:param name: name of the network.
|
||||||
:param device: VLAN interface name.
|
:param device: VLAN interface name.
|
||||||
:param vlan: VLAN ID.
|
:param vlan: VLAN ID.
|
||||||
|
:param inventory_hostname: Ansible inventory hostname.
|
||||||
:returns: parent interface name.
|
:returns: parent interface name.
|
||||||
|
:raises: ansible.errors.AnsibleFilterError
|
||||||
"""
|
"""
|
||||||
return re.sub(r'\.{}$'.format(vlan), '', device)
|
parent = net_parent(context, name, inventory_hostname)
|
||||||
|
if not parent:
|
||||||
|
parent = re.sub(r'\.{}$'.format(vlan), '', device)
|
||||||
|
return parent
|
||||||
|
|
||||||
|
|
||||||
@jinja2.pass_context
|
@jinja2.pass_context
|
||||||
@ -190,6 +198,11 @@ def net_mask(context, name, inventory_hostname=None):
|
|||||||
return str(netaddr.IPNetwork(cidr).netmask) if cidr is not None else None
|
return str(netaddr.IPNetwork(cidr).netmask) if cidr is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
@jinja2.pass_context
|
||||||
|
def net_parent(context, name, inventory_hostname=None):
|
||||||
|
return net_attr(context, name, 'parent', inventory_hostname)
|
||||||
|
|
||||||
|
|
||||||
@jinja2.pass_context
|
@jinja2.pass_context
|
||||||
def net_prefix(context, name, inventory_hostname=None):
|
def net_prefix(context, name, inventory_hostname=None):
|
||||||
cidr = net_cidr(context, name, inventory_hostname)
|
cidr = net_cidr(context, name, inventory_hostname)
|
||||||
@ -545,10 +558,15 @@ def net_is_vlan(context, name, inventory_hostname=None):
|
|||||||
|
|
||||||
@jinja2.pass_context
|
@jinja2.pass_context
|
||||||
def net_is_vlan_interface(context, name, inventory_hostname=None):
|
def net_is_vlan_interface(context, name, inventory_hostname=None):
|
||||||
device = get_and_validate_interface(context, name, inventory_hostname)
|
parent = net_parent(context, name, inventory_hostname)
|
||||||
# Use a heuristic to match conventional VLAN names, ending with a
|
vlan = net_vlan(context, name, inventory_hostname)
|
||||||
# period and a numerical extension to an interface name
|
if parent and vlan:
|
||||||
return re.match(r"^[a-zA-Z0-9_\-]+\.[1-9][\d]{0,3}$", device)
|
return True
|
||||||
|
else:
|
||||||
|
device = get_and_validate_interface(context, name, inventory_hostname)
|
||||||
|
# Use a heuristic to match conventional VLAN names, ending with a
|
||||||
|
# period and a numerical extension to an interface name
|
||||||
|
return re.match(r"^[a-zA-Z0-9_\-]+\.[1-9][\d]{0,3}$", device)
|
||||||
|
|
||||||
|
|
||||||
@jinja2.pass_context
|
@jinja2.pass_context
|
||||||
@ -600,7 +618,10 @@ def net_configdrive_network_device(context, name, inventory_hostname=None):
|
|||||||
bootproto = net_bootproto(context, name, inventory_hostname)
|
bootproto = net_bootproto(context, name, inventory_hostname)
|
||||||
mtu = net_mtu(context, name, inventory_hostname)
|
mtu = net_mtu(context, name, inventory_hostname)
|
||||||
vlan = net_vlan(context, name, inventory_hostname)
|
vlan = net_vlan(context, name, inventory_hostname)
|
||||||
if vlan and '.' in device:
|
parent = net_parent(context, name, inventory_hostname)
|
||||||
|
if vlan and parent:
|
||||||
|
backend = parent
|
||||||
|
elif vlan and '.' in device:
|
||||||
backend = [device.split('.')[0]]
|
backend = [device.split('.')[0]]
|
||||||
else:
|
else:
|
||||||
backend = None
|
backend = None
|
||||||
@ -678,6 +699,7 @@ def get_filters():
|
|||||||
'net_fqdn': _make_attr_filter('fqdn'),
|
'net_fqdn': _make_attr_filter('fqdn'),
|
||||||
'net_ip': net_ip,
|
'net_ip': net_ip,
|
||||||
'net_interface': net_interface,
|
'net_interface': net_interface,
|
||||||
|
'net_parent': net_parent,
|
||||||
'net_no_ip': net_no_ip,
|
'net_no_ip': net_no_ip,
|
||||||
'net_cidr': net_cidr,
|
'net_cidr': net_cidr,
|
||||||
'net_mask': net_mask,
|
'net_mask': net_mask,
|
||||||
|
@ -25,6 +25,11 @@ def _net_interface(context, name):
|
|||||||
return context.get(name + '_interface')
|
return context.get(name + '_interface')
|
||||||
|
|
||||||
|
|
||||||
|
@jinja2.pass_context
|
||||||
|
def _net_parent(context, name):
|
||||||
|
return context.get(name + '_parent')
|
||||||
|
|
||||||
|
|
||||||
@jinja2.pass_context
|
@jinja2.pass_context
|
||||||
def _net_vlan(context, name):
|
def _net_vlan(context, name):
|
||||||
return context.get(name + '_vlan')
|
return context.get(name + '_vlan')
|
||||||
@ -42,6 +47,7 @@ class FakeTemplar(object):
|
|||||||
self.variables = variables
|
self.variables = variables
|
||||||
self.env = jinja2.Environment()
|
self.env = jinja2.Environment()
|
||||||
self.env.filters['net_interface'] = _net_interface
|
self.env.filters['net_interface'] = _net_interface
|
||||||
|
self.env.filters['net_parent'] = _net_parent
|
||||||
self.env.filters['net_vlan'] = _net_vlan
|
self.env.filters['net_vlan'] = _net_vlan
|
||||||
self.env.filters['net_select_bridges'] = _net_select_bridges
|
self.env.filters['net_select_bridges'] = _net_select_bridges
|
||||||
|
|
||||||
|
@ -42,6 +42,15 @@ class BaseNetworkdTest(unittest.TestCase):
|
|||||||
# net4: bond on bond0 with members eth0 and eth1.
|
# net4: bond on bond0 with members eth0 and eth1.
|
||||||
"net4_interface": "bond0",
|
"net4_interface": "bond0",
|
||||||
"net4_bond_slaves": ["eth0", "eth1"],
|
"net4_bond_slaves": ["eth0", "eth1"],
|
||||||
|
# net5: VLAN on vlan.5 with VLAN 5 on interface eth0.
|
||||||
|
"net5_interface": "vlan.5",
|
||||||
|
"net5_parent": "eth0",
|
||||||
|
"net5_vlan": 5,
|
||||||
|
# net6: VLAN on vlan6 with VLAN 6 on interface eth0.
|
||||||
|
"net6_interface": "vlan6",
|
||||||
|
"net6_parent": "eth0",
|
||||||
|
"net6_vlan": 6,
|
||||||
|
# NOTE(priteau): net7 is used in test_veth_on_vlan
|
||||||
# Prefix for networkd config file names.
|
# Prefix for networkd config file names.
|
||||||
"networkd_prefix": "50-kayobe-",
|
"networkd_prefix": "50-kayobe-",
|
||||||
# Veth pair patch link prefix and suffix.
|
# Veth pair patch link prefix and suffix.
|
||||||
@ -132,6 +141,52 @@ class TestNetworkdNetDevs(BaseNetworkdTest):
|
|||||||
self.assertRaises(errors.AnsibleFilterError,
|
self.assertRaises(errors.AnsibleFilterError,
|
||||||
networkd.networkd_netdevs, self.context, ["net2"])
|
networkd.networkd_netdevs, self.context, ["net2"])
|
||||||
|
|
||||||
|
def test_vlan_with_parent(self):
|
||||||
|
devs = networkd.networkd_netdevs(self.context,
|
||||||
|
["net1", "net2", "net5", "net6"])
|
||||||
|
expected = {
|
||||||
|
"50-kayobe-eth0.2": [
|
||||||
|
{
|
||||||
|
"NetDev": [
|
||||||
|
{"Name": "eth0.2"},
|
||||||
|
{"Kind": "vlan"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"VLAN": [
|
||||||
|
{"Id": 2},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"50-kayobe-vlan.5": [
|
||||||
|
{
|
||||||
|
"NetDev": [
|
||||||
|
{"Name": "vlan.5"},
|
||||||
|
{"Kind": "vlan"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"VLAN": [
|
||||||
|
{"Id": 5},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"50-kayobe-vlan6": [
|
||||||
|
{
|
||||||
|
"NetDev": [
|
||||||
|
{"Name": "vlan6"},
|
||||||
|
{"Kind": "vlan"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"VLAN": [
|
||||||
|
{"Id": 6},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
self.assertEqual(expected, devs)
|
||||||
|
|
||||||
def test_bridge(self):
|
def test_bridge(self):
|
||||||
devs = networkd.networkd_netdevs(self.context, ["net3"])
|
devs = networkd.networkd_netdevs(self.context, ["net3"])
|
||||||
expected = {
|
expected = {
|
||||||
@ -437,7 +492,8 @@ class TestNetworkdNetworks(BaseNetworkdTest):
|
|||||||
self.assertEqual(expected, nets)
|
self.assertEqual(expected, nets)
|
||||||
|
|
||||||
def test_vlan_with_parent(self):
|
def test_vlan_with_parent(self):
|
||||||
nets = networkd.networkd_networks(self.context, ["net1", "net2"])
|
nets = networkd.networkd_networks(self.context,
|
||||||
|
["net1", "net2", "net5", "net6"])
|
||||||
expected = {
|
expected = {
|
||||||
"50-kayobe-eth0": [
|
"50-kayobe-eth0": [
|
||||||
{
|
{
|
||||||
@ -449,6 +505,8 @@ class TestNetworkdNetworks(BaseNetworkdTest):
|
|||||||
"Network": [
|
"Network": [
|
||||||
{"Address": "1.2.3.4/24"},
|
{"Address": "1.2.3.4/24"},
|
||||||
{"VLAN": "eth0.2"},
|
{"VLAN": "eth0.2"},
|
||||||
|
{"VLAN": "vlan.5"},
|
||||||
|
{"VLAN": "vlan6"},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -458,6 +516,20 @@ class TestNetworkdNetworks(BaseNetworkdTest):
|
|||||||
{"Name": "eth0.2"}
|
{"Name": "eth0.2"}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
"50-kayobe-vlan.5": [
|
||||||
|
{
|
||||||
|
"Match": [
|
||||||
|
{"Name": "vlan.5"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"50-kayobe-vlan6": [
|
||||||
|
{
|
||||||
|
"Match": [
|
||||||
|
{"Name": "vlan6"}
|
||||||
|
]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
self.assertEqual(expected, nets)
|
self.assertEqual(expected, nets)
|
||||||
@ -946,11 +1018,11 @@ class TestNetworkdNetworks(BaseNetworkdTest):
|
|||||||
# needs patching to OVS. The parent interface is a bridge, and the veth
|
# needs patching to OVS. The parent interface is a bridge, and the veth
|
||||||
# pair should be plugged into it.
|
# pair should be plugged into it.
|
||||||
self._update_context({
|
self._update_context({
|
||||||
"provision_wl_net_name": "net5",
|
"provision_wl_net_name": "net7",
|
||||||
"net3_bridge_ports": [],
|
"net3_bridge_ports": [],
|
||||||
"net5_interface": "br0.42",
|
"net7_interface": "br0.42",
|
||||||
"net5_vlan": 42})
|
"net7_vlan": 42})
|
||||||
nets = networkd.networkd_networks(self.context, ["net3", "net5"])
|
nets = networkd.networkd_networks(self.context, ["net3", "net7"])
|
||||||
expected = {
|
expected = {
|
||||||
"50-kayobe-br0": [
|
"50-kayobe-br0": [
|
||||||
{
|
{
|
||||||
|
@ -19,6 +19,9 @@ controller_extra_network_interfaces:
|
|||||||
- test_net_bond
|
- test_net_bond
|
||||||
- test_net_bond_vlan
|
- test_net_bond_vlan
|
||||||
- test_net_bridge_noip
|
- test_net_bridge_noip
|
||||||
|
{% if ansible_os_family == "Debian" %}
|
||||||
|
- test_net_systemd_vlan
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
# Custom IP routing tables.
|
# Custom IP routing tables.
|
||||||
network_route_tables:
|
network_route_tables:
|
||||||
@ -79,6 +82,15 @@ test_net_bridge_noip_interface: br1
|
|||||||
test_net_bridge_noip_bridge_ports: [dummy7]
|
test_net_bridge_noip_bridge_ports: [dummy7]
|
||||||
test_net_bridge_noip_no_ip: true
|
test_net_bridge_noip_no_ip: true
|
||||||
|
|
||||||
|
{% if ansible_os_family == "Debian" %}
|
||||||
|
# vlan45: VLAN interface of bond0 using systemd-networkd style
|
||||||
|
test_net_systemd_vlan_cidr: 192.168.41.0/24
|
||||||
|
test_net_systemd_vlan_interface: "vlan{% raw %}{{ test_net_systemd_vlan_vlan }}{% endraw %}"
|
||||||
|
test_net_systemd_vlan_parent: "{% raw %}{{ test_net_bond_interface }}{% endraw %}"
|
||||||
|
test_net_systemd_vlan_vlan: 45
|
||||||
|
test_net_systemd_vlan_zone: public
|
||||||
|
{% 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:
|
||||||
- name: md0
|
- name: md0
|
||||||
|
@ -98,6 +98,14 @@ def test_network_bridge_no_ip(host):
|
|||||||
assert not '192.168.40.1' in interface.addresses
|
assert not '192.168.40.1' in interface.addresses
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not _is_apt(),
|
||||||
|
reason="systemd-networkd VLANs only supported on Ubuntu")
|
||||||
|
def test_network_systemd_vlan(host):
|
||||||
|
interface = host.interface('vlan45')
|
||||||
|
assert interface.exists
|
||||||
|
assert '192.168.41.1' in interface.addresses
|
||||||
|
|
||||||
|
|
||||||
def test_additional_user_account(host):
|
def test_additional_user_account(host):
|
||||||
user = host.user("kayobe-test-user")
|
user = host.user("kayobe-test-user")
|
||||||
assert user.name == "kayobe-test-user"
|
assert user.name == "kayobe-test-user"
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds support for configuring arbitrarily named VLAN interfaces using
|
||||||
|
``systemd-networkd``. See `story 2010266
|
||||||
|
<https://storyboard.openstack.org/#!/story/2010266>`__ for details.
|
Loading…
Reference in New Issue
Block a user