summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/cloud_tests/configs/modules/set_password_list.yaml23
-rw-r--r--tests/cloud_tests/configs/modules/set_password_list_string.yaml40
-rw-r--r--tests/cloud_tests/configs/modules/timezone.yaml4
-rw-r--r--tests/cloud_tests/testcases/__init__.py2
-rw-r--r--tests/cloud_tests/testcases/base.py56
-rw-r--r--tests/cloud_tests/testcases/modules/set_password_list.py20
-rw-r--r--tests/cloud_tests/testcases/modules/set_password_list_string.py11
-rw-r--r--tests/cloud_tests/testcases/modules/timezone.py2
-rw-r--r--tests/unittests/test_datasource/test_configdrive.py7
-rw-r--r--tests/unittests/test_datasource/test_digitalocean.py14
-rw-r--r--tests/unittests/test_datasource/test_gce.py14
-rw-r--r--tests/unittests/test_datasource/test_opennebula.py9
-rw-r--r--tests/unittests/test_distros/test_netconfig.py354
-rw-r--r--tests/unittests/test_distros/test_resolv.py2
-rw-r--r--tests/unittests/test_handler/test_handler_disk_setup.py44
-rw-r--r--tests/unittests/test_net.py633
-rw-r--r--tests/unittests/test_version.py14
17 files changed, 1204 insertions, 45 deletions
diff --git a/tests/cloud_tests/configs/modules/set_password_list.yaml b/tests/cloud_tests/configs/modules/set_password_list.yaml
index 36129047..a2a89c9d 100644
--- a/tests/cloud_tests/configs/modules/set_password_list.yaml
+++ b/tests/cloud_tests/configs/modules/set_password_list.yaml
@@ -6,22 +6,29 @@ cloud_config: |
ssh_pwauth: yes
users:
- name: tom
- password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
+ # md5 gotomgo
+ passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0"
lock_passwd: false
- name: dick
- password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
+ # md5 gocubsgo
+ passwd: "$1$ssisyfpf$YqvuJLfrrW6Cg/l53Pi1n1"
lock_passwd: false
- name: harry
- password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
+ # sha512 goharrygo
+ passwd: "$6$LF$9Z2p6rWK6TNC1DC6393ec0As.18KRAvKDbfsGJEdWN3sRQRwpdfoh37EQ3yUh69tP4GSrGW5XKHxMLiKowJgm/"
lock_passwd: false
- name: jane
- password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE.
+ # sha256 gojanego
+ passwd: "$5$iW$XsxmWCdpwIW8Yhv.Jn/R3uk6A4UaicfW5Xp7C9p9pg."
+ lock_passwd: false
+ - name: "mikey"
lock_passwd: false
chpasswd:
- list: |
- tom:mypassword123!
- dick:R
- harry:Random
+ list:
+ - tom:mypassword123!
+ - dick:RANDOM
+ - harry:RANDOM
+ - mikey:$5$xZ$B2YGGEx2AOf4PeW48KC6.QyT1W2B4rZ9Qbltudtha89
collect_scripts:
shadow: |
#!/bin/bash
diff --git a/tests/cloud_tests/configs/modules/set_password_list_string.yaml b/tests/cloud_tests/configs/modules/set_password_list_string.yaml
new file mode 100644
index 00000000..c2a0f631
--- /dev/null
+++ b/tests/cloud_tests/configs/modules/set_password_list_string.yaml
@@ -0,0 +1,40 @@
+#
+# Set password of list of users as a string
+#
+cloud_config: |
+ #cloud-config
+ ssh_pwauth: yes
+ users:
+ - name: tom
+ # md5 gotomgo
+ passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0"
+ lock_passwd: false
+ - name: dick
+ # md5 gocubsgo
+ passwd: "$1$ssisyfpf$YqvuJLfrrW6Cg/l53Pi1n1"
+ lock_passwd: false
+ - name: harry
+ # sha512 goharrygo
+ passwd: "$6$LF$9Z2p6rWK6TNC1DC6393ec0As.18KRAvKDbfsGJEdWN3sRQRwpdfoh37EQ3yUh69tP4GSrGW5XKHxMLiKowJgm/"
+ lock_passwd: false
+ - name: jane
+ # sha256 gojanego
+ passwd: "$5$iW$XsxmWCdpwIW8Yhv.Jn/R3uk6A4UaicfW5Xp7C9p9pg."
+ lock_passwd: false
+ - name: "mikey"
+ lock_passwd: false
+ chpasswd:
+ list: |
+ tom:mypassword123!
+ dick:RANDOM
+ harry:RANDOM
+ mikey:$5$xZ$B2YGGEx2AOf4PeW48KC6.QyT1W2B4rZ9Qbltudtha89
+collect_scripts:
+ shadow: |
+ #!/bin/bash
+ cat /etc/shadow
+ sshd_config: |
+ #!/bin/bash
+ grep '^PasswordAuth' /etc/ssh/sshd_config
+
+# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/configs/modules/timezone.yaml b/tests/cloud_tests/configs/modules/timezone.yaml
index 6a05aba1..8c96ed47 100644
--- a/tests/cloud_tests/configs/modules/timezone.yaml
+++ b/tests/cloud_tests/configs/modules/timezone.yaml
@@ -7,6 +7,8 @@ cloud_config: |
collect_scripts:
timezone: |
#!/bin/bash
- date +%Z
+ # date will convert this to system's configured time zone.
+ # use a static date to avoid dealing with daylight savings.
+ date "+%Z" --date="Thu, 03 Nov 2016 00:47:00 -0400"
# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/__init__.py b/tests/cloud_tests/testcases/__init__.py
index 182c090a..a1d86d45 100644
--- a/tests/cloud_tests/testcases/__init__.py
+++ b/tests/cloud_tests/testcases/__init__.py
@@ -21,7 +21,7 @@ def discover_tests(test_name):
raise ValueError('no test verifier found at: {}'.format(testmod_name))
return [mod for name, mod in inspect.getmembers(testmod)
- if inspect.isclass(mod) and base_test in mod.__bases__ and
+ if inspect.isclass(mod) and base_test in inspect.getmro(mod) and
getattr(mod, '__test__', True)]
diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py
index 5395b9a3..64d5507a 100644
--- a/tests/cloud_tests/testcases/base.py
+++ b/tests/cloud_tests/testcases/base.py
@@ -2,6 +2,7 @@
from cloudinit import util as c_util
+import crypt
import json
import unittest
@@ -14,6 +15,9 @@ class CloudTestCase(unittest.TestCase):
conf = None
_cloud_config = None
+ def shortDescription(self):
+ return None
+
@property
def cloud_config(self):
"""
@@ -78,4 +82,56 @@ class CloudTestCase(unittest.TestCase):
result = self.get_status_data(self.get_data_file('result.json'))
self.assertEqual(len(result['errors']), 0)
+
+class PasswordListTest(CloudTestCase):
+ def test_shadow_passwords(self):
+ shadow = self.get_data_file('shadow')
+ users = {}
+ dupes = []
+ for line in shadow.splitlines():
+ user, encpw = line.split(":")[0:2]
+ if user in users:
+ dupes.append(user)
+ users[user] = encpw
+
+ jane_enc = "$5$iW$XsxmWCdpwIW8Yhv.Jn/R3uk6A4UaicfW5Xp7C9p9pg."
+ self.assertEqual([], dupes)
+ self.assertEqual(jane_enc, users['jane'])
+
+ mikey_enc = "$5$xZ$B2YGGEx2AOf4PeW48KC6.QyT1W2B4rZ9Qbltudtha89"
+ self.assertEqual(mikey_enc, users['mikey'])
+
+ # shadow entry is $N$salt$, so we encrypt with the same format
+ # and salt and expect the result.
+ tom = "mypassword123!"
+ fmtsalt = users['tom'][0:users['tom'].rfind("$") + 1]
+ tom_enc = crypt.crypt(tom, fmtsalt)
+ self.assertEqual(tom_enc, users['tom'])
+
+ harry_enc = ("$6$LF$9Z2p6rWK6TNC1DC6393ec0As.18KRAvKDbfsG"
+ "JEdWN3sRQRwpdfoh37EQ3yUh69tP4GSrGW5XKHxMLiKowJgm/")
+ dick_enc = "$1$ssisyfpf$YqvuJLfrrW6Cg/l53Pi1n1"
+
+ # these should have been changed to random values.
+ self.assertNotEqual(harry_enc, users['harry'])
+ self.assertTrue(users['harry'].startswith("$"))
+ self.assertNotEqual(dick_enc, users['dick'])
+ self.assertTrue(users['dick'].startswith("$"))
+
+ self.assertNotEqual(users['harry'], users['dick'])
+
+ def test_shadow_expected_users(self):
+ """Test every tom, dick, and harry user in shadow"""
+ out = self.get_data_file('shadow')
+ self.assertIn('tom:', out)
+ self.assertIn('dick:', out)
+ self.assertIn('harry:', out)
+ self.assertIn('jane:', out)
+ self.assertIn('mikey:', out)
+
+ def test_sshd_config(self):
+ """Test sshd config allows passwords"""
+ out = self.get_data_file('sshd_config')
+ self.assertIn('PasswordAuthentication yes', out)
+
# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/set_password_list.py b/tests/cloud_tests/testcases/modules/set_password_list.py
index b764362f..6819d259 100644
--- a/tests/cloud_tests/testcases/modules/set_password_list.py
+++ b/tests/cloud_tests/testcases/modules/set_password_list.py
@@ -4,22 +4,8 @@
from tests.cloud_tests.testcases import base
-class TestPasswordList(base.CloudTestCase):
- """Test password module"""
-
- # TODO: Verify dick and harry passwords are random
- # TODO: Verify tom's password was changed
-
- def test_shadow(self):
- """Test every tom, dick, and harry user in shadow"""
- out = self.get_data_file('shadow')
- self.assertIn('tom:', out)
- self.assertIn('dick:', out)
- self.assertIn('harry:', out)
-
- def test_sshd_config(self):
- """Test sshd config allows passwords"""
- out = self.get_data_file('sshd_config')
- self.assertIn('PasswordAuthentication yes', out)
+class TestPasswordList(base.PasswordListTest, base.CloudTestCase):
+ """Test password setting via list in chpasswd/list"""
+ __test__ = True
# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/set_password_list_string.py b/tests/cloud_tests/testcases/modules/set_password_list_string.py
new file mode 100644
index 00000000..2c34fada
--- /dev/null
+++ b/tests/cloud_tests/testcases/modules/set_password_list_string.py
@@ -0,0 +1,11 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""cloud-init Integration Test Verify Script"""
+from tests.cloud_tests.testcases import base
+
+
+class TestPasswordListString(base.PasswordListTest, base.CloudTestCase):
+ """Test password setting via string in chpasswd/list"""
+ __test__ = True
+
+# vi: ts=4 expandtab
diff --git a/tests/cloud_tests/testcases/modules/timezone.py b/tests/cloud_tests/testcases/modules/timezone.py
index 272c266f..bf91d490 100644
--- a/tests/cloud_tests/testcases/modules/timezone.py
+++ b/tests/cloud_tests/testcases/modules/timezone.py
@@ -10,6 +10,6 @@ class TestTimezone(base.CloudTestCase):
def test_timezone(self):
"""Test date prints correct timezone"""
out = self.get_data_file('timezone')
- self.assertIn('HST', out)
+ self.assertEqual('HDT', out.rstrip())
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/test_datasource/test_configdrive.py
index 55153357..337be667 100644
--- a/tests/unittests/test_datasource/test_configdrive.py
+++ b/tests/unittests/test_datasource/test_configdrive.py
@@ -645,7 +645,7 @@ class TestConvertNetworkData(TestCase):
routes)
eni_renderer = eni.Renderer()
eni_renderer.render_network_state(
- self.tmp, network_state.parse_net_config_data(ncfg))
+ network_state.parse_net_config_data(ncfg), self.tmp)
with open(os.path.join(self.tmp, "etc",
"network", "interfaces"), 'r') as f:
eni_rendering = f.read()
@@ -665,8 +665,9 @@ class TestConvertNetworkData(TestCase):
ncfg = openstack.convert_net_json(NETWORK_DATA_BOND,
known_macs=KNOWN_MACS)
eni_renderer = eni.Renderer()
+
eni_renderer.render_network_state(
- self.tmp, network_state.parse_net_config_data(ncfg))
+ network_state.parse_net_config_data(ncfg), self.tmp)
with open(os.path.join(self.tmp, "etc",
"network", "interfaces"), 'r') as f:
eni_rendering = f.read()
@@ -697,7 +698,7 @@ class TestConvertNetworkData(TestCase):
known_macs=KNOWN_MACS)
eni_renderer = eni.Renderer()
eni_renderer.render_network_state(
- self.tmp, network_state.parse_net_config_data(ncfg))
+ network_state.parse_net_config_data(ncfg), self.tmp)
with open(os.path.join(self.tmp, "etc",
"network", "interfaces"), 'r') as f:
eni_rendering = f.read()
diff --git a/tests/unittests/test_datasource/test_digitalocean.py b/tests/unittests/test_datasource/test_digitalocean.py
index 9be6bc19..61d6e001 100644
--- a/tests/unittests/test_datasource/test_digitalocean.py
+++ b/tests/unittests/test_datasource/test_digitalocean.py
@@ -194,7 +194,12 @@ class TestDataSourceDigitalOcean(TestCase):
class TestNetworkConvert(TestCase):
- def _get_networking(self):
+ @mock.patch('cloudinit.net.get_interfaces_by_mac')
+ def _get_networking(self, m_get_by_mac):
+ m_get_by_mac.return_value = {
+ '04:01:57:d1:9e:01': 'ens1', '04:01:57:d1:9e:02': 'ens2',
+ 'b8:ae:ed:75:5f:9a': 'enp0s25',
+ 'ae:cc:08:7c:88:00': 'meta2p1'}
netcfg = digitalocean.convert_network_configuration(
DO_META['interfaces'], DO_META['dns']['nameservers'])
self.assertIn('config', netcfg)
@@ -302,10 +307,15 @@ class TestNetworkConvert(TestCase):
self.assertEqual(ipv4_def.get('netmask'), subn_def.get('netmask'))
self.assertNotIn('gateway', subn_def)
- def test_convert_without_private(self):
+ @mock.patch('cloudinit.net.get_interfaces_by_mac')
+ def test_convert_without_private(self, m_get_by_mac):
+ m_get_by_mac.return_value = {
+ 'b8:ae:ed:75:5f:9a': 'enp0s25',
+ 'ae:cc:08:7c:88:00': 'meta2p1'}
netcfg = digitalocean.convert_network_configuration(
DO_META_2['interfaces'], DO_META_2['dns']['nameservers'])
+ # print(netcfg)
byname = {}
for i in netcfg['config']:
if 'name' in i:
diff --git a/tests/unittests/test_datasource/test_gce.py b/tests/unittests/test_datasource/test_gce.py
index 4f83454e..3eaa58e3 100644
--- a/tests/unittests/test_datasource/test_gce.py
+++ b/tests/unittests/test_datasource/test_gce.py
@@ -5,6 +5,7 @@
# This file is part of cloud-init. See LICENSE file for license information.
import httpretty
+import mock
import re
from base64 import b64encode, b64decode
@@ -71,6 +72,11 @@ class TestDataSourceGCE(test_helpers.HttprettyTestCase):
self.ds = DataSourceGCE.DataSourceGCE(
settings.CFG_BUILTIN, None,
helpers.Paths({}))
+ self.m_platform_reports_gce = mock.patch(
+ 'cloudinit.sources.DataSourceGCE.platform_reports_gce',
+ return_value=True)
+ self.m_platform_reports_gce.start()
+ self.addCleanup(self.m_platform_reports_gce.stop)
super(TestDataSourceGCE, self).setUp()
def test_connection(self):
@@ -153,7 +159,13 @@ class TestDataSourceGCE(test_helpers.HttprettyTestCase):
def test_only_last_part_of_zone_used_for_availability_zone(self):
_set_mock_metadata()
- self.ds.get_data()
+ r = self.ds.get_data()
+ self.assertEqual(True, r)
self.assertEqual('bar', self.ds.availability_zone)
+ def test_get_data_returns_false_if_not_on_gce(self):
+ self.m_platform_reports_gce.return_value = False
+ self.assertEqual(False, self.ds.get_data())
+
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_datasource/test_opennebula.py b/tests/unittests/test_datasource/test_opennebula.py
index a266e952..bce66125 100644
--- a/tests/unittests/test_datasource/test_opennebula.py
+++ b/tests/unittests/test_datasource/test_opennebula.py
@@ -195,7 +195,9 @@ class TestOpenNebulaDataSource(TestCase):
self.assertTrue('userdata' in results)
self.assertEqual(USER_DATA, results['userdata'])
- def test_hostname(self):
+ @mock.patch(DS_PATH + ".get_physical_nics_by_mac")
+ def test_hostname(self, m_get_phys_by_mac):
+ m_get_phys_by_mac.return_value = {'02:00:0a:12:01:01': 'eth0'}
for k in ('HOSTNAME', 'PUBLIC_IP', 'IP_PUBLIC', 'ETH0_IP'):
my_d = os.path.join(self.tmp, k)
populate_context_dir(my_d, {k: PUBLIC_IP})
@@ -205,11 +207,14 @@ class TestOpenNebulaDataSource(TestCase):
self.assertTrue('local-hostname' in results['metadata'])
self.assertEqual(PUBLIC_IP, results['metadata']['local-hostname'])
- def test_network_interfaces(self):
+ @mock.patch(DS_PATH + ".get_physical_nics_by_mac")
+ def test_network_interfaces(self, m_get_phys_by_mac):
+ m_get_phys_by_mac.return_value = {'02:00:0a:12:01:01': 'eth0'}
populate_context_dir(self.seed_dir, {'ETH0_IP': '1.2.3.4'})
results = ds.read_context_disk_dir(self.seed_dir)
self.assertTrue('network-interfaces' in results)
+ self.assertTrue('1.2.3.4' in results['network-interfaces'])
def test_find_candidates(self):
def my_devs_with(criteria):
diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
index bde3bb50..88370669 100644
--- a/tests/unittests/test_distros/test_netconfig.py
+++ b/tests/unittests/test_distros/test_netconfig.py
@@ -17,6 +17,7 @@ from ..helpers import TestCase
from cloudinit import distros
from cloudinit.distros.parsers.sys_conf import SysConf
from cloudinit import helpers
+from cloudinit.net import eni
from cloudinit import settings
from cloudinit import util
@@ -28,10 +29,10 @@ 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
+ netmask 255.255.255.0
+ network 192.168.0.0
auto eth1
iface eth1 inet dhcp
@@ -67,6 +68,100 @@ iface eth1 inet6 static
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_OUTPUT = """
+# This file is generated from information provided by
+# the datasource. Changes to it will not persist across an instance.
+# 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
+ broadcast 192.168.1.0
+ gateway 192.168.1.254
+ netmask 255.255.255.0
+
+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': 'static'}],
+ '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.
+# 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/255.255.255.0
+ gateway4: 192.168.1.254
+ eth1:
+ dhcp4: true
+"""
+
+V2_NET_CFG = {
+ 'ethernets': {
+ 'eth7': {
+ 'addresses': ['192.168.1.5/255.255.255.0'],
+ '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.
+# 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:
+ eth7:
+ addresses:
+ - 192.168.1.5/255.255.255.0
+ gateway4: 192.168.1.254
+ eth9:
+ dhcp4: true
+"""
+
class WriteBuffer(object):
def __init__(self):
@@ -83,12 +178,14 @@ class WriteBuffer(object):
class TestNetCfgDistro(TestCase):
- def _get_distro(self, dname):
+ 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, paths)
+ return cls(dname, cfg.get('system_info'), paths)
def test_simple_write_ub(self):
ub_distro = self._get_distro('ubuntu')
@@ -116,6 +213,110 @@ class TestNetCfgDistro(TestCase):
self.assertEqual(str(write_buf).strip(), BASE_NET_CFG.strip())
self.assertEqual(write_buf.mode, 0o644)
+ def test_apply_network_config_eni_ub(self):
+ ub_distro = self._get_distro('ubuntu')
+ with ExitStack() as mocks:
+ write_bufs = {}
+
+ def replace_write(filename, content, mode=0o644, omode="wb"):
+ buf = WriteBuffer()
+ buf.mode = mode
+ buf.omode = omode
+ buf.write(content)
+ write_bufs[filename] = buf
+
+ # eni availability checks
+ mocks.enter_context(
+ mock.patch.object(util, 'which', return_value=True))
+ mocks.enter_context(
+ mock.patch.object(eni, 'available', return_value=True))
+ mocks.enter_context(
+ mock.patch.object(util, 'ensure_dir'))
+ mocks.enter_context(
+ mock.patch.object(util, 'write_file', replace_write))
+ mocks.enter_context(
+ mock.patch.object(os.path, 'isfile', return_value=False))
+ mocks.enter_context(
+ mock.patch("cloudinit.net.eni.glob.glob",
+ return_value=[]))
+
+ ub_distro.apply_network_config(V1_NET_CFG, False)
+
+ self.assertEqual(len(write_bufs), 2)
+ eni_name = '/etc/network/interfaces.d/50-cloud-init.cfg'
+ self.assertIn(eni_name, write_bufs)
+ write_buf = write_bufs[eni_name]
+ self.assertEqual(str(write_buf).strip(), V1_NET_CFG_OUTPUT.strip())
+ self.assertEqual(write_buf.mode, 0o644)
+
+ def test_apply_network_config_v1_to_netplan_ub(self):
+ renderers = ['netplan']
+ ub_distro = self._get_distro('ubuntu', renderers=renderers)
+ with ExitStack() as mocks:
+ write_bufs = {}
+
+ def replace_write(filename, content, mode=0o644, omode="wb"):
+ buf = WriteBuffer()
+ buf.mode = mode
+ buf.omode = omode
+ buf.write(content)
+ write_bufs[filename] = buf
+
+ mocks.enter_context(
+ mock.patch.object(util, 'which', return_value=True))
+ mocks.enter_context(
+ mock.patch.object(util, 'write_file', replace_write))
+ mocks.enter_context(
+ mock.patch.object(util, 'ensure_dir'))
+ mocks.enter_context(
+ mock.patch.object(util, 'subp', return_value=(0, 0)))
+ mocks.enter_context(
+ mock.patch.object(os.path, 'isfile', return_value=False))
+
+ ub_distro.apply_network_config(V1_NET_CFG, False)
+
+ self.assertEqual(len(write_bufs), 1)
+ netplan_name = '/etc/netplan/50-cloud-init.yaml'
+ self.assertIn(netplan_name, write_bufs)
+ write_buf = write_bufs[netplan_name]
+ self.assertEqual(str(write_buf).strip(),
+ V1_TO_V2_NET_CFG_OUTPUT.strip())
+ self.assertEqual(write_buf.mode, 0o644)
+
+ def test_apply_network_config_v2_passthrough_ub(self):
+ renderers = ['netplan']
+ ub_distro = self._get_distro('ubuntu', renderers=renderers)
+ with ExitStack() as mocks:
+ write_bufs = {}
+
+ def replace_write(filename, content, mode=0o644, omode="wb"):
+ buf = WriteBuffer()
+ buf.mode = mode
+ buf.omode = omode
+ buf.write(content)
+ write_bufs[filename] = buf
+
+ mocks.enter_context(
+ mock.patch.object(util, 'which', return_value=True))
+ mocks.enter_context(
+ mock.patch.object(util, 'write_file', replace_write))
+ mocks.enter_context(
+ mock.patch.object(util, 'ensure_dir'))
+ mocks.enter_context(
+ mock.patch.object(util, 'subp', return_value=(0, 0)))
+ mocks.enter_context(
+ mock.patch.object(os.path, 'isfile', return_value=False))
+
+ ub_distro.apply_network_config(V2_NET_CFG, False)
+
+ self.assertEqual(len(write_bufs), 1)
+ netplan_name = '/etc/netplan/50-cloud-init.yaml'
+ self.assertIn(netplan_name, write_bufs)
+ write_buf = write_bufs[netplan_name]
+ self.assertEqual(str(write_buf).strip(),
+ V2_TO_V2_NET_CFG_OUTPUT.strip())
+ self.assertEqual(write_buf.mode, 0o644)
+
def assertCfgEquals(self, blob1, blob2):
b1 = dict(SysConf(blob1.strip().splitlines()))
b2 = dict(SysConf(blob2.strip().splitlines()))
@@ -195,6 +396,79 @@ NETWORKING=yes
self.assertCfgEquals(expected_buf, str(write_buf))
self.assertEqual(write_buf.mode, 0o644)
+ def test_apply_network_config_rh(self):
+ renderers = ['sysconfig']
+ rh_distro = self._get_distro('rhel', renderers=renderers)
+
+ write_bufs = {}
+
+ def replace_write(filename, content, mode=0o644, omode="wb"):
+ buf = WriteBuffer()
+ buf.mode = mode
+ buf.omode = omode
+ buf.write(content)
+ write_bufs[filename] = buf
+
+ with ExitStack() as mocks:
+ # sysconfig availability checks
+ mocks.enter_context(
+ mock.patch.object(util, 'which', return_value=True))
+ mocks.enter_context(
+ mock.patch.object(util, 'write_file', replace_write))
+ mocks.enter_context(
+ mock.patch.object(util, 'load_file', return_value=''))
+ mocks.enter_context(
+ mock.patch.object(os.path, 'isfile', return_value=True))
+
+ rh_distro.apply_network_config(V1_NET_CFG, False)
+
+ self.assertEqual(len(write_bufs), 5)
+
+ # eth0
+ self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth0',
+ write_bufs)
+ write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth0']
+ expected_buf = '''
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=static
+DEVICE=eth0
+IPADDR=192.168.1.5
+NETMASK=255.255.255.0
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+'''
+ self.assertCfgEquals(expected_buf, str(write_buf))
+ self.assertEqual(write_buf.mode, 0o644)
+
+ # eth1
+ self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth1',
+ write_bufs)
+ write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth1']
+ expected_buf = '''
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=dhcp
+DEVICE=eth1
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+'''
+ self.assertCfgEquals(expected_buf, str(write_buf))
+ self.assertEqual(write_buf.mode, 0o644)
+
+ self.assertIn('/etc/sysconfig/network', write_bufs)
+ write_buf = write_bufs['/etc/sysconfig/network']
+ expected_buf = '''
+# Created by cloud-init v. 0.7
+NETWORKING=yes
+'''
+ self.assertCfgEquals(expected_buf, str(write_buf))
+ self.assertEqual(write_buf.mode, 0o644)
+
def test_write_ipv6_rhel(self):
rh_distro = self._get_distro('rhel')
@@ -274,6 +548,78 @@ IPV6_AUTOCONF=no
self.assertCfgEquals(expected_buf, str(write_buf))
self.assertEqual(write_buf.mode, 0o644)
+ def test_apply_network_config_ipv6_rh(self):
+ renderers = ['sysconfig']
+ rh_distro = self._get_distro('rhel', renderers=renderers)
+
+ write_bufs = {}
+
+ def replace_write(filename, content, mode=0o644, omode="wb"):
+ buf = WriteBuffer()
+ buf.mode = mode
+ buf.omode = omode
+ buf.write(content)
+ write_bufs[filename] = buf
+
+ with ExitStack() as mocks:
+ mocks.enter_context(
+ mock.patch.object(util, 'which', return_value=True))
+ mocks.enter_context(
+ mock.patch.object(util, 'write_file', replace_write))
+ mocks.enter_context(
+ mock.patch.object(util, 'load_file', return_value=''))
+ mocks.enter_context(
+ mock.patch.object(os.path, 'isfile', return_value=True))
+
+ rh_distro.apply_network_config(V1_NET_CFG_IPV6, False)
+
+ self.assertEqual(len(write_bufs), 5)
+
+ self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth0',
+ write_bufs)
+ write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth0']
+ expected_buf = '''
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=static
+DEVICE=eth0
+IPV6ADDR=2607:f0d0:1002:0011::2
+IPV6INIT=yes
+NETMASK=64
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+'''
+ self.assertCfgEquals(expected_buf, str(write_buf))
+ self.assertEqual(write_buf.mode, 0o644)
+ self.assertIn('/etc/sysconfig/network-scripts/ifcfg-eth1',
+ write_bufs)
+ write_buf = write_bufs['/etc/sysconfig/network-scripts/ifcfg-eth1']
+ expected_buf = '''
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=dhcp
+DEVICE=eth1
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+'''
+ self.assertCfgEquals(expected_buf, str(write_buf))
+ self.assertEqual(write_buf.mode, 0o644)
+
+ self.assertIn('/etc/sysconfig/network', write_bufs)
+ write_buf = write_bufs['/etc/sysconfig/network']
+ expected_buf = '''
+# Created by cloud-init v. 0.7
+NETWORKING=yes
+NETWORKING_IPV6=yes
+IPV6_AUTOCONF=no
+'''
+ self.assertCfgEquals(expected_buf, str(write_buf))
+ self.assertEqual(write_buf.mode, 0o644)
+
def test_simple_write_freebsd(self):
fbsd_distro = self._get_distro('freebsd')
diff --git a/tests/unittests/test_distros/test_resolv.py b/tests/unittests/test_distros/test_resolv.py
index 6b535a95..c9d03475 100644
--- a/tests/unittests/test_distros/test_resolv.py
+++ b/tests/unittests/test_distros/test_resolv.py
@@ -46,7 +46,7 @@ class TestResolvHelper(TestCase):
self.assertNotIn('10.3', rp.nameservers)
self.assertEqual(len(rp.nameservers), 3)
rp.add_nameserver('10.2')
- self.assertRaises(ValueError, rp.add_nameserver, '10.3')
+ rp.add_nameserver('10.3')
self.assertNotIn('10.3', rp.nameservers)
def test_search_domains(self):
diff --git a/tests/unittests/test_handler/test_handler_disk_setup.py b/tests/unittests/test_handler/test_handler_disk_setup.py
index 227f0497..7ff39225 100644
--- a/tests/unittests/test_handler/test_handler_disk_setup.py
+++ b/tests/unittests/test_handler/test_handler_disk_setup.py
@@ -103,4 +103,48 @@ class TestGetPartitionMbrLayout(TestCase):
',{0},83\n,,82'.format(expected_partition_size),
cc_disk_setup.get_partition_mbr_layout(disk_size, [33, [66, 82]]))
+
+class TestUpdateFsSetupDevices(TestCase):
+ def test_regression_1634678(self):
+ # Cf. https://bugs.launchpad.net/cloud-init/+bug/1634678
+ fs_setup = {
+ 'partition': 'auto',
+ 'device': '/dev/xvdb1',
+ 'overwrite': False,
+ 'label': 'test',
+ 'filesystem': 'ext4'
+ }
+
+ cc_disk_setup.update_fs_setup_devices([fs_setup],
+ lambda device: device)
+
+ self.assertEqual({
+ '_origname': '/dev/xvdb1',
+ 'partition': 'auto',
+ 'device': '/dev/xvdb1',
+ 'overwrite': False,
+ 'label': 'test',
+ 'filesystem': 'ext4'
+ }, fs_setup)
+
+ def test_dotted_devname(self):
+ fs_setup = {
+ 'partition': 'auto',
+ 'device': 'ephemeral0.0',
+ 'label': 'test2',
+ 'filesystem': 'xfs'
+ }
+
+ cc_disk_setup.update_fs_setup_devices([fs_setup],
+ lambda device: device)
+
+ self.assertEqual({
+ '_origname': 'ephemeral0.0',
+ '_partition': 'auto',
+ 'partition': '0',
+ 'device': 'ephemeral0',
+ 'label': 'test2',
+ 'filesystem': 'xfs'
+ }, fs_setup)
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
index 4b03ff72..9cc5e4ab 100644
--- a/tests/unittests/test_net.py
+++ b/tests/unittests/test_net.py
@@ -3,7 +3,9 @@
from cloudinit import net
from cloudinit.net import cmdline
from cloudinit.net import eni
+from cloudinit.net import netplan
from cloudinit.net import network_state
+from cloudinit.net import renderers
from cloudinit.net import sysconfig
from cloudinit.sources.helpers import openstack
from cloudinit import util
@@ -248,6 +250,100 @@ nameserver 172.19.0.12
('etc/udev/rules.d/70-persistent-net.rules',
"".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',
'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))]
+ },
+ {
+ 'in_data': {
+ "services": [{"type": "dns", "address": "172.19.0.12"}],
+ "networks": [{
+ "network_id": "public-ipv4",
+ "type": "ipv4", "netmask": "255.255.252.0",
+ "link": "tap1a81968a-79",
+ "routes": [{
+ "netmask": "0.0.0.0",
+ "network": "0.0.0.0",
+ "gateway": "172.19.3.254",
+ }],
+ "ip_address": "172.19.1.34", "id": "network0"
+ }, {
+ "network_id": "public-ipv6",
+ "type": "ipv6", "netmask": "",
+ "link": "tap1a81968a-79",
+ "routes": [
+ {
+ "gateway": "2001:DB8::1",
+ "netmask": "::",
+ "network": "::"
+ }
+ ],
+ "ip_address": "2001:DB8::10", "id": "network1"
+ }],
+ "links": [
+ {
+ "ethernet_mac_address": "fa:16:3e:ed:9a:59",
+ "mtu": None, "type": "bridge", "id":
+ "tap1a81968a-79",
+ "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f"
+ },
+ ],
+ },
+ 'in_macs': {
+ 'fa:16:3e:ed:9a:59': 'eth0',
+ },
+ 'out_sysconfig': [
+ ('etc/sysconfig/network-scripts/ifcfg-eth0',
+ """
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=none
+DEVICE=eth0
+HWADDR=fa:16:3e:ed:9a:59
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+""".lstrip()),
+ ('etc/sysconfig/network-scripts/ifcfg-eth0:0',
+ """
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=static
+DEFROUTE=yes
+DEVICE=eth0:0
+GATEWAY=172.19.3.254
+HWADDR=fa:16:3e:ed:9a:59
+IPADDR=172.19.1.34
+NETMASK=255.255.252.0
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+""".lstrip()),
+ ('etc/sysconfig/network-scripts/ifcfg-eth0:1',
+ """
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=static
+DEFROUTE=yes
+DEVICE=eth0:1
+HWADDR=fa:16:3e:ed:9a:59
+IPV6ADDR=2001:DB8::10
+IPV6INIT=yes
+IPV6_DEFAULTGW=2001:DB8::1
+NETMASK=
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+""".lstrip()),
+ ('etc/resolv.conf',
+ """
+; Created by cloud-init on instance boot automatically, do not edit.
+;
+nameserver 172.19.0.12
+""".lstrip()),
+ ('etc/udev/rules.d/70-persistent-net.rules',
+ "".join(['SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ',
+ 'ATTR{address}=="fa:16:3e:ed:9a:59", NAME="eth0"\n']))]
}
]
@@ -313,6 +409,41 @@ NETWORK_CONFIGS = {
post-up route add default gw 65.61.151.37 || true
pre-down route del default gw 65.61.151.37 || true
""").rstrip(' '),
+ 'expected_netplan': textwrap.dedent("""
+ network:
+ version: 2
+ ethernets:
+ eth1:
+ match:
+ macaddress: cf:d6:af:48:e8:80
+ nameservers:
+ addresses:
+ - 1.2.3.4
+ - 5.6.7.8
+ search:
+ - wark.maas
+ set-name: eth1
+ eth99:
+ addresses:
+ - 192.168.21.3/24
+ dhcp4: true
+ match:
+ macaddress: c0:d6:9f:2c:e8:80
+ nameservers:
+ addresses:
+ - 8.8.8.8
+ - 8.8.4.4
+ - 1.2.3.4
+ - 5.6.7.8
+ search:
+ - barley.maas
+ - sach.maas
+ - wark.maas
+ routes:
+ - to: 0.0.0.0/0.0.0.0
+ via: 65.61.151.37
+ set-name: eth99
+ """).rstrip(' '),
'yaml': textwrap.dedent("""
version: 1
config:
@@ -355,6 +486,14 @@ NETWORK_CONFIGS = {
# control-alias iface0
iface iface0 inet6 dhcp
""").rstrip(' '),
+ 'expected_netplan': textwrap.dedent("""
+ network:
+ version: 2
+ ethernets:
+ iface0:
+ dhcp4: true
+ dhcp6: true
+ """).rstrip(' '),
'yaml': textwrap.dedent("""\
version: 1
config:
@@ -429,6 +568,126 @@ iface eth0.101 inet static
post-up route add -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
"""),
+ 'expected_netplan': textwrap.dedent("""
+ network:
+ version: 2
+ ethernets:
+ eth0:
+ match:
+ macaddress: c0:d6:9f:2c:e8:80
+ nameservers:
+ addresses:
+ - 8.8.8.8
+ - 4.4.4.4
+ - 8.8.4.4
+ search:
+ - barley.maas
+ - wark.maas
+ - foobar.maas
+ set-name: eth0
+ eth1:
+ match:
+ macaddress: aa:d6:9f:2c:e8:80
+ nameservers:
+ addresses:
+ - 8.8.8.8
+ - 4.4.4.4
+ - 8.8.4.4
+ search:
+ - barley.maas
+ - wark.maas
+ - foobar.maas
+ set-name: eth1
+ eth2:
+ match:
+ macaddress: c0:bb:9f:2c:e8:80
+ nameservers:
+ addresses:
+ - 8.8.8.8
+ - 4.4.4.4
+ - 8.8.4.4
+ search:
+ - barley.maas
+ - wark.maas
+ - foobar.maas
+ set-name: eth2
+ eth3:
+ match:
+ macaddress: 66:bb:9f:2c:e8:80
+ nameservers:
+ addresses:
+ - 8.8.8.8
+ - 4.4.4.4
+ - 8.8.4.4
+ search:
+ - barley.maas
+ - wark.maas
+ - foobar.maas
+ set-name: eth3
+ eth4:
+ match:
+ macaddress: 98:bb:9f:2c:e8:80
+ nameservers:
+ addresses:
+ - 8.8.8.8
+ - 4.4.4.4
+ - 8.8.4.4
+ search:
+ - barley.maas
+ - wark.maas
+ - foobar.maas
+ set-name: eth4
+ eth5:
+ dhcp4: true
+ match:
+ macaddress: 98:bb:9f:2c:e8:8a
+ nameservers:
+ addresses:
+ - 8.8.8.8
+ - 4.4.4.4
+ - 8.8.4.4
+ search:
+ - barley.maas
+ - wark.maas
+ - foobar.maas
+ set-name: eth5
+ bonds:
+ bond0:
+ dhcp6: true
+ interfaces:
+ - eth1
+ - eth2
+ parameters:
+ mode: active-backup
+ bridges:
+ br0:
+ addresses:
+ - 192.168.14.2/24
+ - 2001:1::1/64
+ interfaces:
+ - eth3
+ - eth4
+ vlans:
+ bond0.200:
+ dhcp4: true
+ id: 200
+ link: bond0
+ eth0.101:
+ addresses:
+ - 192.168.0.2/24
+ - 192.168.2.10/24
+ gateway4: 192.168.0.1
+ id: 101
+ link: eth0
+ nameservers:
+ addresses:
+ - 192.168.0.10
+ - 10.23.23.134
+ search:
+ - barley.maas
+ - sacchromyces.maas
+ - brettanomyces.maas
+ """).rstrip(' '),
'yaml': textwrap.dedent("""
version: 1
config:
@@ -543,6 +802,14 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true
}
}
+CONFIG_V1_EXPLICIT_LOOPBACK = {
+ 'version': 1,
+ 'config': [{'name': 'eth0', 'type': 'physical',
+ 'subnets': [{'control': 'auto', 'type': 'dhcp'}]},
+ {'name': 'lo', 'type': 'loopback',
+ 'subnets': [{'control': 'auto', 'type': 'loopback'}]},
+ ]}
+
def _setup_test(tmp_dir, mock_get_devicelist, mock_read_sys_net,
mock_sys_dev_path):
@@ -595,7 +862,7 @@ class TestSysConfigRendering(CiTestCase):
os.makedirs(render_dir)
renderer = sysconfig.Renderer()
- renderer.render_network_state(render_dir, ns)
+ renderer.render_network_state(ns, render_dir)
render_file = 'etc/sysconfig/network-scripts/ifcfg-eth1000'
with open(os.path.join(render_dir, render_file)) as fh:
@@ -623,11 +890,32 @@ USERCTL=no
ns = network_state.parse_net_config_data(network_cfg,
skip_broken=False)
renderer = sysconfig.Renderer()
- renderer.render_network_state(render_dir, ns)
+ renderer.render_network_state(ns, render_dir)
for fn, expected_content in os_sample.get('out_sysconfig', []):
with open(os.path.join(render_dir, fn)) as fh:
self.assertEqual(expected_content, fh.read())
+ def test_config_with_explicit_loopback(self):
+ ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK)
+ render_dir = self.tmp_path("render")
+ os.makedirs(render_dir)
+ renderer = sysconfig.Renderer()
+ renderer.render_network_state(ns, render_dir)
+ found = dir2dict(render_dir)
+ nspath = '/etc/sysconfig/network-scripts/'
+ self.assertNotIn(nspath + 'ifcfg-lo', found.keys())
+ expected = """\
+# Created by cloud-init on instance boot automatically, do not edit.
+#
+BOOTPROTO=dhcp
+DEVICE=eth0
+NM_CONTROLLED=no
+ONBOOT=yes
+TYPE=Ethernet
+USERCTL=no
+"""
+ self.assertEqual(expected, found[nspath + 'ifcfg-eth0'])
+
class TestEniNetRendering(CiTestCase):
@@ -652,7 +940,7 @@ class TestEniNetRendering(CiTestCase):
{'links_path_prefix': None,
'eni_path': 'interfaces', 'netrules_path': None,
})
- renderer.render_network_state(render_dir, ns)
+ renderer.render_network_state(ns, render_dir)
self.assertTrue(os.path.exists(os.path.join(render_dir,
'interfaces')))
@@ -668,6 +956,179 @@ iface eth1000 inet dhcp
"""
self.assertEqual(expected.lstrip(), contents.lstrip())
+ def test_config_with_explicit_loopback(self):
+ tmp_dir = self.tmp_dir()
+ ns = network_state.parse_net_config_data(CONFIG_V1_EXPLICIT_LOOPBACK)
+ renderer = eni.Renderer()
+ renderer.render_network_state(ns, tmp_dir)
+ expected = """\
+auto lo
+iface lo inet loopback
+
+auto eth0
+iface eth0 inet dhcp
+"""
+ self.assertEqual(
+ expected, dir2dict(tmp_dir)['/etc/network/interfaces'])
+
+
+class TestNetplanNetRendering(CiTestCase):
+
+ @mock.patch("cloudinit.net.netplan._clean_default")
+ @mock.patch("cloudinit.net.sys_dev_path")
+ @mock.patch("cloudinit.net.read_sys_net")
+ @mock.patch("cloudinit.net.get_devicelist")
+ def test_default_generation(self, mock_get_devicelist,
+ mock_read_sys_net,
+ mock_sys_dev_path,
+ mock_clean_default):
+ tmp_dir = self.tmp_dir()
+ _setup_test(tmp_dir, mock_get_devicelist,
+ mock_read_sys_net, mock_sys_dev_path)
+
+ network_cfg = net.generate_fallback_config()
+ ns = network_state.parse_net_config_data(network_cfg,
+ skip_broken=False)
+
+ render_dir = os.path.join(tmp_dir, "render")
+ os.makedirs(render_dir)
+
+ render_target = 'netplan.yaml'
+ renderer = netplan.Renderer(
+ {'netplan_path': render_target, 'postcmds': False})
+ renderer.render_network_state(render_dir, ns)
+
+ self.assertTrue(os.path.exists(os.path.join(render_dir,
+ render_target)))
+ with open(os.path.join(render_dir, render_target)) as fh:
+ contents = fh.read()
+ print(contents)
+
+ expected = """
+network:
+ version: 2
+ ethernets:
+ eth1000:
+ dhcp4: true
+ match:
+ macaddress: 07-1c-c6-75-a4-be
+ set-name: eth1000
+"""
+ self.assertEqual(expected.lstrip(), contents.lstrip())
+ self.assertEqual(1, mock_clean_default.call_count)
+
+
+class TestNetplanCleanDefault(CiTestCase):
+ snapd_known_path = 'etc/netplan/00-snapd-config.yaml'
+ snapd_known_content = textwrap.dedent("""\
+ # This is the initial network config.
+ # It can be overwritten by cloud-init or console-conf.
+ network:
+ version: 2
+ ethernets:
+ all-en:
+ match:
+ name: "en*"
+ dhcp4: true
+ all-eth:
+ match:
+ name: "eth*"
+ dhcp4: true
+ """)
+ stub_known = {
+ 'run/systemd/network/10-netplan-all-en.network': 'foo-en',
+ 'run/systemd/network/10-netplan-all-eth.network': 'foo-eth',
+ 'run/systemd/generator/netplan.stamp': 'stamp',
+ }
+
+ def test_clean_known_config_cleaned(self):
+ content = {self.snapd_known_path: self.snapd_known_content, }
+ content.update(self.stub_known)
+ tmpd = self.tmp_dir()
+ files = sorted(populate_dir(tmpd, content))
+ netplan._clean_default(target=tmpd)
+ found = [t for t in files if os.path.exists(t)]
+ self.assertEqual([], found)
+
+ def test_clean_unknown_config_not_cleaned(self):
+ content = {self.snapd_known_path: self.snapd_known_content, }
+ content.update(self.stub_known)
+ content[self.snapd_known_path] += "# user put a comment\n"
+ tmpd = self.tmp_dir()
+ files = sorted(populate_dir(tmpd, content))
+ netplan._clean_default(target=tmpd)
+ found = [t for t in files if os.path.exists(t)]
+ self.assertEqual(files, found)
+
+ def test_clean_known_config_cleans_only_expected(self):
+ astamp = "run/systemd/generator/another.stamp"
+ anet = "run/systemd/network/10-netplan-all-lo.network"
+ ayaml = "etc/netplan/01-foo-config.yaml"
+ content = {
+ self.snapd_known_path: self.snapd_known_content,
+ astamp: "stamp",
+ anet: "network",
+ ayaml: "yaml",
+ }
+ content.update(self.stub_known)
+
+ tmpd = self.tmp_dir()
+ files = sorted(populate_dir(tmpd, content))
+ netplan._clean_default(target=tmpd)
+ found = [t for t in files if os.path.exists(t)]
+ expected = [util.target_path(tmpd, f) for f in (astamp, anet, ayaml)]
+ self.assertEqual(sorted(expected), found)
+
+
+class TestNetplanPostcommands(CiTestCase):
+ mycfg = {
+ 'config': [{"type": "physical", "name": "eth0",
+ "mac_address": "c0:d6:9f:2c:e8:80",
+ "subnets": [{"type": "dhcp"}]}],
+ 'version': 1}
+
+ @mock.patch.object(netplan.Renderer, '_netplan_generate')
+ @mock.patch.object(netplan.Renderer, '_net_setup_link')
+ def test_netplan_render_calls_postcmds(self, mock_netplan_generate,
+ mock_net_setup_link):
+ tmp_dir = self.tmp_dir()
+ ns = network_state.parse_net_config_data(self.mycfg,
+ skip_broken=False)
+
+ render_dir = os.path.join(tmp_dir, "render")
+ os.makedirs(render_dir)
+
+ render_target = 'netplan.yaml'
+ renderer = netplan.Renderer(
+ {'netplan_path': render_target, 'postcmds': True})
+ renderer.render_network_state(render_dir, ns)
+
+ mock_netplan_generate.assert_called_with(run=True)
+ mock_net_setup_link.assert_called_with(run=True)
+
+ @mock.patch.object(netplan, "get_devicelist")
+ @mock.patch('cloudinit.util.subp')
+ def test_netplan_postcmds(self, mock_subp, mock_devlist):
+ mock_devlist.side_effect = [['lo']]
+ tmp_dir = self.tmp_dir()
+ ns = network_state.parse_net_config_data(self.mycfg,
+ skip_broken=False)
+
+ render_dir = os.path.join(tmp_dir, "render")
+ os.makedirs(render_dir)
+
+ render_target = 'netplan.yaml'
+ renderer = netplan.Renderer(
+ {'netplan_path': render_target, 'postcmds': True})
+ renderer.render_network_state(render_dir, ns)
+
+ expected = [
+ mock.call(['netplan', 'generate'], capture=True),
+ mock.call(['udevadm', 'test-builtin', 'net_setup_link',
+ '/sys/class/net/lo'], capture=True),
+ ]
+ mock_subp.assert_has_calls(expected)
+
class TestEniNetworkStateToEni(CiTestCase):
mycfg = {
@@ -814,6 +1275,50 @@ class TestCmdlineReadKernelConfig(CiTestCase):
self.assertEqual(found['config'], expected)
+class TestNetplanRoundTrip(CiTestCase):
+ def _render_and_read(self, network_config=None, state=None,
+ netplan_path=None, dir=None):
+ if dir is None:
+ dir = self.tmp_dir()
+
+ if network_config:
+ ns = network_state.parse_net_config_data(network_config)
+ elif state:
+ ns = state
+ else:
+ raise ValueError("Expected data or state, got neither")
+
+ if netplan_path is None:
+ netplan_path = 'etc/netplan/50-cloud-init.yaml'
+
+ renderer = netplan.Renderer(
+ config={'netplan_path': netplan_path})
+
+ renderer.render_network_state(dir, ns)
+ return dir2dict(dir)
+
+ def testsimple_render_small_netplan(self):
+ entry = NETWORK_CONFIGS['small']
+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self.assertEqual(
+ entry['expected_netplan'].splitlines(),
+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
+ def testsimple_render_v4_and_v6(self):
+ entry = NETWORK_CONFIGS['v4_and_v6']
+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self.assertEqual(
+ entry['expected_netplan'].splitlines(),
+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
+ def testsimple_render_all(self):
+ entry = NETWORK_CONFIGS['all']
+ files = self._render_and_read(network_config=yaml.load(entry['yaml']))
+ self.assertEqual(
+ entry['expected_netplan'].splitlines(),
+ files['/etc/netplan/50-cloud-init.yaml'].splitlines())
+
+
class TestEniRoundTrip(CiTestCase):
def _render_and_read(self, network_config=None, state=None, eni_path=None,
links_prefix=None, netrules_path=None, dir=None):
@@ -834,7 +1339,7 @@ class TestEniRoundTrip(CiTestCase):
config={'eni_path': eni_path, 'links_path_prefix': links_prefix,
'netrules_path': netrules_path})
- renderer.render_network_state(dir, ns)
+ renderer.render_network_state(ns, dir)
return dir2dict(dir)
def testsimple_convert_and_render(self):
@@ -912,6 +1417,126 @@ class TestEniRoundTrip(CiTestCase):
expected, [line for line in found if line])
+class TestNetRenderers(CiTestCase):
+ @mock.patch("cloudinit.net.renderers.sysconfig.available")
+ @mock.patch("cloudinit.net.renderers.eni.available")
+ def test_eni_and_sysconfig_available(self, m_eni_avail, m_sysc_avail):
+ m_eni_avail.return_value = True
+ m_sysc_avail.return_value = True
+ found = renderers.search(priority=['sysconfig', 'eni'], first=False)
+ names = [f[0] for f in found]
+ self.assertEqual(['sysconfig', 'eni'], names)
+
+ @mock.patch("cloudinit.net.renderers.eni.available")
+ def test_search_returns_empty_on_none(self, m_eni_avail):
+ m_eni_avail.return_value = False
+ found = renderers.search(priority=['eni'], first=False)
+ self.assertEqual([], found)
+
+ @mock.patch("cloudinit.net.renderers.sysconfig.available")
+ @mock.patch("cloudinit.net.renderers.eni.available")
+ def test_first_in_priority(self, m_eni_avail, m_sysc_avail):
+ # available should only be called until one is found.
+ m_eni_avail.return_value = True
+ m_sysc_avail.side_effect = Exception("Should not call me")
+ found = renderers.search(priority=['eni', 'sysconfig'], first=True)
+ self.assertEqual(['eni'], [found[0]])
+
+ @mock.patch("cloudinit.net.renderers.sysconfig.available")
+ @mock.patch("cloudinit.net.renderers.eni.available")
+ def test_select_positive(self, m_eni_avail, m_sysc_avail):
+ m_eni_avail.return_value = True
+ m_sysc_avail.return_value = False
+ found = renderers.select(priority=['sysconfig', 'eni'])
+ self.assertEqual('eni', found[0])
+
+ @mock.patch("cloudinit.net.renderers.sysconfig.available")
+ @mock.patch("cloudinit.net.renderers.eni.available")
+ def test_select_none_found_raises(self, m_eni_avail, m_sysc_avail):
+ # if select finds nothing, should raise exception.
+ m_eni_avail.return_value = False
+ m_sysc_avail.return_value = False
+
+ self.assertRaises(net.RendererNotFoundError, renderers.select,
+ priority=['sysconfig', 'eni'])
+
+
+class TestGetInterfacesByMac(CiTestCase):
+ _data = {'devices': ['enp0s1', 'enp0s2', 'bond1', 'bridge1',
+ 'bridge1-nic', 'tun0'],
+ 'bonds': ['bond1'],
+ 'bridges': ['bridge1'],
+ 'own_macs': ['enp0s1', 'enp0s2', 'bridge1-nic', 'bridge1'],
+ 'macs': {'enp0s1': 'aa:aa:aa:aa:aa:01',
+ 'enp0s2': 'aa:aa:aa:aa:aa:02',
+ 'bond1': 'aa:aa:aa:aa:aa:01',
+ 'bridge1': 'aa:aa:aa:aa:aa:03',
+ 'bridge1-nic': 'aa:aa:aa:aa:aa:03',
+ 'tun0': None}}
+ data = {}
+
+ def _se_get_devicelist(self):
+ return self.data['devices']
+
+ def _se_get_interface_mac(self, name):
+ return self.data['macs'][name]
+
+ def _se_is_bridge(self, name):
+ return name in self.data['bridges']
+
+ def _se_interface_has_own_mac(self, name):
+ return name in self.data['own_macs']
+
+ def _mock_setup(self):
+ self.data = copy.deepcopy(self._data)
+ mocks = ('get_devicelist', 'get_interface_mac', 'is_bridge',
+ 'interface_has_own_mac')
+ self.mocks = {}
+ for n in mocks:
+ m = mock.patch('cloudinit.net.' + n,
+ side_effect=getattr(self, '_se_' + n))
+ self.addCleanup(m.stop)
+ self.mocks[n] = m.start()
+
+ def test_raise_exception_on_duplicate_macs(self):
+ self._mock_setup()
+ self.data['macs']['bridge1-nic'] = self.data['macs']['enp0s1']
+ self.assertRaises(RuntimeError, net.get_interfaces_by_mac)
+
+ def test_excludes_any_without_mac_address(self):
+ self._mock_setup()
+ ret = net.get_interfaces_by_mac()
+ self.assertIn('tun0', self._se_get_devicelist())
+ self.assertNotIn('tun0', ret.values())
+
+ def test_excludes_stolen_macs(self):
+ self._mock_setup()
+ ret = net.get_interfaces_by_mac()
+ self.mocks['interface_has_own_mac'].assert_has_calls(
+ [mock.call('enp0s1'), mock.call('bond1')], any_order=True)
+ self.assertEqual(
+ {'aa:aa:aa:aa:aa:01': 'enp0s1', 'aa:aa:aa:aa:aa:02': 'enp0s2',
+ 'aa:aa:aa:aa:aa:03': 'bridge1-nic'},
+ ret)
+
+ def test_excludes_bridges(self):
+ self._mock_setup()
+ # add a device 'b1', make all return they have their "own mac",
+ # set everything other than 'b1' to be a bridge.
+ # then expect b1 is the only thing left.
+ self.data['macs']['b1'] = 'aa:aa:aa:aa:aa:b1'
+ self.data['devices'].append('b1')
+ self.data['bonds'] = []
+ self.data['own_macs'] = self.data['devices']
+ self.data['bridges'] = [f for f in self.data['devices'] if f != "b1"]
+ ret = net.get_interfaces_by_mac()
+ self.assertEqual({'aa:aa:aa:aa:aa:b1': 'b1'}, ret)
+ self.mocks['is_bridge'].assert_has_calls(
+ [mock.call('bridge1'), mock.call('enp0s1'), mock.call('bond1'),
+ mock.call('b1')],
+ any_order=True)
+
+
def _gzip_data(data):
with io.BytesIO() as iobuf:
gzfp = gzip.GzipFile(mode="wb", fileobj=iobuf)
diff --git a/tests/unittests/test_version.py b/tests/unittests/test_version.py
new file mode 100644
index 00000000..1662ce09
--- /dev/null
+++ b/tests/unittests/test_version.py
@@ -0,0 +1,14 @@
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from .helpers import CiTestCase
+from cloudinit import version
+
+
+class TestExportsFeatures(CiTestCase):
+ def test_has_network_config_v1(self):
+ self.assertIn('NETWORK_CONFIG_V1', version.FEATURES)
+
+ def test_has_network_config_v2(self):
+ self.assertIn('NETWORK_CONFIG_V2', version.FEATURES)
+
+# vi: ts=4 expandtab