#
# Common tests for netplan generator
#
# Copyright (C) 2018 Canonical, Ltd.
# Author: Mathieu Trudel-Lapierre <mathieu.trudel.lapierre@canonical.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import textwrap

from .base import UDEV_NO_MAC_RULE, TestBase, ND_DHCP4, ND_DHCP6, ND_DHCPYES, ND_EMPTY, NM_MANAGED, NM_UNMANAGED


class TestNetworkd(TestBase):
    '''networkd output'''

    def test_optional(self):
        self.generate('''network:
  version: 2
  ethernets:
    eth0:
      dhcp6: true
      optional: true''')
        self.assert_networkd({'eth0.network': '''[Match]
Name=eth0

[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6

[DHCP]
RouteMetric=100
UseMTU=true
'''})
        self.assert_networkd_udev(None)

    def config_with_optional_addresses(self, eth_name, optional_addresses):
        return '''network:
  version: 2
  ethernets:
    {}:
      dhcp6: true
      optional-addresses: {}'''.format(eth_name, optional_addresses)

    def test_optional_addresses(self):
        eth_name = self.eth_name()
        self.generate(self.config_with_optional_addresses(eth_name, '["dhcp4"]'))
        self.assertEqual(self.get_optional_addresses(eth_name), set(["dhcp4"]))

    def test_optional_addresses_multiple(self):
        eth_name = self.eth_name()
        self.generate(self.config_with_optional_addresses(eth_name, '[dhcp4, ipv4-ll, ipv6-ra, dhcp6, dhcp4, static]'))
        self.assertEqual(
            self.get_optional_addresses(eth_name),
            set(["ipv4-ll", "ipv6-ra", "dhcp4", "dhcp6", "static"]))

    def test_optional_addresses_invalid(self):
        eth_name = self.eth_name()
        err = self.generate(self.config_with_optional_addresses(eth_name, '["invalid"]'), expect_fail=True)
        self.assertIn('invalid value for optional-addresses', err)

    def test_activation_mode_off(self):
        self.generate('''network:
  version: 2
  ethernets:
    eth0:
      dhcp6: true
      activation-mode: off''')
        self.assert_networkd({'eth0.network': '''[Match]
Name=eth0

[Link]
ActivationPolicy=always-down

[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6

[DHCP]
RouteMetric=100
UseMTU=true
'''})
        self.assert_networkd_udev(None)

    def test_activation_mode_manual(self):
        self.generate('''network:
  version: 2
  ethernets:
    eth0:
      dhcp6: true
      activation-mode: manual''')
        self.assert_networkd({'eth0.network': '''[Match]
Name=eth0

[Link]
ActivationPolicy=manual

[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6

[DHCP]
RouteMetric=100
UseMTU=true
'''})
        self.assert_networkd_udev(None)

    def test_mtu_all(self):
        self.generate(textwrap.dedent("""
            network:
              version: 2
              ethernets:
                eth1:
                  mtu: 9000
                  dhcp4: n
                  ipv6-mtu: 2000
              bonds:
                bond0:
                  interfaces:
                  - eth1
                  mtu: 9000
              vlans:
                bond0.108:
                  link: bond0
                  id: 108"""))
        self.assert_networkd({
            'bond0.108.netdev': '[NetDev]\nName=bond0.108\nKind=vlan\n\n[VLAN]\nId=108\n',
            'bond0.108.network': '''[Match]
Name=bond0.108

[Network]
LinkLocalAddressing=ipv6
ConfigureWithoutCarrier=yes
''',
            'bond0.netdev': '[NetDev]\nName=bond0\nMTUBytes=9000\nKind=bond\n',
            'bond0.network': '''[Match]
Name=bond0

[Link]
MTUBytes=9000

[Network]
LinkLocalAddressing=ipv6
ConfigureWithoutCarrier=yes
VLAN=bond0.108
''',
            'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMTUBytes=9000\n',
            'eth1.network': '''[Match]
Name=eth1

[Link]
MTUBytes=9000

[Network]
LinkLocalAddressing=no
IPv6MTUBytes=2000
Bond=bond0
'''
        })
        self.assert_networkd_udev(None)

    def test_eth_global_renderer(self):
        self.generate('''network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      dhcp4: true''')

        self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'})
        self.assert_nm(None)
        self.assert_nm_udev(NM_UNMANAGED % 'eth0')
        # should not allow NM to manage everything
        self.assertFalse(os.path.exists(self.nm_enable_all_conf))

    def test_eth_type_renderer(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    renderer: networkd
    eth0:
      dhcp4: true''')

        self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'})
        self.assert_nm(None)
        # should allow NM to manage everything else
        self.assertTrue(os.path.exists(self.nm_enable_all_conf))
        self.assert_nm_udev(NM_UNMANAGED % 'eth0')

    def test_eth_def_renderer(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    renderer: NetworkManager
    eth0:
      renderer: networkd
      dhcp4: true''')

        self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'})
        self.assert_networkd_udev(None)
        self.assert_nm(None)
        self.assert_nm_udev(NM_UNMANAGED % 'eth0')

    def test_eth_dhcp6(self):
        self.generate('''network:
  version: 2
  ethernets:
    eth0: {dhcp6: true}''')
        self.assert_networkd({'eth0.network': ND_DHCP6 % 'eth0'})

    def test_eth_dhcp6_no_accept_ra(self):
        self.generate('''network:
  version: 2
  ethernets:
    eth0:
      dhcp6: true
      accept-ra: n''')
        self.assert_networkd({'eth0.network': '''[Match]
Name=eth0

[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6
IPv6AcceptRA=no

[DHCP]
RouteMetric=100
UseMTU=true
'''})

    def test_eth_dhcp6_accept_ra(self):
        self.generate('''network:
  version: 2
  ethernets:
    eth0:
      dhcp6: true
      accept-ra: yes''')
        self.assert_networkd({'eth0.network': '''[Match]
Name=eth0

[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6
IPv6AcceptRA=yes

[DHCP]
RouteMetric=100
UseMTU=true
'''})

    def test_eth_dhcp6_accept_ra_unset(self):
        self.generate('''network:
  version: 2
  ethernets:
    eth0:
      dhcp6: true''')
        self.assert_networkd({'eth0.network': '''[Match]
Name=eth0

[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6

[DHCP]
RouteMetric=100
UseMTU=true
'''})

    def test_eth_dhcp4_and_6(self):
        self.generate('''network:
  version: 2
  ethernets:
    eth0: {dhcp4: true, dhcp6: true}''')
        self.assert_networkd({'eth0.network': ND_DHCPYES % 'eth0'})

    def test_eth_manual_addresses(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      addresses:
        - 192.168.14.2/24
        - 2001:FFfe::1/64''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=ipv6
Address=192.168.14.2/24
Address=2001:FFfe::1/64
'''})

    def test_eth_manual_addresses_dhcp(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      dhcp4: yes
      addresses:
        - 192.168.14.2/24
        - 2001:FFfe::1/64''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
DHCP=ipv4
LinkLocalAddressing=ipv6
Address=192.168.14.2/24
Address=2001:FFfe::1/64

[DHCP]
RouteMetric=100
UseMTU=true
'''})

    def test_eth_address_option_lifetime_zero(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      addresses:
        - 192.168.14.2/24:
            lifetime: 0
        - 2001:FFfe::1/64''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=ipv6
Address=2001:FFfe::1/64

[Address]
Address=192.168.14.2/24
PreferredLifetime=0
'''})

    def test_eth_address_option_lifetime_forever(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      addresses:
        - 192.168.14.2/24:
            lifetime: forever
        - 2001:FFfe::1/64''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=ipv6
Address=2001:FFfe::1/64

[Address]
Address=192.168.14.2/24
PreferredLifetime=forever
'''})

    def test_eth_address_option_label(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      addresses:
        - 192.168.14.2/24:
            label: test-label
        - 2001:FFfe::1/64''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=ipv6
Address=2001:FFfe::1/64

[Address]
Address=192.168.14.2/24
Label=test-label
'''})

    def test_eth_address_option_multi_pass(self):
        self.generate('''network:
  version: 2
  bridges:
    br0:
      interfaces: [engreen]
  ethernets:
    engreen:
      addresses:
        - 192.168.14.2/24:
            label: test-label
        - 2001:FFfe::1/64:
            label: ip6''')

        self.assert_networkd({
            'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=no
Bridge=br0

[Address]
Address=192.168.14.2/24
Label=test-label

[Address]
Address=2001:FFfe::1/64
Label=ip6
''',
            'br0.network': ND_EMPTY % ('br0', 'ipv6'),
            'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n'})

    def test_bond_arp_ip_targets_multi_pass(self):
        self.generate('''network:
  bonds:
    bond0:
      interfaces:
      - eno1
      parameters:
        arp-ip-targets:
          - 10.10.10.10
          - 20.20.20.20
  ethernets:
    eno1: {}
  version: 2''')
        self.assert_networkd({'bond0.netdev': '''[NetDev]
Name=bond0
Kind=bond

[Bond]
ARPIPTargets=10.10.10.10 20.20.20.20
''',
                              'bond0.network': '''[Match]
Name=bond0

[Network]
LinkLocalAddressing=ipv6
ConfigureWithoutCarrier=yes
''',
                              'eno1.network': '''[Match]
Name=eno1

[Network]
LinkLocalAddressing=no
Bond=bond0
'''})

    def test_dhcp_critical_true(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      critical: yes
''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=ipv6
KeepConfiguration=true
'''})

    def test_dhcp_identifier_mac(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      dhcp4: yes
      dhcp-identifier: mac
''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
DHCP=ipv4
LinkLocalAddressing=ipv6

[DHCP]
ClientIdentifier=mac
RouteMetric=100
UseMTU=true
'''})

    def test_dhcp_identifier_duid(self):
        # This option should be silently ignored, since it's the default
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      dhcp4: yes
      dhcp-identifier: duid
''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
DHCP=ipv4
LinkLocalAddressing=ipv6

[DHCP]
RouteMetric=100
UseMTU=true
'''})

    def test_eth_ipv6_privacy(self):
        self.generate('''network:
  version: 2
  ethernets:
    eth0:
      dhcp6: true
      ipv6-privacy: true''')
        self.assert_networkd({'eth0.network': '''[Match]
Name=eth0

[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6
IPv6PrivacyExtensions=yes

[DHCP]
RouteMetric=100
UseMTU=true
'''})

    def test_eth_ignore_carrier_true(self):
        self.generate('''network:
  version: 2
  ethernets:
    eth0:
      ignore-carrier: yes
''')

        self.assert_networkd({'eth0.network': '''[Match]
Name=eth0

[Network]
LinkLocalAddressing=ipv6
ConfigureWithoutCarrier=yes
'''})

    def test_gateway4(self):
        err = self.generate('''network:
  version: 2
  ethernets:
    engreen:
      addresses: ["192.168.14.2/24"]
      gateway4: 192.168.14.1''')
        self.assertIn("`gateway4` has been deprecated, use default routes instead.", err)
        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=ipv6
Address=192.168.14.2/24
Gateway=192.168.14.1
'''})

    def test_gateway6(self):
        err = self.generate('''network:
  version: 2
  ethernets:
    engreen:
      addresses: ["2001:FFfe::1/64"]
      gateway6: 2001:FFfe::2''')
        self.assertIn("`gateway6` has been deprecated, use default routes instead.", err)
        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=ipv6
Address=2001:FFfe::1/64
Gateway=2001:FFfe::2
'''})

    def test_gateway_full(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      addresses: ["192.168.14.2/24", "2001:FFfe::1/64"]
      gateway4: 192.168.14.1
      gateway6: "2001:FFfe::2"''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=ipv6
Address=192.168.14.2/24
Address=2001:FFfe::1/64
Gateway=192.168.14.1
Gateway=2001:FFfe::2
'''})

    def test_gateways_multi_pass(self):
        self.generate('''network:
  version: 2
  bridges:
    br0:
      interfaces: [engreen]
  ethernets:
    engreen:
      addresses: ["192.168.14.2/24", "2001:FFfe::1/64"]
      gateway4: 192.168.14.1
      gateway6: "2001:FFfe::2"''')

        self.assert_networkd({
            'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=no
Address=192.168.14.2/24
Address=2001:FFfe::1/64
Gateway=192.168.14.1
Gateway=2001:FFfe::2
Bridge=br0
''',
            'br0.network': ND_EMPTY % ('br0', 'ipv6'),
            'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n'})

    def test_nameserver(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      addresses: ["192.168.14.2/24"]
      nameservers:
        addresses: [1.2.3.4, "1234::FFFF"]
    enblue:
      addresses: ["192.168.1.3/24"]
      nameservers:
        search: [lab, kitchen]
        addresses: [8.8.8.8]''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=ipv6
Address=192.168.14.2/24
DNS=1.2.3.4
DNS=1234::FFFF
''',
                              'enblue.network': '''[Match]
Name=enblue

[Network]
LinkLocalAddressing=ipv6
Address=192.168.1.3/24
DNS=8.8.8.8
Domains=lab kitchen
'''})

    def test_link_local_all(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      dhcp4: yes
      dhcp6: yes
      link-local: [ ipv4, ipv6 ]
          ''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
DHCP=yes
LinkLocalAddressing=yes

[DHCP]
RouteMetric=100
UseMTU=true
'''})

    def test_link_local_ipv4(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      dhcp4: yes
      dhcp6: yes
      link-local: [ ipv4 ]
          ''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
DHCP=yes
LinkLocalAddressing=ipv4

[DHCP]
RouteMetric=100
UseMTU=true
'''})

    def test_link_local_ipv6(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      dhcp4: yes
      dhcp6: yes
      link-local: [ ipv6 ]
          ''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
DHCP=yes
LinkLocalAddressing=ipv6

[DHCP]
RouteMetric=100
UseMTU=true
'''})

    def test_link_local_disabled(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      dhcp4: yes
      dhcp6: yes
      link-local: [ ]
          ''')

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
DHCP=yes
LinkLocalAddressing=no

[DHCP]
RouteMetric=100
UseMTU=true
'''})

    def test_ip6_addr_gen_mode(self):
        self.generate('''network:
  version: 2
  renderer: networkd
  ethernets:
    engreen:
      dhcp6: yes
      ipv6-address-generation: stable-privacy
    enblue:
      dhcp6: yes
      ipv6-address-generation: eui64''')
        self.assert_networkd({'engreen.network': '''[Match]\nName=engreen\n
[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6
IPv6Token=prefixstable

[DHCP]
RouteMetric=100
UseMTU=true
''',
                              'enblue.network': '''[Match]\nName=enblue\n
[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6

[DHCP]
RouteMetric=100
UseMTU=true
'''})

    def test_ip6_addr_gen_token(self):
        self.generate('''network:
  version: 2
  renderer: networkd
  ethernets:
    engreen:
      dhcp6: yes
      ipv6-address-token: ::2
    enblue:
      dhcp6: yes
      ipv6-address-token: "::2"''')
        self.assert_networkd({'engreen.network': '''[Match]\nName=engreen\n
[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6
IPv6Token=static:::2

[DHCP]
RouteMetric=100
UseMTU=true
''',
                              'enblue.network': '''[Match]\nName=enblue\n
[Network]
DHCP=ipv6
LinkLocalAddressing=ipv6
IPv6Token=static:::2

[DHCP]
RouteMetric=100
UseMTU=true
'''})

    def test_nd_udev_rules_escaped(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    def1:
      match:
        driver: "abc\\"xyz\\n0\\n\\n1"
      set-name: "eth\\"\\n\\nxyz\\n0"''', skip_generated_yaml_validation=True)

        self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('abc\\"xyz\\n0\\n\\n1', 'eth\\"\\n\\nxyz\\n0'))})

    def test_nd_file_paths_escaped(self):
        self.generate('''network:
  version: 2
  ethernets:
    "abc/../../xyz0":
      match:
        driver: "drv"
      set-name: "eth123"''')

        self.assert_networkd_udev({'abc%2F..%2F..%2Fxyz0.rules': (UDEV_NO_MAC_RULE % ('drv', 'eth123'))})
        self.assert_networkd({'abc%2F..%2F..%2Fxyz0.network': '''[Match]\nDriver=drv
Name=eth123

[Network]
LinkLocalAddressing=ipv6
''',
                              'abc%2F..%2F..%2Fxyz0.link': '''[Match]\nDriver=drv\n
[Link]
Name=eth123
WakeOnLan=off
'''})

        self.generate('''network:
  version: 2
  wifis:
    "abc/../../xyz0":
      dhcp4: true
      access-points:
        "mywifi":
          password: "aaaaaaaa"''')

        self.assert_wpa_supplicant("abc%2F..%2F..%2Fxyz0", """ctrl_interface=/run/wpa_supplicant

network={
  ssid=P"mywifi"
  key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
  ieee80211w=1
  psk="aaaaaaaa"
}
""")


class TestNetworkManager(TestBase):

    def test_empty_conf(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager''')
        self.assert_nm({})

    def test_mtu_all(self):
        self.generate(textwrap.dedent("""
            network:
              version: 2
              renderer: NetworkManager
              ethernets:
                eth1:
                  mtu: 1280
                  dhcp4: n
              bonds:
                bond0:
                  interfaces:
                  - eth1
                  mtu: 9000
              vlans:
                bond0.108:
                  link: bond0
                  id: 108"""))
        self.assert_nm({
            'bond0.108': '''[connection]
id=netplan-bond0.108
type=vlan
interface-name=bond0.108

[vlan]
id=108
parent=bond0

[ipv4]
method=link-local

[ipv6]
method=ignore
''',
            'bond0': '''[connection]
id=netplan-bond0
type=bond
interface-name=bond0

[ethernet]
mtu=9000

[ipv4]
method=link-local

[ipv6]
method=ignore
''',
            'eth1': '''[connection]
id=netplan-eth1
type=ethernet
interface-name=eth1
slave-type=bond # wokeignore:rule=slave
master=bond0 # wokeignore:rule=master

[ethernet]
wake-on-lan=0
mtu=1280

[ipv4]
method=link-local

[ipv6]
method=ignore
''',
        })

    def test_activation_mode_off(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    eth0:
      activation-mode: off''', expect_fail=True)

    def test_activation_mode_manual(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    eth0:
      activation-mode: manual''')

        self.assert_nm({'eth0': '''[connection]
id=netplan-eth0
type=ethernet
autoconnect=false
interface-name=eth0

[ethernet]
wake-on-lan=0

[ipv4]
method=link-local

[ipv6]
method=ignore
'''})
        self.assert_networkd({})
        self.assert_nm_udev(NM_MANAGED % 'eth0')

    def test_ipv6_mtu(self):
        self.generate(textwrap.dedent("""
            network:
              version: 2
              renderer: NetworkManager
              ethernets:
                eth1:
                  mtu: 9000
                  ipv6-mtu: 2000"""), expect_fail=True)

    def test_eth_global_renderer(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    eth0:
      dhcp4: true''')

        self.assert_nm({'eth0': '''[connection]
id=netplan-eth0
type=ethernet
interface-name=eth0

[ethernet]
wake-on-lan=0

[ipv4]
method=auto

[ipv6]
method=ignore
'''})
        self.assert_networkd({})
        self.assert_nm_udev(NM_MANAGED % 'eth0')

    def test_eth_type_renderer(self):
        self.generate('''network:
  version: 2
  renderer: networkd
  ethernets:
    renderer: NetworkManager
    eth0:
      dhcp4: true''')

        self.assert_nm({'eth0': '''[connection]
id=netplan-eth0
type=ethernet
interface-name=eth0

[ethernet]
wake-on-lan=0

[ipv4]
method=auto

[ipv6]
method=ignore
'''})
        self.assert_networkd({})
        self.assert_nm_udev(NM_MANAGED % 'eth0')

    def test_eth_def_renderer(self):
        self.generate('''network:
  version: 2
  renderer: networkd
  ethernets:
    renderer: networkd
    eth0:
      renderer: NetworkManager''')

        self.assert_nm({'eth0': '''[connection]
id=netplan-eth0
type=ethernet
interface-name=eth0

[ethernet]
wake-on-lan=0

[ipv4]
method=link-local

[ipv6]
method=ignore
'''})
        self.assert_networkd({})
        self.assert_nm_udev(NM_MANAGED % 'eth0')

    def test_global_renderer_only(self):
        self.generate(None, confs={'01-default-nm.yaml': 'network: {version: 2, renderer: NetworkManager}'})
        # should allow NM to manage everything else
        self.assertTrue(os.path.exists(self.nm_enable_all_conf))
        # but not configure anything else
        self.assert_nm(None, None)
        self.assert_networkd({})
        self.assert_nm_udev(None)

    def test_eth_dhcp6(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    eth0: {dhcp6: true}''')
        self.assert_nm({'eth0': '''[connection]
id=netplan-eth0
type=ethernet
interface-name=eth0

[ethernet]
wake-on-lan=0

[ipv4]
method=link-local

[ipv6]
method=auto
ip6-privacy=0
'''})

    def test_eth_dhcp4_and_6(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    eth0: {dhcp4: true, dhcp6: true}''')
        self.assert_nm({'eth0': '''[connection]
id=netplan-eth0
type=ethernet
interface-name=eth0

[ethernet]
wake-on-lan=0

[ipv4]
method=auto

[ipv6]
method=auto
ip6-privacy=0
'''})

    def test_ip6_addr_gen_mode(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    engreen:
      dhcp6: yes
      ipv6-address-generation: stable-privacy
    enblue:
      dhcp6: yes
      ipv6-address-generation: eui64''')
        self.assert_nm({'engreen': '''[connection]
id=netplan-engreen
type=ethernet
interface-name=engreen

[ethernet]
wake-on-lan=0

[ipv4]
method=link-local

[ipv6]
method=auto
addr-gen-mode=1
ip6-privacy=0
''',
                        'enblue': '''[connection]
id=netplan-enblue
type=ethernet
interface-name=enblue

[ethernet]
wake-on-lan=0

[ipv4]
method=link-local

[ipv6]
method=auto
addr-gen-mode=0
ip6-privacy=0
'''})

    def test_ip6_addr_gen_token(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    engreen:
      dhcp6: yes
      ipv6-address-token: ::2
    enblue:
      dhcp6: yes
      ipv6-address-token: "::2"''')
        self.assert_nm({'engreen': '''[connection]
id=netplan-engreen
type=ethernet
interface-name=engreen

[ethernet]
wake-on-lan=0

[ipv4]
method=link-local

[ipv6]
method=auto
addr-gen-mode=0
token=::2
ip6-privacy=0
''',
                        'enblue': '''[connection]
id=netplan-enblue
type=ethernet
interface-name=enblue

[ethernet]
wake-on-lan=0

[ipv4]
method=link-local

[ipv6]
method=auto
addr-gen-mode=0
token=::2
ip6-privacy=0
'''})

    def test_eth_manual_addresses(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    engreen:
      addresses:
        - 192.168.14.2/24
        - 172.16.0.4/16
        - 2001:FFfe::1/64''')

        self.assert_nm({'engreen': '''[connection]
id=netplan-engreen
type=ethernet
interface-name=engreen

[ethernet]
wake-on-lan=0

[ipv4]
method=manual
address1=192.168.14.2/24
address2=172.16.0.4/16

[ipv6]
method=manual
address1=2001:FFfe::1/64
ip6-privacy=0
'''})
        self.assert_networkd({})
        self.assert_nm_udev(NM_MANAGED % 'engreen')

    def test_eth_manual_addresses_dhcp(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    engreen:
      dhcp4: yes
      addresses:
        - 192.168.14.2/24
        - 2001:FFfe::1/64''')

        self.assert_nm({'engreen': '''[connection]
id=netplan-engreen
type=ethernet
interface-name=engreen

[ethernet]
wake-on-lan=0

[ipv4]
method=auto
address1=192.168.14.2/24

[ipv6]
method=manual
address1=2001:FFfe::1/64
ip6-privacy=0
'''})

    def test_eth_ipv6_privacy(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    eth0: {dhcp6: true, ipv6-privacy: true}''')
        self.assert_nm({'eth0': '''[connection]
id=netplan-eth0
type=ethernet
interface-name=eth0

[ethernet]
wake-on-lan=0

[ipv4]
method=link-local

[ipv6]
method=auto
ip6-privacy=2
'''})

    def test_gateway(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    engreen:
      addresses: ["192.168.14.2/24", "2001:FFfe::1/64"]
      gateway4: 192.168.14.1
      gateway6: 2001:FFfe::2''')

        self.assert_nm({'engreen': '''[connection]
id=netplan-engreen
type=ethernet
interface-name=engreen

[ethernet]
wake-on-lan=0

[ipv4]
method=manual
address1=192.168.14.2/24
gateway=192.168.14.1

[ipv6]
method=manual
address1=2001:FFfe::1/64
ip6-privacy=0
gateway=2001:FFfe::2
'''})

    def test_nameserver(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    engreen:
      addresses: ["192.168.14.2/24"]
      nameservers:
        addresses: [1.2.3.4, 2.3.4.5, "1234::FFFF"]
        search: [lab, kitchen]
    enblue:
      addresses: ["192.168.1.3/24"]
      nameservers:
        addresses: [8.8.8.8]''')

        self.assert_nm({'engreen': '''[connection]
id=netplan-engreen
type=ethernet
interface-name=engreen

[ethernet]
wake-on-lan=0

[ipv4]
method=manual
address1=192.168.14.2/24
dns=1.2.3.4;2.3.4.5;
dns-search=lab;kitchen;

[ipv6]
method=manual
ip6-privacy=0
dns=1234::FFFF;
dns-search=lab;kitchen;
''',
                        'enblue': '''[connection]
id=netplan-enblue
type=ethernet
interface-name=enblue

[ethernet]
wake-on-lan=0

[ipv4]
method=manual
address1=192.168.1.3/24
dns=8.8.8.8;

[ipv6]
method=ignore
'''})

    def test_nm_udev_rules_escaped(self):
        self.generate('''network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      match:
        name: "eth\\"0"
      dhcp4: true''')
        self.assert_nm_udev(NM_UNMANAGED % 'eth\\"0')

        self.generate('''network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      match:
        name: "eth0"
      set-name: "eth\\n0"
      dhcp4: true''', skip_generated_yaml_validation=True)
        self.assert_nm_udev(NM_UNMANAGED % 'eth\\n0' + NM_UNMANAGED % 'eth0')

    def test_nm_file_paths_escaped(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    "abc/../../xyz0":
      match:
        driver: "drv"
      set-name: "eth123"''')

        self.assert_nm({'abc%2F..%2F..%2Fxyz0': '''[connection]
id=netplan-abc/../../xyz0
type=ethernet
interface-name=eth123

[ethernet]
wake-on-lan=0

[ipv4]
method=link-local

[ipv6]
method=ignore
'''})


class TestForwardDeclaration(TestBase):

    def test_fwdecl_bridge_on_bond(self):
        self.generate('''network:
  version: 2
  bridges:
    br0:
      interfaces: ['bond0']
      dhcp4: true
  bonds:
    bond0:
      interfaces: ['eth0', 'eth1']
  ethernets:
    eth0:
      match:
        macaddress: 00:01:02:03:04:05
      set-name: eth0
    eth1:
      match:
        macaddress: 02:01:02:03:04:05
      set-name: eth1
''')

        self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n',
                              'br0.network': '''[Match]
Name=br0

[Network]
DHCP=ipv4
LinkLocalAddressing=ipv6
ConfigureWithoutCarrier=yes

[DHCP]
RouteMetric=100
UseMTU=true
''',
                              'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n',
                              'bond0.network': '''[Match]
Name=bond0

[Network]
LinkLocalAddressing=no
ConfigureWithoutCarrier=yes
Bridge=br0
''',
                              'eth0.link': '''[Match]
PermanentMACAddress=00:01:02:03:04:05

[Link]
Name=eth0
WakeOnLan=off
''',
                              'eth0.network': '''[Match]
PermanentMACAddress=00:01:02:03:04:05
Name=eth0

[Network]
LinkLocalAddressing=no
Bond=bond0
''',
                              'eth1.link': '''[Match]
PermanentMACAddress=02:01:02:03:04:05

[Link]
Name=eth1
WakeOnLan=off
''',
                              'eth1.network': '''[Match]
PermanentMACAddress=02:01:02:03:04:05
Name=eth1

[Network]
LinkLocalAddressing=no
Bond=bond0
'''})

    def test_fwdecl_feature_blend(self):
        self.generate('''network:
  version: 2
  vlans:
    vlan1:
      link: 'br0'
      id: 1
      dhcp4: true
  bridges:
    br0:
      interfaces: ['bond0', 'eth2']
      parameters:
        path-cost:
          eth2: 1000
          bond0: 8888
  bonds:
    bond0:
      interfaces: ['eth0', 'br1']
  ethernets:
    eth0:
      match:
        macaddress: 00:01:02:03:04:05
      set-name: eth0
  bridges:
    br1:
      interfaces: ['eth1']
  ethernets:
    eth1:
      match:
        macaddress: 02:01:02:03:04:05
      set-name: eth1
    eth2:
      match:
        name: eth2
''', skip_generated_yaml_validation=True)
        # XXX: We need to skeip the generated YAML validation, as the pyYAML
        #      parser overrides the duplicate "ethernets"/"bridges" keys, while
        #      the netplan C YAML parser merges them into the netdef

        self.assert_networkd({'vlan1.netdev': '[NetDev]\nName=vlan1\nKind=vlan\n\n'
                                              '[VLAN]\nId=1\n',
                              'vlan1.network': '''[Match]
Name=vlan1

[Network]
DHCP=ipv4
LinkLocalAddressing=ipv6
ConfigureWithoutCarrier=yes

[DHCP]
RouteMetric=100
UseMTU=true
''',
                              'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n\n'
                                            '[Bridge]\nSTP=true\n',
                              'br0.network': '''[Match]
Name=br0

[Network]
LinkLocalAddressing=ipv6
ConfigureWithoutCarrier=yes
VLAN=vlan1
''',
                              'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n',
                              'bond0.network': '''[Match]
Name=bond0

[Network]
LinkLocalAddressing=no
ConfigureWithoutCarrier=yes
Bridge=br0

[Bridge]
Cost=8888
''',
                              'eth2.network': '[Match]\nName=eth2\n\n'
                                              '[Network]\nLinkLocalAddressing=no\nBridge=br0\n\n'
                                              '[Bridge]\nCost=1000\n',
                              'br1.netdev': '[NetDev]\nName=br1\nKind=bridge\n',
                              'br1.network': '''[Match]
Name=br1

[Network]
LinkLocalAddressing=no
ConfigureWithoutCarrier=yes
Bond=bond0
''',
                              'eth0.link': '''[Match]
PermanentMACAddress=00:01:02:03:04:05

[Link]
Name=eth0
WakeOnLan=off
''',
                              'eth0.network': '''[Match]
PermanentMACAddress=00:01:02:03:04:05
Name=eth0

[Network]
LinkLocalAddressing=no
Bond=bond0
''',
                              'eth1.link': '''[Match]
PermanentMACAddress=02:01:02:03:04:05

[Link]
Name=eth1
WakeOnLan=off
''',
                              'eth1.network': '''[Match]
PermanentMACAddress=02:01:02:03:04:05
Name=eth1

[Network]
LinkLocalAddressing=no
Bridge=br1
'''})

    def test_fwdecl_will_not_lead_to_duplicates(self):
        '''
        When the parser needs more than one pass we shouldn't
        emit duplicated configuration
        Testcase for LP: #2007682'''

        self.generate('''network:
  bonds:
    aggi:
      routing-policy:
        - table: 1
          from: 1.2.3.4
      nameservers:
        addresses:
          - 1.2.3.4
        search:
          - example.com
      interfaces:
        - eth0
  ethernets:
    eth0:
      ignore-carrier: true
''')

        self.assert_networkd({'aggi.netdev': '[NetDev]\nName=aggi\nKind=bond\n',
                              'aggi.network': '''[Match]
Name=aggi

[Network]
LinkLocalAddressing=ipv6
DNS=1.2.3.4
Domains=example.com
ConfigureWithoutCarrier=yes

[RoutingPolicyRule]
From=1.2.3.4
Table=1
''',
                              'eth0.network': ND_EMPTY % ('eth0', 'no') + 'Bond=aggi\n'})

    def test_check_parser_second_pass_will_not_lead_to_duplicate_access_point(self):
        '''
        When the parser needs more than one pass we shouldn't
        try to load the same access-point again from wifi devices.
        Testcase for LP: #1809994'''

        self.generate('''network:
  bridges:
    br0:
      interfaces:
        - eth0
  ethernets:
    eth0:
      dhcp4: false
  wifis:
    wlan0:
      dhcp4: true
      access-points:
        "mywifi":
          password: "aaaaaaaa"
''')


class TestMerging(TestBase):
    '''multiple *.yaml merging'''

    def test_global_backend(self):
        self.generate('''network:
  version: 2
  renderer: NetworkManager
  ethernets:
    engreen:
      dhcp4: y''',
                      confs={'backend': 'network:\n  renderer: networkd'})

        self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'})
        self.assert_nm(None)
        self.assert_nm_udev(NM_UNMANAGED % 'engreen')

    def test_add_def(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      dhcp4: true''',
                      confs={'blue': '''network:
  version: 2
  ethernets:
    enblue:
      dhcp4: true'''})

        self.assert_networkd({'enblue.network': ND_DHCP4 % 'enblue',
                              'engreen.network': ND_DHCP4 % 'engreen'})
        # Skip on codecov.io; GLib changed hashtable elements ordering between
        # releases, so we can't depend on the exact order.
        # TODO: (cyphermox) turn this into an "assert_in_nm()" function.
        if "CODECOV_TOKEN" not in os.environ:  # pragma: nocover
            self.assert_nm(None)
        self.assert_nm_udev(NM_UNMANAGED % 'engreen' + NM_UNMANAGED % 'enblue')

    def test_change_def(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      wakeonlan: true
      dhcp4: false''',
                      confs={'green-dhcp': '''network:
  version: 2
  ethernets:
    engreen:
      dhcp4: true'''})

        self.assert_networkd({'engreen.link': '[Match]\nOriginalName=engreen\n\n[Link]\nWakeOnLan=magic\n',
                              'engreen.network': ND_DHCP4 % 'engreen'})

    def test_cleanup_old_config(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen: {dhcp4: true}
    enyellow: {renderer: NetworkManager}''',
                      confs={'blue': '''network:
  version: 2
  ethernets:
    enblue:
      dhcp4: true'''})

        os.unlink(os.path.join(self.confdir, 'blue.yaml'))
        self.generate('''network:
  version: 2
  ethernets:
    engreen: {dhcp4: true}''')

        self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'})
        self.assert_nm(None)
        self.assert_nm_udev(NM_UNMANAGED % 'engreen')

    def test_ref(self):
        self.generate('''network:
  version: 2
  ethernets:
    eno1: {}
    switchports:
      match:
        driver: yayroute''',
                      confs={'bridges': '''network:
  version: 2
  bridges:
    br0:
      interfaces: [eno1, switchports]
      dhcp4: true'''}, skip_generated_yaml_validation=True)
        # XXX: We need to skip the generated YAML validation, as the 'bridges'
        #      conf is invalid in itself (missing eno1 & switchports defs) and
        #      can only be parsed if merged with the main YAML

        self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n',
                              'br0.network': '''[Match]
Name=br0

[Network]
DHCP=ipv4
LinkLocalAddressing=ipv6
ConfigureWithoutCarrier=yes

[DHCP]
RouteMetric=100
UseMTU=true
''',
                              'eno1.network': '[Match]\nName=eno1\n\n'
                                              '[Network]\nLinkLocalAddressing=no\nBridge=br0\n',
                              'switchports.network': '[Match]\nDriver=yayroute\n\n'
                                                     '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'})

    def test_def_in_run(self):
        rundir = os.path.join(self.workdir.name, 'run', 'netplan')
        os.makedirs(rundir)
        # override b.yaml definition for enred
        with open(os.path.join(rundir, 'b.yaml'), 'w') as f:
            f.write('''network:
  version: 2
  ethernets: {enred: {dhcp4: true}}''')

        # append new definition for enblue
        with open(os.path.join(rundir, 'c.yaml'), 'w') as f:
            f.write('''network:
  version: 2
  ethernets: {enblue: {dhcp4: true}}''')

        self.generate('''network:
  version: 2
  ethernets:
    engreen: {dhcp4: true}''', confs={'b': '''network:
  version: 2
  ethernets: {enred: {wakeonlan: true}}'''})

        # b.yaml in /run/ should completely shadow b.yaml in /etc, thus no enred.link
        self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen',
                              'enred.network': ND_DHCP4 % 'enred',
                              'enblue.network': ND_DHCP4 % 'enblue'})

    def test_def_in_lib(self):
        libdir = os.path.join(self.workdir.name, 'lib', 'netplan')
        rundir = os.path.join(self.workdir.name, 'run', 'netplan')
        os.makedirs(libdir)
        os.makedirs(rundir)
        # b.yaml is in /etc/netplan too which should have precedence
        with open(os.path.join(libdir, 'b.yaml'), 'w') as f:
            f.write('''network:
  version: 2
  ethernets: {notme: {dhcp4: true}}''')

        # /run should trump /lib too
        with open(os.path.join(libdir, 'c.yaml'), 'w') as f:
            f.write('''network:
  version: 2
  ethernets: {alsonot: {dhcp4: true}}''')
        with open(os.path.join(rundir, 'c.yaml'), 'w') as f:
            f.write('''network:
  version: 2
  ethernets: {enyellow: {dhcp4: true}}''')

        # this should be considered
        with open(os.path.join(libdir, 'd.yaml'), 'w') as f:
            f.write('''network:
  version: 2
  ethernets: {enblue: {dhcp4: true}}''')

        self.generate('''network:
  version: 2
  ethernets:
    engreen: {dhcp4: true}''', confs={'b': '''network:
  version: 2
  ethernets: {enred: {wakeonlan: true}}'''})

        self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen',
                              'enred.link': '[Match]\nOriginalName=enred\n\n[Link]\nWakeOnLan=magic\n',
                              'enred.network': '''[Match]
Name=enred

[Network]
LinkLocalAddressing=ipv6
''',
                              'enyellow.network': ND_DHCP4 % 'enyellow',
                              'enblue.network': ND_DHCP4 % 'enblue'})

    def test_wifi_access_points_merging(self):
        self.generate('''network:
  version: 2
  wifis:
    wlan0:
      dhcp4: true
      access-points:
        "mywifi":
          password: "aaaaaaaa"''',
                      confs={'newwifi': '''network:
  version: 2
  wifis:
    wlan0:
      dhcp4: true
      access-points:
        "mynewwifi":
          password: "aaaaaaaa"'''})

        self.assert_wpa_supplicant("wlan0", """ctrl_interface=/run/wpa_supplicant

network={
  ssid=P"mynewwifi"
  key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
  ieee80211w=1
  psk="aaaaaaaa"
}
network={
  ssid=P"mywifi"
  key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
  ieee80211w=1
  psk="aaaaaaaa"
}
""")

    def test_wifi_access_points_overwriting(self):
        ''' If we find an AP that is already defined we drop the first one.
            XXX: this test must be removed once we implement support for AP merging
        '''
        self.generate('''network:
  version: 2
  wifis:
    wlan0:
      dhcp4: true
      access-points:
        "mywifi":
          password: "aaaaaaaa"''',
                      confs={'newwifi': '''network:
  version: 2
  wifis:
    wlan0:
      dhcp4: true
      access-points:
        "mywifi":
          password: "bbbbbbbb"'''})

        self.assert_wpa_supplicant("wlan0", """ctrl_interface=/run/wpa_supplicant

network={
  ssid=P"mywifi"
  key_mgmt=WPA-PSK WPA-PSK-SHA256 SAE
  ieee80211w=1
  psk="bbbbbbbb"
}
""")

    def test_gateway6_redefinition_works(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      addresses: [2001:FFfe::1/62]
      gateway6: 2001:FFfe::2''', confs={'b': '''network:
  ethernets:
    engreen:
      gateway6: 2001:FFfe::34'''}, expect_fail=False)

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=ipv6
Address=2001:FFfe::1/62
Gateway=2001:FFfe::34
'''})

    def test_gateway4_redefinition_works(self):
        self.generate('''network:
  version: 2
  ethernets:
    engreen:
      addresses: [192.168.0.1/24]
      gateway4: 192.168.0.123''', confs={'b': '''network:
  ethernets:
    engreen:
      gateway4: 192.168.0.254'''}, expect_fail=False)

        self.assert_networkd({'engreen.network': '''[Match]
Name=engreen

[Network]
LinkLocalAddressing=ipv6
Address=192.168.0.1/24
Gateway=192.168.0.254
'''})
