# 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",
        # List of route tables.
        "network_route_tables": [],
    }

    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_eth(self):
        devs = networkd.networkd_netdevs(self.context, ["net1"])
        expected = {}
        self.assertEqual(expected, devs)

    def test_eth_untagged_vlan(self):
        # An untagged interface on a network with a VLAN defined should not
        # create a VLAN subinterface.
        self._update_context({"net1_vlan": 42})
        devs = networkd.networkd_netdevs(self.context, ["net1"])
        expected = {}
        self.assertEqual(expected, 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_bridge_untagged_vlan(self):
        # A bridge on a network with a VLAN defined should not create a VLAN
        # subinterface.
        self._update_context({"net3_vlan": 42})
        devs = networkd.networkd_netdevs(self.context, ["net3"])
        expected = {
            "50-kayobe-br0": [
                {
                    "NetDev": [
                        {"Name": "br0"},
                        {"Kind": "bridge"},
                    ]
                },
            ]
        }
        self.assertEqual(expected, devs)

    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_bond_untagged_vlan(self):
        # A bridge on a network with a VLAN defined should not create a VLAN
        # subinterface.
        self._update_context({"net4_vlan": 42})
        devs = networkd.networkd_netdevs(self.context, ["net4"])
        expected = {
            "50-kayobe-bond0": [
                {
                    "NetDev": [
                        {"Name": "bond0"},
                        {"Kind": "bond"},
                    ]
                },
            ]
        }
        self.assertEqual(expected, devs)

    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_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"},
                    ]
                },
            ]
        }
        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",
                    "table": 42,
                },
            ],
            "net1_rules": [
                {
                    "from": "1.2.7.0/24",
                    "table": 43,
                },
                {
                    "to": "1.2.8.0/24",
                    "table": 44,
                    "priority": 1,
                },
            ],
            "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"},
                        {"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"},
                        {"Table": 42},
                    ]
                },
                {
                    "RoutingPolicyRule": [
                        {"From": "1.2.7.0/24"},
                        {"Table": 43},
                    ]
                },
                {
                    "RoutingPolicyRule": [
                        {"To": "1.2.8.0/24"},
                        {"Priority": 1},
                        {"Table": 44},
                    ]
                },
            ]
        }
        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": [
                {
                    "Match": [
                        {"Name": "eth0"}
                    ],
                },
                {
                    "Network": [
                        {"VLAN": "eth0.2"},
                    ]
                },
            ],
            "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"},
                        {"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_with_bridge_port_vlan(self):
        # Test the case where one of the bridge ports has a VLAN subinterface.
        self._update_context({
            "net2_interface": "eth1.2",
        })
        nets = networkd.networkd_networks(self.context, ["net2", "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"},
                        {"VLAN": "eth1.2"}
                    ]
                },
            ],
            "50-kayobe-eth1.2": [
                {
                    "Match": [
                        {"Name": "eth1.2"}
                    ]
                },
            ],
        }
        self.assertEqual(expected, nets)

    def test_bridge_with_bridge_port_vlan_net(self):
        # Test the case where one of the bridge ports has a VLAN subinterface,
        # and is also a Kayobe network (here eth0 is net1).
        self._update_context({
            "net1_ips": None,
        })
        nets = networkd.networkd_networks(self.context,
                                          ["net1", "net2", "net3"])
        expected = {
            "50-kayobe-br0": [
                {
                    "Match": [
                        {"Name": "br0"}
                    ]
                },
            ],
            "50-kayobe-eth0": [
                {
                    "Match": [
                        {"Name": "eth0"}
                    ]
                },
                {
                    "Network": [
                        {"Bridge": "br0"},
                        {"VLAN": "eth0.2"}
                    ]
                },
            ],
            "50-kayobe-eth0.2": [
                {
                    "Match": [
                        {"Name": "eth0.2"}
                    ]
                },
            ],
            "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_with_bond_member_vlan(self):
        # Test the case where one of the bond members has a VLAN subinterface.
        self._update_context({
            "net2_interface": "eth1.2",
        })
        nets = networkd.networkd_networks(self.context, ["net2", "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"},
                        {"VLAN": "eth1.2"},
                    ]
                },
            ],
            "50-kayobe-eth1.2": [
                {
                    "Match": [
                        {"Name": "eth1.2"}
                    ]
                },
            ],
        }
        self.assertEqual(expected, nets)

    def test_bond_with_bond_member_vlan_net(self):
        # Test the case where one of the bond members has a VLAN subinterface,
        # and is also a Kayobe network (here eth0 is net1).
        self._update_context({
            "net1_ips": None,
        })
        nets = networkd.networkd_networks(self.context,
                                          ["net1", "net2", "net4"])
        expected = {
            "50-kayobe-bond0": [
                {
                    "Match": [
                        {"Name": "bond0"}
                    ]
                },
            ],
            "50-kayobe-eth0": [
                {
                    "Match": [
                        {"Name": "eth0"}
                    ]
                },
                {
                    "Network": [
                        {"Bond": "bond0"},
                        {"VLAN": "eth0.2"},
                    ]
                },
            ],
            "50-kayobe-eth0.2": [
                {
                    "Match": [
                        {"Name": "eth0.2"}
                    ]
                },
            ],
            "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_with_mtu(self):
        self._update_context({"external_net_names": ["net3"],
                              "net3_bridge_ports": [],
                              "net3_mtu": 1400})
        nets = networkd.networkd_networks(self.context, ["net3"])
        expected = {
            "50-kayobe-br0": [
                {
                    "Match": [
                        {"Name": "br0"}
                    ]
                },
                {
                    "Link": [
                        {"MTUBytes": 1400},
                    ]
                },
            ],
            "50-kayobe-p-br0-phy": [
                {
                    "Match": [
                        {"Name": "p-br0-phy"}
                    ]
                },
                {
                    "Network": [
                        {"Bridge": "br0"},
                    ]
                },
                {
                    "Link": [
                        {"MTUBytes": 1400},
                    ]
                },
            ],
            "50-kayobe-p-br0-ovs": [
                {
                    "Match": [
                        {"Name": "p-br0-ovs"}
                    ]
                },
                {
                    "Network": [
                        {"ConfigureWithoutCarrier": "true"},
                    ]
                },
                {
                    "Link": [
                        {"MTUBytes": 1400},
                    ]
                },
            ],
        }
        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"},
                    ]
                },
            ]
        }
        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"},
                        {"VLAN": "eth0.2"},
                    ]
                },
            ],
            "50-kayobe-eth0.2": [
                {
                    "Match": [
                        {"Name": "eth0.2"}
                    ]
                },
            ]
        }
        self.assertEqual(expected, nets)
