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``
|
||||
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``
|
||||
Boot protocol for the interface. Valid values are ``static`` and ``dhcp``.
|
||||
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
|
||||
network to the name of the VLAN interface. The interface name must be of the
|
||||
form ``<parent interface>.<VLAN ID>``.
|
||||
network to the name of the VLAN interface. The interface name must normally be
|
||||
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
|
||||
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 }}"
|
||||
|
||||
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
|
||||
a VLAN interface.
|
||||
|
||||
|
@ -123,7 +123,11 @@ class ActionModule(ActionBase):
|
||||
# tagged interface may be shared between these networks.
|
||||
vlan = self._templar.template("{{ '%s' | net_vlan }}" %
|
||||
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, "")
|
||||
return iface
|
||||
elif required:
|
||||
|
@ -612,7 +612,8 @@ def networkd_networks(context, names, inventory_hostname=None):
|
||||
inventory_hostname)
|
||||
vlan = networks.net_vlan(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.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.
|
||||
vlan = net_vlan(context, name, inventory_hostname)
|
||||
if vlan:
|
||||
parent_or_device = get_vlan_parent(device, vlan)
|
||||
parent_or_device = get_vlan_parent(
|
||||
context, name, device, vlan, inventory_hostname)
|
||||
else:
|
||||
parent_or_device = device
|
||||
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.
|
||||
|
||||
:param context: a Jinja2 Context object.
|
||||
:param name: name of the network.
|
||||
:param device: VLAN interface name.
|
||||
:param vlan: VLAN ID.
|
||||
:param inventory_hostname: Ansible inventory hostname.
|
||||
: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
|
||||
@ -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
|
||||
|
||||
|
||||
@jinja2.pass_context
|
||||
def net_parent(context, name, inventory_hostname=None):
|
||||
return net_attr(context, name, 'parent', inventory_hostname)
|
||||
|
||||
|
||||
@jinja2.pass_context
|
||||
def net_prefix(context, name, inventory_hostname=None):
|
||||
cidr = net_cidr(context, name, inventory_hostname)
|
||||
@ -545,10 +558,15 @@ def net_is_vlan(context, name, inventory_hostname=None):
|
||||
|
||||
@jinja2.pass_context
|
||||
def net_is_vlan_interface(context, name, inventory_hostname=None):
|
||||
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)
|
||||
parent = net_parent(context, name, inventory_hostname)
|
||||
vlan = net_vlan(context, name, inventory_hostname)
|
||||
if parent and vlan:
|
||||
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
|
||||
@ -600,7 +618,10 @@ def net_configdrive_network_device(context, name, inventory_hostname=None):
|
||||
bootproto = net_bootproto(context, name, inventory_hostname)
|
||||
mtu = net_mtu(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]]
|
||||
else:
|
||||
backend = None
|
||||
@ -678,6 +699,7 @@ def get_filters():
|
||||
'net_fqdn': _make_attr_filter('fqdn'),
|
||||
'net_ip': net_ip,
|
||||
'net_interface': net_interface,
|
||||
'net_parent': net_parent,
|
||||
'net_no_ip': net_no_ip,
|
||||
'net_cidr': net_cidr,
|
||||
'net_mask': net_mask,
|
||||
|
@ -25,6 +25,11 @@ def _net_interface(context, name):
|
||||
return context.get(name + '_interface')
|
||||
|
||||
|
||||
@jinja2.pass_context
|
||||
def _net_parent(context, name):
|
||||
return context.get(name + '_parent')
|
||||
|
||||
|
||||
@jinja2.pass_context
|
||||
def _net_vlan(context, name):
|
||||
return context.get(name + '_vlan')
|
||||
@ -42,6 +47,7 @@ class FakeTemplar(object):
|
||||
self.variables = variables
|
||||
self.env = jinja2.Environment()
|
||||
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_select_bridges'] = _net_select_bridges
|
||||
|
||||
|
@ -42,6 +42,15 @@ class BaseNetworkdTest(unittest.TestCase):
|
||||
# net4: bond on bond0 with members eth0 and eth1.
|
||||
"net4_interface": "bond0",
|
||||
"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.
|
||||
"networkd_prefix": "50-kayobe-",
|
||||
# Veth pair patch link prefix and suffix.
|
||||
@ -132,6 +141,52 @@ class TestNetworkdNetDevs(BaseNetworkdTest):
|
||||
self.assertRaises(errors.AnsibleFilterError,
|
||||
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):
|
||||
devs = networkd.networkd_netdevs(self.context, ["net3"])
|
||||
expected = {
|
||||
@ -437,7 +492,8 @@ class TestNetworkdNetworks(BaseNetworkdTest):
|
||||
self.assertEqual(expected, nets)
|
||||
|
||||
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 = {
|
||||
"50-kayobe-eth0": [
|
||||
{
|
||||
@ -449,6 +505,8 @@ class TestNetworkdNetworks(BaseNetworkdTest):
|
||||
"Network": [
|
||||
{"Address": "1.2.3.4/24"},
|
||||
{"VLAN": "eth0.2"},
|
||||
{"VLAN": "vlan.5"},
|
||||
{"VLAN": "vlan6"},
|
||||
]
|
||||
},
|
||||
],
|
||||
@ -458,6 +516,20 @@ class TestNetworkdNetworks(BaseNetworkdTest):
|
||||
{"Name": "eth0.2"}
|
||||
]
|
||||
},
|
||||
],
|
||||
"50-kayobe-vlan.5": [
|
||||
{
|
||||
"Match": [
|
||||
{"Name": "vlan.5"}
|
||||
]
|
||||
},
|
||||
],
|
||||
"50-kayobe-vlan6": [
|
||||
{
|
||||
"Match": [
|
||||
{"Name": "vlan6"}
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
self.assertEqual(expected, nets)
|
||||
@ -946,11 +1018,11 @@ class TestNetworkdNetworks(BaseNetworkdTest):
|
||||
# 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",
|
||||
"provision_wl_net_name": "net7",
|
||||
"net3_bridge_ports": [],
|
||||
"net5_interface": "br0.42",
|
||||
"net5_vlan": 42})
|
||||
nets = networkd.networkd_networks(self.context, ["net3", "net5"])
|
||||
"net7_interface": "br0.42",
|
||||
"net7_vlan": 42})
|
||||
nets = networkd.networkd_networks(self.context, ["net3", "net7"])
|
||||
expected = {
|
||||
"50-kayobe-br0": [
|
||||
{
|
||||
|
@ -19,6 +19,9 @@ controller_extra_network_interfaces:
|
||||
- test_net_bond
|
||||
- test_net_bond_vlan
|
||||
- test_net_bridge_noip
|
||||
{% if ansible_os_family == "Debian" %}
|
||||
- test_net_systemd_vlan
|
||||
{% endif %}
|
||||
|
||||
# Custom IP routing 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_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.
|
||||
controller_mdadm_arrays:
|
||||
- name: md0
|
||||
|
@ -98,6 +98,14 @@ def test_network_bridge_no_ip(host):
|
||||
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):
|
||||
user = host.user("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