|
Server : Apache System : Linux server.mata-lashes.com 3.10.0-1160.90.1.el7.x86_64 #1 SMP Thu May 4 15:21:22 UTC 2023 x86_64 User : matalashes ( 1004) PHP Version : 8.1.29 Disable Function : NONE Directory : /usr/src/cloud-init/tests/unittests/net/ |
Upload File : |
# This file is part of cloud-init. See LICENSE file for license information.
import os
import signal
from textwrap import dedent
import pytest
import responses
from cloudinit.net.dhcp import (
InvalidDHCPLeaseFileError,
IscDhclient,
NoDHCPLeaseError,
NoDHCPLeaseInterfaceError,
NoDHCPLeaseMissingDhclientError,
maybe_perform_dhcp_discovery,
networkd_load_leases,
)
from cloudinit.net.ephemeral import EphemeralDHCPv4
from cloudinit.util import ensure_file, subp, write_file
from tests.unittests.helpers import (
CiTestCase,
ResponsesTestCase,
mock,
populate_dir,
)
from tests.unittests.util import MockDistro
PID_F = "/run/dhclient.pid"
LEASE_F = "/run/dhclient.lease"
DHCLIENT = "/sbin/dhclient"
class TestParseDHCPLeasesFile(CiTestCase):
def test_parse_empty_lease_file_errors(self):
"""parse_dhcp_lease_file errors when file content is empty."""
empty_file = self.tmp_path("leases")
ensure_file(empty_file)
with self.assertRaises(InvalidDHCPLeaseFileError) as context_manager:
IscDhclient.parse_dhcp_lease_file(empty_file)
error = context_manager.exception
self.assertIn("Cannot parse empty dhcp lease file", str(error))
def test_parse_malformed_lease_file_content_errors(self):
"""IscDhclient.parse_dhcp_lease_file errors when file content isn't
dhcp leases.
"""
non_lease_file = self.tmp_path("leases")
write_file(non_lease_file, "hi mom.")
with self.assertRaises(InvalidDHCPLeaseFileError) as context_manager:
IscDhclient.parse_dhcp_lease_file(non_lease_file)
error = context_manager.exception
self.assertIn("Cannot parse dhcp lease file", str(error))
def test_parse_multiple_leases(self):
"""IscDhclient.parse_dhcp_lease_file returns a list of all leases
within.
"""
lease_file = self.tmp_path("leases")
content = dedent(
"""
lease {
interface "wlp3s0";
fixed-address 192.168.2.74;
filename "http://192.168.2.50/boot.php?mac=${netX}";
option subnet-mask 255.255.255.0;
option routers 192.168.2.1;
renew 4 2017/07/27 18:02:30;
expire 5 2017/07/28 07:08:15;
}
lease {
interface "wlp3s0";
fixed-address 192.168.2.74;
filename "http://192.168.2.50/boot.php?mac=${netX}";
option subnet-mask 255.255.255.0;
option routers 192.168.2.1;
}
"""
)
expected = [
{
"interface": "wlp3s0",
"fixed-address": "192.168.2.74",
"subnet-mask": "255.255.255.0",
"routers": "192.168.2.1",
"renew": "4 2017/07/27 18:02:30",
"expire": "5 2017/07/28 07:08:15",
"filename": "http://192.168.2.50/boot.php?mac=${netX}",
},
{
"interface": "wlp3s0",
"fixed-address": "192.168.2.74",
"filename": "http://192.168.2.50/boot.php?mac=${netX}",
"subnet-mask": "255.255.255.0",
"routers": "192.168.2.1",
},
]
write_file(lease_file, content)
self.assertCountEqual(
expected, IscDhclient.parse_dhcp_lease_file(lease_file)
)
class TestDHCPRFC3442(CiTestCase):
def test_parse_lease_finds_rfc3442_classless_static_routes(self):
"""IscDhclient.parse_dhcp_lease_file returns
rfc3442-classless-static-routes.
"""
lease_file = self.tmp_path("leases")
content = dedent(
"""
lease {
interface "wlp3s0";
fixed-address 192.168.2.74;
option subnet-mask 255.255.255.0;
option routers 192.168.2.1;
option rfc3442-classless-static-routes 0,130,56,240,1;
renew 4 2017/07/27 18:02:30;
expire 5 2017/07/28 07:08:15;
}
"""
)
expected = [
{
"interface": "wlp3s0",
"fixed-address": "192.168.2.74",
"subnet-mask": "255.255.255.0",
"routers": "192.168.2.1",
"rfc3442-classless-static-routes": "0,130,56,240,1",
"renew": "4 2017/07/27 18:02:30",
"expire": "5 2017/07/28 07:08:15",
}
]
write_file(lease_file, content)
self.assertCountEqual(
expected, IscDhclient.parse_dhcp_lease_file(lease_file)
)
def test_parse_lease_finds_classless_static_routes(self):
"""
IscDhclient.parse_dhcp_lease_file returns classless-static-routes
for Centos lease format.
"""
lease_file = self.tmp_path("leases")
content = dedent(
"""
lease {
interface "wlp3s0";
fixed-address 192.168.2.74;
option subnet-mask 255.255.255.0;
option routers 192.168.2.1;
option classless-static-routes 0 130.56.240.1;
renew 4 2017/07/27 18:02:30;
expire 5 2017/07/28 07:08:15;
}
"""
)
expected = [
{
"interface": "wlp3s0",
"fixed-address": "192.168.2.74",
"subnet-mask": "255.255.255.0",
"routers": "192.168.2.1",
"classless-static-routes": "0 130.56.240.1",
"renew": "4 2017/07/27 18:02:30",
"expire": "5 2017/07/28 07:08:15",
}
]
write_file(lease_file, content)
self.assertCountEqual(
expected, IscDhclient.parse_dhcp_lease_file(lease_file)
)
@mock.patch("cloudinit.net.ephemeral.EphemeralIPv4Network")
@mock.patch("cloudinit.net.ephemeral.maybe_perform_dhcp_discovery")
def test_obtain_lease_parses_static_routes(self, m_maybe, m_ipv4):
"""EphemeralDHPCv4 parses rfc3442 routes for EphemeralIPv4Network"""
lease = [
{
"interface": "wlp3s0",
"fixed-address": "192.168.2.74",
"subnet-mask": "255.255.255.0",
"routers": "192.168.2.1",
"rfc3442-classless-static-routes": "0,130,56,240,1",
"renew": "4 2017/07/27 18:02:30",
"expire": "5 2017/07/28 07:08:15",
}
]
m_maybe.return_value = lease
eph = EphemeralDHCPv4(
MockDistro(),
)
eph.obtain_lease()
expected_kwargs = {
"interface": "wlp3s0",
"ip": "192.168.2.74",
"prefix_or_mask": "255.255.255.0",
"broadcast": "192.168.2.255",
"static_routes": [("0.0.0.0/0", "130.56.240.1")],
"router": "192.168.2.1",
}
m_ipv4.assert_called_with(**expected_kwargs)
@mock.patch("cloudinit.net.ephemeral.EphemeralIPv4Network")
@mock.patch("cloudinit.net.ephemeral.maybe_perform_dhcp_discovery")
def test_obtain_centos_lease_parses_static_routes(self, m_maybe, m_ipv4):
"""
EphemeralDHPCv4 parses rfc3442 routes for EphemeralIPv4Network
for Centos Lease format
"""
lease = [
{
"interface": "wlp3s0",
"fixed-address": "192.168.2.74",
"subnet-mask": "255.255.255.0",
"routers": "192.168.2.1",
"classless-static-routes": "0 130.56.240.1",
"renew": "4 2017/07/27 18:02:30",
"expire": "5 2017/07/28 07:08:15",
}
]
m_maybe.return_value = lease
eph = EphemeralDHCPv4(
MockDistro(),
)
eph.obtain_lease()
expected_kwargs = {
"interface": "wlp3s0",
"ip": "192.168.2.74",
"prefix_or_mask": "255.255.255.0",
"broadcast": "192.168.2.255",
"static_routes": [("0.0.0.0/0", "130.56.240.1")],
"router": "192.168.2.1",
}
m_ipv4.assert_called_with(**expected_kwargs)
class TestDHCPParseStaticRoutes(CiTestCase):
with_logs = True
def test_parse_static_routes_empty_string(self):
self.assertEqual([], IscDhclient.parse_static_routes(""))
def test_parse_static_routes_invalid_input_returns_empty_list(self):
rfc3442 = "32,169,254,169,254,130,56,248"
self.assertEqual([], IscDhclient.parse_static_routes(rfc3442))
def test_parse_static_routes_bogus_width_returns_empty_list(self):
rfc3442 = "33,169,254,169,254,130,56,248"
self.assertEqual([], IscDhclient.parse_static_routes(rfc3442))
def test_parse_static_routes_single_ip(self):
rfc3442 = "32,169,254,169,254,130,56,248,255"
self.assertEqual(
[("169.254.169.254/32", "130.56.248.255")],
IscDhclient.parse_static_routes(rfc3442),
)
def test_parse_static_routes_single_ip_handles_trailing_semicolon(self):
rfc3442 = "32,169,254,169,254,130,56,248,255;"
self.assertEqual(
[("169.254.169.254/32", "130.56.248.255")],
IscDhclient.parse_static_routes(rfc3442),
)
def test_parse_static_routes_default_route(self):
rfc3442 = "0,130,56,240,1"
self.assertEqual(
[("0.0.0.0/0", "130.56.240.1")],
IscDhclient.parse_static_routes(rfc3442),
)
def test_unspecified_gateway(self):
rfc3442 = "32,169,254,169,254,0,0,0,0"
self.assertEqual(
[("169.254.169.254/32", "0.0.0.0")],
IscDhclient.parse_static_routes(rfc3442),
)
def test_parse_static_routes_class_c_b_a(self):
class_c = "24,192,168,74,192,168,0,4"
class_b = "16,172,16,172,16,0,4"
class_a = "8,10,10,0,0,4"
rfc3442 = ",".join([class_c, class_b, class_a])
self.assertEqual(
sorted(
[
("192.168.74.0/24", "192.168.0.4"),
("172.16.0.0/16", "172.16.0.4"),
("10.0.0.0/8", "10.0.0.4"),
]
),
sorted(IscDhclient.parse_static_routes(rfc3442)),
)
def test_parse_static_routes_logs_error_truncated(self):
bad_rfc3442 = {
"class_c": "24,169,254,169,10",
"class_b": "16,172,16,10",
"class_a": "8,10,10",
"gateway": "0,0",
"netlen": "33,0",
}
for rfc3442 in bad_rfc3442.values():
self.assertEqual([], IscDhclient.parse_static_routes(rfc3442))
logs = self.logs.getvalue()
self.assertEqual(len(bad_rfc3442.keys()), len(logs.splitlines()))
def test_parse_static_routes_returns_valid_routes_until_parse_err(self):
class_c = "24,192,168,74,192,168,0,4"
class_b = "16,172,16,172,16,0,4"
class_a_error = "8,10,10,0,0"
rfc3442 = ",".join([class_c, class_b, class_a_error])
self.assertEqual(
sorted(
[
("192.168.74.0/24", "192.168.0.4"),
("172.16.0.0/16", "172.16.0.4"),
]
),
sorted(IscDhclient.parse_static_routes(rfc3442)),
)
logs = self.logs.getvalue()
self.assertIn(rfc3442, logs.splitlines()[0])
def test_redhat_format(self):
redhat_format = "24.191.168.128 192.168.128.1,0 192.168.128.1"
self.assertEqual(
sorted(
[
("191.168.128.0/24", "192.168.128.1"),
("0.0.0.0/0", "192.168.128.1"),
]
),
sorted(IscDhclient.parse_static_routes(redhat_format)),
)
def test_redhat_format_with_a_space_too_much_after_comma(self):
redhat_format = "24.191.168.128 192.168.128.1, 0 192.168.128.1"
self.assertEqual(
sorted(
[
("191.168.128.0/24", "192.168.128.1"),
("0.0.0.0/0", "192.168.128.1"),
]
),
sorted(IscDhclient.parse_static_routes(redhat_format)),
)
class TestDHCPDiscoveryClean(CiTestCase):
with_logs = True
ib_address_prefix = "00:00:00:00:00:00:00:00:00:00:00:00"
@mock.patch("cloudinit.net.dhcp.find_fallback_nic")
def test_no_fallback_nic_found(self, m_fallback_nic):
"""Log and do nothing when nic is absent and no fallback is found."""
m_fallback_nic.return_value = None # No fallback nic found
with pytest.raises(NoDHCPLeaseInterfaceError):
maybe_perform_dhcp_discovery(MockDistro())
self.assertIn(
"Skip dhcp_discovery: Unable to find fallback nic.",
self.logs.getvalue(),
)
@mock.patch("cloudinit.net.dhcp.find_fallback_nic", return_value="eth9")
@mock.patch("cloudinit.net.dhcp.os.remove")
@mock.patch("cloudinit.net.dhcp.subp.subp")
@mock.patch("cloudinit.net.dhcp.subp.which")
def test_dhclient_exits_with_error(
self, m_which, m_subp, m_remove, m_fallback
):
"""Log and do nothing when nic is absent and no fallback is found."""
m_subp.side_effect = [
("", ""),
subp.ProcessExecutionError(exit_code=-5),
]
with pytest.raises(NoDHCPLeaseError):
maybe_perform_dhcp_discovery(MockDistro())
self.assertIn(
"DHCP client selected: dhclient",
self.logs.getvalue(),
)
@mock.patch("cloudinit.net.dhcp.find_fallback_nic", return_value="eth9")
@mock.patch("cloudinit.net.dhcp.os.remove")
@mock.patch("cloudinit.net.dhcp.subp.subp")
@mock.patch("cloudinit.net.dhcp.subp.which")
def test_dhcp_client_failover(self, m_which, m_subp, m_remove, m_fallback):
"""Log and do nothing when nic is absent and no fallback is found."""
m_subp.side_effect = [
("", ""),
subp.ProcessExecutionError(exit_code=-5),
]
m_which.side_effect = [False, True]
with pytest.raises(NoDHCPLeaseError):
maybe_perform_dhcp_discovery(MockDistro())
self.assertIn(
"DHCP client not found: dhclient",
self.logs.getvalue(),
)
self.assertIn(
"DHCP client not found: dhcpcd",
self.logs.getvalue(),
)
@mock.patch("cloudinit.net.dhcp.find_fallback_nic", return_value=None)
def test_provided_nic_does_not_exist(self, m_fallback_nic):
"""When the provided nic doesn't exist, log a message and no-op."""
with pytest.raises(NoDHCPLeaseInterfaceError):
maybe_perform_dhcp_discovery(MockDistro(), "idontexist")
self.assertIn(
"Skip dhcp_discovery: nic idontexist not found in get_devicelist.",
self.logs.getvalue(),
)
@mock.patch("cloudinit.net.dhcp.subp.which")
@mock.patch("cloudinit.net.dhcp.find_fallback_nic")
def test_absent_dhclient_command(self, m_fallback, m_which):
"""When dhclient doesn't exist in the OS, log the issue and no-op."""
m_fallback.return_value = "eth9"
m_which.return_value = None # dhclient isn't found
with pytest.raises(NoDHCPLeaseMissingDhclientError):
maybe_perform_dhcp_discovery(MockDistro())
self.assertIn(
"Skip dhclient configuration: No dhclient command found.",
self.logs.getvalue(),
)
@mock.patch("cloudinit.net.dhcp.os.remove")
@mock.patch("time.sleep", mock.MagicMock())
@mock.patch("cloudinit.net.dhcp.os.kill")
@mock.patch("cloudinit.net.dhcp.subp.subp")
@mock.patch("cloudinit.net.dhcp.subp.which", return_value="/sbin/dhclient")
@mock.patch("cloudinit.net.dhcp.util.wait_for_files", return_value=False)
def test_dhcp_discovery_warns_invalid_pid(
self, m_wait, m_which, m_subp, m_kill, m_remove
):
"""dhcp_discovery logs a warning when pidfile contains invalid content.
Lease processing still occurs and no proc kill is attempted.
"""
m_subp.return_value = ("", "")
lease_content = dedent(
"""
lease {
interface "eth9";
fixed-address 192.168.2.74;
option subnet-mask 255.255.255.0;
option routers 192.168.2.1;
}
"""
)
with mock.patch(
"cloudinit.util.load_file", return_value=lease_content
):
self.assertCountEqual(
[
{
"interface": "eth9",
"fixed-address": "192.168.2.74",
"subnet-mask": "255.255.255.0",
"routers": "192.168.2.1",
}
],
IscDhclient.parse_dhcp_lease_file("lease"),
)
with self.assertRaises(InvalidDHCPLeaseFileError):
with mock.patch("cloudinit.util.load_file", return_value=""):
IscDhclient().dhcp_discovery("eth9")
self.assertIn(
"dhclient(pid=, parentpid=unknown) failed "
"to daemonize after 10.0 seconds",
self.logs.getvalue(),
)
m_kill.assert_not_called()
@mock.patch("cloudinit.net.dhcp.os.remove")
@mock.patch("cloudinit.net.dhcp.util.get_proc_ppid")
@mock.patch("cloudinit.net.dhcp.os.kill")
@mock.patch("cloudinit.net.dhcp.util.wait_for_files")
@mock.patch("cloudinit.net.dhcp.subp.which", return_value="/sbin/dhclient")
@mock.patch("cloudinit.net.dhcp.subp.subp")
def test_dhcp_discovery_waits_on_lease_and_pid(
self, m_subp, m_which, m_wait, m_kill, m_getppid, m_remove
):
"""dhcp_discovery waits for the presence of pidfile and dhcp.leases."""
m_subp.return_value = ("", "")
# Don't create pid or leases file
m_wait.return_value = [PID_F] # Return the missing pidfile wait for
m_getppid.return_value = 1 # Indicate that dhclient has daemonized
self.assertEqual([], IscDhclient().dhcp_discovery("eth9"))
self.assertEqual(
mock.call([PID_F, LEASE_F], maxwait=5, naplen=0.01),
m_wait.call_args_list[0],
)
self.assertIn(
"WARNING: dhclient did not produce expected files: dhclient.pid",
self.logs.getvalue(),
)
m_kill.assert_not_called()
@mock.patch("cloudinit.net.dhcp.is_ib_interface", return_value=False)
@mock.patch("cloudinit.net.dhcp.os.remove")
@mock.patch("cloudinit.net.dhcp.util.get_proc_ppid")
@mock.patch("cloudinit.net.dhcp.os.kill")
@mock.patch("cloudinit.net.dhcp.subp.subp")
@mock.patch("cloudinit.net.dhcp.subp.which", return_value="/sbin/dhclient")
@mock.patch("cloudinit.util.wait_for_files", return_value=False)
def test_dhcp_discovery(
self,
m_wait,
m_which,
m_subp,
m_kill,
m_getppid,
m_remove,
mocked_is_ib_interface,
):
"""dhcp_discovery brings up the interface and runs dhclient.
It also returns the parsed dhcp.leases file.
"""
m_subp.return_value = ("", "")
lease_content = dedent(
"""
lease {
interface "eth9";
fixed-address 192.168.2.74;
option subnet-mask 255.255.255.0;
option routers 192.168.2.1;
}
"""
)
my_pid = 1
m_getppid.return_value = 1 # Indicate that dhclient has daemonized
with mock.patch(
"cloudinit.util.load_file", side_effect=["1", lease_content]
):
self.assertCountEqual(
[
{
"interface": "eth9",
"fixed-address": "192.168.2.74",
"subnet-mask": "255.255.255.0",
"routers": "192.168.2.1",
}
],
IscDhclient().dhcp_discovery("eth9"),
)
# Interface was brought up before dhclient called
m_subp.assert_has_calls(
[
mock.call(
["ip", "link", "set", "dev", "eth9", "up"], capture=True
),
mock.call(
[
DHCLIENT,
"-1",
"-v",
"-lf",
LEASE_F,
"-pf",
PID_F,
"eth9",
"-sf",
"/bin/true",
],
capture=True,
),
]
)
m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)])
mocked_is_ib_interface.assert_called_once_with("eth9")
@mock.patch("cloudinit.temp_utils.get_tmp_ancestor", return_value="/tmp")
@mock.patch("cloudinit.util.write_file")
@mock.patch(
"cloudinit.net.dhcp.get_interface_mac",
return_value="%s:AA:AA:AA:00:00:AA:AA:AA" % ib_address_prefix,
)
@mock.patch("cloudinit.net.dhcp.is_ib_interface", return_value=True)
@mock.patch("cloudinit.net.dhcp.os.remove")
@mock.patch("cloudinit.net.dhcp.util.get_proc_ppid", return_value=1)
@mock.patch("cloudinit.net.dhcp.os.kill")
@mock.patch("cloudinit.net.dhcp.subp.which", return_value="/sbin/dhclient")
@mock.patch("cloudinit.net.dhcp.subp.subp", return_value=("", ""))
@mock.patch("cloudinit.util.wait_for_files", return_value=False)
def test_dhcp_discovery_ib(
self,
m_wait,
m_subp,
m_which,
m_kill,
m_getppid,
m_remove,
mocked_is_ib_interface,
get_interface_mac,
mocked_write_file,
mocked_get_tmp_ancestor,
):
"""dhcp_discovery brings up the interface and runs dhclient.
It also returns the parsed dhcp.leases file.
"""
lease_content = dedent(
"""
lease {
interface "ib0";
fixed-address 192.168.2.74;
option subnet-mask 255.255.255.0;
option routers 192.168.2.1;
}
"""
)
my_pid = 1
with mock.patch(
"cloudinit.util.load_file", side_effect=["1", lease_content]
):
self.assertCountEqual(
[
{
"interface": "ib0",
"fixed-address": "192.168.2.74",
"subnet-mask": "255.255.255.0",
"routers": "192.168.2.1",
}
],
IscDhclient().dhcp_discovery("ib0"),
)
# Interface was brought up before dhclient called
m_subp.assert_has_calls(
[
mock.call(
["ip", "link", "set", "dev", "ib0", "up"], capture=True
),
mock.call(
[
DHCLIENT,
"-1",
"-v",
"-lf",
LEASE_F,
"-pf",
PID_F,
"ib0",
"-sf",
"/bin/true",
"-cf",
"/tmp/ib0-dhclient.conf",
],
capture=True,
),
]
)
m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)])
mocked_is_ib_interface.assert_called_once_with("ib0")
get_interface_mac.assert_called_once_with("ib0")
mocked_get_tmp_ancestor.assert_called_once_with(needs_exe=True)
mocked_write_file.assert_called_once_with(
"/tmp/ib0-dhclient.conf",
'interface "ib0" {send dhcp-client-identifier '
"20:AA:AA:AA:00:00:AA:AA:AA;}",
)
@mock.patch("cloudinit.net.dhcp.os.remove")
@mock.patch("cloudinit.net.dhcp.util.get_proc_ppid")
@mock.patch("cloudinit.net.dhcp.os.kill")
@mock.patch("cloudinit.net.dhcp.subp.subp")
@mock.patch("cloudinit.net.dhcp.subp.which", return_value="/sbin/dhclient")
@mock.patch("cloudinit.util.wait_for_files")
def test_dhcp_output_error_stream(
self, m_wait, m_which, m_subp, m_kill, m_getppid, m_remove
):
""" "dhcp_log_func is called with the output and error streams of
dhclient when the callable is passed."""
dhclient_err = "FAKE DHCLIENT ERROR"
dhclient_out = "FAKE DHCLIENT OUT"
m_subp.return_value = (dhclient_out, dhclient_err)
tmpdir = self.tmp_dir()
lease_content = dedent(
"""
lease {
interface "eth9";
fixed-address 192.168.2.74;
option subnet-mask 255.255.255.0;
option routers 192.168.2.1;
}
"""
)
lease_file = os.path.join(tmpdir, "dhcp.leases")
write_file(lease_file, lease_content)
pid_file = os.path.join(tmpdir, "dhclient.pid")
my_pid = 1
write_file(pid_file, "%d\n" % my_pid)
m_getppid.return_value = 1 # Indicate that dhclient has daemonized
def dhcp_log_func(out, err):
self.assertEqual(out, dhclient_out)
self.assertEqual(err, dhclient_err)
IscDhclient().dhcp_discovery("eth9", dhcp_log_func=dhcp_log_func)
class TestSystemdParseLeases(CiTestCase):
lxd_lease = dedent(
"""\
# This is private data. Do not parse.
ADDRESS=10.75.205.242
NETMASK=255.255.255.0
ROUTER=10.75.205.1
SERVER_ADDRESS=10.75.205.1
NEXT_SERVER=10.75.205.1
BROADCAST=10.75.205.255
T1=1580
T2=2930
LIFETIME=3600
DNS=10.75.205.1
DOMAINNAME=lxd
HOSTNAME=a1
CLIENTID=ffe617693400020000ab110c65a6a0866931c2
"""
)
lxd_parsed = {
"ADDRESS": "10.75.205.242",
"NETMASK": "255.255.255.0",
"ROUTER": "10.75.205.1",
"SERVER_ADDRESS": "10.75.205.1",
"NEXT_SERVER": "10.75.205.1",
"BROADCAST": "10.75.205.255",
"T1": "1580",
"T2": "2930",
"LIFETIME": "3600",
"DNS": "10.75.205.1",
"DOMAINNAME": "lxd",
"HOSTNAME": "a1",
"CLIENTID": "ffe617693400020000ab110c65a6a0866931c2",
}
azure_lease = dedent(
"""\
# This is private data. Do not parse.
ADDRESS=10.132.0.5
NETMASK=255.255.255.255
ROUTER=10.132.0.1
SERVER_ADDRESS=169.254.169.254
NEXT_SERVER=10.132.0.1
MTU=1460
T1=43200
T2=75600
LIFETIME=86400
DNS=169.254.169.254
NTP=169.254.169.254
DOMAINNAME=c.ubuntu-foundations.internal
DOMAIN_SEARCH_LIST=c.ubuntu-foundations.internal google.internal
HOSTNAME=tribaal-test-171002-1349.c.ubuntu-foundations.internal
ROUTES=10.132.0.1/32,0.0.0.0 0.0.0.0/0,10.132.0.1
CLIENTID=ff405663a200020000ab11332859494d7a8b4c
OPTION_245=624c3620
"""
)
azure_parsed = {
"ADDRESS": "10.132.0.5",
"NETMASK": "255.255.255.255",
"ROUTER": "10.132.0.1",
"SERVER_ADDRESS": "169.254.169.254",
"NEXT_SERVER": "10.132.0.1",
"MTU": "1460",
"T1": "43200",
"T2": "75600",
"LIFETIME": "86400",
"DNS": "169.254.169.254",
"NTP": "169.254.169.254",
"DOMAINNAME": "c.ubuntu-foundations.internal",
"DOMAIN_SEARCH_LIST": "c.ubuntu-foundations.internal google.internal",
"HOSTNAME": "tribaal-test-171002-1349.c.ubuntu-foundations.internal",
"ROUTES": "10.132.0.1/32,0.0.0.0 0.0.0.0/0,10.132.0.1",
"CLIENTID": "ff405663a200020000ab11332859494d7a8b4c",
"OPTION_245": "624c3620",
}
def setUp(self):
super(TestSystemdParseLeases, self).setUp()
self.lease_d = self.tmp_dir()
def test_no_leases_returns_empty_dict(self):
"""A leases dir with no lease files should return empty dictionary."""
self.assertEqual({}, networkd_load_leases(self.lease_d))
def test_no_leases_dir_returns_empty_dict(self):
"""A non-existing leases dir should return empty dict."""
enodir = os.path.join(self.lease_d, "does-not-exist")
self.assertEqual({}, networkd_load_leases(enodir))
def test_single_leases_file(self):
"""A leases dir with one leases file."""
populate_dir(self.lease_d, {"2": self.lxd_lease})
self.assertEqual(
{"2": self.lxd_parsed}, networkd_load_leases(self.lease_d)
)
def test_single_azure_leases_file(self):
"""On Azure, option 245 should be present, verify it specifically."""
populate_dir(self.lease_d, {"1": self.azure_lease})
self.assertEqual(
{"1": self.azure_parsed}, networkd_load_leases(self.lease_d)
)
def test_multiple_files(self):
"""Multiple leases files on azure with one found return that value."""
self.maxDiff = None
populate_dir(
self.lease_d, {"1": self.azure_lease, "9": self.lxd_lease}
)
self.assertEqual(
{"1": self.azure_parsed, "9": self.lxd_parsed},
networkd_load_leases(self.lease_d),
)
class TestEphemeralDhcpNoNetworkSetup(ResponsesTestCase):
@mock.patch("cloudinit.net.dhcp.maybe_perform_dhcp_discovery")
def test_ephemeral_dhcp_no_network_if_url_connectivity(self, m_dhcp):
"""No EphemeralDhcp4 network setup when connectivity_url succeeds."""
url = "http://example.org/index.html"
self.responses.add(responses.GET, url)
with EphemeralDHCPv4(
MockDistro(),
connectivity_url_data={"url": url},
) as lease:
self.assertIsNone(lease)
# Ensure that no teardown happens:
m_dhcp.assert_not_called()
@mock.patch("cloudinit.net.dhcp.subp.subp")
@mock.patch("cloudinit.net.ephemeral.maybe_perform_dhcp_discovery")
def test_ephemeral_dhcp_setup_network_if_url_connectivity(
self, m_dhcp, m_subp
):
"""No EphemeralDhcp4 network setup when connectivity_url succeeds."""
url = "http://example.org/index.html"
fake_lease = {
"interface": "eth9",
"fixed-address": "192.168.2.2",
"subnet-mask": "255.255.0.0",
}
m_dhcp.return_value = [fake_lease]
m_subp.return_value = ("", "")
self.responses.add(responses.GET, url, body=b"", status=404)
with EphemeralDHCPv4(
MockDistro(),
connectivity_url_data={"url": url},
) as lease:
self.assertEqual(fake_lease, lease)
# Ensure that dhcp discovery occurs
m_dhcp.assert_called_once()
@pytest.mark.parametrize(
"error_class",
[
NoDHCPLeaseInterfaceError,
NoDHCPLeaseInterfaceError,
NoDHCPLeaseMissingDhclientError,
],
)
class TestEphemeralDhcpLeaseErrors:
@mock.patch("cloudinit.net.ephemeral.maybe_perform_dhcp_discovery")
def test_obtain_lease_raises_error(self, m_dhcp, error_class):
m_dhcp.side_effect = [error_class()]
with pytest.raises(error_class):
EphemeralDHCPv4(
MockDistro(),
).obtain_lease()
assert len(m_dhcp.mock_calls) == 1
@mock.patch("cloudinit.net.ephemeral.maybe_perform_dhcp_discovery")
def test_obtain_lease_umbrella_error(self, m_dhcp, error_class):
m_dhcp.side_effect = [error_class()]
with pytest.raises(NoDHCPLeaseError):
EphemeralDHCPv4(
MockDistro(),
).obtain_lease()
assert len(m_dhcp.mock_calls) == 1
@mock.patch("cloudinit.net.ephemeral.maybe_perform_dhcp_discovery")
def test_ctx_mgr_raises_error(self, m_dhcp, error_class):
m_dhcp.side_effect = [error_class()]
with pytest.raises(error_class):
with EphemeralDHCPv4(
MockDistro(),
):
pass
assert len(m_dhcp.mock_calls) == 1
@mock.patch("cloudinit.net.ephemeral.maybe_perform_dhcp_discovery")
def test_ctx_mgr_umbrella_error(self, m_dhcp, error_class):
m_dhcp.side_effect = [error_class()]
with pytest.raises(NoDHCPLeaseError):
with EphemeralDHCPv4(
MockDistro(),
):
pass
assert len(m_dhcp.mock_calls) == 1