# This file is part of cloud-init. See LICENSE file for license information.

import copy
import os
import re
from io import StringIO
from textwrap import dedent
from unittest import mock

from cloudinit import distros, helpers, safeyaml, settings, subp, util
from cloudinit.distros.parsers.sys_conf import SysConf
from tests.unittests.helpers import FilesystemMockingTestCase, dir2dict

BASE_NET_CFG = """
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
    address 192.168.1.5
    broadcast 192.168.1.0
    gateway 192.168.1.254
    netmask 255.255.255.0
    network 192.168.0.0

auto eth1
iface eth1 inet dhcp
"""

BASE_NET_CFG_FROM_V2 = """
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
    address 192.168.1.5/24
    gateway 192.168.1.254

auto eth1
iface eth1 inet dhcp
"""

BASE_NET_CFG_IPV6 = """
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
    address 192.168.1.5
    netmask 255.255.255.0
    network 192.168.0.0
    broadcast 192.168.1.0
    gateway 192.168.1.254

iface eth0 inet6 static
    address 2607:f0d0:1002:0011::2
    netmask 64
    gateway 2607:f0d0:1002:0011::1

iface eth1 inet static
    address 192.168.1.6
    netmask 255.255.255.0
    network 192.168.0.0
    broadcast 192.168.1.0
    gateway 192.168.1.254

iface eth1 inet6 static
    address 2607:f0d0:1002:0011::3
    netmask 64
    gateway 2607:f0d0:1002:0011::1
"""

V1_NET_CFG = {
    "config": [
        {
            "name": "eth0",
            "subnets": [
                {
                    "address": "192.168.1.5",
                    "broadcast": "192.168.1.0",
                    "gateway": "192.168.1.254",
                    "netmask": "255.255.255.0",
                    "type": "static",
                }
            ],
            "type": "physical",
        },
        {
            "name": "eth1",
            "subnets": [{"control": "auto", "type": "dhcp4"}],
            "type": "physical",
        },
    ],
    "version": 1,
}

V1_NET_CFG_WITH_DUPS = """\
# same value in interface specific dns and global dns
# should produce single entry in network file
version: 1
config:
    - type: physical
      name: eth0
      subnets:
          - type: static
            address: 192.168.0.102/24
            dns_nameservers: [1.2.3.4]
            dns_search: [test.com]
            interface: eth0
    - type: nameserver
      address: [1.2.3.4]
      search: [test.com]
"""

V1_NET_CFG_OUTPUT = """\
# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
    address 192.168.1.5/24
    broadcast 192.168.1.0
    gateway 192.168.1.254

auto eth1
iface eth1 inet dhcp
"""

V1_NET_CFG_IPV6_OUTPUT = """\
# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet6 static
    address 2607:f0d0:1002:0011::2/64
    gateway 2607:f0d0:1002:0011::1

auto eth1
iface eth1 inet dhcp
"""

V1_NET_CFG_IPV6 = {
    "config": [
        {
            "name": "eth0",
            "subnets": [
                {
                    "address": "2607:f0d0:1002:0011::2",
                    "gateway": "2607:f0d0:1002:0011::1",
                    "netmask": "64",
                    "type": "static6",
                }
            ],
            "type": "physical",
        },
        {
            "name": "eth1",
            "subnets": [{"control": "auto", "type": "dhcp4"}],
            "type": "physical",
        },
    ],
    "version": 1,
}


V1_TO_V2_NET_CFG_OUTPUT = """\
# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    version: 2
    ethernets:
        eth0:
            addresses:
            - 192.168.1.5/24
            gateway4: 192.168.1.254
        eth1:
            dhcp4: true
"""

V1_TO_V2_NET_CFG_IPV6_OUTPUT = """\
# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    version: 2
    ethernets:
        eth0:
            addresses:
            - 2607:f0d0:1002:0011::2/64
            gateway6: 2607:f0d0:1002:0011::1
        eth1:
            dhcp4: true
"""

V2_NET_CFG = {
    "ethernets": {
        "eth7": {"addresses": ["192.168.1.5/24"], "gateway4": "192.168.1.254"},
        "eth9": {"dhcp4": True},
    },
    "version": 2,
}


V2_TO_V2_NET_CFG_OUTPUT = """\
# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    ethernets:
        eth7:
            addresses:
            - 192.168.1.5/24
            gateway4: 192.168.1.254
        eth9:
            dhcp4: true
    version: 2
"""


class WriteBuffer(object):
    def __init__(self):
        self.buffer = StringIO()
        self.mode = None
        self.omode = None

    def write(self, text):
        self.buffer.write(text)

    def __str__(self):
        return self.buffer.getvalue()


class TestNetCfgDistroBase(FilesystemMockingTestCase):
    def setUp(self):
        super(TestNetCfgDistroBase, self).setUp()
        self.add_patch("cloudinit.util.system_is_snappy", "m_snappy")

    def _get_distro(self, dname, renderers=None):
        cls = distros.fetch(dname)
        cfg = settings.CFG_BUILTIN
        cfg["system_info"]["distro"] = dname
        if renderers:
            cfg["system_info"]["network"] = {"renderers": renderers}
        paths = helpers.Paths({})
        return cls(dname, cfg.get("system_info"), paths)

    def assertCfgEquals(self, blob1, blob2):
        b1 = dict(SysConf(blob1.strip().splitlines()))
        b2 = dict(SysConf(blob2.strip().splitlines()))
        self.assertEqual(b1, b2)
        for (k, v) in b1.items():
            self.assertIn(k, b2)
        for (k, v) in b2.items():
            self.assertIn(k, b1)
        for (k, v) in b1.items():
            self.assertEqual(v, b2[k])


class TestNetCfgDistroFreeBSD(TestNetCfgDistroBase):
    def setUp(self):
        super(TestNetCfgDistroFreeBSD, self).setUp()
        self.distro = self._get_distro("freebsd", renderers=["freebsd"])

    def _apply_and_verify_freebsd(
        self, apply_fn, config, expected_cfgs=None, bringup=False
    ):
        if not expected_cfgs:
            raise ValueError("expected_cfg must not be None")

        tmpd = None
        with mock.patch("cloudinit.net.freebsd.available") as m_avail:
            m_avail.return_value = True
            with self.reRooted(tmpd) as tmpd:
                util.ensure_dir("/etc")
                util.ensure_file("/etc/rc.conf")
                util.ensure_file("/etc/resolv.conf")
                apply_fn(config, bringup)

        results = dir2dict(tmpd)
        for cfgpath, expected in expected_cfgs.items():
            print("----------")
            print(expected)
            print("^^^^ expected | rendered VVVVVVV")
            print(results[cfgpath])
            print("----------")
            self.assertEqual(
                set(expected.split("\n")), set(results[cfgpath].split("\n"))
            )
            self.assertEqual(0o644, get_mode(cfgpath, tmpd))

    @mock.patch("cloudinit.net.get_interfaces_by_mac")
    def test_apply_network_config_freebsd_standard(self, ifaces_mac):
        ifaces_mac.return_value = {
            "00:15:5d:4c:73:00": "eth0",
        }
        rc_conf_expected = """\
defaultrouter=192.168.1.254
ifconfig_eth0='192.168.1.5 netmask 255.255.255.0'
ifconfig_eth1=DHCP
"""

        expected_cfgs = {
            "/etc/rc.conf": rc_conf_expected,
            "/etc/resolv.conf": "",
        }
        self._apply_and_verify_freebsd(
            self.distro.apply_network_config,
            V1_NET_CFG,
            expected_cfgs=expected_cfgs.copy(),
        )

    @mock.patch("cloudinit.net.get_interfaces_by_mac")
    def test_apply_network_config_freebsd_ifrename(self, ifaces_mac):
        ifaces_mac.return_value = {
            "00:15:5d:4c:73:00": "vtnet0",
        }
        rc_conf_expected = """\
ifconfig_vtnet0_name=eth0
defaultrouter=192.168.1.254
ifconfig_eth0='192.168.1.5 netmask 255.255.255.0'
ifconfig_eth1=DHCP
"""

        V1_NET_CFG_RENAME = copy.deepcopy(V1_NET_CFG)
        V1_NET_CFG_RENAME["config"][0]["mac_address"] = "00:15:5d:4c:73:00"

        expected_cfgs = {
            "/etc/rc.conf": rc_conf_expected,
            "/etc/resolv.conf": "",
        }
        self._apply_and_verify_freebsd(
            self.distro.apply_network_config,
            V1_NET_CFG_RENAME,
            expected_cfgs=expected_cfgs.copy(),
        )

    @mock.patch("cloudinit.net.get_interfaces_by_mac")
    def test_apply_network_config_freebsd_nameserver(self, ifaces_mac):
        ifaces_mac.return_value = {
            "00:15:5d:4c:73:00": "eth0",
        }

        V1_NET_CFG_DNS = copy.deepcopy(V1_NET_CFG)
        ns = ["1.2.3.4"]
        V1_NET_CFG_DNS["config"][0]["subnets"][0]["dns_nameservers"] = ns
        expected_cfgs = {"/etc/resolv.conf": "nameserver 1.2.3.4\n"}
        self._apply_and_verify_freebsd(
            self.distro.apply_network_config,
            V1_NET_CFG_DNS,
            expected_cfgs=expected_cfgs.copy(),
        )


class TestNetCfgDistroUbuntuEni(TestNetCfgDistroBase):
    def setUp(self):
        super(TestNetCfgDistroUbuntuEni, self).setUp()
        self.distro = self._get_distro("ubuntu", renderers=["eni"])

    def eni_path(self):
        return "/etc/network/interfaces.d/50-cloud-init.cfg"

    def _apply_and_verify_eni(
        self, apply_fn, config, expected_cfgs=None, bringup=False
    ):
        if not expected_cfgs:
            raise ValueError("expected_cfg must not be None")

        tmpd = None
        with mock.patch("cloudinit.net.eni.available") as m_avail:
            m_avail.return_value = True
            with self.reRooted(tmpd) as tmpd:
                apply_fn(config, bringup)

        results = dir2dict(tmpd)
        for cfgpath, expected in expected_cfgs.items():
            print("----------")
            print(expected)
            print("^^^^ expected | rendered VVVVVVV")
            print(results[cfgpath])
            print("----------")
            self.assertEqual(expected, results[cfgpath])
            self.assertEqual(0o644, get_mode(cfgpath, tmpd))

    def test_apply_network_config_eni_ub(self):
        expected_cfgs = {
            self.eni_path(): V1_NET_CFG_OUTPUT,
        }
        # ub_distro.apply_network_config(V1_NET_CFG, False)
        self._apply_and_verify_eni(
            self.distro.apply_network_config,
            V1_NET_CFG,
            expected_cfgs=expected_cfgs.copy(),
        )

    def test_apply_network_config_ipv6_ub(self):
        expected_cfgs = {self.eni_path(): V1_NET_CFG_IPV6_OUTPUT}
        self._apply_and_verify_eni(
            self.distro.apply_network_config,
            V1_NET_CFG_IPV6,
            expected_cfgs=expected_cfgs.copy(),
        )


class TestNetCfgDistroUbuntuNetplan(TestNetCfgDistroBase):
    def setUp(self):
        super(TestNetCfgDistroUbuntuNetplan, self).setUp()
        self.distro = self._get_distro("ubuntu", renderers=["netplan"])
        self.devlist = ["eth0", "lo"]

    def _apply_and_verify_netplan(
        self, apply_fn, config, expected_cfgs=None, bringup=False
    ):
        if not expected_cfgs:
            raise ValueError("expected_cfg must not be None")

        tmpd = None
        with mock.patch("cloudinit.net.netplan.available", return_value=True):
            with mock.patch(
                "cloudinit.net.netplan.get_devicelist",
                return_value=self.devlist,
            ):
                with self.reRooted(tmpd) as tmpd:
                    apply_fn(config, bringup)

        results = dir2dict(tmpd)
        for cfgpath, expected in expected_cfgs.items():
            print("----------")
            print(expected)
            print("^^^^ expected | rendered VVVVVVV")
            print(results[cfgpath])
            print("----------")
            self.assertEqual(expected, results[cfgpath])
            self.assertEqual(0o644, get_mode(cfgpath, tmpd))

    def netplan_path(self):
        return "/etc/netplan/50-cloud-init.yaml"

    def test_apply_network_config_v1_to_netplan_ub(self):
        expected_cfgs = {
            self.netplan_path(): V1_TO_V2_NET_CFG_OUTPUT,
        }

        # ub_distro.apply_network_config(V1_NET_CFG, False)
        self._apply_and_verify_netplan(
            self.distro.apply_network_config,
            V1_NET_CFG,
            expected_cfgs=expected_cfgs.copy(),
        )

    def test_apply_network_config_v1_ipv6_to_netplan_ub(self):
        expected_cfgs = {
            self.netplan_path(): V1_TO_V2_NET_CFG_IPV6_OUTPUT,
        }

        # ub_distro.apply_network_config(V1_NET_CFG_IPV6, False)
        self._apply_and_verify_netplan(
            self.distro.apply_network_config,
            V1_NET_CFG_IPV6,
            expected_cfgs=expected_cfgs.copy(),
        )

    def test_apply_network_config_v2_passthrough_ub(self):
        expected_cfgs = {
            self.netplan_path(): V2_TO_V2_NET_CFG_OUTPUT,
        }
        # ub_distro.apply_network_config(V2_NET_CFG, False)
        self._apply_and_verify_netplan(
            self.distro.apply_network_config,
            V2_NET_CFG,
            expected_cfgs=expected_cfgs.copy(),
        )


class TestNetCfgDistroRedhat(TestNetCfgDistroBase):
    def setUp(self):
        super(TestNetCfgDistroRedhat, self).setUp()
        self.distro = self._get_distro("rhel", renderers=["sysconfig"])

    def ifcfg_path(self, ifname):
        return "/etc/sysconfig/network-scripts/ifcfg-%s" % ifname

    def control_path(self):
        return "/etc/sysconfig/network"

    def _apply_and_verify(
        self, apply_fn, config, expected_cfgs=None, bringup=False
    ):
        if not expected_cfgs:
            raise ValueError("expected_cfg must not be None")

        tmpd = None
        with mock.patch("cloudinit.net.sysconfig.available") as m_avail:
            m_avail.return_value = True
            with self.reRooted(tmpd) as tmpd:
                apply_fn(config, bringup)

        results = dir2dict(tmpd)
        for cfgpath, expected in expected_cfgs.items():
            self.assertCfgEquals(expected, results[cfgpath])
            self.assertEqual(0o644, get_mode(cfgpath, tmpd))

    def test_apply_network_config_rh(self):
        expected_cfgs = {
            self.ifcfg_path("eth0"): dedent(
                """\
                BOOTPROTO=none
                DEFROUTE=yes
                DEVICE=eth0
                GATEWAY=192.168.1.254
                IPADDR=192.168.1.5
                NETMASK=255.255.255.0
                NM_CONTROLLED=no
                ONBOOT=yes
                TYPE=Ethernet
                USERCTL=no
                """
            ),
            self.ifcfg_path("eth1"): dedent(
                """\
                BOOTPROTO=dhcp
                DEVICE=eth1
                NM_CONTROLLED=no
                ONBOOT=yes
                TYPE=Ethernet
                USERCTL=no
                """
            ),
            self.control_path(): dedent(
                """\
                NETWORKING=yes
                """
            ),
        }
        # rh_distro.apply_network_config(V1_NET_CFG, False)
        self._apply_and_verify(
            self.distro.apply_network_config,
            V1_NET_CFG,
            expected_cfgs=expected_cfgs.copy(),
        )

    def test_apply_network_config_ipv6_rh(self):
        expected_cfgs = {
            self.ifcfg_path("eth0"): dedent(
                """\
                BOOTPROTO=none
                DEFROUTE=yes
                DEVICE=eth0
                IPV6ADDR=2607:f0d0:1002:0011::2/64
                IPV6INIT=yes
                IPV6_AUTOCONF=no
                IPV6_DEFAULTGW=2607:f0d0:1002:0011::1
                IPV6_FORCE_ACCEPT_RA=no
                NM_CONTROLLED=no
                ONBOOT=yes
                TYPE=Ethernet
                USERCTL=no
                """
            ),
            self.ifcfg_path("eth1"): dedent(
                """\
                BOOTPROTO=dhcp
                DEVICE=eth1
                NM_CONTROLLED=no
                ONBOOT=yes
                TYPE=Ethernet
                USERCTL=no
                """
            ),
            self.control_path(): dedent(
                """\
                NETWORKING=yes
                NETWORKING_IPV6=yes
                IPV6_AUTOCONF=no
                """
            ),
        }
        # rh_distro.apply_network_config(V1_NET_CFG_IPV6, False)
        self._apply_and_verify(
            self.distro.apply_network_config,
            V1_NET_CFG_IPV6,
            expected_cfgs=expected_cfgs.copy(),
        )

    def test_vlan_render_unsupported(self):
        """Render officially unsupported vlan names."""
        cfg = {
            "version": 2,
            "ethernets": {
                "eth0": {
                    "addresses": ["192.10.1.2/24"],
                    "match": {"macaddress": "00:16:3e:60:7c:df"},
                }
            },
            "vlans": {
                "infra0": {
                    "addresses": ["10.0.1.2/16"],
                    "id": 1001,
                    "link": "eth0",
                }
            },
        }
        expected_cfgs = {
            self.ifcfg_path("eth0"): dedent(
                """\
                BOOTPROTO=none
                DEVICE=eth0
                HWADDR=00:16:3e:60:7c:df
                IPADDR=192.10.1.2
                NETMASK=255.255.255.0
                NM_CONTROLLED=no
                ONBOOT=yes
                TYPE=Ethernet
                USERCTL=no
                """
            ),
            self.ifcfg_path("infra0"): dedent(
                """\
                BOOTPROTO=none
                DEVICE=infra0
                IPADDR=10.0.1.2
                NETMASK=255.255.0.0
                NM_CONTROLLED=no
                ONBOOT=yes
                PHYSDEV=eth0
                USERCTL=no
                VLAN=yes
                """
            ),
            self.control_path(): dedent(
                """\
                NETWORKING=yes
                """
            ),
        }
        self._apply_and_verify(
            self.distro.apply_network_config, cfg, expected_cfgs=expected_cfgs
        )

    def test_vlan_render(self):
        cfg = {
            "version": 2,
            "ethernets": {"eth0": {"addresses": ["192.10.1.2/24"]}},
            "vlans": {
                "eth0.1001": {
                    "addresses": ["10.0.1.2/16"],
                    "id": 1001,
                    "link": "eth0",
                }
            },
        }
        expected_cfgs = {
            self.ifcfg_path("eth0"): dedent(
                """\
                BOOTPROTO=none
                DEVICE=eth0
                IPADDR=192.10.1.2
                NETMASK=255.255.255.0
                NM_CONTROLLED=no
                ONBOOT=yes
                TYPE=Ethernet
                USERCTL=no
                """
            ),
            self.ifcfg_path("eth0.1001"): dedent(
                """\
                BOOTPROTO=none
                DEVICE=eth0.1001
                IPADDR=10.0.1.2
                NETMASK=255.255.0.0
                NM_CONTROLLED=no
                ONBOOT=yes
                PHYSDEV=eth0
                USERCTL=no
                VLAN=yes
                """
            ),
            self.control_path(): dedent(
                """\
                NETWORKING=yes
                """
            ),
        }
        self._apply_and_verify(
            self.distro.apply_network_config, cfg, expected_cfgs=expected_cfgs
        )


class TestNetCfgDistroOpensuse(TestNetCfgDistroBase):
    def setUp(self):
        super(TestNetCfgDistroOpensuse, self).setUp()
        self.distro = self._get_distro("opensuse", renderers=["sysconfig"])

    def ifcfg_path(self, ifname):
        return "/etc/sysconfig/network/ifcfg-%s" % ifname

    def _apply_and_verify(
        self, apply_fn, config, expected_cfgs=None, bringup=False
    ):
        if not expected_cfgs:
            raise ValueError("expected_cfg must not be None")

        tmpd = None
        with mock.patch("cloudinit.net.sysconfig.available") as m_avail:
            m_avail.return_value = True
            with self.reRooted(tmpd) as tmpd:
                apply_fn(config, bringup)

        results = dir2dict(tmpd)
        for cfgpath, expected in expected_cfgs.items():
            self.assertCfgEquals(expected, results[cfgpath])
            self.assertEqual(0o644, get_mode(cfgpath, tmpd))

    def test_apply_network_config_opensuse(self):
        """Opensuse uses apply_network_config and renders sysconfig"""
        expected_cfgs = {
            self.ifcfg_path("eth0"): dedent(
                """\
                BOOTPROTO=static
                IPADDR=192.168.1.5
                NETMASK=255.255.255.0
                STARTMODE=auto
                """
            ),
            self.ifcfg_path("eth1"): dedent(
                """\
                BOOTPROTO=dhcp4
                STARTMODE=auto
                """
            ),
        }
        self._apply_and_verify(
            self.distro.apply_network_config,
            V1_NET_CFG,
            expected_cfgs=expected_cfgs.copy(),
        )

    def test_apply_network_config_ipv6_opensuse(self):
        """Opensuse uses apply_network_config and renders sysconfig w/ipv6"""
        expected_cfgs = {
            self.ifcfg_path("eth0"): dedent(
                """\
                BOOTPROTO=static
                IPADDR6=2607:f0d0:1002:0011::2/64
                STARTMODE=auto
            """
            ),
            self.ifcfg_path("eth1"): dedent(
                """\
                BOOTPROTO=dhcp4
                STARTMODE=auto
            """
            ),
        }
        self._apply_and_verify(
            self.distro.apply_network_config,
            V1_NET_CFG_IPV6,
            expected_cfgs=expected_cfgs.copy(),
        )


class TestNetCfgDistroArch(TestNetCfgDistroBase):
    def setUp(self):
        super(TestNetCfgDistroArch, self).setUp()
        self.distro = self._get_distro("arch", renderers=["netplan"])

    def _apply_and_verify(
        self,
        apply_fn,
        config,
        expected_cfgs=None,
        bringup=False,
        with_netplan=False,
    ):
        if not expected_cfgs:
            raise ValueError("expected_cfg must not be None")

        tmpd = None
        with mock.patch(
            "cloudinit.net.netplan.available", return_value=with_netplan
        ):
            with self.reRooted(tmpd) as tmpd:
                apply_fn(config, bringup)

        results = dir2dict(tmpd)
        for cfgpath, expected in expected_cfgs.items():
            print("----------")
            print(expected)
            print("^^^^ expected | rendered VVVVVVV")
            print(results[cfgpath])
            print("----------")
            self.assertEqual(expected, results[cfgpath])
            self.assertEqual(0o644, get_mode(cfgpath, tmpd))

    def netctl_path(self, iface):
        return "/etc/netctl/%s" % iface

    def netplan_path(self):
        return "/etc/netplan/50-cloud-init.yaml"

    def test_apply_network_config_v1_without_netplan(self):
        # Note that this is in fact an invalid netctl config:
        #  "Address=None/None"
        # But this is what the renderer has been writing out for a long time,
        # and the test's purpose is to assert that the netctl renderer is
        # still being used in absence of netplan, not the correctness of the
        # rendered netctl config.
        expected_cfgs = {
            self.netctl_path("eth0"): dedent(
                """\
                Address=192.168.1.5/255.255.255.0
                Connection=ethernet
                DNS=()
                Gateway=192.168.1.254
                IP=static
                Interface=eth0
                """
            ),
            self.netctl_path("eth1"): dedent(
                """\
                Address=None/None
                Connection=ethernet
                DNS=()
                Gateway=
                IP=dhcp
                Interface=eth1
                """
            ),
        }

        # ub_distro.apply_network_config(V1_NET_CFG, False)
        self._apply_and_verify(
            self.distro.apply_network_config,
            V1_NET_CFG,
            expected_cfgs=expected_cfgs.copy(),
            with_netplan=False,
        )

    def test_apply_network_config_v1_with_netplan(self):
        expected_cfgs = {
            self.netplan_path(): dedent(
                """\
                # generated by cloud-init
                network:
                    version: 2
                    ethernets:
                        eth0:
                            addresses:
                            - 192.168.1.5/24
                            gateway4: 192.168.1.254
                        eth1:
                            dhcp4: true
                """
            ),
        }

        with mock.patch(
            "cloudinit.net.netplan.get_devicelist", return_value=[]
        ):
            self._apply_and_verify(
                self.distro.apply_network_config,
                V1_NET_CFG,
                expected_cfgs=expected_cfgs.copy(),
                with_netplan=True,
            )


class TestNetCfgDistroPhoton(TestNetCfgDistroBase):
    def setUp(self):
        super(TestNetCfgDistroPhoton, self).setUp()
        self.distro = self._get_distro("photon", renderers=["networkd"])

    def create_conf_dict(self, contents):
        content_dict = {}
        for line in contents:
            if line:
                line = line.strip()
                if line and re.search(r"^\[(.+)\]$", line):
                    content_dict[line] = []
                    key = line
                elif line:
                    assert key
                    content_dict[key].append(line)

        return content_dict

    def compare_dicts(self, actual, expected):
        for k, v in actual.items():
            self.assertEqual(sorted(expected[k]), sorted(v))

    def _apply_and_verify(
        self, apply_fn, config, expected_cfgs=None, bringup=False
    ):
        if not expected_cfgs:
            raise ValueError("expected_cfg must not be None")

        tmpd = None
        with mock.patch("cloudinit.net.networkd.available") as m_avail:
            m_avail.return_value = True
            with self.reRooted(tmpd) as tmpd:
                apply_fn(config, bringup)

        results = dir2dict(tmpd)
        for cfgpath, expected in expected_cfgs.items():
            actual = self.create_conf_dict(results[cfgpath].splitlines())
            self.compare_dicts(actual, expected)
            self.assertEqual(0o644, get_mode(cfgpath, tmpd))

    def nwk_file_path(self, ifname):
        return "/etc/systemd/network/10-cloud-init-%s.network" % ifname

    def net_cfg_1(self, ifname):
        ret = (
            """\
        [Match]
        Name=%s
        [Network]
        DHCP=no
        [Address]
        Address=192.168.1.5/24
        [Route]
        Gateway=192.168.1.254"""
            % ifname
        )
        return ret

    def net_cfg_2(self, ifname):
        ret = (
            """\
        [Match]
        Name=%s
        [Network]
        DHCP=ipv4"""
            % ifname
        )
        return ret

    def test_photon_network_config_v1(self):
        tmp = self.net_cfg_1("eth0").splitlines()
        expected_eth0 = self.create_conf_dict(tmp)

        tmp = self.net_cfg_2("eth1").splitlines()
        expected_eth1 = self.create_conf_dict(tmp)

        expected_cfgs = {
            self.nwk_file_path("eth0"): expected_eth0,
            self.nwk_file_path("eth1"): expected_eth1,
        }

        self._apply_and_verify(
            self.distro.apply_network_config, V1_NET_CFG, expected_cfgs.copy()
        )

    def test_photon_network_config_v2(self):
        tmp = self.net_cfg_1("eth7").splitlines()
        expected_eth7 = self.create_conf_dict(tmp)

        tmp = self.net_cfg_2("eth9").splitlines()
        expected_eth9 = self.create_conf_dict(tmp)

        expected_cfgs = {
            self.nwk_file_path("eth7"): expected_eth7,
            self.nwk_file_path("eth9"): expected_eth9,
        }

        self._apply_and_verify(
            self.distro.apply_network_config, V2_NET_CFG, expected_cfgs.copy()
        )

    def test_photon_network_config_v1_with_duplicates(self):
        expected = """\
        [Match]
        Name=eth0
        [Network]
        DHCP=no
        DNS=1.2.3.4
        Domains=test.com
        [Address]
        Address=192.168.0.102/24"""

        net_cfg = safeyaml.load(V1_NET_CFG_WITH_DUPS)

        expected = self.create_conf_dict(expected.splitlines())
        expected_cfgs = {
            self.nwk_file_path("eth0"): expected,
        }

        self._apply_and_verify(
            self.distro.apply_network_config, net_cfg, expected_cfgs.copy()
        )


def get_mode(path, target=None):
    return os.stat(subp.target_path(target, path)).st_mode & 0o777


# vi: ts=4 expandtab
