summaryrefslogtreecommitdiff
path: root/tests/unittests/test_datasource
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unittests/test_datasource')
-rw-r--r--tests/unittests/test_datasource/test_aliyun.py6
-rw-r--r--tests/unittests/test_datasource/test_altcloud.py21
-rw-r--r--tests/unittests/test_datasource/test_azure.py1076
-rw-r--r--tests/unittests/test_datasource/test_azure_helper.py804
-rw-r--r--tests/unittests/test_datasource/test_hetzner.py20
-rw-r--r--tests/unittests/test_datasource/test_nocloud.py3
-rw-r--r--tests/unittests/test_datasource/test_openstack.py42
-rw-r--r--tests/unittests/test_datasource/test_ovf.py16
-rw-r--r--tests/unittests/test_datasource/test_scaleway.py8
9 files changed, 1698 insertions, 298 deletions
diff --git a/tests/unittests/test_datasource/test_aliyun.py b/tests/unittests/test_datasource/test_aliyun.py
index b626229e..eb2828d5 100644
--- a/tests/unittests/test_datasource/test_aliyun.py
+++ b/tests/unittests/test_datasource/test_aliyun.py
@@ -188,7 +188,7 @@ class TestIsAliYun(test_helpers.CiTestCase):
ALIYUN_PRODUCT = 'Alibaba Cloud ECS'
read_dmi_data_expected = [mock.call('system-product-name')]
- @mock.patch("cloudinit.sources.DataSourceAliYun.util.read_dmi_data")
+ @mock.patch("cloudinit.sources.DataSourceAliYun.dmi.read_dmi_data")
def test_true_on_aliyun_product(self, m_read_dmi_data):
"""Should return true if the dmi product data has expected value."""
m_read_dmi_data.return_value = self.ALIYUN_PRODUCT
@@ -197,7 +197,7 @@ class TestIsAliYun(test_helpers.CiTestCase):
m_read_dmi_data.call_args_list)
self.assertEqual(True, ret)
- @mock.patch("cloudinit.sources.DataSourceAliYun.util.read_dmi_data")
+ @mock.patch("cloudinit.sources.DataSourceAliYun.dmi.read_dmi_data")
def test_false_on_empty_string(self, m_read_dmi_data):
"""Should return false on empty value returned."""
m_read_dmi_data.return_value = ""
@@ -206,7 +206,7 @@ class TestIsAliYun(test_helpers.CiTestCase):
m_read_dmi_data.call_args_list)
self.assertEqual(False, ret)
- @mock.patch("cloudinit.sources.DataSourceAliYun.util.read_dmi_data")
+ @mock.patch("cloudinit.sources.DataSourceAliYun.dmi.read_dmi_data")
def test_false_on_unknown_string(self, m_read_dmi_data):
"""Should return false on an unrelated string."""
m_read_dmi_data.return_value = "cubs win"
diff --git a/tests/unittests/test_datasource/test_altcloud.py b/tests/unittests/test_datasource/test_altcloud.py
index fc59d1d5..7a5393ac 100644
--- a/tests/unittests/test_datasource/test_altcloud.py
+++ b/tests/unittests/test_datasource/test_altcloud.py
@@ -14,6 +14,7 @@ import os
import shutil
import tempfile
+from cloudinit import dmi
from cloudinit import helpers
from cloudinit import subp
from cloudinit import util
@@ -88,14 +89,14 @@ class TestGetCloudType(CiTestCase):
super(TestGetCloudType, self).setUp()
self.tmp = self.tmp_dir()
self.paths = helpers.Paths({'cloud_dir': self.tmp})
- self.dmi_data = util.read_dmi_data
+ self.dmi_data = dmi.read_dmi_data
# We have a different code path for arm to deal with LP1243287
# We have to switch arch to x86_64 to avoid test failure
force_arch('x86_64')
def tearDown(self):
# Reset
- util.read_dmi_data = self.dmi_data
+ dmi.read_dmi_data = self.dmi_data
force_arch()
def test_cloud_info_file_ioerror(self):
@@ -123,7 +124,7 @@ class TestGetCloudType(CiTestCase):
Test method get_cloud_type() for RHEVm systems.
Forcing read_dmi_data return to match a RHEVm system: RHEV Hypervisor
'''
- util.read_dmi_data = _dmi_data('RHEV')
+ dmi.read_dmi_data = _dmi_data('RHEV')
dsrc = dsac.DataSourceAltCloud({}, None, self.paths)
self.assertEqual('RHEV', dsrc.get_cloud_type())
@@ -132,7 +133,7 @@ class TestGetCloudType(CiTestCase):
Test method get_cloud_type() for vSphere systems.
Forcing read_dmi_data return to match a vSphere system: RHEV Hypervisor
'''
- util.read_dmi_data = _dmi_data('VMware Virtual Platform')
+ dmi.read_dmi_data = _dmi_data('VMware Virtual Platform')
dsrc = dsac.DataSourceAltCloud({}, None, self.paths)
self.assertEqual('VSPHERE', dsrc.get_cloud_type())
@@ -141,7 +142,7 @@ class TestGetCloudType(CiTestCase):
Test method get_cloud_type() for unknown systems.
Forcing read_dmi_data return to match an unrecognized return.
'''
- util.read_dmi_data = _dmi_data('Unrecognized Platform')
+ dmi.read_dmi_data = _dmi_data('Unrecognized Platform')
dsrc = dsac.DataSourceAltCloud({}, None, self.paths)
self.assertEqual('UNKNOWN', dsrc.get_cloud_type())
@@ -219,7 +220,7 @@ class TestGetDataNoCloudInfoFile(CiTestCase):
self.tmp = self.tmp_dir()
self.paths = helpers.Paths(
{'cloud_dir': self.tmp, 'run_dir': self.tmp})
- self.dmi_data = util.read_dmi_data
+ self.dmi_data = dmi.read_dmi_data
dsac.CLOUD_INFO_FILE = \
'no such file'
# We have a different code path for arm to deal with LP1243287
@@ -230,14 +231,14 @@ class TestGetDataNoCloudInfoFile(CiTestCase):
# Reset
dsac.CLOUD_INFO_FILE = \
'/etc/sysconfig/cloud-info'
- util.read_dmi_data = self.dmi_data
+ dmi.read_dmi_data = self.dmi_data
# Return back to original arch
force_arch()
def test_rhev_no_cloud_file(self):
'''Test No cloud info file module get_data() forcing RHEV.'''
- util.read_dmi_data = _dmi_data('RHEV Hypervisor')
+ dmi.read_dmi_data = _dmi_data('RHEV Hypervisor')
dsrc = dsac.DataSourceAltCloud({}, None, self.paths)
dsrc.user_data_rhevm = lambda: True
self.assertEqual(True, dsrc.get_data())
@@ -245,7 +246,7 @@ class TestGetDataNoCloudInfoFile(CiTestCase):
def test_vsphere_no_cloud_file(self):
'''Test No cloud info file module get_data() forcing VSPHERE.'''
- util.read_dmi_data = _dmi_data('VMware Virtual Platform')
+ dmi.read_dmi_data = _dmi_data('VMware Virtual Platform')
dsrc = dsac.DataSourceAltCloud({}, None, self.paths)
dsrc.user_data_vsphere = lambda: True
self.assertEqual(True, dsrc.get_data())
@@ -253,7 +254,7 @@ class TestGetDataNoCloudInfoFile(CiTestCase):
def test_failure_no_cloud_file(self):
'''Test No cloud info file module get_data() forcing unrecognized.'''
- util.read_dmi_data = _dmi_data('Unrecognized Platform')
+ dmi.read_dmi_data = _dmi_data('Unrecognized Platform')
dsrc = dsac.DataSourceAltCloud({}, None, self.paths)
self.assertEqual(False, dsrc.get_data())
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
index 47e03bd1..e363c1f9 100644
--- a/tests/unittests/test_datasource/test_azure.py
+++ b/tests/unittests/test_datasource/test_azure.py
@@ -11,6 +11,7 @@ from cloudinit.version import version_string as vs
from cloudinit.tests.helpers import (
HttprettyTestCase, CiTestCase, populate_dir, mock, wrap_and_call,
ExitStack, resourceLocation)
+from cloudinit.sources.helpers import netlink
import copy
import crypt
@@ -78,6 +79,8 @@ def construct_valid_ovf_env(data=None, pubkeys=None,
if platform_settings:
for k, v in platform_settings.items():
content += "<%s>%s</%s>\n" % (k, v, k)
+ if "PreprovisionedVMType" not in platform_settings:
+ content += """<PreprovisionedVMType i:nil="true" />"""
content += """</PlatformSettings></wa:PlatformSettingsSection>
</Environment>"""
@@ -102,7 +105,13 @@ NETWORK_METADATA = {
"vmId": "ff702a6b-cb6a-4fcd-ad68-b4ce38227642",
"vmScaleSetName": "",
"vmSize": "Standard_DS1_v2",
- "zone": ""
+ "zone": "",
+ "publicKeys": [
+ {
+ "keyData": "key1",
+ "path": "path1"
+ }
+ ]
},
"network": {
"interface": [
@@ -150,14 +159,50 @@ SECONDARY_INTERFACE = {
}
}
+IMDS_NETWORK_METADATA = {
+ "interface": [
+ {
+ "macAddress": "000D3A047598",
+ "ipv6": {
+ "ipAddress": []
+ },
+ "ipv4": {
+ "subnet": [
+ {
+ "prefix": "24",
+ "address": "10.0.0.0"
+ }
+ ],
+ "ipAddress": [
+ {
+ "privateIpAddress": "10.0.0.4",
+ "publicIpAddress": "104.46.124.81"
+ }
+ ]
+ }
+ }
+ ]
+}
+
MOCKPATH = 'cloudinit.sources.DataSourceAzure.'
class TestParseNetworkConfig(CiTestCase):
maxDiff = None
+ fallback_config = {
+ 'version': 1,
+ 'config': [{
+ 'type': 'physical', 'name': 'eth0',
+ 'mac_address': '00:11:22:33:44:55',
+ 'params': {'driver': 'hv_netsvc'},
+ 'subnets': [{'type': 'dhcp'}],
+ }]
+ }
- def test_single_ipv4_nic_configuration(self):
+ @mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
+ return_value=None)
+ def test_single_ipv4_nic_configuration(self, m_driver):
"""parse_network_config emits dhcp on single nic with ipv4"""
expected = {'ethernets': {
'eth0': {'dhcp4': True,
@@ -167,7 +212,9 @@ class TestParseNetworkConfig(CiTestCase):
'set-name': 'eth0'}}, 'version': 2}
self.assertEqual(expected, dsaz.parse_network_config(NETWORK_METADATA))
- def test_increases_route_metric_for_non_primary_nics(self):
+ @mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
+ return_value=None)
+ def test_increases_route_metric_for_non_primary_nics(self, m_driver):
"""parse_network_config increases route-metric for each nic"""
expected = {'ethernets': {
'eth0': {'dhcp4': True,
@@ -194,7 +241,9 @@ class TestParseNetworkConfig(CiTestCase):
imds_data['network']['interface'].append(third_intf)
self.assertEqual(expected, dsaz.parse_network_config(imds_data))
- def test_ipv4_and_ipv6_route_metrics_match_for_nics(self):
+ @mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
+ return_value=None)
+ def test_ipv4_and_ipv6_route_metrics_match_for_nics(self, m_driver):
"""parse_network_config emits matching ipv4 and ipv6 route-metrics."""
expected = {'ethernets': {
'eth0': {'addresses': ['10.0.0.5/24', '2001:dead:beef::2/128'],
@@ -236,7 +285,9 @@ class TestParseNetworkConfig(CiTestCase):
imds_data['network']['interface'].append(third_intf)
self.assertEqual(expected, dsaz.parse_network_config(imds_data))
- def test_ipv4_secondary_ips_will_be_static_addrs(self):
+ @mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
+ return_value=None)
+ def test_ipv4_secondary_ips_will_be_static_addrs(self, m_driver):
"""parse_network_config emits primary ipv4 as dhcp others are static"""
expected = {'ethernets': {
'eth0': {'addresses': ['10.0.0.5/24'],
@@ -256,7 +307,9 @@ class TestParseNetworkConfig(CiTestCase):
}
self.assertEqual(expected, dsaz.parse_network_config(imds_data))
- def test_ipv6_secondary_ips_will_be_static_cidrs(self):
+ @mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
+ return_value=None)
+ def test_ipv6_secondary_ips_will_be_static_cidrs(self, m_driver):
"""parse_network_config emits primary ipv6 as dhcp others are static"""
expected = {'ethernets': {
'eth0': {'addresses': ['10.0.0.5/24', '2001:dead:beef::2/10'],
@@ -295,6 +348,42 @@ class TestParseNetworkConfig(CiTestCase):
}}, 'version': 2}
self.assertEqual(expected, dsaz.parse_network_config(NETWORK_METADATA))
+ @mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
+ return_value=None)
+ @mock.patch('cloudinit.net.generate_fallback_config')
+ def test_parse_network_config_uses_fallback_cfg_when_no_network_metadata(
+ self, m_fallback_config, m_driver):
+ """parse_network_config generates fallback network config when the
+ IMDS instance metadata is corrupted/invalid, such as when
+ network metadata is not present.
+ """
+ imds_metadata_missing_network_metadata = copy.deepcopy(
+ NETWORK_METADATA)
+ del imds_metadata_missing_network_metadata['network']
+ m_fallback_config.return_value = self.fallback_config
+ self.assertEqual(
+ self.fallback_config,
+ dsaz.parse_network_config(
+ imds_metadata_missing_network_metadata))
+
+ @mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
+ return_value=None)
+ @mock.patch('cloudinit.net.generate_fallback_config')
+ def test_parse_network_config_uses_fallback_cfg_when_no_interface_metadata(
+ self, m_fallback_config, m_driver):
+ """parse_network_config generates fallback network config when the
+ IMDS instance metadata is corrupted/invalid, such as when
+ network interface metadata is not present.
+ """
+ imds_metadata_missing_interface_metadata = copy.deepcopy(
+ NETWORK_METADATA)
+ del imds_metadata_missing_interface_metadata['network']['interface']
+ m_fallback_config.return_value = self.fallback_config
+ self.assertEqual(
+ self.fallback_config,
+ dsaz.parse_network_config(
+ imds_metadata_missing_interface_metadata))
+
class TestGetMetadataFromIMDS(HttprettyTestCase):
@@ -302,11 +391,11 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
def setUp(self):
super(TestGetMetadataFromIMDS, self).setUp()
- self.network_md_url = dsaz.IMDS_URL + "instance?api-version=2017-12-01"
+ self.network_md_url = dsaz.IMDS_URL + "instance?api-version=2019-06-01"
@mock.patch(MOCKPATH + 'readurl')
- @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
- @mock.patch(MOCKPATH + 'net.is_up')
+ @mock.patch(MOCKPATH + 'EphemeralDHCPv4', autospec=True)
+ @mock.patch(MOCKPATH + 'net.is_up', autospec=True)
def test_get_metadata_does_not_dhcp_if_network_is_up(
self, m_net_is_up, m_dhcp, m_readurl):
"""Do not perform DHCP setup when nic is already up."""
@@ -323,9 +412,66 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
"Crawl of Azure Instance Metadata Service (IMDS) took", # log_time
self.logs.getvalue())
- @mock.patch(MOCKPATH + 'readurl')
- @mock.patch(MOCKPATH + 'EphemeralDHCPv4WithReporting')
+ @mock.patch(MOCKPATH + 'readurl', autospec=True)
+ @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
+ @mock.patch(MOCKPATH + 'net.is_up')
+ def test_get_compute_metadata_uses_compute_url(
+ self, m_net_is_up, m_dhcp, m_readurl):
+ """Make sure readurl is called with the correct url when accessing
+ network metadata"""
+ m_net_is_up.return_value = True
+ m_readurl.return_value = url_helper.StringResponse(
+ json.dumps(IMDS_NETWORK_METADATA).encode('utf-8'))
+
+ dsaz.get_metadata_from_imds(
+ 'eth0', retries=3, md_type=dsaz.metadata_type.compute)
+ m_readurl.assert_called_with(
+ "http://169.254.169.254/metadata/instance?api-version="
+ "2019-06-01", exception_cb=mock.ANY,
+ headers=mock.ANY, retries=mock.ANY,
+ timeout=mock.ANY)
+
+ @mock.patch(MOCKPATH + 'readurl', autospec=True)
+ @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
@mock.patch(MOCKPATH + 'net.is_up')
+ def test_get_network_metadata_uses_network_url(
+ self, m_net_is_up, m_dhcp, m_readurl):
+ """Make sure readurl is called with the correct url when accessing
+ network metadata"""
+ m_net_is_up.return_value = True
+ m_readurl.return_value = url_helper.StringResponse(
+ json.dumps(IMDS_NETWORK_METADATA).encode('utf-8'))
+
+ dsaz.get_metadata_from_imds(
+ 'eth0', retries=3, md_type=dsaz.metadata_type.network)
+ m_readurl.assert_called_with(
+ "http://169.254.169.254/metadata/instance/network?api-version="
+ "2019-06-01", exception_cb=mock.ANY,
+ headers=mock.ANY, retries=mock.ANY,
+ timeout=mock.ANY)
+
+ @mock.patch(MOCKPATH + 'readurl', autospec=True)
+ @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
+ @mock.patch(MOCKPATH + 'net.is_up')
+ def test_get_default_metadata_uses_compute_url(
+ self, m_net_is_up, m_dhcp, m_readurl):
+ """Make sure readurl is called with the correct url when accessing
+ network metadata"""
+ m_net_is_up.return_value = True
+ m_readurl.return_value = url_helper.StringResponse(
+ json.dumps(IMDS_NETWORK_METADATA).encode('utf-8'))
+
+ dsaz.get_metadata_from_imds(
+ 'eth0', retries=3)
+ m_readurl.assert_called_with(
+ "http://169.254.169.254/metadata/instance?api-version="
+ "2019-06-01", exception_cb=mock.ANY,
+ headers=mock.ANY, retries=mock.ANY,
+ timeout=mock.ANY)
+
+ @mock.patch(MOCKPATH + 'readurl', autospec=True)
+ @mock.patch(MOCKPATH + 'EphemeralDHCPv4WithReporting', autospec=True)
+ @mock.patch(MOCKPATH + 'net.is_up', autospec=True)
def test_get_metadata_performs_dhcp_when_network_is_down(
self, m_net_is_up, m_dhcp, m_readurl):
"""Perform DHCP setup when nic is not up."""
@@ -349,7 +495,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS)
@mock.patch('cloudinit.url_helper.time.sleep')
- @mock.patch(MOCKPATH + 'net.is_up')
+ @mock.patch(MOCKPATH + 'net.is_up', autospec=True)
def test_get_metadata_from_imds_empty_when_no_imds_present(
self, m_net_is_up, m_sleep):
"""Return empty dict when IMDS network metadata is absent."""
@@ -370,7 +516,7 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
@mock.patch('requests.Session.request')
@mock.patch('cloudinit.url_helper.time.sleep')
- @mock.patch(MOCKPATH + 'net.is_up')
+ @mock.patch(MOCKPATH + 'net.is_up', autospec=True)
def test_get_metadata_from_imds_retries_on_timeout(
self, m_net_is_up, m_sleep, m_request):
"""Retry IMDS network metadata on timeout errors."""
@@ -400,6 +546,8 @@ class TestGetMetadataFromIMDS(HttprettyTestCase):
class TestAzureDataSource(CiTestCase):
+ with_logs = True
+
def setUp(self):
super(TestAzureDataSource, self).setUp()
self.tmp = self.tmp_dir()
@@ -465,7 +613,7 @@ scbus-1 on xpt0 bus 0
])
return dsaz
- def _get_ds(self, data, agent_command=None, distro=None,
+ def _get_ds(self, data, agent_command=None, distro='ubuntu',
apply_network=None):
def dsdevs():
@@ -488,9 +636,12 @@ scbus-1 on xpt0 bus 0
dsaz.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d
- self.get_metadata_from_fabric = mock.MagicMock(return_value={
- 'public-keys': [],
- })
+ self.m_is_platform_viable = mock.MagicMock(autospec=True)
+ self.m_get_metadata_from_fabric = mock.MagicMock(
+ return_value={'public-keys': []})
+ self.m_report_failure_to_fabric = mock.MagicMock(autospec=True)
+ self.m_ephemeral_dhcpv4 = mock.MagicMock()
+ self.m_ephemeral_dhcpv4_with_reporting = mock.MagicMock()
self.instance_id = 'D0DF4C54-4ECB-4A4B-9954-5BDF3ED5C3B8'
@@ -507,15 +658,25 @@ scbus-1 on xpt0 bus 0
(dsaz, 'perform_hostname_bounce', mock.MagicMock()),
(dsaz, 'get_hostname', mock.MagicMock()),
(dsaz, 'set_hostname', mock.MagicMock()),
- (dsaz, 'get_metadata_from_fabric', self.get_metadata_from_fabric),
+ (dsaz, '_is_platform_viable',
+ self.m_is_platform_viable),
+ (dsaz, 'get_metadata_from_fabric',
+ self.m_get_metadata_from_fabric),
+ (dsaz, 'report_failure_to_fabric',
+ self.m_report_failure_to_fabric),
+ (dsaz, 'EphemeralDHCPv4', self.m_ephemeral_dhcpv4),
+ (dsaz, 'EphemeralDHCPv4WithReporting',
+ self.m_ephemeral_dhcpv4_with_reporting),
+ (dsaz, 'get_boot_telemetry', mock.MagicMock()),
+ (dsaz, 'get_system_info', mock.MagicMock()),
(dsaz.subp, 'which', lambda x: True),
- (dsaz.util, 'read_dmi_data', mock.MagicMock(
+ (dsaz.dmi, 'read_dmi_data', mock.MagicMock(
side_effect=_dmi_mocks)),
(dsaz.util, 'wait_for_files', mock.MagicMock(
side_effect=_wait_for_files)),
])
- if distro is not None:
+ if isinstance(distro, str):
distro_cls = distros.fetch(distro)
distro = distro_cls(distro, data.get('sys_cfg', {}), self.paths)
dsrc = dsaz.DataSourceAzure(
@@ -571,15 +732,87 @@ scbus-1 on xpt0 bus 0
dev = ds.get_resource_disk_on_freebsd(1)
self.assertEqual("da1", dev)
- @mock.patch(MOCKPATH + '_is_platform_viable')
- def test_call_is_platform_viable_seed(self, m_is_platform_viable):
+ def test_not_is_platform_viable_seed_should_return_no_datasource(self):
"""Check seed_dir using _is_platform_viable and return False."""
# Return a non-matching asset tag value
- m_is_platform_viable.return_value = False
- dsrc = dsaz.DataSourceAzure(
- {}, distro=None, paths=self.paths)
- self.assertFalse(dsrc.get_data())
- m_is_platform_viable.assert_called_with(dsrc.seed_dir)
+ data = {}
+ dsrc = self._get_ds(data)
+ self.m_is_platform_viable.return_value = False
+ with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata, \
+ mock.patch.object(dsrc, '_report_failure') as m_report_failure:
+ ret = dsrc.get_data()
+ self.m_is_platform_viable.assert_called_with(dsrc.seed_dir)
+ self.assertFalse(ret)
+ self.assertNotIn('agent_invoked', data)
+ # Assert that for non viable platforms,
+ # there is no communication with the Azure datasource.
+ self.assertEqual(
+ 0,
+ m_crawl_metadata.call_count)
+ self.assertEqual(
+ 0,
+ m_report_failure.call_count)
+
+ def test_platform_viable_but_no_devs_should_return_no_datasource(self):
+ """For platforms where the Azure platform is viable
+ (which is indicated by the matching asset tag),
+ the absence of any devs at all (devs == candidate sources
+ for crawling Azure datasource) is NOT expected.
+ Report failure to Azure as this is an unexpected fatal error.
+ """
+ data = {}
+ dsrc = self._get_ds(data)
+ with mock.patch.object(dsrc, '_report_failure') as m_report_failure:
+ self.m_is_platform_viable.return_value = True
+ ret = dsrc.get_data()
+ self.m_is_platform_viable.assert_called_with(dsrc.seed_dir)
+ self.assertFalse(ret)
+ self.assertNotIn('agent_invoked', data)
+ self.assertEqual(
+ 1,
+ m_report_failure.call_count)
+
+ def test_crawl_metadata_exception_returns_no_datasource(self):
+ data = {}
+ dsrc = self._get_ds(data)
+ self.m_is_platform_viable.return_value = True
+ with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata:
+ m_crawl_metadata.side_effect = Exception
+ ret = dsrc.get_data()
+ self.m_is_platform_viable.assert_called_with(dsrc.seed_dir)
+ self.assertEqual(
+ 1,
+ m_crawl_metadata.call_count)
+ self.assertFalse(ret)
+ self.assertNotIn('agent_invoked', data)
+
+ def test_crawl_metadata_exception_should_report_failure_with_msg(self):
+ data = {}
+ dsrc = self._get_ds(data)
+ self.m_is_platform_viable.return_value = True
+ with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata, \
+ mock.patch.object(dsrc, '_report_failure') as m_report_failure:
+ m_crawl_metadata.side_effect = Exception
+ dsrc.get_data()
+ self.assertEqual(
+ 1,
+ m_crawl_metadata.call_count)
+ m_report_failure.assert_called_once_with(
+ description=dsaz.DEFAULT_REPORT_FAILURE_USER_VISIBLE_MESSAGE)
+
+ def test_crawl_metadata_exc_should_log_could_not_crawl_msg(self):
+ data = {}
+ dsrc = self._get_ds(data)
+ self.m_is_platform_viable.return_value = True
+ with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata:
+ m_crawl_metadata.side_effect = Exception
+ dsrc.get_data()
+ self.assertEqual(
+ 1,
+ m_crawl_metadata.call_count)
+ self.assertIn(
+ "Could not crawl Azure metadata",
+ self.logs.getvalue())
def test_basic_seed_dir(self):
odata = {'HostName': "myhost", 'UserName': "myuser"}
@@ -653,6 +886,7 @@ scbus-1 on xpt0 bus 0
'sys_cfg': {}}
dsrc = self._get_ds(data)
expected_cfg = {
+ 'PreprovisionedVMType': None,
'PreprovisionedVm': False,
'datasource': {'Azure': {'agent_command': 'my_command'}},
'system_info': {'default_user': {'name': u'myuser'}}}
@@ -700,7 +934,7 @@ scbus-1 on xpt0 bus 0
'cloudinit.sources.DataSourceAzure.DataSourceAzure._report_ready')
@mock.patch('cloudinit.sources.DataSourceAzure.DataSourceAzure._poll_imds')
def test_crawl_metadata_on_reprovision_reports_ready(
- self, poll_imds_func, report_ready_func, m_write, m_dhcp
+ self, poll_imds_func, m_report_ready, m_write, m_dhcp
):
"""If reprovisioning, report ready at the end"""
ovfenv = construct_valid_ovf_env(
@@ -714,18 +948,76 @@ scbus-1 on xpt0 bus 0
dsrc = self._get_ds(data)
poll_imds_func.return_value = ovfenv
dsrc.crawl_metadata()
+ self.assertEqual(1, m_report_ready.call_count)
+
+ @mock.patch(
+ 'cloudinit.sources.DataSourceAzure.EphemeralDHCPv4WithReporting')
+ @mock.patch('cloudinit.sources.DataSourceAzure.util.write_file')
+ @mock.patch(
+ 'cloudinit.sources.DataSourceAzure.DataSourceAzure._report_ready')
+ @mock.patch('cloudinit.sources.DataSourceAzure.DataSourceAzure._poll_imds')
+ @mock.patch(
+ 'cloudinit.sources.DataSourceAzure.DataSourceAzure.'
+ '_wait_for_all_nics_ready')
+ def test_crawl_metadata_waits_for_nic_on_savable_vms(
+ self, detect_nics, poll_imds_func, report_ready_func, m_write, m_dhcp
+ ):
+ """If reprovisioning, report ready at the end"""
+ ovfenv = construct_valid_ovf_env(
+ platform_settings={"PreprovisionedVMType": "Savable",
+ "PreprovisionedVm": "True"}
+ )
+
+ data = {
+ 'ovfcontent': ovfenv,
+ 'sys_cfg': {}
+ }
+ dsrc = self._get_ds(data)
+ poll_imds_func.return_value = ovfenv
+ dsrc.crawl_metadata()
+ self.assertEqual(1, report_ready_func.call_count)
+ self.assertEqual(1, detect_nics.call_count)
+
+ @mock.patch(
+ 'cloudinit.sources.DataSourceAzure.EphemeralDHCPv4WithReporting')
+ @mock.patch('cloudinit.sources.DataSourceAzure.util.write_file')
+ @mock.patch(
+ 'cloudinit.sources.DataSourceAzure.DataSourceAzure._report_ready')
+ @mock.patch('cloudinit.sources.DataSourceAzure.DataSourceAzure._poll_imds')
+ @mock.patch(
+ 'cloudinit.sources.DataSourceAzure.DataSourceAzure.'
+ '_wait_for_all_nics_ready')
+ @mock.patch('os.path.isfile')
+ def test_detect_nics_when_marker_present(
+ self, is_file, detect_nics, poll_imds_func, report_ready_func, m_write,
+ m_dhcp):
+ """If reprovisioning, wait for nic attach if marker present"""
+
+ def is_file_ret(key):
+ return key == dsaz.REPROVISION_NIC_ATTACH_MARKER_FILE
+
+ is_file.side_effect = is_file_ret
+ ovfenv = construct_valid_ovf_env()
+
+ data = {
+ 'ovfcontent': ovfenv,
+ 'sys_cfg': {}
+ }
+
+ dsrc = self._get_ds(data)
+ poll_imds_func.return_value = ovfenv
+ dsrc.crawl_metadata()
self.assertEqual(1, report_ready_func.call_count)
+ self.assertEqual(1, detect_nics.call_count)
@mock.patch('cloudinit.sources.DataSourceAzure.util.write_file')
@mock.patch('cloudinit.sources.helpers.netlink.'
'wait_for_media_disconnect_connect')
@mock.patch(
'cloudinit.sources.DataSourceAzure.DataSourceAzure._report_ready')
- @mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
- @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
@mock.patch('cloudinit.sources.DataSourceAzure.readurl')
def test_crawl_metadata_on_reprovision_reports_ready_using_lease(
- self, m_readurl, m_dhcp, m_net, report_ready_func,
+ self, m_readurl, m_report_ready,
m_media_switch, m_write
):
"""If reprovisioning, report ready using the obtained lease"""
@@ -739,20 +1031,30 @@ scbus-1 on xpt0 bus 0
}
dsrc = self._get_ds(data)
- lease = {
- 'interface': 'eth9', 'fixed-address': '192.168.2.9',
- 'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
- 'unknown-245': '624c3620'}
- m_dhcp.return_value = [lease]
- m_media_switch.return_value = None
+ with mock.patch.object(dsrc.distro.networking, 'is_up') \
+ as m_dsrc_distro_networking_is_up:
- reprovision_ovfenv = construct_valid_ovf_env()
- m_readurl.return_value = url_helper.StringResponse(
- reprovision_ovfenv.encode('utf-8'))
+ # For this mock, net should not be up,
+ # so that cached ephemeral won't be used.
+ # This is so that a NEW ephemeral dhcp lease will be discovered
+ # and used instead.
+ m_dsrc_distro_networking_is_up.return_value = False
- dsrc.crawl_metadata()
- self.assertEqual(2, report_ready_func.call_count)
- report_ready_func.assert_called_with(lease=lease)
+ lease = {
+ 'interface': 'eth9', 'fixed-address': '192.168.2.9',
+ 'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
+ 'unknown-245': '624c3620'}
+ self.m_ephemeral_dhcpv4_with_reporting.return_value \
+ .__enter__.return_value = lease
+ m_media_switch.return_value = None
+
+ reprovision_ovfenv = construct_valid_ovf_env()
+ m_readurl.return_value = url_helper.StringResponse(
+ reprovision_ovfenv.encode('utf-8'))
+
+ dsrc.crawl_metadata()
+ self.assertEqual(2, m_report_ready.call_count)
+ m_report_ready.assert_called_with(lease=lease)
def test_waagent_d_has_0700_perms(self):
# we expect /var/lib/waagent to be created 0700
@@ -777,7 +1079,9 @@ scbus-1 on xpt0 bus 0
self.assertTrue(ret)
self.assertEqual(data['agent_invoked'], cfg['agent_command'])
- def test_network_config_set_from_imds(self):
+ @mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
+ return_value=None)
+ def test_network_config_set_from_imds(self, m_driver):
"""Datasource.network_config returns IMDS network data."""
sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
odata = {}
@@ -795,7 +1099,10 @@ scbus-1 on xpt0 bus 0
dsrc.get_data()
self.assertEqual(expected_network_config, dsrc.network_config)
- def test_network_config_set_from_imds_route_metric_for_secondary_nic(self):
+ @mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
+ return_value=None)
+ def test_network_config_set_from_imds_route_metric_for_secondary_nic(
+ self, m_driver):
"""Datasource.network_config adds route-metric to secondary nics."""
sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
odata = {}
@@ -905,7 +1212,7 @@ scbus-1 on xpt0 bus 0
dsrc = self._get_ds(data)
ret = dsrc.get_data()
self.assertTrue(ret)
- self.assertTrue('default_user' in dsrc.cfg['system_info'])
+ self.assertIn('default_user', dsrc.cfg['system_info'])
defuser = dsrc.cfg['system_info']['default_user']
# default user should be updated username and should not be locked.
@@ -919,6 +1226,9 @@ scbus-1 on xpt0 bus 0
crypt.crypt(odata['UserPassword'],
defuser['passwd'][0:pos]))
+ # the same hashed value should also be present in cfg['password']
+ self.assertEqual(defuser['passwd'], dsrc.cfg['password'])
+
def test_user_not_locked_if_password_redacted(self):
odata = {'HostName': "myhost", 'UserName': "myuser",
'UserPassword': dsaz.DEF_PASSWD_REDACTION}
@@ -927,7 +1237,7 @@ scbus-1 on xpt0 bus 0
dsrc = self._get_ds(data)
ret = dsrc.get_data()
self.assertTrue(ret)
- self.assertTrue('default_user' in dsrc.cfg['system_info'])
+ self.assertIn('default_user', dsrc.cfg['system_info'])
defuser = dsrc.cfg['system_info']['default_user']
# default user should be updated username and should not be locked.
@@ -955,14 +1265,6 @@ scbus-1 on xpt0 bus 0
self.assertTrue(ret)
self.assertEqual(dsrc.userdata_raw, mydata.encode('utf-8'))
- def test_no_datasource_expected(self):
- # no source should be found if no seed_dir and no devs
- data = {}
- dsrc = self._get_ds({})
- ret = dsrc.get_data()
- self.assertFalse(ret)
- self.assertFalse('agent_invoked' in data)
-
def test_cfg_has_pubkeys_fingerprint(self):
odata = {'HostName': "myhost", 'UserName': "myuser"}
mypklist = [{'fingerprint': 'fp1', 'path': 'path1', 'value': ''}]
@@ -1095,18 +1397,178 @@ scbus-1 on xpt0 bus 0
dsrc = self._get_ds({'ovfcontent': xml})
dsrc.get_data()
+ def test_dsaz_report_ready_returns_true_when_report_succeeds(
+ self):
+ dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
+ dsrc.ds_cfg['agent_command'] = '__builtin__'
+ self.assertTrue(dsrc._report_ready(lease=mock.MagicMock()))
+
+ def test_dsaz_report_ready_returns_false_and_does_not_propagate_exc(
+ self):
+ dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
+ dsrc.ds_cfg['agent_command'] = '__builtin__'
+ self.m_get_metadata_from_fabric.side_effect = Exception
+ self.assertFalse(dsrc._report_ready(lease=mock.MagicMock()))
+
+ def test_dsaz_report_failure_returns_true_when_report_succeeds(self):
+ dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
+ dsrc.ds_cfg['agent_command'] = '__builtin__'
+
+ with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata:
+ # mock crawl metadata failure to cause report failure
+ m_crawl_metadata.side_effect = Exception
+
+ self.assertTrue(dsrc._report_failure())
+ self.assertEqual(
+ 1,
+ self.m_report_failure_to_fabric.call_count)
+
+ def test_dsaz_report_failure_returns_false_and_does_not_propagate_exc(
+ self):
+ dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
+ dsrc.ds_cfg['agent_command'] = '__builtin__'
+
+ with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata, \
+ mock.patch.object(dsrc, '_ephemeral_dhcp_ctx') \
+ as m_ephemeral_dhcp_ctx, \
+ mock.patch.object(dsrc.distro.networking, 'is_up') \
+ as m_dsrc_distro_networking_is_up:
+ # mock crawl metadata failure to cause report failure
+ m_crawl_metadata.side_effect = Exception
+
+ # setup mocks to allow using cached ephemeral dhcp lease
+ m_dsrc_distro_networking_is_up.return_value = True
+ test_lease_dhcp_option_245 = 'test_lease_dhcp_option_245'
+ test_lease = {'unknown-245': test_lease_dhcp_option_245}
+ m_ephemeral_dhcp_ctx.lease = test_lease
+
+ # We expect 3 calls to report_failure_to_fabric,
+ # because we try 3 different methods of calling report failure.
+ # The different methods are attempted in the following order:
+ # 1. Using cached ephemeral dhcp context to report failure to Azure
+ # 2. Using new ephemeral dhcp to report failure to Azure
+ # 3. Using fallback lease to report failure to Azure
+ self.m_report_failure_to_fabric.side_effect = Exception
+ self.assertFalse(dsrc._report_failure())
+ self.assertEqual(
+ 3,
+ self.m_report_failure_to_fabric.call_count)
+
+ def test_dsaz_report_failure_description_msg(self):
+ dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
+ dsrc.ds_cfg['agent_command'] = '__builtin__'
+
+ with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata:
+ # mock crawl metadata failure to cause report failure
+ m_crawl_metadata.side_effect = Exception
+
+ test_msg = 'Test report failure description message'
+ self.assertTrue(dsrc._report_failure(description=test_msg))
+ self.m_report_failure_to_fabric.assert_called_once_with(
+ dhcp_opts=mock.ANY, description=test_msg)
+
+ def test_dsaz_report_failure_no_description_msg(self):
+ dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
+ dsrc.ds_cfg['agent_command'] = '__builtin__'
+
+ with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata:
+ m_crawl_metadata.side_effect = Exception
+
+ self.assertTrue(dsrc._report_failure()) # no description msg
+ self.m_report_failure_to_fabric.assert_called_once_with(
+ dhcp_opts=mock.ANY, description=None)
+
+ def test_dsaz_report_failure_uses_cached_ephemeral_dhcp_ctx_lease(self):
+ dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
+ dsrc.ds_cfg['agent_command'] = '__builtin__'
+
+ with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata, \
+ mock.patch.object(dsrc, '_ephemeral_dhcp_ctx') \
+ as m_ephemeral_dhcp_ctx, \
+ mock.patch.object(dsrc.distro.networking, 'is_up') \
+ as m_dsrc_distro_networking_is_up:
+ # mock crawl metadata failure to cause report failure
+ m_crawl_metadata.side_effect = Exception
+
+ # setup mocks to allow using cached ephemeral dhcp lease
+ m_dsrc_distro_networking_is_up.return_value = True
+ test_lease_dhcp_option_245 = 'test_lease_dhcp_option_245'
+ test_lease = {'unknown-245': test_lease_dhcp_option_245}
+ m_ephemeral_dhcp_ctx.lease = test_lease
+
+ self.assertTrue(dsrc._report_failure())
+
+ # ensure called with cached ephemeral dhcp lease option 245
+ self.m_report_failure_to_fabric.assert_called_once_with(
+ description=mock.ANY, dhcp_opts=test_lease_dhcp_option_245)
+
+ # ensure cached ephemeral is cleaned
+ self.assertEqual(
+ 1,
+ m_ephemeral_dhcp_ctx.clean_network.call_count)
+
+ def test_dsaz_report_failure_no_net_uses_new_ephemeral_dhcp_lease(self):
+ dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
+ dsrc.ds_cfg['agent_command'] = '__builtin__'
+
+ with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata, \
+ mock.patch.object(dsrc.distro.networking, 'is_up') \
+ as m_dsrc_distro_networking_is_up:
+ # mock crawl metadata failure to cause report failure
+ m_crawl_metadata.side_effect = Exception
+
+ # net is not up and cannot use cached ephemeral dhcp
+ m_dsrc_distro_networking_is_up.return_value = False
+ # setup ephemeral dhcp lease discovery mock
+ test_lease_dhcp_option_245 = 'test_lease_dhcp_option_245'
+ test_lease = {'unknown-245': test_lease_dhcp_option_245}
+ self.m_ephemeral_dhcpv4_with_reporting.return_value \
+ .__enter__.return_value = test_lease
+
+ self.assertTrue(dsrc._report_failure())
+
+ # ensure called with the newly discovered
+ # ephemeral dhcp lease option 245
+ self.m_report_failure_to_fabric.assert_called_once_with(
+ description=mock.ANY, dhcp_opts=test_lease_dhcp_option_245)
+
+ def test_dsaz_report_failure_no_net_and_no_dhcp_uses_fallback_lease(
+ self):
+ dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
+ dsrc.ds_cfg['agent_command'] = '__builtin__'
+
+ with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata, \
+ mock.patch.object(dsrc.distro.networking, 'is_up') \
+ as m_dsrc_distro_networking_is_up:
+ # mock crawl metadata failure to cause report failure
+ m_crawl_metadata.side_effect = Exception
+
+ # net is not up and cannot use cached ephemeral dhcp
+ m_dsrc_distro_networking_is_up.return_value = False
+ # ephemeral dhcp discovery failure,
+ # so cannot use a new ephemeral dhcp
+ self.m_ephemeral_dhcpv4_with_reporting.return_value \
+ .__enter__.side_effect = Exception
+
+ self.assertTrue(dsrc._report_failure())
+
+ # ensure called with fallback lease
+ self.m_report_failure_to_fabric.assert_called_once_with(
+ description=mock.ANY,
+ fallback_lease_file=dsrc.dhclient_lease_file)
+
def test_exception_fetching_fabric_data_doesnt_propagate(self):
"""Errors communicating with fabric should warn, but return True."""
dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
dsrc.ds_cfg['agent_command'] = '__builtin__'
- self.get_metadata_from_fabric.side_effect = Exception
+ self.m_get_metadata_from_fabric.side_effect = Exception
ret = self._get_and_setup(dsrc)
self.assertTrue(ret)
def test_fabric_data_included_in_metadata(self):
dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()})
dsrc.ds_cfg['agent_command'] = '__builtin__'
- self.get_metadata_from_fabric.return_value = {'test': 'value'}
+ self.m_get_metadata_from_fabric.return_value = {'test': 'value'}
ret = self._get_and_setup(dsrc)
self.assertTrue(ret)
self.assertEqual('value', dsrc.metadata['test'])
@@ -1151,8 +1613,10 @@ scbus-1 on xpt0 bus 0
self.assertEqual(
[mock.call("/dev/cd0")], m_check_fbsd_cdrom.call_args_list)
+ @mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
+ return_value=None)
@mock.patch('cloudinit.net.generate_fallback_config')
- def test_imds_network_config(self, mock_fallback):
+ def test_imds_network_config(self, mock_fallback, m_driver):
"""Network config is generated from IMDS network data when present."""
sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
odata = {'HostName': "myhost", 'UserName': "myuser"}
@@ -1208,7 +1672,7 @@ scbus-1 on xpt0 bus 0
@mock.patch('cloudinit.net.get_interface_mac')
@mock.patch('cloudinit.net.get_devicelist')
@mock.patch('cloudinit.net.device_driver')
- @mock.patch('cloudinit.net.generate_fallback_config')
+ @mock.patch('cloudinit.net.generate_fallback_config', autospec=True)
def test_fallback_network_config(self, mock_fallback, mock_dd,
mock_devlist, mock_get_mac):
"""On absent IMDS network data, generate network fallback config."""
@@ -1239,71 +1703,81 @@ scbus-1 on xpt0 bus 0
netconfig = dsrc.network_config
self.assertEqual(netconfig, fallback_config)
- mock_fallback.assert_called_with(blacklist_drivers=['mlx4_core'],
- config_driver=True)
+ mock_fallback.assert_called_with(
+ blacklist_drivers=['mlx4_core', 'mlx5_core'],
+ config_driver=True)
- @mock.patch('cloudinit.net.get_interface_mac')
- @mock.patch('cloudinit.net.get_devicelist')
- @mock.patch('cloudinit.net.device_driver')
- @mock.patch('cloudinit.net.generate_fallback_config')
- def test_fallback_network_config_blacklist(self, mock_fallback, mock_dd,
- mock_devlist, mock_get_mac):
- """On absent network metadata, blacklist mlx from fallback config."""
+ @mock.patch(MOCKPATH + 'net.get_interfaces', autospec=True)
+ @mock.patch(MOCKPATH + 'util.is_FreeBSD')
+ def test_blacklist_through_distro(
+ self, m_is_freebsd, m_net_get_interfaces):
+ """Verify Azure DS updates blacklist drivers in the distro's
+ networking object."""
odata = {'HostName': "myhost", 'UserName': "myuser"}
data = {'ovfcontent': construct_valid_ovf_env(data=odata),
'sys_cfg': {}}
- fallback_config = {
- 'version': 1,
- 'config': [{
- 'type': 'physical', 'name': 'eth0',
- 'mac_address': '00:11:22:33:44:55',
- 'params': {'driver': 'hv_netsvc'},
- 'subnets': [{'type': 'dhcp'}],
- }]
- }
- blacklist_config = {
- 'type': 'physical',
- 'name': 'eth1',
- 'mac_address': '00:11:22:33:44:55',
- 'params': {'driver': 'mlx4_core'}
- }
- mock_fallback.return_value = fallback_config
-
- mock_devlist.return_value = ['eth0', 'eth1']
- mock_dd.side_effect = [
- 'hv_netsvc', # list composition, skipped
- 'mlx4_core', # list composition, match
- 'mlx4_core', # config get driver name
- ]
- mock_get_mac.return_value = '00:11:22:33:44:55'
-
- dsrc = self._get_ds(data)
- # Represent empty response from network imds
- self.m_get_metadata_from_imds.return_value = {}
- ret = dsrc.get_data()
- self.assertTrue(ret)
+ distro_cls = distros.fetch('ubuntu')
+ distro = distro_cls('ubuntu', {}, self.paths)
+ dsrc = self._get_ds(data, distro=distro)
+ dsrc.get_data()
+ self.assertEqual(distro.networking.blacklist_drivers,
+ dsaz.BLACKLIST_DRIVERS)
- netconfig = dsrc.network_config
- expected_config = fallback_config
- expected_config['config'].append(blacklist_config)
- self.assertEqual(netconfig, expected_config)
+ m_is_freebsd.return_value = False
+ distro.networking.get_interfaces_by_mac()
+ m_net_get_interfaces.assert_called_with(
+ blacklist_drivers=dsaz.BLACKLIST_DRIVERS)
- @mock.patch(MOCKPATH + 'subp.subp')
+ @mock.patch(MOCKPATH + 'subp.subp', autospec=True)
def test_get_hostname_with_no_args(self, m_subp):
dsaz.get_hostname()
m_subp.assert_called_once_with(("hostname",), capture=True)
- @mock.patch(MOCKPATH + 'subp.subp')
+ @mock.patch(MOCKPATH + 'subp.subp', autospec=True)
def test_get_hostname_with_string_arg(self, m_subp):
dsaz.get_hostname(hostname_command="hostname")
m_subp.assert_called_once_with(("hostname",), capture=True)
- @mock.patch(MOCKPATH + 'subp.subp')
+ @mock.patch(MOCKPATH + 'subp.subp', autospec=True)
def test_get_hostname_with_iterable_arg(self, m_subp):
dsaz.get_hostname(hostname_command=("hostname",))
m_subp.assert_called_once_with(("hostname",), capture=True)
+ @mock.patch(
+ 'cloudinit.sources.helpers.azure.OpenSSLManager.parse_certificates')
+ def test_get_public_ssh_keys_with_imds(self, m_parse_certificates):
+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
+ odata = {'HostName': "myhost", 'UserName': "myuser"}
+ data = {
+ 'ovfcontent': construct_valid_ovf_env(data=odata),
+ 'sys_cfg': sys_cfg
+ }
+ dsrc = self._get_ds(data)
+ dsrc.get_data()
+ dsrc.setup(True)
+ ssh_keys = dsrc.get_public_ssh_keys()
+ self.assertEqual(ssh_keys, ['key1'])
+ self.assertEqual(m_parse_certificates.call_count, 0)
+
+ @mock.patch(MOCKPATH + 'get_metadata_from_imds')
+ def test_get_public_ssh_keys_without_imds(
+ self,
+ m_get_metadata_from_imds):
+ m_get_metadata_from_imds.return_value = dict()
+ sys_cfg = {'datasource': {'Azure': {'apply_network_config': True}}}
+ odata = {'HostName': "myhost", 'UserName': "myuser"}
+ data = {
+ 'ovfcontent': construct_valid_ovf_env(data=odata),
+ 'sys_cfg': sys_cfg
+ }
+ dsrc = self._get_ds(data)
+ dsaz.get_metadata_from_fabric.return_value = {'public-keys': ['key2']}
+ dsrc.get_data()
+ dsrc.setup(True)
+ ssh_keys = dsrc.get_public_ssh_keys()
+ self.assertEqual(ssh_keys, ['key2'])
+
class TestAzureBounce(CiTestCase):
@@ -1336,7 +1810,7 @@ class TestAzureBounce(CiTestCase):
raise RuntimeError('should not get here')
self.patches.enter_context(
- mock.patch.object(dsaz.util, 'read_dmi_data',
+ mock.patch.object(dsaz.dmi, 'read_dmi_data',
mock.MagicMock(side_effect=_dmi_mocks)))
def setUp(self):
@@ -1365,8 +1839,7 @@ class TestAzureBounce(CiTestCase):
if ovfcontent is not None:
populate_dir(os.path.join(self.paths.seed_dir, "azure"),
{'ovf-env.xml': ovfcontent})
- dsrc = dsaz.DataSourceAzure(
- {}, distro=None, paths=self.paths)
+ dsrc = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths)
if agent_command is not None:
dsrc.ds_cfg['agent_command'] = agent_command
return dsrc
@@ -1850,7 +2323,7 @@ class TestClearCachedData(CiTestCase):
tmp = self.tmp_dir()
paths = helpers.Paths(
{'cloud_dir': tmp, 'run_dir': tmp})
- dsrc = dsaz.DataSourceAzure({}, distro=None, paths=paths)
+ dsrc = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=paths)
clean_values = [dsrc.metadata, dsrc.userdata, dsrc._metadata_imds]
dsrc.metadata = 'md'
dsrc.userdata = 'ud'
@@ -1897,6 +2370,29 @@ class TestPreprovisioningReadAzureOvfFlag(CiTestCase):
ret = dsaz.read_azure_ovf(content)
cfg = ret[2]
self.assertFalse(cfg['PreprovisionedVm'])
+ self.assertEqual(None, cfg["PreprovisionedVMType"])
+
+ def test_read_azure_ovf_with_running_type(self):
+ """The read_azure_ovf method should set PreprovisionedVMType
+ cfg flag to Running."""
+ content = construct_valid_ovf_env(
+ platform_settings={"PreprovisionedVMType": "Running",
+ "PreprovisionedVm": "True"})
+ ret = dsaz.read_azure_ovf(content)
+ cfg = ret[2]
+ self.assertTrue(cfg['PreprovisionedVm'])
+ self.assertEqual("Running", cfg['PreprovisionedVMType'])
+
+ def test_read_azure_ovf_with_savable_type(self):
+ """The read_azure_ovf method should set PreprovisionedVMType
+ cfg flag to Savable."""
+ content = construct_valid_ovf_env(
+ platform_settings={"PreprovisionedVMType": "Savable",
+ "PreprovisionedVm": "True"})
+ ret = dsaz.read_azure_ovf(content)
+ cfg = ret[2]
+ self.assertTrue(cfg['PreprovisionedVm'])
+ self.assertEqual("Savable", cfg['PreprovisionedVMType'])
@mock.patch('os.path.isfile')
@@ -1914,7 +2410,7 @@ class TestPreprovisioningShouldReprovision(CiTestCase):
"""The _should_reprovision method should return true with config
flag present."""
isfile.return_value = False
- dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ dsa = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths)
self.assertTrue(dsa._should_reprovision(
(None, None, {'PreprovisionedVm': True}, None)))
@@ -1922,7 +2418,7 @@ class TestPreprovisioningShouldReprovision(CiTestCase):
"""The _should_reprovision method should return True if the sentinal
exists."""
isfile.return_value = True
- dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ dsa = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths)
self.assertTrue(dsa._should_reprovision(
(None, None, {'preprovisionedvm': False}, None)))
@@ -1930,7 +2426,7 @@ class TestPreprovisioningShouldReprovision(CiTestCase):
"""The _should_reprovision method should return False
if config and sentinal are not present."""
isfile.return_value = False
- dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ dsa = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths)
self.assertFalse(dsa._should_reprovision((None, None, {}, None)))
@mock.patch(MOCKPATH + 'DataSourceAzure._poll_imds')
@@ -1941,11 +2437,232 @@ class TestPreprovisioningShouldReprovision(CiTestCase):
username = "myuser"
odata = {'HostName': hostname, 'UserName': username}
_poll_imds.return_value = construct_valid_ovf_env(data=odata)
- dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ dsa = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths)
dsa._reprovision()
_poll_imds.assert_called_with()
+class TestPreprovisioningHotAttachNics(CiTestCase):
+
+ def setUp(self):
+ super(TestPreprovisioningHotAttachNics, self).setUp()
+ self.tmp = self.tmp_dir()
+ self.waagent_d = self.tmp_path('/var/lib/waagent', self.tmp)
+ self.paths = helpers.Paths({'cloud_dir': self.tmp})
+ dsaz.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d
+ self.paths = helpers.Paths({'cloud_dir': self.tmp})
+
+ @mock.patch('cloudinit.sources.helpers.netlink.wait_for_nic_detach_event',
+ autospec=True)
+ @mock.patch(MOCKPATH + 'util.write_file', autospec=True)
+ def test_nic_detach_writes_marker(self, m_writefile, m_detach):
+ """When we detect that a nic gets detached, we write a marker for it"""
+ dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ nl_sock = mock.MagicMock()
+ dsa._wait_for_nic_detach(nl_sock)
+ m_detach.assert_called_with(nl_sock)
+ self.assertEqual(1, m_detach.call_count)
+ m_writefile.assert_called_with(
+ dsaz.REPROVISION_NIC_DETACHED_MARKER_FILE, mock.ANY)
+
+ @mock.patch(MOCKPATH + 'util.write_file', autospec=True)
+ @mock.patch(MOCKPATH + 'DataSourceAzure.fallback_interface')
+ @mock.patch(MOCKPATH + 'EphemeralDHCPv4WithReporting')
+ @mock.patch(MOCKPATH + 'DataSourceAzure._report_ready')
+ @mock.patch(MOCKPATH + 'DataSourceAzure._wait_for_nic_detach')
+ def test_detect_nic_attach_reports_ready_and_waits_for_detach(
+ self, m_detach, m_report_ready, m_dhcp, m_fallback_if,
+ m_writefile):
+ """Report ready first and then wait for nic detach"""
+ dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ dsa._wait_for_all_nics_ready()
+ m_fallback_if.return_value = "Dummy interface"
+ self.assertEqual(1, m_report_ready.call_count)
+ self.assertEqual(1, m_detach.call_count)
+ self.assertEqual(1, m_writefile.call_count)
+ self.assertEqual(1, m_dhcp.call_count)
+ m_writefile.assert_called_with(dsaz.REPORTED_READY_MARKER_FILE,
+ mock.ANY)
+
+ @mock.patch('os.path.isfile')
+ @mock.patch(MOCKPATH + 'DataSourceAzure.fallback_interface')
+ @mock.patch(MOCKPATH + 'EphemeralDHCPv4WithReporting')
+ @mock.patch(MOCKPATH + 'DataSourceAzure._report_ready')
+ @mock.patch(MOCKPATH + 'DataSourceAzure._wait_for_nic_detach')
+ def test_detect_nic_attach_skips_report_ready_when_marker_present(
+ self, m_detach, m_report_ready, m_dhcp, m_fallback_if, m_isfile):
+ """Skip reporting ready if we already have a marker file."""
+ dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+
+ def isfile(key):
+ return key == dsaz.REPORTED_READY_MARKER_FILE
+
+ m_isfile.side_effect = isfile
+ dsa._wait_for_all_nics_ready()
+ m_fallback_if.return_value = "Dummy interface"
+ self.assertEqual(0, m_report_ready.call_count)
+ self.assertEqual(0, m_dhcp.call_count)
+ self.assertEqual(1, m_detach.call_count)
+
+ @mock.patch('os.path.isfile')
+ @mock.patch(MOCKPATH + 'DataSourceAzure.fallback_interface')
+ @mock.patch(MOCKPATH + 'EphemeralDHCPv4WithReporting')
+ @mock.patch(MOCKPATH + 'DataSourceAzure._report_ready')
+ @mock.patch(MOCKPATH + 'DataSourceAzure._wait_for_nic_detach')
+ def test_detect_nic_attach_skips_nic_detach_when_marker_present(
+ self, m_detach, m_report_ready, m_dhcp, m_fallback_if, m_isfile):
+ """Skip wait for nic detach if it already happened."""
+ dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+
+ m_isfile.return_value = True
+ dsa._wait_for_all_nics_ready()
+ m_fallback_if.return_value = "Dummy interface"
+ self.assertEqual(0, m_report_ready.call_count)
+ self.assertEqual(0, m_dhcp.call_count)
+ self.assertEqual(0, m_detach.call_count)
+
+ @mock.patch(MOCKPATH + 'DataSourceAzure.wait_for_link_up', autospec=True)
+ @mock.patch('cloudinit.sources.helpers.netlink.wait_for_nic_attach_event')
+ @mock.patch('cloudinit.sources.net.find_fallback_nic')
+ @mock.patch(MOCKPATH + 'get_metadata_from_imds')
+ @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
+ @mock.patch(MOCKPATH + 'DataSourceAzure._wait_for_nic_detach')
+ @mock.patch('os.path.isfile')
+ def test_wait_for_nic_attach_if_no_fallback_interface(
+ self, m_isfile, m_detach, m_dhcpv4, m_imds, m_fallback_if,
+ m_attach, m_link_up):
+ """Wait for nic attach if we do not have a fallback interface"""
+ dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ lease = {
+ 'interface': 'eth9', 'fixed-address': '192.168.2.9',
+ 'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
+ 'unknown-245': '624c3620'}
+
+ m_isfile.return_value = True
+ m_attach.return_value = "eth0"
+ dhcp_ctx = mock.MagicMock(lease=lease)
+ dhcp_ctx.obtain_lease.return_value = lease
+ m_dhcpv4.return_value = dhcp_ctx
+ m_imds.return_value = IMDS_NETWORK_METADATA
+ m_fallback_if.return_value = None
+
+ dsa._wait_for_all_nics_ready()
+
+ self.assertEqual(0, m_detach.call_count)
+ self.assertEqual(1, m_attach.call_count)
+ self.assertEqual(1, m_dhcpv4.call_count)
+ self.assertEqual(1, m_imds.call_count)
+ self.assertEqual(1, m_link_up.call_count)
+ m_link_up.assert_called_with(mock.ANY, "eth0")
+
+ @mock.patch(MOCKPATH + 'DataSourceAzure.wait_for_link_up')
+ @mock.patch('cloudinit.sources.helpers.netlink.wait_for_nic_attach_event')
+ @mock.patch('cloudinit.sources.net.find_fallback_nic')
+ @mock.patch(MOCKPATH + 'get_metadata_from_imds')
+ @mock.patch(MOCKPATH + 'EphemeralDHCPv4')
+ @mock.patch(MOCKPATH + 'DataSourceAzure._wait_for_nic_detach')
+ @mock.patch('os.path.isfile')
+ def test_wait_for_nic_attach_multinic_attach(
+ self, m_isfile, m_detach, m_dhcpv4, m_imds, m_fallback_if,
+ m_attach, m_link_up):
+ """Wait for nic attach if we do not have a fallback interface"""
+ dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ lease = {
+ 'interface': 'eth9', 'fixed-address': '192.168.2.9',
+ 'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
+ 'unknown-245': '624c3620'}
+ m_attach_call_count = 0
+
+ def nic_attach_ret(nl_sock, nics_found):
+ nonlocal m_attach_call_count
+ if m_attach_call_count == 0:
+ m_attach_call_count = m_attach_call_count + 1
+ return "eth0"
+ return "eth1"
+
+ def network_metadata_ret(ifname, retries, type):
+ # Simulate two NICs by adding the same one twice.
+ md = IMDS_NETWORK_METADATA
+ md['interface'].append(md['interface'][0])
+ if ifname == "eth0":
+ return md
+ raise requests.Timeout('Fake connection timeout')
+
+ m_isfile.return_value = True
+ m_attach.side_effect = nic_attach_ret
+ dhcp_ctx = mock.MagicMock(lease=lease)
+ dhcp_ctx.obtain_lease.return_value = lease
+ m_dhcpv4.return_value = dhcp_ctx
+ m_imds.side_effect = network_metadata_ret
+ m_fallback_if.return_value = None
+
+ dsa._wait_for_all_nics_ready()
+
+ self.assertEqual(0, m_detach.call_count)
+ self.assertEqual(2, m_attach.call_count)
+ # DHCP and network metadata calls will only happen on the primary NIC.
+ self.assertEqual(1, m_dhcpv4.call_count)
+ self.assertEqual(1, m_imds.call_count)
+ self.assertEqual(2, m_link_up.call_count)
+
+ @mock.patch('cloudinit.distros.networking.LinuxNetworking.try_set_link_up')
+ def test_wait_for_link_up_returns_if_already_up(
+ self, m_is_link_up):
+ """Waiting for link to be up should return immediately if the link is
+ already up."""
+
+ distro_cls = distros.fetch('ubuntu')
+ distro = distro_cls('ubuntu', {}, self.paths)
+ dsa = dsaz.DataSourceAzure({}, distro=distro, paths=self.paths)
+ m_is_link_up.return_value = True
+
+ dsa.wait_for_link_up("eth0")
+ self.assertEqual(1, m_is_link_up.call_count)
+
+ @mock.patch(MOCKPATH + 'util.write_file')
+ @mock.patch('cloudinit.net.read_sys_net')
+ @mock.patch('cloudinit.distros.networking.LinuxNetworking.try_set_link_up')
+ def test_wait_for_link_up_writes_to_device_file(
+ self, m_is_link_up, m_read_sys_net, m_writefile):
+ """Waiting for link to be up should return immediately if the link is
+ already up."""
+
+ distro_cls = distros.fetch('ubuntu')
+ distro = distro_cls('ubuntu', {}, self.paths)
+ dsa = dsaz.DataSourceAzure({}, distro=distro, paths=self.paths)
+
+ callcount = 0
+
+ def linkup(key):
+ nonlocal callcount
+ if callcount == 0:
+ callcount += 1
+ return False
+ return True
+
+ m_is_link_up.side_effect = linkup
+
+ dsa.wait_for_link_up("eth0")
+ self.assertEqual(2, m_is_link_up.call_count)
+ self.assertEqual(1, m_read_sys_net.call_count)
+ self.assertEqual(2, m_writefile.call_count)
+
+ @mock.patch('cloudinit.sources.helpers.netlink.'
+ 'create_bound_netlink_socket')
+ def test_wait_for_all_nics_ready_raises_if_socket_fails(self, m_socket):
+ """Waiting for all nics should raise exception if netlink socket
+ creation fails."""
+
+ m_socket.side_effect = netlink.NetlinkCreateSocketError
+ distro_cls = distros.fetch('ubuntu')
+ distro = distro_cls('ubuntu', {}, self.paths)
+ dsa = dsaz.DataSourceAzure({}, distro=distro, paths=self.paths)
+
+ self.assertRaises(netlink.NetlinkCreateSocketError,
+ dsa._wait_for_all_nics_ready)
+ # dsa._wait_for_all_nics_ready()
+
+
@mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
@mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
@mock.patch('cloudinit.sources.helpers.netlink.'
@@ -1963,8 +2680,8 @@ class TestPreprovisioningPollIMDS(CiTestCase):
@mock.patch('time.sleep', mock.MagicMock())
@mock.patch(MOCKPATH + 'EphemeralDHCPv4')
- def test_poll_imds_re_dhcp_on_timeout(self, m_dhcpv4, report_ready_func,
- fake_resp, m_media_switch, m_dhcp,
+ def test_poll_imds_re_dhcp_on_timeout(self, m_dhcpv4, m_report_ready,
+ m_request, m_media_switch, m_dhcp,
m_net):
"""The poll_imds will retry DHCP on IMDS timeout."""
report_file = self.tmp_path('report_marker', self.tmp)
@@ -1993,21 +2710,38 @@ class TestPreprovisioningPollIMDS(CiTestCase):
# Third try should succeed and stop retries or redhcp
return mock.MagicMock(status_code=200, text="good", content="good")
- fake_resp.side_effect = fake_timeout_once
+ m_request.side_effect = fake_timeout_once
- dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ dsa = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths)
with mock.patch(MOCKPATH + 'REPORTED_READY_MARKER_FILE', report_file):
dsa._poll_imds()
- self.assertEqual(report_ready_func.call_count, 1)
- report_ready_func.assert_called_with(lease=lease)
+ self.assertEqual(m_report_ready.call_count, 1)
+ m_report_ready.assert_called_with(lease=lease)
self.assertEqual(3, m_dhcpv4.call_count, 'Expected 3 DHCP calls')
self.assertEqual(4, self.tries, 'Expected 4 total reads from IMDS')
- def test_poll_imds_report_ready_false(self,
- report_ready_func, fake_resp,
- m_media_switch, m_dhcp, m_net):
- """The poll_imds should not call reporting ready
- when flag is false"""
+ @mock.patch('os.path.isfile')
+ def test_poll_imds_skips_dhcp_if_ctx_present(
+ self, m_isfile, report_ready_func, fake_resp, m_media_switch,
+ m_dhcp, m_net):
+ """The poll_imds function should reuse the dhcp ctx if it is already
+ present. This happens when we wait for nic to be hot-attached before
+ polling for reprovisiondata. Note that if this ctx is set when
+ _poll_imds is called, then it is not expected to be waiting for
+ media_disconnect_connect either."""
+ report_file = self.tmp_path('report_marker', self.tmp)
+ m_isfile.return_value = True
+ dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ dsa._ephemeral_dhcp_ctx = "Dummy dhcp ctx"
+ with mock.patch(MOCKPATH + 'REPORTED_READY_MARKER_FILE', report_file):
+ dsa._poll_imds()
+ self.assertEqual(0, m_dhcp.call_count)
+ self.assertEqual(0, m_media_switch.call_count)
+
+ def test_does_not_poll_imds_report_ready_when_marker_file_exists(
+ self, m_report_ready, m_request, m_media_switch, m_dhcp, m_net):
+ """poll_imds should not call report ready when the reported ready
+ marker file exists"""
report_file = self.tmp_path('report_marker', self.tmp)
write_file(report_file, content='dont run report_ready :)')
m_dhcp.return_value = [{
@@ -2015,18 +2749,56 @@ class TestPreprovisioningPollIMDS(CiTestCase):
'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
'unknown-245': '624c3620'}]
m_media_switch.return_value = None
+ dsa = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths)
+ with mock.patch(MOCKPATH + 'REPORTED_READY_MARKER_FILE', report_file):
+ dsa._poll_imds()
+ self.assertEqual(m_report_ready.call_count, 0)
+
+ def test_poll_imds_report_ready_success_writes_marker_file(
+ self, m_report_ready, m_request, m_media_switch, m_dhcp, m_net):
+ """poll_imds should write the report_ready marker file if
+ reporting ready succeeds"""
+ report_file = self.tmp_path('report_marker', self.tmp)
+ m_dhcp.return_value = [{
+ 'interface': 'eth9', 'fixed-address': '192.168.2.9',
+ 'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
+ 'unknown-245': '624c3620'}]
+ m_media_switch.return_value = None
dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ self.assertFalse(os.path.exists(report_file))
with mock.patch(MOCKPATH + 'REPORTED_READY_MARKER_FILE', report_file):
dsa._poll_imds()
- self.assertEqual(report_ready_func.call_count, 0)
+ self.assertEqual(m_report_ready.call_count, 1)
+ self.assertTrue(os.path.exists(report_file))
+ def test_poll_imds_report_ready_failure_raises_exc_and_doesnt_write_marker(
+ self, m_report_ready, m_request, m_media_switch, m_dhcp, m_net):
+ """poll_imds should write the report_ready marker file if
+ reporting ready succeeds"""
+ report_file = self.tmp_path('report_marker', self.tmp)
+ m_dhcp.return_value = [{
+ 'interface': 'eth9', 'fixed-address': '192.168.2.9',
+ 'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
+ 'unknown-245': '624c3620'}]
+ m_media_switch.return_value = None
+ m_report_ready.return_value = False
+ dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ self.assertFalse(os.path.exists(report_file))
+ with mock.patch(MOCKPATH + 'REPORTED_READY_MARKER_FILE', report_file):
+ self.assertRaises(
+ InvalidMetaDataException,
+ dsa._poll_imds)
+ self.assertEqual(m_report_ready.call_count, 1)
+ self.assertFalse(os.path.exists(report_file))
-@mock.patch(MOCKPATH + 'subp.subp')
-@mock.patch(MOCKPATH + 'util.write_file')
+
+@mock.patch(MOCKPATH + 'DataSourceAzure._report_ready', mock.MagicMock())
+@mock.patch(MOCKPATH + 'subp.subp', mock.MagicMock())
+@mock.patch(MOCKPATH + 'util.write_file', mock.MagicMock())
@mock.patch(MOCKPATH + 'util.is_FreeBSD')
@mock.patch('cloudinit.sources.helpers.netlink.'
'wait_for_media_disconnect_connect')
-@mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network')
+@mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network', autospec=True)
@mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery')
@mock.patch('requests.Session.request')
class TestAzureDataSourcePreprovisioning(CiTestCase):
@@ -2038,24 +2810,24 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
self.paths = helpers.Paths({'cloud_dir': tmp})
dsaz.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d
- def test_poll_imds_returns_ovf_env(self, fake_resp,
+ def test_poll_imds_returns_ovf_env(self, m_request,
m_dhcp, m_net,
m_media_switch,
- m_is_bsd, write_f, subp):
+ m_is_bsd):
"""The _poll_imds method should return the ovf_env.xml."""
m_is_bsd.return_value = False
m_media_switch.return_value = None
m_dhcp.return_value = [{
'interface': 'eth9', 'fixed-address': '192.168.2.9',
'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0'}]
- url = 'http://{0}/metadata/reprovisiondata?api-version=2017-04-02'
+ url = 'http://{0}/metadata/reprovisiondata?api-version=2019-06-01'
host = "169.254.169.254"
full_url = url.format(host)
- fake_resp.return_value = mock.MagicMock(status_code=200, text="ovf",
+ m_request.return_value = mock.MagicMock(status_code=200, text="ovf",
content="ovf")
- dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ dsa = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths)
self.assertTrue(len(dsa._poll_imds()) > 0)
- self.assertEqual(fake_resp.call_args_list,
+ self.assertEqual(m_request.call_args_list,
[mock.call(allow_redirects=True,
headers={'Metadata': 'true',
'User-Agent':
@@ -2070,10 +2842,10 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
static_routes=None)
self.assertEqual(m_net.call_count, 2)
- def test__reprovision_calls__poll_imds(self, fake_resp,
+ def test__reprovision_calls__poll_imds(self, m_request,
m_dhcp, m_net,
m_media_switch,
- m_is_bsd, write_f, subp):
+ m_is_bsd):
"""The _reprovision method should call poll IMDS."""
m_is_bsd.return_value = False
m_media_switch.return_value = None
@@ -2081,27 +2853,31 @@ class TestAzureDataSourcePreprovisioning(CiTestCase):
'interface': 'eth9', 'fixed-address': '192.168.2.9',
'routers': '192.168.2.1', 'subnet-mask': '255.255.255.0',
'unknown-245': '624c3620'}]
- url = 'http://{0}/metadata/reprovisiondata?api-version=2017-04-02'
+ url = 'http://{0}/metadata/reprovisiondata?api-version=2019-06-01'
host = "169.254.169.254"
full_url = url.format(host)
hostname = "myhost"
username = "myuser"
odata = {'HostName': hostname, 'UserName': username}
content = construct_valid_ovf_env(data=odata)
- fake_resp.return_value = mock.MagicMock(status_code=200, text=content,
+ m_request.return_value = mock.MagicMock(status_code=200, text=content,
content=content)
- dsa = dsaz.DataSourceAzure({}, distro=None, paths=self.paths)
+ dsa = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths)
md, _ud, cfg, _d = dsa._reprovision()
self.assertEqual(md['local-hostname'], hostname)
self.assertEqual(cfg['system_info']['default_user']['name'], username)
- self.assertEqual(fake_resp.call_args_list,
- [mock.call(allow_redirects=True,
- headers={'Metadata': 'true',
- 'User-Agent':
- 'Cloud-Init/%s' % vs()},
- method='GET',
- timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS,
- url=full_url)])
+ self.assertIn(
+ mock.call(
+ allow_redirects=True,
+ headers={
+ 'Metadata': 'true',
+ 'User-Agent': 'Cloud-Init/%s' % vs()
+ },
+ method='GET',
+ timeout=dsaz.IMDS_TIMEOUT_IN_SECONDS,
+ url=full_url
+ ),
+ m_request.call_args_list)
self.assertEqual(m_dhcp.call_count, 2)
m_net.assert_any_call(
broadcast='192.168.2.255', interface='eth9', ip='192.168.2.9',
@@ -2163,14 +2939,14 @@ class TestWBIsPlatformViable(CiTestCase):
"""White box tests for _is_platform_viable."""
with_logs = True
- @mock.patch(MOCKPATH + 'util.read_dmi_data')
+ @mock.patch(MOCKPATH + 'dmi.read_dmi_data')
def test_true_on_non_azure_chassis(self, m_read_dmi_data):
"""Return True if DMI chassis-asset-tag is AZURE_CHASSIS_ASSET_TAG."""
m_read_dmi_data.return_value = dsaz.AZURE_CHASSIS_ASSET_TAG
self.assertTrue(dsaz._is_platform_viable('doesnotmatter'))
@mock.patch(MOCKPATH + 'os.path.exists')
- @mock.patch(MOCKPATH + 'util.read_dmi_data')
+ @mock.patch(MOCKPATH + 'dmi.read_dmi_data')
def test_true_on_azure_ovf_env_in_seed_dir(self, m_read_dmi_data, m_exist):
"""Return True if ovf-env.xml exists in known seed dirs."""
# Non-matching Azure chassis-asset-tag
@@ -2191,7 +2967,7 @@ class TestWBIsPlatformViable(CiTestCase):
MOCKPATH,
{'os.path.exists': False,
# Non-matching Azure chassis-asset-tag
- 'util.read_dmi_data': dsaz.AZURE_CHASSIS_ASSET_TAG + 'X',
+ 'dmi.read_dmi_data': dsaz.AZURE_CHASSIS_ASSET_TAG + 'X',
'subp.which': None},
dsaz._is_platform_viable, 'doesnotmatter'))
self.assertIn(
diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/test_datasource/test_azure_helper.py
index 5e6d3d2d..b8899807 100644
--- a/tests/unittests/test_datasource/test_azure_helper.py
+++ b/tests/unittests/test_datasource/test_azure_helper.py
@@ -1,10 +1,12 @@
# This file is part of cloud-init. See LICENSE file for license information.
+import copy
import os
import re
import unittest
from textwrap import dedent
from xml.etree import ElementTree
+from xml.sax.saxutils import escape, unescape
from cloudinit.sources.helpers import azure as azure_helper
from cloudinit.tests.helpers import CiTestCase, ExitStack, mock, populate_dir
@@ -70,6 +72,15 @@ HEALTH_REPORT_XML_TEMPLATE = '''\
</Health>
'''
+HEALTH_DETAIL_SUBSECTION_XML_TEMPLATE = dedent('''\
+ <Details>
+ <SubStatus>{health_substatus}</SubStatus>
+ <Description>{health_description}</Description>
+ </Details>
+ ''')
+
+HEALTH_REPORT_DESCRIPTION_TRIM_LEN = 512
+
class SentinelException(Exception):
pass
@@ -281,29 +292,25 @@ class TestAzureEndpointHttpClient(CiTestCase):
super(TestAzureEndpointHttpClient, self).setUp()
patches = ExitStack()
self.addCleanup(patches.close)
-
- self.readurl = patches.enter_context(
- mock.patch.object(azure_helper.url_helper, 'readurl'))
- patches.enter_context(
- mock.patch.object(azure_helper.time, 'sleep', mock.MagicMock()))
+ self.m_http_with_retries = patches.enter_context(
+ mock.patch.object(azure_helper, 'http_with_retries'))
def test_non_secure_get(self):
client = azure_helper.AzureEndpointHttpClient(mock.MagicMock())
url = 'MyTestUrl'
response = client.get(url, secure=False)
- self.assertEqual(1, self.readurl.call_count)
- self.assertEqual(self.readurl.return_value, response)
+ self.assertEqual(1, self.m_http_with_retries.call_count)
+ self.assertEqual(self.m_http_with_retries.return_value, response)
self.assertEqual(
- mock.call(url, headers=self.regular_headers,
- timeout=5, retries=10, sec_between=5),
- self.readurl.call_args)
+ mock.call(url, headers=self.regular_headers),
+ self.m_http_with_retries.call_args)
def test_non_secure_get_raises_exception(self):
client = azure_helper.AzureEndpointHttpClient(mock.MagicMock())
- self.readurl.side_effect = SentinelException
url = 'MyTestUrl'
- with self.assertRaises(SentinelException):
- client.get(url, secure=False)
+ self.m_http_with_retries.side_effect = SentinelException
+ self.assertRaises(SentinelException, client.get, url, secure=False)
+ self.assertEqual(1, self.m_http_with_retries.call_count)
def test_secure_get(self):
url = 'MyTestUrl'
@@ -315,39 +322,37 @@ class TestAzureEndpointHttpClient(CiTestCase):
})
client = azure_helper.AzureEndpointHttpClient(m_certificate)
response = client.get(url, secure=True)
- self.assertEqual(1, self.readurl.call_count)
- self.assertEqual(self.readurl.return_value, response)
+ self.assertEqual(1, self.m_http_with_retries.call_count)
+ self.assertEqual(self.m_http_with_retries.return_value, response)
self.assertEqual(
- mock.call(url, headers=expected_headers,
- timeout=5, retries=10, sec_between=5),
- self.readurl.call_args)
+ mock.call(url, headers=expected_headers),
+ self.m_http_with_retries.call_args)
def test_secure_get_raises_exception(self):
url = 'MyTestUrl'
client = azure_helper.AzureEndpointHttpClient(mock.MagicMock())
- self.readurl.side_effect = SentinelException
- with self.assertRaises(SentinelException):
- client.get(url, secure=True)
+ self.m_http_with_retries.side_effect = SentinelException
+ self.assertRaises(SentinelException, client.get, url, secure=True)
+ self.assertEqual(1, self.m_http_with_retries.call_count)
def test_post(self):
m_data = mock.MagicMock()
url = 'MyTestUrl'
client = azure_helper.AzureEndpointHttpClient(mock.MagicMock())
response = client.post(url, data=m_data)
- self.assertEqual(1, self.readurl.call_count)
- self.assertEqual(self.readurl.return_value, response)
+ self.assertEqual(1, self.m_http_with_retries.call_count)
+ self.assertEqual(self.m_http_with_retries.return_value, response)
self.assertEqual(
- mock.call(url, data=m_data, headers=self.regular_headers,
- timeout=5, retries=10, sec_between=5),
- self.readurl.call_args)
+ mock.call(url, data=m_data, headers=self.regular_headers),
+ self.m_http_with_retries.call_args)
def test_post_raises_exception(self):
m_data = mock.MagicMock()
url = 'MyTestUrl'
client = azure_helper.AzureEndpointHttpClient(mock.MagicMock())
- self.readurl.side_effect = SentinelException
- with self.assertRaises(SentinelException):
- client.post(url, data=m_data)
+ self.m_http_with_retries.side_effect = SentinelException
+ self.assertRaises(SentinelException, client.post, url, data=m_data)
+ self.assertEqual(1, self.m_http_with_retries.call_count)
def test_post_with_extra_headers(self):
url = 'MyTestUrl'
@@ -356,21 +361,179 @@ class TestAzureEndpointHttpClient(CiTestCase):
client.post(url, extra_headers=extra_headers)
expected_headers = self.regular_headers.copy()
expected_headers.update(extra_headers)
- self.assertEqual(1, self.readurl.call_count)
+ self.assertEqual(1, self.m_http_with_retries.call_count)
self.assertEqual(
- mock.call(mock.ANY, data=mock.ANY, headers=expected_headers,
- timeout=5, retries=10, sec_between=5),
- self.readurl.call_args)
+ mock.call(url, data=mock.ANY, headers=expected_headers),
+ self.m_http_with_retries.call_args)
def test_post_with_sleep_with_extra_headers_raises_exception(self):
m_data = mock.MagicMock()
url = 'MyTestUrl'
extra_headers = {'test': 'header'}
client = azure_helper.AzureEndpointHttpClient(mock.MagicMock())
- self.readurl.side_effect = SentinelException
- with self.assertRaises(SentinelException):
- client.post(
- url, data=m_data, extra_headers=extra_headers)
+ self.m_http_with_retries.side_effect = SentinelException
+ self.assertRaises(
+ SentinelException, client.post,
+ url, data=m_data, extra_headers=extra_headers)
+ self.assertEqual(1, self.m_http_with_retries.call_count)
+
+
+class TestAzureHelperHttpWithRetries(CiTestCase):
+
+ with_logs = True
+
+ max_readurl_attempts = 240
+ default_readurl_timeout = 5
+ periodic_logging_attempts = 12
+
+ def setUp(self):
+ super(TestAzureHelperHttpWithRetries, self).setUp()
+ patches = ExitStack()
+ self.addCleanup(patches.close)
+
+ self.m_readurl = patches.enter_context(
+ mock.patch.object(
+ azure_helper.url_helper, 'readurl', mock.MagicMock()))
+ patches.enter_context(
+ mock.patch.object(azure_helper.time, 'sleep', mock.MagicMock()))
+
+ def test_http_with_retries(self):
+ self.m_readurl.return_value = 'TestResp'
+ self.assertEqual(
+ azure_helper.http_with_retries('testurl'),
+ self.m_readurl.return_value)
+ self.assertEqual(self.m_readurl.call_count, 1)
+
+ def test_http_with_retries_propagates_readurl_exc_and_logs_exc(
+ self):
+ self.m_readurl.side_effect = SentinelException
+
+ self.assertRaises(
+ SentinelException, azure_helper.http_with_retries, 'testurl')
+ self.assertEqual(self.m_readurl.call_count, self.max_readurl_attempts)
+
+ self.assertIsNotNone(
+ re.search(
+ r'Failed HTTP request with Azure endpoint \S* during '
+ r'attempt \d+ with exception: \S*',
+ self.logs.getvalue()))
+ self.assertIsNone(
+ re.search(
+ r'Successful HTTP request with Azure endpoint \S* after '
+ r'\d+ attempts',
+ self.logs.getvalue()))
+
+ def test_http_with_retries_delayed_success_due_to_temporary_readurl_exc(
+ self):
+ self.m_readurl.side_effect = \
+ [SentinelException] * self.periodic_logging_attempts + \
+ ['TestResp']
+ self.m_readurl.return_value = 'TestResp'
+
+ response = azure_helper.http_with_retries('testurl')
+ self.assertEqual(
+ response,
+ self.m_readurl.return_value)
+ self.assertEqual(
+ self.m_readurl.call_count,
+ self.periodic_logging_attempts + 1)
+
+ def test_http_with_retries_long_delay_logs_periodic_failure_msg(self):
+ self.m_readurl.side_effect = \
+ [SentinelException] * self.periodic_logging_attempts + \
+ ['TestResp']
+ self.m_readurl.return_value = 'TestResp'
+
+ azure_helper.http_with_retries('testurl')
+
+ self.assertEqual(
+ self.m_readurl.call_count,
+ self.periodic_logging_attempts + 1)
+ self.assertIsNotNone(
+ re.search(
+ r'Failed HTTP request with Azure endpoint \S* during '
+ r'attempt \d+ with exception: \S*',
+ self.logs.getvalue()))
+ self.assertIsNotNone(
+ re.search(
+ r'Successful HTTP request with Azure endpoint \S* after '
+ r'\d+ attempts',
+ self.logs.getvalue()))
+
+ def test_http_with_retries_short_delay_does_not_log_periodic_failure_msg(
+ self):
+ self.m_readurl.side_effect = \
+ [SentinelException] * \
+ (self.periodic_logging_attempts - 1) + \
+ ['TestResp']
+ self.m_readurl.return_value = 'TestResp'
+
+ azure_helper.http_with_retries('testurl')
+ self.assertEqual(
+ self.m_readurl.call_count,
+ self.periodic_logging_attempts)
+
+ self.assertIsNone(
+ re.search(
+ r'Failed HTTP request with Azure endpoint \S* during '
+ r'attempt \d+ with exception: \S*',
+ self.logs.getvalue()))
+ self.assertIsNotNone(
+ re.search(
+ r'Successful HTTP request with Azure endpoint \S* after '
+ r'\d+ attempts',
+ self.logs.getvalue()))
+
+ def test_http_with_retries_calls_url_helper_readurl_with_args_kwargs(self):
+ testurl = mock.MagicMock()
+ kwargs = {
+ 'headers': mock.MagicMock(),
+ 'data': mock.MagicMock(),
+ # timeout kwarg should not be modified or deleted if present
+ 'timeout': mock.MagicMock()
+ }
+ azure_helper.http_with_retries(testurl, **kwargs)
+ self.m_readurl.assert_called_once_with(testurl, **kwargs)
+
+ def test_http_with_retries_adds_timeout_kwarg_if_not_present(self):
+ testurl = mock.MagicMock()
+ kwargs = {
+ 'headers': mock.MagicMock(),
+ 'data': mock.MagicMock()
+ }
+ expected_kwargs = copy.deepcopy(kwargs)
+ expected_kwargs['timeout'] = self.default_readurl_timeout
+
+ azure_helper.http_with_retries(testurl, **kwargs)
+ self.m_readurl.assert_called_once_with(testurl, **expected_kwargs)
+
+ def test_http_with_retries_deletes_retries_kwargs_passed_in(
+ self):
+ """http_with_retries already implements retry logic,
+ so url_helper.readurl should not have retries.
+ http_with_retries should delete kwargs that
+ cause url_helper.readurl to retry.
+ """
+ testurl = mock.MagicMock()
+ kwargs = {
+ 'headers': mock.MagicMock(),
+ 'data': mock.MagicMock(),
+ 'timeout': mock.MagicMock(),
+ 'retries': mock.MagicMock(),
+ 'infinite': mock.MagicMock()
+ }
+ expected_kwargs = copy.deepcopy(kwargs)
+ expected_kwargs.pop('retries', None)
+ expected_kwargs.pop('infinite', None)
+
+ azure_helper.http_with_retries(testurl, **kwargs)
+ self.m_readurl.assert_called_once_with(testurl, **expected_kwargs)
+ self.assertIn(
+ 'retries kwarg passed in for communication with Azure endpoint.',
+ self.logs.getvalue())
+ self.assertIn(
+ 'infinite kwarg passed in for communication with Azure endpoint.',
+ self.logs.getvalue())
class TestOpenSSLManager(CiTestCase):
@@ -461,17 +624,24 @@ class TestOpenSSLManagerActions(CiTestCase):
class TestGoalStateHealthReporter(CiTestCase):
+ maxDiff = None
+
default_parameters = {
'incarnation': 1634,
'container_id': 'MyContainerId',
'instance_id': 'MyInstanceId'
}
- test_endpoint = 'TestEndpoint'
- test_url = 'http://{0}/machine?comp=health'.format(test_endpoint)
+ test_azure_endpoint = 'TestEndpoint'
+ test_health_report_url = 'http://{0}/machine?comp=health'.format(
+ test_azure_endpoint)
test_default_headers = {'Content-Type': 'text/xml; charset=utf-8'}
provisioning_success_status = 'Ready'
+ provisioning_not_ready_status = 'NotReady'
+ provisioning_failure_substatus = 'ProvisioningFailed'
+ provisioning_failure_err_description = (
+ 'Test error message containing provisioning failure details')
def setUp(self):
super(TestGoalStateHealthReporter, self).setUp()
@@ -496,17 +666,40 @@ class TestGoalStateHealthReporter(CiTestCase):
self.GoalState.return_value.incarnation = \
self.default_parameters['incarnation']
+ def _text_from_xpath_in_xroot(self, xroot, xpath):
+ element = xroot.find(xpath)
+ if element is not None:
+ return element.text
+ return None
+
def _get_formatted_health_report_xml_string(self, **kwargs):
return HEALTH_REPORT_XML_TEMPLATE.format(**kwargs)
+ def _get_formatted_health_detail_subsection_xml_string(self, **kwargs):
+ return HEALTH_DETAIL_SUBSECTION_XML_TEMPLATE.format(**kwargs)
+
def _get_report_ready_health_document(self):
return self._get_formatted_health_report_xml_string(
- incarnation=self.default_parameters['incarnation'],
- container_id=self.default_parameters['container_id'],
- instance_id=self.default_parameters['instance_id'],
- health_status=self.provisioning_success_status,
+ incarnation=escape(str(self.default_parameters['incarnation'])),
+ container_id=escape(self.default_parameters['container_id']),
+ instance_id=escape(self.default_parameters['instance_id']),
+ health_status=escape(self.provisioning_success_status),
health_detail_subsection='')
+ def _get_report_failure_health_document(self):
+ health_detail_subsection = \
+ self._get_formatted_health_detail_subsection_xml_string(
+ health_substatus=escape(self.provisioning_failure_substatus),
+ health_description=escape(
+ self.provisioning_failure_err_description))
+
+ return self._get_formatted_health_report_xml_string(
+ incarnation=escape(str(self.default_parameters['incarnation'])),
+ container_id=escape(self.default_parameters['container_id']),
+ instance_id=escape(self.default_parameters['instance_id']),
+ health_status=escape(self.provisioning_not_ready_status),
+ health_detail_subsection=health_detail_subsection)
+
def test_send_ready_signal_sends_post_request(self):
with mock.patch.object(
azure_helper.GoalStateHealthReporter,
@@ -514,55 +707,130 @@ class TestGoalStateHealthReporter(CiTestCase):
client = azure_helper.AzureEndpointHttpClient(mock.MagicMock())
reporter = azure_helper.GoalStateHealthReporter(
azure_helper.GoalState(mock.MagicMock(), mock.MagicMock()),
- client, self.test_endpoint)
+ client, self.test_azure_endpoint)
reporter.send_ready_signal()
self.assertEqual(1, self.post.call_count)
self.assertEqual(
mock.call(
- self.test_url,
+ self.test_health_report_url,
data=m_build_report.return_value,
extra_headers=self.test_default_headers),
self.post.call_args)
- def test_build_report_for_health_document(self):
+ def test_send_failure_signal_sends_post_request(self):
+ with mock.patch.object(
+ azure_helper.GoalStateHealthReporter,
+ 'build_report') as m_build_report:
+ client = azure_helper.AzureEndpointHttpClient(mock.MagicMock())
+ reporter = azure_helper.GoalStateHealthReporter(
+ azure_helper.GoalState(mock.MagicMock(), mock.MagicMock()),
+ client, self.test_azure_endpoint)
+ reporter.send_failure_signal(
+ description=self.provisioning_failure_err_description)
+
+ self.assertEqual(1, self.post.call_count)
+ self.assertEqual(
+ mock.call(
+ self.test_health_report_url,
+ data=m_build_report.return_value,
+ extra_headers=self.test_default_headers),
+ self.post.call_args)
+
+ def test_build_report_for_ready_signal_health_document(self):
health_document = self._get_report_ready_health_document()
reporter = azure_helper.GoalStateHealthReporter(
azure_helper.GoalState(mock.MagicMock(), mock.MagicMock()),
azure_helper.AzureEndpointHttpClient(mock.MagicMock()),
- self.test_endpoint)
+ self.test_azure_endpoint)
generated_health_document = reporter.build_report(
incarnation=self.default_parameters['incarnation'],
container_id=self.default_parameters['container_id'],
instance_id=self.default_parameters['instance_id'],
status=self.provisioning_success_status)
+
self.assertEqual(health_document, generated_health_document)
- self.assertIn(
- '<GoalStateIncarnation>{}</GoalStateIncarnation>'.format(
- str(self.default_parameters['incarnation'])),
- generated_health_document)
- self.assertIn(
- ''.join([
- '<ContainerId>',
- self.default_parameters['container_id'],
- '</ContainerId>']),
- generated_health_document)
- self.assertIn(
- ''.join([
- '<InstanceId>',
- self.default_parameters['instance_id'],
- '</InstanceId>']),
- generated_health_document)
- self.assertIn(
- ''.join([
- '<State>',
- self.provisioning_success_status,
- '</State>']),
- generated_health_document
+
+ generated_xroot = ElementTree.fromstring(generated_health_document)
+ self.assertEqual(
+ self._text_from_xpath_in_xroot(
+ generated_xroot, './GoalStateIncarnation'),
+ str(self.default_parameters['incarnation']))
+ self.assertEqual(
+ self._text_from_xpath_in_xroot(
+ generated_xroot, './Container/ContainerId'),
+ str(self.default_parameters['container_id']))
+ self.assertEqual(
+ self._text_from_xpath_in_xroot(
+ generated_xroot,
+ './Container/RoleInstanceList/Role/InstanceId'),
+ str(self.default_parameters['instance_id']))
+ self.assertEqual(
+ self._text_from_xpath_in_xroot(
+ generated_xroot,
+ './Container/RoleInstanceList/Role/Health/State'),
+ escape(self.provisioning_success_status))
+ self.assertIsNone(
+ self._text_from_xpath_in_xroot(
+ generated_xroot,
+ './Container/RoleInstanceList/Role/Health/Details'))
+ self.assertIsNone(
+ self._text_from_xpath_in_xroot(
+ generated_xroot,
+ './Container/RoleInstanceList/Role/Health/Details/SubStatus'))
+ self.assertIsNone(
+ self._text_from_xpath_in_xroot(
+ generated_xroot,
+ './Container/RoleInstanceList/Role/Health/Details/Description')
)
- self.assertNotIn('<Details>', generated_health_document)
- self.assertNotIn('<SubStatus>', generated_health_document)
- self.assertNotIn('<Description>', generated_health_document)
+
+ def test_build_report_for_failure_signal_health_document(self):
+ health_document = self._get_report_failure_health_document()
+ reporter = azure_helper.GoalStateHealthReporter(
+ azure_helper.GoalState(mock.MagicMock(), mock.MagicMock()),
+ azure_helper.AzureEndpointHttpClient(mock.MagicMock()),
+ self.test_azure_endpoint)
+ generated_health_document = reporter.build_report(
+ incarnation=self.default_parameters['incarnation'],
+ container_id=self.default_parameters['container_id'],
+ instance_id=self.default_parameters['instance_id'],
+ status=self.provisioning_not_ready_status,
+ substatus=self.provisioning_failure_substatus,
+ description=self.provisioning_failure_err_description)
+
+ self.assertEqual(health_document, generated_health_document)
+
+ generated_xroot = ElementTree.fromstring(generated_health_document)
+ self.assertEqual(
+ self._text_from_xpath_in_xroot(
+ generated_xroot, './GoalStateIncarnation'),
+ str(self.default_parameters['incarnation']))
+ self.assertEqual(
+ self._text_from_xpath_in_xroot(
+ generated_xroot, './Container/ContainerId'),
+ self.default_parameters['container_id'])
+ self.assertEqual(
+ self._text_from_xpath_in_xroot(
+ generated_xroot,
+ './Container/RoleInstanceList/Role/InstanceId'),
+ self.default_parameters['instance_id'])
+ self.assertEqual(
+ self._text_from_xpath_in_xroot(
+ generated_xroot,
+ './Container/RoleInstanceList/Role/Health/State'),
+ escape(self.provisioning_not_ready_status))
+ self.assertEqual(
+ self._text_from_xpath_in_xroot(
+ generated_xroot,
+ './Container/RoleInstanceList/Role/Health/Details/'
+ 'SubStatus'),
+ escape(self.provisioning_failure_substatus))
+ self.assertEqual(
+ self._text_from_xpath_in_xroot(
+ generated_xroot,
+ './Container/RoleInstanceList/Role/Health/Details/'
+ 'Description'),
+ escape(self.provisioning_failure_err_description))
def test_send_ready_signal_calls_build_report(self):
with mock.patch.object(
@@ -571,7 +839,7 @@ class TestGoalStateHealthReporter(CiTestCase):
reporter = azure_helper.GoalStateHealthReporter(
azure_helper.GoalState(mock.MagicMock(), mock.MagicMock()),
azure_helper.AzureEndpointHttpClient(mock.MagicMock()),
- self.test_endpoint)
+ self.test_azure_endpoint)
reporter.send_ready_signal()
self.assertEqual(1, m_build_report.call_count)
@@ -583,6 +851,131 @@ class TestGoalStateHealthReporter(CiTestCase):
status=self.provisioning_success_status),
m_build_report.call_args)
+ def test_send_failure_signal_calls_build_report(self):
+ with mock.patch.object(
+ azure_helper.GoalStateHealthReporter, 'build_report'
+ ) as m_build_report:
+ reporter = azure_helper.GoalStateHealthReporter(
+ azure_helper.GoalState(mock.MagicMock(), mock.MagicMock()),
+ azure_helper.AzureEndpointHttpClient(mock.MagicMock()),
+ self.test_azure_endpoint)
+ reporter.send_failure_signal(
+ description=self.provisioning_failure_err_description)
+
+ self.assertEqual(1, m_build_report.call_count)
+ self.assertEqual(
+ mock.call(
+ incarnation=self.default_parameters['incarnation'],
+ container_id=self.default_parameters['container_id'],
+ instance_id=self.default_parameters['instance_id'],
+ status=self.provisioning_not_ready_status,
+ substatus=self.provisioning_failure_substatus,
+ description=self.provisioning_failure_err_description),
+ m_build_report.call_args)
+
+ def test_build_report_escapes_chars(self):
+ incarnation = 'jd8\'9*&^<\'A><A[p&o+\"SD()*&&&LKAJSD23'
+ container_id = '&&<\"><><ds8\'9+7&d9a86!@($09asdl;<>'
+ instance_id = 'Opo>>>jas\'&d;[p&fp\"a<<!!@&&'
+ health_status = '&<897\"6&>&aa\'sd!@&!)((*<&>'
+ health_substatus = '&as\"d<<a&s>d<\'^@!5&6<7'
+ health_description = '&&&>!#$\"&&<as\'1!@$d&>><>&\"sd<67<]>>'
+
+ health_detail_subsection = \
+ self._get_formatted_health_detail_subsection_xml_string(
+ health_substatus=escape(health_substatus),
+ health_description=escape(health_description))
+ health_document = self._get_formatted_health_report_xml_string(
+ incarnation=escape(incarnation),
+ container_id=escape(container_id),
+ instance_id=escape(instance_id),
+ health_status=escape(health_status),
+ health_detail_subsection=health_detail_subsection)
+
+ reporter = azure_helper.GoalStateHealthReporter(
+ azure_helper.GoalState(mock.MagicMock(), mock.MagicMock()),
+ azure_helper.AzureEndpointHttpClient(mock.MagicMock()),
+ self.test_azure_endpoint)
+ generated_health_document = reporter.build_report(
+ incarnation=incarnation,
+ container_id=container_id,
+ instance_id=instance_id,
+ status=health_status,
+ substatus=health_substatus,
+ description=health_description)
+
+ self.assertEqual(health_document, generated_health_document)
+
+ def test_build_report_conforms_to_length_limits(self):
+ reporter = azure_helper.GoalStateHealthReporter(
+ azure_helper.GoalState(mock.MagicMock(), mock.MagicMock()),
+ azure_helper.AzureEndpointHttpClient(mock.MagicMock()),
+ self.test_azure_endpoint)
+ long_err_msg = 'a9&ea8>>>e as1< d\"q2*&(^%\'a=5<' * 100
+ generated_health_document = reporter.build_report(
+ incarnation=self.default_parameters['incarnation'],
+ container_id=self.default_parameters['container_id'],
+ instance_id=self.default_parameters['instance_id'],
+ status=self.provisioning_not_ready_status,
+ substatus=self.provisioning_failure_substatus,
+ description=long_err_msg)
+
+ generated_xroot = ElementTree.fromstring(generated_health_document)
+ generated_health_report_description = self._text_from_xpath_in_xroot(
+ generated_xroot,
+ './Container/RoleInstanceList/Role/Health/Details/Description')
+ self.assertEqual(
+ len(unescape(generated_health_report_description)),
+ HEALTH_REPORT_DESCRIPTION_TRIM_LEN)
+
+ def test_trim_description_then_escape_conforms_to_len_limits_worst_case(
+ self):
+ """When unescaped characters are XML-escaped, the length increases.
+ Char Escape String
+ < &lt;
+ > &gt;
+ " &quot;
+ ' &apos;
+ & &amp;
+
+ We (step 1) trim the health report XML's description field,
+ and then (step 2) XML-escape the health report XML's description field.
+
+ The health report XML's description field limit within cloud-init
+ is HEALTH_REPORT_DESCRIPTION_TRIM_LEN.
+
+ The Azure platform's limit on the health report XML's description field
+ is 4096 chars.
+
+ For worst-case chars, there is a 5x blowup in length
+ when the chars are XML-escaped.
+ ' and " when XML-escaped have a 5x blowup.
+
+ Ensure that (1) trimming and then (2) XML-escaping does not blow past
+ the Azure platform's limit for health report XML's description field
+ (4096 chars).
+ """
+ reporter = azure_helper.GoalStateHealthReporter(
+ azure_helper.GoalState(mock.MagicMock(), mock.MagicMock()),
+ azure_helper.AzureEndpointHttpClient(mock.MagicMock()),
+ self.test_azure_endpoint)
+ long_err_msg = '\'\"' * 10000
+ generated_health_document = reporter.build_report(
+ incarnation=self.default_parameters['incarnation'],
+ container_id=self.default_parameters['container_id'],
+ instance_id=self.default_parameters['instance_id'],
+ status=self.provisioning_not_ready_status,
+ substatus=self.provisioning_failure_substatus,
+ description=long_err_msg)
+
+ generated_xroot = ElementTree.fromstring(generated_health_document)
+ generated_health_report_description = self._text_from_xpath_in_xroot(
+ generated_xroot,
+ './Container/RoleInstanceList/Role/Health/Details/Description')
+ # The escaped description string should be less than
+ # the Azure platform limit for the escaped description string.
+ self.assertLessEqual(len(generated_health_report_description), 4096)
+
class TestWALinuxAgentShim(CiTestCase):
@@ -598,7 +991,7 @@ class TestWALinuxAgentShim(CiTestCase):
self.GoalState = patches.enter_context(
mock.patch.object(azure_helper, 'GoalState'))
self.OpenSSLManager = patches.enter_context(
- mock.patch.object(azure_helper, 'OpenSSLManager'))
+ mock.patch.object(azure_helper, 'OpenSSLManager', autospec=True))
patches.enter_context(
mock.patch.object(azure_helper.time, 'sleep', mock.MagicMock()))
@@ -609,24 +1002,50 @@ class TestWALinuxAgentShim(CiTestCase):
self.GoalState.return_value.container_id = self.test_container_id
self.GoalState.return_value.instance_id = self.test_instance_id
- def test_azure_endpoint_client_uses_certificate_during_report_ready(self):
+ def test_http_client_does_not_use_certificate_for_report_ready(self):
shim = wa_shim()
shim.register_with_azure_and_fetch_data()
self.assertEqual(
- [mock.call(self.OpenSSLManager.return_value.certificate)],
+ [mock.call(None)],
+ self.AzureEndpointHttpClient.call_args_list)
+
+ def test_http_client_does_not_use_certificate_for_report_failure(self):
+ shim = wa_shim()
+ shim.register_with_azure_and_report_failure(description='TestDesc')
+ self.assertEqual(
+ [mock.call(None)],
self.AzureEndpointHttpClient.call_args_list)
def test_correct_url_used_for_goalstate_during_report_ready(self):
self.find_endpoint.return_value = 'test_endpoint'
shim = wa_shim()
shim.register_with_azure_and_fetch_data()
- get = self.AzureEndpointHttpClient.return_value.get
+ m_get = self.AzureEndpointHttpClient.return_value.get
+ self.assertEqual(
+ [mock.call('http://test_endpoint/machine/?comp=goalstate')],
+ m_get.call_args_list)
+ self.assertEqual(
+ [mock.call(
+ m_get.return_value.contents,
+ self.AzureEndpointHttpClient.return_value,
+ False
+ )],
+ self.GoalState.call_args_list)
+
+ def test_correct_url_used_for_goalstate_during_report_failure(self):
+ self.find_endpoint.return_value = 'test_endpoint'
+ shim = wa_shim()
+ shim.register_with_azure_and_report_failure(description='TestDesc')
+ m_get = self.AzureEndpointHttpClient.return_value.get
self.assertEqual(
[mock.call('http://test_endpoint/machine/?comp=goalstate')],
- get.call_args_list)
+ m_get.call_args_list)
self.assertEqual(
- [mock.call(get.return_value.contents,
- self.AzureEndpointHttpClient.return_value)],
+ [mock.call(
+ m_get.return_value.contents,
+ self.AzureEndpointHttpClient.return_value,
+ False
+ )],
self.GoalState.call_args_list)
def test_certificates_used_to_determine_public_keys(self):
@@ -667,6 +1086,16 @@ class TestWALinuxAgentShim(CiTestCase):
self.AzureEndpointHttpClient.return_value.post
.call_args_list)
+ def test_correct_url_used_for_report_failure(self):
+ self.find_endpoint.return_value = 'test_endpoint'
+ shim = wa_shim()
+ shim.register_with_azure_and_report_failure(description='TestDesc')
+ expected_url = 'http://test_endpoint/machine?comp=health'
+ self.assertEqual(
+ [mock.call(expected_url, data=mock.ANY, extra_headers=mock.ANY)],
+ self.AzureEndpointHttpClient.return_value.post
+ .call_args_list)
+
def test_goal_state_values_used_for_report_ready(self):
shim = wa_shim()
shim.register_with_azure_and_fetch_data()
@@ -678,44 +1107,128 @@ class TestWALinuxAgentShim(CiTestCase):
self.assertIn(self.test_container_id, posted_document)
self.assertIn(self.test_instance_id, posted_document)
- def test_xml_elems_in_report_ready(self):
+ def test_goal_state_values_used_for_report_failure(self):
+ shim = wa_shim()
+ shim.register_with_azure_and_report_failure(description='TestDesc')
+ posted_document = (
+ self.AzureEndpointHttpClient.return_value.post
+ .call_args[1]['data']
+ )
+ self.assertIn(self.test_incarnation, posted_document)
+ self.assertIn(self.test_container_id, posted_document)
+ self.assertIn(self.test_instance_id, posted_document)
+
+ def test_xml_elems_in_report_ready_post(self):
shim = wa_shim()
shim.register_with_azure_and_fetch_data()
health_document = HEALTH_REPORT_XML_TEMPLATE.format(
- incarnation=self.test_incarnation,
- container_id=self.test_container_id,
- instance_id=self.test_instance_id,
- health_status='Ready',
+ incarnation=escape(self.test_incarnation),
+ container_id=escape(self.test_container_id),
+ instance_id=escape(self.test_instance_id),
+ health_status=escape('Ready'),
health_detail_subsection='')
posted_document = (
self.AzureEndpointHttpClient.return_value.post
.call_args[1]['data'])
self.assertEqual(health_document, posted_document)
+ def test_xml_elems_in_report_failure_post(self):
+ shim = wa_shim()
+ shim.register_with_azure_and_report_failure(description='TestDesc')
+ health_document = HEALTH_REPORT_XML_TEMPLATE.format(
+ incarnation=escape(self.test_incarnation),
+ container_id=escape(self.test_container_id),
+ instance_id=escape(self.test_instance_id),
+ health_status=escape('NotReady'),
+ health_detail_subsection=HEALTH_DETAIL_SUBSECTION_XML_TEMPLATE
+ .format(
+ health_substatus=escape('ProvisioningFailed'),
+ health_description=escape('TestDesc')))
+ posted_document = (
+ self.AzureEndpointHttpClient.return_value.post
+ .call_args[1]['data'])
+ self.assertEqual(health_document, posted_document)
+
+ @mock.patch.object(azure_helper, 'GoalStateHealthReporter', autospec=True)
+ def test_register_with_azure_and_fetch_data_calls_send_ready_signal(
+ self, m_goal_state_health_reporter):
+ shim = wa_shim()
+ shim.register_with_azure_and_fetch_data()
+ self.assertEqual(
+ 1,
+ m_goal_state_health_reporter.return_value.send_ready_signal
+ .call_count)
+
+ @mock.patch.object(azure_helper, 'GoalStateHealthReporter', autospec=True)
+ def test_register_with_azure_and_report_failure_calls_send_failure_signal(
+ self, m_goal_state_health_reporter):
+ shim = wa_shim()
+ shim.register_with_azure_and_report_failure(description='TestDesc')
+ m_goal_state_health_reporter.return_value.send_failure_signal \
+ .assert_called_once_with(description='TestDesc')
+
+ def test_register_with_azure_and_report_failure_does_not_need_certificates(
+ self):
+ shim = wa_shim()
+ with mock.patch.object(
+ shim, '_fetch_goal_state_from_azure', autospec=True
+ ) as m_fetch_goal_state_from_azure:
+ shim.register_with_azure_and_report_failure(description='TestDesc')
+ m_fetch_goal_state_from_azure.assert_called_once_with(
+ need_certificate=False)
+
def test_clean_up_can_be_called_at_any_time(self):
shim = wa_shim()
shim.clean_up()
+ def test_openssl_manager_not_instantiated_by_shim_report_status(self):
+ shim = wa_shim()
+ shim.register_with_azure_and_fetch_data()
+ shim.register_with_azure_and_report_failure(description='TestDesc')
+ shim.clean_up()
+ self.OpenSSLManager.assert_not_called()
+
def test_clean_up_after_report_ready(self):
shim = wa_shim()
shim.register_with_azure_and_fetch_data()
shim.clean_up()
- self.assertEqual(
- 1, self.OpenSSLManager.return_value.clean_up.call_count)
+ self.OpenSSLManager.return_value.clean_up.assert_not_called()
+
+ def test_clean_up_after_report_failure(self):
+ shim = wa_shim()
+ shim.register_with_azure_and_report_failure(description='TestDesc')
+ shim.clean_up()
+ self.OpenSSLManager.return_value.clean_up.assert_not_called()
def test_fetch_goalstate_during_report_ready_raises_exc_on_get_exc(self):
self.AzureEndpointHttpClient.return_value.get \
- .side_effect = (SentinelException)
+ .side_effect = SentinelException
shim = wa_shim()
self.assertRaises(SentinelException,
shim.register_with_azure_and_fetch_data)
+ def test_fetch_goalstate_during_report_failure_raises_exc_on_get_exc(self):
+ self.AzureEndpointHttpClient.return_value.get \
+ .side_effect = SentinelException
+ shim = wa_shim()
+ self.assertRaises(SentinelException,
+ shim.register_with_azure_and_report_failure,
+ description='TestDesc')
+
def test_fetch_goalstate_during_report_ready_raises_exc_on_parse_exc(self):
self.GoalState.side_effect = SentinelException
shim = wa_shim()
self.assertRaises(SentinelException,
shim.register_with_azure_and_fetch_data)
+ def test_fetch_goalstate_during_report_failure_raises_exc_on_parse_exc(
+ self):
+ self.GoalState.side_effect = SentinelException
+ shim = wa_shim()
+ self.assertRaises(SentinelException,
+ shim.register_with_azure_and_report_failure,
+ description='TestDesc')
+
def test_failure_to_send_report_ready_health_doc_bubbles_up(self):
self.AzureEndpointHttpClient.return_value.post \
.side_effect = SentinelException
@@ -723,55 +1236,132 @@ class TestWALinuxAgentShim(CiTestCase):
self.assertRaises(SentinelException,
shim.register_with_azure_and_fetch_data)
+ def test_failure_to_send_report_failure_health_doc_bubbles_up(self):
+ self.AzureEndpointHttpClient.return_value.post \
+ .side_effect = SentinelException
+ shim = wa_shim()
+ self.assertRaises(SentinelException,
+ shim.register_with_azure_and_report_failure,
+ description='TestDesc')
+
class TestGetMetadataGoalStateXMLAndReportReadyToFabric(CiTestCase):
- @mock.patch.object(azure_helper, 'WALinuxAgentShim')
- def test_data_from_shim_returned(self, shim):
+ def setUp(self):
+ super(TestGetMetadataGoalStateXMLAndReportReadyToFabric, self).setUp()
+ patches = ExitStack()
+ self.addCleanup(patches.close)
+
+ self.m_shim = patches.enter_context(
+ mock.patch.object(azure_helper, 'WALinuxAgentShim'))
+
+ def test_data_from_shim_returned(self):
ret = azure_helper.get_metadata_from_fabric()
self.assertEqual(
- shim.return_value.register_with_azure_and_fetch_data.return_value,
+ self.m_shim.return_value.register_with_azure_and_fetch_data
+ .return_value,
ret)
- @mock.patch.object(azure_helper, 'WALinuxAgentShim')
- def test_success_calls_clean_up(self, shim):
+ def test_success_calls_clean_up(self):
azure_helper.get_metadata_from_fabric()
- self.assertEqual(1, shim.return_value.clean_up.call_count)
+ self.assertEqual(1, self.m_shim.return_value.clean_up.call_count)
- @mock.patch.object(azure_helper, 'WALinuxAgentShim')
- def test_failure_in_registration_calls_clean_up(self, shim):
- shim.return_value.register_with_azure_and_fetch_data.side_effect = (
- SentinelException)
+ def test_failure_in_registration_propagates_exc_and_calls_clean_up(
+ self):
+ self.m_shim.return_value.register_with_azure_and_fetch_data \
+ .side_effect = SentinelException
self.assertRaises(SentinelException,
azure_helper.get_metadata_from_fabric)
- self.assertEqual(1, shim.return_value.clean_up.call_count)
+ self.assertEqual(1, self.m_shim.return_value.clean_up.call_count)
- @mock.patch.object(azure_helper, 'WALinuxAgentShim')
- def test_calls_shim_register_with_azure_and_fetch_data(self, shim):
+ def test_calls_shim_register_with_azure_and_fetch_data(self):
m_pubkey_info = mock.MagicMock()
azure_helper.get_metadata_from_fabric(pubkey_info=m_pubkey_info)
self.assertEqual(
1,
- shim.return_value
+ self.m_shim.return_value
.register_with_azure_and_fetch_data.call_count)
self.assertEqual(
mock.call(pubkey_info=m_pubkey_info),
- shim.return_value
+ self.m_shim.return_value
.register_with_azure_and_fetch_data.call_args)
- @mock.patch.object(azure_helper, 'WALinuxAgentShim')
- def test_instantiates_shim_with_kwargs(self, shim):
+ def test_instantiates_shim_with_kwargs(self):
m_fallback_lease_file = mock.MagicMock()
m_dhcp_options = mock.MagicMock()
azure_helper.get_metadata_from_fabric(
fallback_lease_file=m_fallback_lease_file,
dhcp_opts=m_dhcp_options)
- self.assertEqual(1, shim.call_count)
+ self.assertEqual(1, self.m_shim.call_count)
self.assertEqual(
mock.call(
fallback_lease_file=m_fallback_lease_file,
dhcp_options=m_dhcp_options),
- shim.call_args)
+ self.m_shim.call_args)
+
+
+class TestGetMetadataGoalStateXMLAndReportFailureToFabric(CiTestCase):
+
+ def setUp(self):
+ super(
+ TestGetMetadataGoalStateXMLAndReportFailureToFabric, self).setUp()
+ patches = ExitStack()
+ self.addCleanup(patches.close)
+
+ self.m_shim = patches.enter_context(
+ mock.patch.object(azure_helper, 'WALinuxAgentShim'))
+
+ def test_success_calls_clean_up(self):
+ azure_helper.report_failure_to_fabric()
+ self.assertEqual(
+ 1,
+ self.m_shim.return_value.clean_up.call_count)
+
+ def test_failure_in_shim_report_failure_propagates_exc_and_calls_clean_up(
+ self):
+ self.m_shim.return_value.register_with_azure_and_report_failure \
+ .side_effect = SentinelException
+ self.assertRaises(SentinelException,
+ azure_helper.report_failure_to_fabric)
+ self.assertEqual(
+ 1,
+ self.m_shim.return_value.clean_up.call_count)
+
+ def test_report_failure_to_fabric_with_desc_calls_shim_report_failure(
+ self):
+ azure_helper.report_failure_to_fabric(description='TestDesc')
+ self.m_shim.return_value.register_with_azure_and_report_failure \
+ .assert_called_once_with(description='TestDesc')
+
+ def test_report_failure_to_fabric_with_no_desc_calls_shim_report_failure(
+ self):
+ azure_helper.report_failure_to_fabric()
+ # default err message description should be shown to the user
+ # if no description is passed in
+ self.m_shim.return_value.register_with_azure_and_report_failure \
+ .assert_called_once_with(
+ description=azure_helper
+ .DEFAULT_REPORT_FAILURE_USER_VISIBLE_MESSAGE)
+
+ def test_report_failure_to_fabric_empty_desc_calls_shim_report_failure(
+ self):
+ azure_helper.report_failure_to_fabric(description='')
+ # default err message description should be shown to the user
+ # if an empty description is passed in
+ self.m_shim.return_value.register_with_azure_and_report_failure \
+ .assert_called_once_with(
+ description=azure_helper
+ .DEFAULT_REPORT_FAILURE_USER_VISIBLE_MESSAGE)
+
+ def test_instantiates_shim_with_kwargs(self):
+ m_fallback_lease_file = mock.MagicMock()
+ m_dhcp_options = mock.MagicMock()
+ azure_helper.report_failure_to_fabric(
+ fallback_lease_file=m_fallback_lease_file,
+ dhcp_opts=m_dhcp_options)
+ self.m_shim.assert_called_once_with(
+ fallback_lease_file=m_fallback_lease_file,
+ dhcp_options=m_dhcp_options)
class TestExtractIpAddressFromNetworkd(CiTestCase):
diff --git a/tests/unittests/test_datasource/test_hetzner.py b/tests/unittests/test_datasource/test_hetzner.py
index d0879545..eadb92f1 100644
--- a/tests/unittests/test_datasource/test_hetzner.py
+++ b/tests/unittests/test_datasource/test_hetzner.py
@@ -77,10 +77,11 @@ class TestDataSourceHetzner(CiTestCase):
@mock.patch('cloudinit.net.find_fallback_nic')
@mock.patch('cloudinit.sources.helpers.hetzner.read_metadata')
@mock.patch('cloudinit.sources.helpers.hetzner.read_userdata')
- @mock.patch('cloudinit.sources.DataSourceHetzner.on_hetzner')
- def test_read_data(self, m_on_hetzner, m_usermd, m_readmd, m_fallback_nic,
- m_net):
- m_on_hetzner.return_value = True
+ @mock.patch('cloudinit.sources.DataSourceHetzner.get_hcloud_data')
+ def test_read_data(self, m_get_hcloud_data, m_usermd, m_readmd,
+ m_fallback_nic, m_net):
+ m_get_hcloud_data.return_value = (True,
+ str(METADATA.get('instance-id')))
m_readmd.return_value = METADATA.copy()
m_usermd.return_value = USERDATA
m_fallback_nic.return_value = 'eth0'
@@ -107,11 +108,12 @@ class TestDataSourceHetzner(CiTestCase):
@mock.patch('cloudinit.sources.helpers.hetzner.read_metadata')
@mock.patch('cloudinit.net.find_fallback_nic')
- @mock.patch('cloudinit.sources.DataSourceHetzner.on_hetzner')
- def test_not_on_hetzner_returns_false(self, m_on_hetzner, m_find_fallback,
- m_read_md):
- """If helper 'on_hetzner' returns False, return False from get_data."""
- m_on_hetzner.return_value = False
+ @mock.patch('cloudinit.sources.DataSourceHetzner.get_hcloud_data')
+ def test_not_on_hetzner_returns_false(self, m_get_hcloud_data,
+ m_find_fallback, m_read_md):
+ """If helper 'get_hcloud_data' returns False,
+ return False from get_data."""
+ m_get_hcloud_data.return_value = (False, None)
ds = self.get_ds()
ret = ds.get_data()
diff --git a/tests/unittests/test_datasource/test_nocloud.py b/tests/unittests/test_datasource/test_nocloud.py
index 2e6b53ff..02cc9b38 100644
--- a/tests/unittests/test_datasource/test_nocloud.py
+++ b/tests/unittests/test_datasource/test_nocloud.py
@@ -1,5 +1,6 @@
# This file is part of cloud-init. See LICENSE file for license information.
+from cloudinit import dmi
from cloudinit import helpers
from cloudinit.sources.DataSourceNoCloud import (
DataSourceNoCloud as dsNoCloud,
@@ -30,7 +31,7 @@ class TestNoCloudDataSource(CiTestCase):
self.mocks.enter_context(
mock.patch.object(util, 'get_cmdline', return_value=self.cmdline))
self.mocks.enter_context(
- mock.patch.object(util, 'read_dmi_data', return_value=None))
+ mock.patch.object(dmi, 'read_dmi_data', return_value=None))
def _test_fs_config_is_read(self, fs_label, fs_label_to_search):
vfat_device = 'device-1'
diff --git a/tests/unittests/test_datasource/test_openstack.py b/tests/unittests/test_datasource/test_openstack.py
index 3cfba74d..415755aa 100644
--- a/tests/unittests/test_datasource/test_openstack.py
+++ b/tests/unittests/test_datasource/test_openstack.py
@@ -459,7 +459,7 @@ class TestDetectOpenStack(test_helpers.CiTestCase):
ds.detect_openstack(), 'Expected detect_openstack == True')
@test_helpers.mock.patch(MOCK_PATH + 'util.get_proc_env')
- @test_helpers.mock.patch(MOCK_PATH + 'util.read_dmi_data')
+ @test_helpers.mock.patch(MOCK_PATH + 'dmi.read_dmi_data')
def test_not_detect_openstack_intel_x86_ec2(self, m_dmi, m_proc_env,
m_is_x86):
"""Return False on EC2 platforms."""
@@ -479,7 +479,7 @@ class TestDetectOpenStack(test_helpers.CiTestCase):
ds.detect_openstack(), 'Expected detect_openstack == False on EC2')
m_proc_env.assert_called_with(1)
- @test_helpers.mock.patch(MOCK_PATH + 'util.read_dmi_data')
+ @test_helpers.mock.patch(MOCK_PATH + 'dmi.read_dmi_data')
def test_detect_openstack_intel_product_name_compute(self, m_dmi,
m_is_x86):
"""Return True on OpenStack compute and nova instances."""
@@ -491,7 +491,7 @@ class TestDetectOpenStack(test_helpers.CiTestCase):
self.assertTrue(
ds.detect_openstack(), 'Failed to detect_openstack')
- @test_helpers.mock.patch(MOCK_PATH + 'util.read_dmi_data')
+ @test_helpers.mock.patch(MOCK_PATH + 'dmi.read_dmi_data')
def test_detect_openstack_opentelekomcloud_chassis_asset_tag(self, m_dmi,
m_is_x86):
"""Return True on OpenStack reporting OpenTelekomCloud asset-tag."""
@@ -509,7 +509,7 @@ class TestDetectOpenStack(test_helpers.CiTestCase):
ds.detect_openstack(),
'Expected detect_openstack == True on OpenTelekomCloud')
- @test_helpers.mock.patch(MOCK_PATH + 'util.read_dmi_data')
+ @test_helpers.mock.patch(MOCK_PATH + 'dmi.read_dmi_data')
def test_detect_openstack_sapccloud_chassis_asset_tag(self, m_dmi,
m_is_x86):
"""Return True on OpenStack reporting SAP CCloud VM asset-tag."""
@@ -527,7 +527,7 @@ class TestDetectOpenStack(test_helpers.CiTestCase):
ds.detect_openstack(),
'Expected detect_openstack == True on SAP CCloud VM')
- @test_helpers.mock.patch(MOCK_PATH + 'util.read_dmi_data')
+ @test_helpers.mock.patch(MOCK_PATH + 'dmi.read_dmi_data')
def test_detect_openstack_oraclecloud_chassis_asset_tag(self, m_dmi,
m_is_x86):
"""Return True on OpenStack reporting Oracle cloud asset-tag."""
@@ -548,8 +548,38 @@ class TestDetectOpenStack(test_helpers.CiTestCase):
ds.detect_openstack(accept_oracle=False),
'Expected detect_openstack == False.')
+ def _test_detect_openstack_nova_compute_chassis_asset_tag(self, m_dmi,
+ m_is_x86,
+ chassis_tag):
+ """Return True on OpenStack reporting generic asset-tag."""
+ m_is_x86.return_value = True
+
+ def fake_dmi_read(dmi_key):
+ if dmi_key == 'system-product-name':
+ return 'Generic OpenStack Platform'
+ if dmi_key == 'chassis-asset-tag':
+ return chassis_tag
+ assert False, 'Unexpected dmi read of %s' % dmi_key
+
+ m_dmi.side_effect = fake_dmi_read
+ self.assertTrue(
+ ds.detect_openstack(),
+ 'Expected detect_openstack == True on Generic OpenStack Platform')
+
+ @test_helpers.mock.patch(MOCK_PATH + 'dmi.read_dmi_data')
+ def test_detect_openstack_nova_chassis_asset_tag(self, m_dmi,
+ m_is_x86):
+ self._test_detect_openstack_nova_compute_chassis_asset_tag(
+ m_dmi, m_is_x86, 'OpenStack Nova')
+
+ @test_helpers.mock.patch(MOCK_PATH + 'dmi.read_dmi_data')
+ def test_detect_openstack_compute_chassis_asset_tag(self, m_dmi,
+ m_is_x86):
+ self._test_detect_openstack_nova_compute_chassis_asset_tag(
+ m_dmi, m_is_x86, 'OpenStack Compute')
+
@test_helpers.mock.patch(MOCK_PATH + 'util.get_proc_env')
- @test_helpers.mock.patch(MOCK_PATH + 'util.read_dmi_data')
+ @test_helpers.mock.patch(MOCK_PATH + 'dmi.read_dmi_data')
def test_detect_openstack_by_proc_1_environ(self, m_dmi, m_proc_env,
m_is_x86):
"""Return True when nova product_name specified in /proc/1/environ."""
diff --git a/tests/unittests/test_datasource/test_ovf.py b/tests/unittests/test_datasource/test_ovf.py
index 1d088577..16773de5 100644
--- a/tests/unittests/test_datasource/test_ovf.py
+++ b/tests/unittests/test_datasource/test_ovf.py
@@ -129,7 +129,7 @@ class TestDatasourceOVF(CiTestCase):
ds = self.datasource(sys_cfg={}, distro={}, paths=paths)
retcode = wrap_and_call(
'cloudinit.sources.DataSourceOVF',
- {'util.read_dmi_data': None,
+ {'dmi.read_dmi_data': None,
'transport_iso9660': NOT_FOUND,
'transport_vmware_guestinfo': NOT_FOUND},
ds.get_data)
@@ -145,7 +145,7 @@ class TestDatasourceOVF(CiTestCase):
paths=paths)
retcode = wrap_and_call(
'cloudinit.sources.DataSourceOVF',
- {'util.read_dmi_data': 'vmware',
+ {'dmi.read_dmi_data': 'vmware',
'transport_iso9660': NOT_FOUND,
'transport_vmware_guestinfo': NOT_FOUND},
ds.get_data)
@@ -174,7 +174,7 @@ class TestDatasourceOVF(CiTestCase):
with self.assertRaises(CustomScriptNotFound) as context:
wrap_and_call(
'cloudinit.sources.DataSourceOVF',
- {'util.read_dmi_data': 'vmware',
+ {'dmi.read_dmi_data': 'vmware',
'util.del_dir': True,
'search_file': self.tdir,
'wait_for_imc_cfg_file': conf_file,
@@ -211,7 +211,7 @@ class TestDatasourceOVF(CiTestCase):
with self.assertRaises(RuntimeError) as context:
wrap_and_call(
'cloudinit.sources.DataSourceOVF',
- {'util.read_dmi_data': 'vmware',
+ {'dmi.read_dmi_data': 'vmware',
'util.del_dir': True,
'search_file': self.tdir,
'wait_for_imc_cfg_file': conf_file,
@@ -246,7 +246,7 @@ class TestDatasourceOVF(CiTestCase):
with self.assertRaises(CustomScriptNotFound) as context:
wrap_and_call(
'cloudinit.sources.DataSourceOVF',
- {'util.read_dmi_data': 'vmware',
+ {'dmi.read_dmi_data': 'vmware',
'util.del_dir': True,
'search_file': self.tdir,
'wait_for_imc_cfg_file': conf_file,
@@ -290,7 +290,7 @@ class TestDatasourceOVF(CiTestCase):
with self.assertRaises(CustomScriptNotFound) as context:
wrap_and_call(
'cloudinit.sources.DataSourceOVF',
- {'util.read_dmi_data': 'vmware',
+ {'dmi.read_dmi_data': 'vmware',
'util.del_dir': True,
'search_file': self.tdir,
'wait_for_imc_cfg_file': conf_file,
@@ -313,7 +313,7 @@ class TestDatasourceOVF(CiTestCase):
self.assertEqual('ovf', ds.cloud_name)
self.assertEqual('ovf', ds.platform_type)
- with mock.patch(MPATH + 'util.read_dmi_data', return_value='!VMware'):
+ with mock.patch(MPATH + 'dmi.read_dmi_data', return_value='!VMware'):
with mock.patch(MPATH + 'transport_vmware_guestinfo') as m_guestd:
with mock.patch(MPATH + 'transport_iso9660') as m_iso9660:
m_iso9660.return_value = NOT_FOUND
@@ -334,7 +334,7 @@ class TestDatasourceOVF(CiTestCase):
self.assertEqual('ovf', ds.cloud_name)
self.assertEqual('ovf', ds.platform_type)
- with mock.patch(MPATH + 'util.read_dmi_data', return_value='VMWare'):
+ with mock.patch(MPATH + 'dmi.read_dmi_data', return_value='VMWare'):
with mock.patch(MPATH + 'transport_vmware_guestinfo') as m_guestd:
with mock.patch(MPATH + 'transport_iso9660') as m_iso9660:
m_iso9660.return_value = NOT_FOUND
diff --git a/tests/unittests/test_datasource/test_scaleway.py b/tests/unittests/test_datasource/test_scaleway.py
index 9d82bda9..32f3274a 100644
--- a/tests/unittests/test_datasource/test_scaleway.py
+++ b/tests/unittests/test_datasource/test_scaleway.py
@@ -87,7 +87,7 @@ class TestOnScaleway(CiTestCase):
@mock.patch('cloudinit.util.get_cmdline')
@mock.patch('os.path.exists')
- @mock.patch('cloudinit.util.read_dmi_data')
+ @mock.patch('cloudinit.dmi.read_dmi_data')
def test_not_on_scaleway(self, m_read_dmi_data, m_file_exists,
m_get_cmdline):
self.install_mocks(
@@ -105,7 +105,7 @@ class TestOnScaleway(CiTestCase):
@mock.patch('cloudinit.util.get_cmdline')
@mock.patch('os.path.exists')
- @mock.patch('cloudinit.util.read_dmi_data')
+ @mock.patch('cloudinit.dmi.read_dmi_data')
def test_on_scaleway_dmi(self, m_read_dmi_data, m_file_exists,
m_get_cmdline):
"""
@@ -121,7 +121,7 @@ class TestOnScaleway(CiTestCase):
@mock.patch('cloudinit.util.get_cmdline')
@mock.patch('os.path.exists')
- @mock.patch('cloudinit.util.read_dmi_data')
+ @mock.patch('cloudinit.dmi.read_dmi_data')
def test_on_scaleway_var_run_scaleway(self, m_read_dmi_data, m_file_exists,
m_get_cmdline):
"""
@@ -136,7 +136,7 @@ class TestOnScaleway(CiTestCase):
@mock.patch('cloudinit.util.get_cmdline')
@mock.patch('os.path.exists')
- @mock.patch('cloudinit.util.read_dmi_data')
+ @mock.patch('cloudinit.dmi.read_dmi_data')
def test_on_scaleway_cmdline(self, m_read_dmi_data, m_file_exists,
m_get_cmdline):
"""