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

import copy
import errno
import mock
import os
import textwrap
import yaml

import cloudinit.net as net
from cloudinit.util import ensure_file, write_file, ProcessExecutionError
from cloudinit.tests.helpers import CiTestCase


class TestSysDevPath(CiTestCase):

    def test_sys_dev_path(self):
        """sys_dev_path returns a path under SYS_CLASS_NET for a device."""
        dev = 'something'
        path = 'attribute'
        expected = net.SYS_CLASS_NET + dev + '/' + path
        self.assertEqual(expected, net.sys_dev_path(dev, path))

    def test_sys_dev_path_without_path(self):
        """When path param isn't provided it defaults to empty string."""
        dev = 'something'
        expected = net.SYS_CLASS_NET + dev + '/'
        self.assertEqual(expected, net.sys_dev_path(dev))


class TestReadSysNet(CiTestCase):
    with_logs = True

    def setUp(self):
        super(TestReadSysNet, self).setUp()
        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
        self.m_sys_path = sys_mock.start()
        self.sysdir = self.tmp_dir() + '/'
        self.m_sys_path.return_value = self.sysdir
        self.addCleanup(sys_mock.stop)

    def test_read_sys_net_strips_contents_of_sys_path(self):
        """read_sys_net strips whitespace from the contents of a sys file."""
        content = 'some stuff with trailing whitespace\t\r\n'
        write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
        self.assertEqual(content.strip(), net.read_sys_net('dev', 'attr'))

    def test_read_sys_net_reraises_oserror(self):
        """read_sys_net raises OSError/IOError when file doesn't exist."""
        # Non-specific Exception because versions of python OSError vs IOError.
        with self.assertRaises(Exception) as context_manager:  # noqa: H202
            net.read_sys_net('dev', 'attr')
        error = context_manager.exception
        self.assertIn('No such file or directory', str(error))

    def test_read_sys_net_handles_error_with_on_enoent(self):
        """read_sys_net handles OSError/IOError with on_enoent if provided."""
        handled_errors = []

        def on_enoent(e):
            handled_errors.append(e)

        net.read_sys_net('dev', 'attr', on_enoent=on_enoent)
        error = handled_errors[0]
        self.assertIsInstance(error, Exception)
        self.assertIn('No such file or directory', str(error))

    def test_read_sys_net_translates_content(self):
        """read_sys_net translates content when translate dict is provided."""
        content = "you're welcome\n"
        write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
        translate = {"you're welcome": 'de nada'}
        self.assertEqual(
            'de nada',
            net.read_sys_net('dev', 'attr', translate=translate))

    def test_read_sys_net_errors_on_translation_failures(self):
        """read_sys_net raises a KeyError and logs details on failure."""
        content = "you're welcome\n"
        write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
        with self.assertRaises(KeyError) as context_manager:
            net.read_sys_net('dev', 'attr', translate={})
        error = context_manager.exception
        self.assertEqual('"you\'re welcome"', str(error))
        self.assertIn(
            "Found unexpected (not translatable) value 'you're welcome' in "
            "'{0}dev/attr".format(self.sysdir),
            self.logs.getvalue())

    def test_read_sys_net_handles_handles_with_onkeyerror(self):
        """read_sys_net handles translation errors calling on_keyerror."""
        content = "you're welcome\n"
        write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
        handled_errors = []

        def on_keyerror(e):
            handled_errors.append(e)

        net.read_sys_net('dev', 'attr', translate={}, on_keyerror=on_keyerror)
        error = handled_errors[0]
        self.assertIsInstance(error, KeyError)
        self.assertEqual('"you\'re welcome"', str(error))

    def test_read_sys_net_safe_false_on_translate_failure(self):
        """read_sys_net_safe returns False on translation failures."""
        content = "you're welcome\n"
        write_file(os.path.join(self.sysdir, 'dev', 'attr'), content)
        self.assertFalse(net.read_sys_net_safe('dev', 'attr', translate={}))

    def test_read_sys_net_safe_returns_false_on_noent_failure(self):
        """read_sys_net_safe returns False on file not found failures."""
        self.assertFalse(net.read_sys_net_safe('dev', 'attr'))

    def test_read_sys_net_int_returns_none_on_error(self):
        """read_sys_net_safe returns None on failures."""
        self.assertFalse(net.read_sys_net_int('dev', 'attr'))

    def test_read_sys_net_int_returns_none_on_valueerror(self):
        """read_sys_net_safe returns None when content is not an int."""
        write_file(os.path.join(self.sysdir, 'dev', 'attr'), 'NOTINT\n')
        self.assertFalse(net.read_sys_net_int('dev', 'attr'))

    def test_read_sys_net_int_returns_integer_from_content(self):
        """read_sys_net_safe returns None on failures."""
        write_file(os.path.join(self.sysdir, 'dev', 'attr'), '1\n')
        self.assertEqual(1, net.read_sys_net_int('dev', 'attr'))

    def test_is_up_true(self):
        """is_up is True if sys/net/devname/operstate is 'up' or 'unknown'."""
        for state in ['up', 'unknown']:
            write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
            self.assertTrue(net.is_up('eth0'))

    def test_is_up_false(self):
        """is_up is False if sys/net/devname/operstate is 'down' or invalid."""
        for state in ['down', 'incomprehensible']:
            write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
            self.assertFalse(net.is_up('eth0'))

    def test_is_wireless(self):
        """is_wireless is True when /sys/net/devname/wireless exists."""
        self.assertFalse(net.is_wireless('eth0'))
        ensure_file(os.path.join(self.sysdir, 'eth0', 'wireless'))
        self.assertTrue(net.is_wireless('eth0'))

    def test_is_bridge(self):
        """is_bridge is True when /sys/net/devname/bridge exists."""
        self.assertFalse(net.is_bridge('eth0'))
        ensure_file(os.path.join(self.sysdir, 'eth0', 'bridge'))
        self.assertTrue(net.is_bridge('eth0'))

    def test_is_bond(self):
        """is_bond is True when /sys/net/devname/bonding exists."""
        self.assertFalse(net.is_bond('eth0'))
        ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
        self.assertTrue(net.is_bond('eth0'))

    def test_is_vlan(self):
        """is_vlan is True when /sys/net/devname/uevent has DEVTYPE=vlan."""
        ensure_file(os.path.join(self.sysdir, 'eth0', 'uevent'))
        self.assertFalse(net.is_vlan('eth0'))
        content = 'junk\nDEVTYPE=vlan\njunk\n'
        write_file(os.path.join(self.sysdir, 'eth0', 'uevent'), content)
        self.assertTrue(net.is_vlan('eth0'))

    def test_is_connected_when_physically_connected(self):
        """is_connected is True when /sys/net/devname/iflink reports 2."""
        self.assertFalse(net.is_connected('eth0'))
        write_file(os.path.join(self.sysdir, 'eth0', 'iflink'), "2")
        self.assertTrue(net.is_connected('eth0'))

    def test_is_connected_when_wireless_and_carrier_active(self):
        """is_connected is True if wireless /sys/net/devname/carrier is 1."""
        self.assertFalse(net.is_connected('eth0'))
        ensure_file(os.path.join(self.sysdir, 'eth0', 'wireless'))
        self.assertFalse(net.is_connected('eth0'))
        write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), "1")
        self.assertTrue(net.is_connected('eth0'))

    def test_is_physical(self):
        """is_physical is True when /sys/net/devname/device exists."""
        self.assertFalse(net.is_physical('eth0'))
        ensure_file(os.path.join(self.sysdir, 'eth0', 'device'))
        self.assertTrue(net.is_physical('eth0'))

    def test_is_present(self):
        """is_present is True when /sys/net/devname exists."""
        self.assertFalse(net.is_present('eth0'))
        ensure_file(os.path.join(self.sysdir, 'eth0', 'device'))
        self.assertTrue(net.is_present('eth0'))


class TestGenerateFallbackConfig(CiTestCase):

    def setUp(self):
        super(TestGenerateFallbackConfig, self).setUp()
        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
        self.m_sys_path = sys_mock.start()
        self.sysdir = self.tmp_dir() + '/'
        self.m_sys_path.return_value = self.sysdir
        self.addCleanup(sys_mock.stop)
        self.add_patch('cloudinit.net.util.udevadm_settle', 'm_settle')

    def test_generate_fallback_finds_connected_eth_with_mac(self):
        """generate_fallback_config finds any connected device with a mac."""
        write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
        write_file(os.path.join(self.sysdir, 'eth1', 'carrier'), '1')
        mac = 'aa:bb:cc:aa:bb:cc'
        write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
        expected = {
            'config': [{'type': 'physical', 'mac_address': mac,
                        'name': 'eth1', 'subnets': [{'type': 'dhcp'}]}],
            'version': 1}
        self.assertEqual(expected, net.generate_fallback_config())

    def test_generate_fallback_finds_dormant_eth_with_mac(self):
        """generate_fallback_config finds any dormant device with a mac."""
        write_file(os.path.join(self.sysdir, 'eth0', 'dormant'), '1')
        mac = 'aa:bb:cc:aa:bb:cc'
        write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
        expected = {
            'config': [{'type': 'physical', 'mac_address': mac,
                        'name': 'eth0', 'subnets': [{'type': 'dhcp'}]}],
            'version': 1}
        self.assertEqual(expected, net.generate_fallback_config())

    def test_generate_fallback_finds_eth_by_operstate(self):
        """generate_fallback_config finds any dormant device with a mac."""
        mac = 'aa:bb:cc:aa:bb:cc'
        write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
        expected = {
            'config': [{'type': 'physical', 'mac_address': mac,
                        'name': 'eth0', 'subnets': [{'type': 'dhcp'}]}],
            'version': 1}
        valid_operstates = ['dormant', 'down', 'lowerlayerdown', 'unknown']
        for state in valid_operstates:
            write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), state)
            self.assertEqual(expected, net.generate_fallback_config())
        write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), 'noworky')
        self.assertIsNone(net.generate_fallback_config())

    def test_generate_fallback_config_skips_veth(self):
        """generate_fallback_config will skip any veth interfaces."""
        # A connected veth which gets ignored
        write_file(os.path.join(self.sysdir, 'veth0', 'carrier'), '1')
        self.assertIsNone(net.generate_fallback_config())

    def test_generate_fallback_config_skips_bridges(self):
        """generate_fallback_config will skip any bridges interfaces."""
        # A connected veth which gets ignored
        write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
        mac = 'aa:bb:cc:aa:bb:cc'
        write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
        ensure_file(os.path.join(self.sysdir, 'eth0', 'bridge'))
        self.assertIsNone(net.generate_fallback_config())

    def test_generate_fallback_config_skips_bonds(self):
        """generate_fallback_config will skip any bonded interfaces."""
        # A connected veth which gets ignored
        write_file(os.path.join(self.sysdir, 'eth0', 'carrier'), '1')
        mac = 'aa:bb:cc:aa:bb:cc'
        write_file(os.path.join(self.sysdir, 'eth0', 'address'), mac)
        ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
        self.assertIsNone(net.generate_fallback_config())


class TestGetDeviceList(CiTestCase):

    def setUp(self):
        super(TestGetDeviceList, self).setUp()
        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
        self.m_sys_path = sys_mock.start()
        self.sysdir = self.tmp_dir() + '/'
        self.m_sys_path.return_value = self.sysdir
        self.addCleanup(sys_mock.stop)

    def test_get_devicelist_raise_oserror(self):
        """get_devicelist raise any non-ENOENT OSerror."""
        error = OSError('Can not do it')
        error.errno = errno.EPERM  # Set non-ENOENT
        self.m_sys_path.side_effect = error
        with self.assertRaises(OSError) as context_manager:
            net.get_devicelist()
        exception = context_manager.exception
        self.assertEqual('Can not do it', str(exception))

    def test_get_devicelist_empty_without_sys_net(self):
        """get_devicelist returns empty list when missing SYS_CLASS_NET."""
        self.m_sys_path.return_value = 'idontexist'
        self.assertEqual([], net.get_devicelist())

    def test_get_devicelist_empty_with_no_devices_in_sys_net(self):
        """get_devicelist returns empty directoty listing for SYS_CLASS_NET."""
        self.assertEqual([], net.get_devicelist())

    def test_get_devicelist_lists_any_subdirectories_in_sys_net(self):
        """get_devicelist returns a directory listing for SYS_CLASS_NET."""
        write_file(os.path.join(self.sysdir, 'eth0', 'operstate'), 'up')
        write_file(os.path.join(self.sysdir, 'eth1', 'operstate'), 'up')
        self.assertItemsEqual(['eth0', 'eth1'], net.get_devicelist())


class TestGetInterfaceMAC(CiTestCase):

    def setUp(self):
        super(TestGetInterfaceMAC, self).setUp()
        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
        self.m_sys_path = sys_mock.start()
        self.sysdir = self.tmp_dir() + '/'
        self.m_sys_path.return_value = self.sysdir
        self.addCleanup(sys_mock.stop)

    def test_get_interface_mac_false_with_no_mac(self):
        """get_device_list returns False when no mac is reported."""
        ensure_file(os.path.join(self.sysdir, 'eth0', 'bonding'))
        mac_path = os.path.join(self.sysdir, 'eth0', 'address')
        self.assertFalse(os.path.exists(mac_path))
        self.assertFalse(net.get_interface_mac('eth0'))

    def test_get_interface_mac(self):
        """get_interfaces returns the mac from SYS_CLASS_NET/dev/address."""
        mac = 'aa:bb:cc:aa:bb:cc'
        write_file(os.path.join(self.sysdir, 'eth1', 'address'), mac)
        self.assertEqual(mac, net.get_interface_mac('eth1'))

    def test_get_interface_mac_grabs_bonding_address(self):
        """get_interfaces returns the source device mac for bonded devices."""
        source_dev_mac = 'aa:bb:cc:aa:bb:cc'
        bonded_mac = 'dd:ee:ff:dd:ee:ff'
        write_file(os.path.join(self.sysdir, 'eth1', 'address'), bonded_mac)
        write_file(
            os.path.join(self.sysdir, 'eth1', 'bonding_slave', 'perm_hwaddr'),
            source_dev_mac)
        self.assertEqual(source_dev_mac, net.get_interface_mac('eth1'))

    def test_get_interfaces_empty_list_without_sys_net(self):
        """get_interfaces returns an empty list when missing SYS_CLASS_NET."""
        self.m_sys_path.return_value = 'idontexist'
        self.assertEqual([], net.get_interfaces())

    def test_get_interfaces_by_mac_skips_empty_mac(self):
        """Ignore 00:00:00:00:00:00 addresses from get_interfaces_by_mac."""
        empty_mac = '00:00:00:00:00:00'
        mac = 'aa:bb:cc:aa:bb:cc'
        write_file(os.path.join(self.sysdir, 'eth1', 'address'), empty_mac)
        write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0')
        write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0')
        write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac)
        expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]
        self.assertEqual(expected, net.get_interfaces())

    def test_get_interfaces_by_mac_skips_missing_mac(self):
        """Ignore interfaces without an address from get_interfaces_by_mac."""
        write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '0')
        address_path = os.path.join(self.sysdir, 'eth1', 'address')
        self.assertFalse(os.path.exists(address_path))
        mac = 'aa:bb:cc:aa:bb:cc'
        write_file(os.path.join(self.sysdir, 'eth2', 'addr_assign_type'), '0')
        write_file(os.path.join(self.sysdir, 'eth2', 'address'), mac)
        expected = [('eth2', 'aa:bb:cc:aa:bb:cc', None, None)]
        self.assertEqual(expected, net.get_interfaces())


class TestInterfaceHasOwnMAC(CiTestCase):

    def setUp(self):
        super(TestInterfaceHasOwnMAC, self).setUp()
        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
        self.m_sys_path = sys_mock.start()
        self.sysdir = self.tmp_dir() + '/'
        self.m_sys_path.return_value = self.sysdir
        self.addCleanup(sys_mock.stop)

    def test_interface_has_own_mac_false_when_stolen(self):
        """Return False from interface_has_own_mac when address is stolen."""
        write_file(os.path.join(self.sysdir, 'eth1', 'addr_assign_type'), '2')
        self.assertFalse(net.interface_has_own_mac('eth1'))

    def test_interface_has_own_mac_true_when_not_stolen(self):
        """Return False from interface_has_own_mac when mac isn't stolen."""
        valid_assign_types = ['0', '1', '3']
        assign_path = os.path.join(self.sysdir, 'eth1', 'addr_assign_type')
        for _type in valid_assign_types:
            write_file(assign_path, _type)
            self.assertTrue(net.interface_has_own_mac('eth1'))

    def test_interface_has_own_mac_strict_errors_on_absent_assign_type(self):
        """When addr_assign_type is absent, interface_has_own_mac errors."""
        with self.assertRaises(ValueError):
            net.interface_has_own_mac('eth1', strict=True)


@mock.patch('cloudinit.net.util.subp')
class TestEphemeralIPV4Network(CiTestCase):

    with_logs = True

    def setUp(self):
        super(TestEphemeralIPV4Network, self).setUp()
        sys_mock = mock.patch('cloudinit.net.get_sys_class_path')
        self.m_sys_path = sys_mock.start()
        self.sysdir = self.tmp_dir() + '/'
        self.m_sys_path.return_value = self.sysdir
        self.addCleanup(sys_mock.stop)

    def test_ephemeral_ipv4_network_errors_on_missing_params(self, m_subp):
        """No required params for EphemeralIPv4Network can be None."""
        required_params = {
            'interface': 'eth0', 'ip': '192.168.2.2',
            'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'}
        for key in required_params.keys():
            params = copy.deepcopy(required_params)
            params[key] = None
            with self.assertRaises(ValueError) as context_manager:
                net.EphemeralIPv4Network(**params)
            error = context_manager.exception
            self.assertIn('Cannot init network on', str(error))
            self.assertEqual(0, m_subp.call_count)

    def test_ephemeral_ipv4_network_errors_invalid_mask_prefix(self, m_subp):
        """Raise an error when prefix_or_mask is not a netmask or prefix."""
        params = {
            'interface': 'eth0', 'ip': '192.168.2.2',
            'broadcast': '192.168.2.255'}
        invalid_masks = ('invalid', 'invalid.', '123.123.123')
        for error_val in invalid_masks:
            params['prefix_or_mask'] = error_val
            with self.assertRaises(ValueError) as context_manager:
                with net.EphemeralIPv4Network(**params):
                    pass
            error = context_manager.exception
            self.assertIn('Cannot setup network: netmask', str(error))
            self.assertEqual(0, m_subp.call_count)

    def test_ephemeral_ipv4_network_performs_teardown(self, m_subp):
        """EphemeralIPv4Network performs teardown on the device if setup."""
        expected_setup_calls = [
            mock.call(
                ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
                 'broadcast', '192.168.2.255', 'dev', 'eth0'],
                capture=True, update_env={'LANG': 'C'}),
            mock.call(
                ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'],
                capture=True)]
        expected_teardown_calls = [
            mock.call(
                ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0',
                 'down'], capture=True),
            mock.call(
                ['ip', '-family', 'inet', 'addr', 'del', '192.168.2.2/24',
                 'dev', 'eth0'], capture=True)]
        params = {
            'interface': 'eth0', 'ip': '192.168.2.2',
            'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'}
        with net.EphemeralIPv4Network(**params):
            self.assertEqual(expected_setup_calls, m_subp.call_args_list)
        m_subp.assert_has_calls(expected_teardown_calls)

    def test_ephemeral_ipv4_network_noop_when_configured(self, m_subp):
        """EphemeralIPv4Network handles exception when address is setup.

        It performs no cleanup as the interface was already setup.
        """
        params = {
            'interface': 'eth0', 'ip': '192.168.2.2',
            'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255'}
        m_subp.side_effect = ProcessExecutionError(
            '', 'RTNETLINK answers: File exists', 2)
        expected_calls = [
            mock.call(
                ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
                 'broadcast', '192.168.2.255', 'dev', 'eth0'],
                capture=True, update_env={'LANG': 'C'})]
        with net.EphemeralIPv4Network(**params):
            pass
        self.assertEqual(expected_calls, m_subp.call_args_list)
        self.assertIn(
            'Skip ephemeral network setup, eth0 already has address',
            self.logs.getvalue())

    def test_ephemeral_ipv4_network_with_prefix(self, m_subp):
        """EphemeralIPv4Network takes a valid prefix to setup the network."""
        params = {
            'interface': 'eth0', 'ip': '192.168.2.2',
            'prefix_or_mask': '24', 'broadcast': '192.168.2.255'}
        for prefix_val in ['24', 16]:  # prefix can be int or string
            params['prefix_or_mask'] = prefix_val
            with net.EphemeralIPv4Network(**params):
                pass
        m_subp.assert_has_calls([mock.call(
            ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
             'broadcast', '192.168.2.255', 'dev', 'eth0'],
            capture=True, update_env={'LANG': 'C'})])
        m_subp.assert_has_calls([mock.call(
            ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/16',
             'broadcast', '192.168.2.255', 'dev', 'eth0'],
            capture=True, update_env={'LANG': 'C'})])

    def test_ephemeral_ipv4_network_with_new_default_route(self, m_subp):
        """Add the route when router is set and no default route exists."""
        params = {
            'interface': 'eth0', 'ip': '192.168.2.2',
            'prefix_or_mask': '255.255.255.0', 'broadcast': '192.168.2.255',
            'router': '192.168.2.1'}
        m_subp.return_value = '', ''  # Empty response from ip route gw check
        expected_setup_calls = [
            mock.call(
                ['ip', '-family', 'inet', 'addr', 'add', '192.168.2.2/24',
                 'broadcast', '192.168.2.255', 'dev', 'eth0'],
                capture=True, update_env={'LANG': 'C'}),
            mock.call(
                ['ip', '-family', 'inet', 'link', 'set', 'dev', 'eth0', 'up'],
                capture=True),
            mock.call(
                ['ip', 'route', 'show', '0.0.0.0/0'], capture=True),
            mock.call(
                ['ip', '-4', 'route', 'add', 'default', 'via',
                 '192.168.2.1', 'dev', 'eth0'], capture=True)]
        expected_teardown_calls = [mock.call(
            ['ip', '-4', 'route', 'del', 'default', 'dev', 'eth0'],
            capture=True)]

        with net.EphemeralIPv4Network(**params):
            self.assertEqual(expected_setup_calls, m_subp.call_args_list)
        m_subp.assert_has_calls(expected_teardown_calls)


class TestApplyNetworkCfgNames(CiTestCase):
    V1_CONFIG = textwrap.dedent("""\
        version: 1
        config:
            - type: physical
              name: interface0
              mac_address: "52:54:00:12:34:00"
              subnets:
                  - type: static
                    address: 10.0.2.15
                    netmask: 255.255.255.0
                    gateway: 10.0.2.2
    """)
    V2_CONFIG = textwrap.dedent("""\
      version: 2
      ethernets:
          interface0:
            match:
              macaddress: "52:54:00:12:34:00"
            addresses:
              - 10.0.2.15/24
            gateway4: 10.0.2.2
            set-name: interface0
    """)

    V2_CONFIG_NO_SETNAME = textwrap.dedent("""\
      version: 2
      ethernets:
          interface0:
            match:
              macaddress: "52:54:00:12:34:00"
            addresses:
              - 10.0.2.15/24
            gateway4: 10.0.2.2
    """)

    V2_CONFIG_NO_MAC = textwrap.dedent("""\
      version: 2
      ethernets:
          interface0:
            match:
              driver: virtio-net
            addresses:
              - 10.0.2.15/24
            gateway4: 10.0.2.2
            set-name: interface0
    """)

    @mock.patch('cloudinit.net.device_devid')
    @mock.patch('cloudinit.net.device_driver')
    @mock.patch('cloudinit.net._rename_interfaces')
    def test_apply_v1_renames(self, m_rename_interfaces, m_device_driver,
                              m_device_devid):
        m_device_driver.return_value = 'virtio_net'
        m_device_devid.return_value = '0x15d8'

        net.apply_network_config_names(yaml.load(self.V1_CONFIG))

        call = ['52:54:00:12:34:00', 'interface0', 'virtio_net', '0x15d8']
        m_rename_interfaces.assert_called_with([call])

    @mock.patch('cloudinit.net.device_devid')
    @mock.patch('cloudinit.net.device_driver')
    @mock.patch('cloudinit.net._rename_interfaces')
    def test_apply_v2_renames(self, m_rename_interfaces, m_device_driver,
                              m_device_devid):
        m_device_driver.return_value = 'virtio_net'
        m_device_devid.return_value = '0x15d8'

        net.apply_network_config_names(yaml.load(self.V2_CONFIG))

        call = ['52:54:00:12:34:00', 'interface0', 'virtio_net', '0x15d8']
        m_rename_interfaces.assert_called_with([call])

    @mock.patch('cloudinit.net._rename_interfaces')
    def test_apply_v2_renames_skips_without_setname(self, m_rename_interfaces):
        net.apply_network_config_names(yaml.load(self.V2_CONFIG_NO_SETNAME))
        m_rename_interfaces.assert_called_with([])

    @mock.patch('cloudinit.net._rename_interfaces')
    def test_apply_v2_renames_skips_without_mac(self, m_rename_interfaces):
        net.apply_network_config_names(yaml.load(self.V2_CONFIG_NO_MAC))
        m_rename_interfaces.assert_called_with([])

    def test_apply_v2_renames_raises_runtime_error_on_unknown_version(self):
        with self.assertRaises(RuntimeError):
            net.apply_network_config_names(yaml.load("version: 3"))