Skip to content

Commit

Permalink
azure: pps imds (#1093)
Browse files Browse the repository at this point in the history
Without UDF support, DS Azure cannot mount the provisioning ISO,
which contains platform metadata necessary to support
pre-provisioning. The required metadata is made available in IMDS 
starting with api version 2021-08-01. This change will leverage IMDS
to obtain the required metadata to support pre-preprovisioning if 
provisioning ISO was not available.
  • Loading branch information
anhvoms authored Nov 2, 2021
1 parent d54e23b commit 48467aa
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 29 deletions.
74 changes: 50 additions & 24 deletions cloudinit/sources/DataSourceAzure.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,16 @@
IMDS_TIMEOUT_IN_SECONDS = 2
IMDS_URL = "http://169.254.169.254/metadata"
IMDS_VER_MIN = "2019-06-01"
IMDS_VER_WANT = "2021-01-01"

IMDS_VER_WANT = "2021-08-01"
IMDS_EXTENDED_VER_MIN = "2021-03-01"

# This holds SSH key data including if the source was
# from IMDS, as well as the SSH key data itself.
SSHKeys = namedtuple("SSHKeys", ("keys_from_imds", "ssh_keys"))


class metadata_type(Enum):
compute = "{}/instance".format(IMDS_URL)
all = "{}/instance".format(IMDS_URL)
network = "{}/instance/network".format(IMDS_URL)
reprovisiondata = "{}/reprovisiondata".format(IMDS_URL)

Expand Down Expand Up @@ -494,10 +494,21 @@ def crawl_metadata(self):
"Found provisioning metadata in %s" % metadata_source,
logger_func=LOG.debug)

perform_reprovision = reprovision or self._should_reprovision(ret)
imds_md = self.get_imds_data_with_api_fallback(
self.fallback_interface,
retries=10
)
if not imds_md and not ovf_is_accessible:
msg = 'No OVF or IMDS available'
report_diagnostic_event(msg)
raise sources.InvalidMetaDataException(msg)

perform_reprovision = (
reprovision or
self._should_reprovision(ret, imds_md))
perform_reprovision_after_nic_attach = (
reprovision_after_nic_attach or
self._should_reprovision_after_nic_attach(ret))
self._should_reprovision_after_nic_attach(ret, imds_md))

if perform_reprovision or perform_reprovision_after_nic_attach:
if util.is_FreeBSD():
Expand All @@ -507,15 +518,12 @@ def crawl_metadata(self):
if perform_reprovision_after_nic_attach:
self._wait_for_all_nics_ready()
ret = self._reprovision()
# fetch metadata again as it has changed after reprovisioning
imds_md = self.get_imds_data_with_api_fallback(
self.fallback_interface,
retries=10
)

imds_md = self.get_imds_data_with_api_fallback(
self.fallback_interface,
retries=10
)
if not imds_md and not ovf_is_accessible:
msg = 'No OVF or IMDS available'
report_diagnostic_event(msg)
raise sources.InvalidMetaDataException(msg)
(md, userdata_raw, cfg, files) = ret
self.seed = metadata_source
crawled_data.update({
Expand Down Expand Up @@ -691,7 +699,7 @@ def get_imds_data_with_api_fallback(
self,
fallback_nic,
retries,
md_type=metadata_type.compute,
md_type=metadata_type.all,
exc_cb=retry_on_url_exc,
infinite=False):
"""
Expand Down Expand Up @@ -1407,7 +1415,17 @@ def _report_ready(self, lease: dict) -> bool:
"connectivity issues: %s" % e, logger_func=LOG.warning)
return False

def _should_reprovision_after_nic_attach(self, candidate_metadata) -> bool:
def _ppstype_from_imds(self, imds_md: dict = None) -> str:
try:
return imds_md['extended']['compute']['ppsType']
except Exception as e:
report_diagnostic_event(
"Could not retrieve pps configuration from IMDS: %s" %
e, logger_func=LOG.debug)
return None

def _should_reprovision_after_nic_attach(
self, ovf_md, imds_md=None) -> bool:
"""Whether or not we should wait for nic attach and then poll
IMDS for reprovisioning data. Also sets a marker file to poll IMDS.
Expand All @@ -1419,14 +1437,16 @@ def _should_reprovision_after_nic_attach(self, candidate_metadata) -> bool:
the ISO, thus cloud-init needs to have a way of knowing that it should
jump back into the waiting mode in order to retrieve the ovf_env.
@param candidate_metadata: Metadata obtained from reading ovf-env.
@param ovf_md: Metadata obtained from reading ovf-env.
@param imds_md: Metadata obtained from IMDS
@return: Whether to reprovision after waiting for nics to be attached.
"""
if not candidate_metadata:
if not ovf_md:
return False
(_md, _userdata_raw, cfg, _files) = candidate_metadata
(_md, _userdata_raw, cfg, _files) = ovf_md
path = REPROVISION_NIC_ATTACH_MARKER_FILE
if (cfg.get('PreprovisionedVMType', None) == "Savable" or
self._ppstype_from_imds(imds_md) == "Savable" or
os.path.isfile(path)):
if not os.path.isfile(path):
LOG.info("Creating a marker file to wait for nic attach: %s",
Expand All @@ -1436,7 +1456,7 @@ def _should_reprovision_after_nic_attach(self, candidate_metadata) -> bool:
return True
return False

def _should_reprovision(self, ret):
def _should_reprovision(self, ovf_md, imds_md=None):
"""Whether or not we should poll IMDS for reprovisioning data.
Also sets a marker file to poll IMDS.
Expand All @@ -1447,12 +1467,13 @@ def _should_reprovision(self, ret):
However, since the VM reports ready to the Fabric, we will not attach
the ISO, thus cloud-init needs to have a way of knowing that it should
jump back into the polling loop in order to retrieve the ovf_env."""
if not ret:
if not ovf_md:
return False
(_md, _userdata_raw, cfg, _files) = ret
(_md, _userdata_raw, cfg, _files) = ovf_md
path = REPROVISION_MARKER_FILE
if (cfg.get('PreprovisionedVm') is True or
cfg.get('PreprovisionedVMType', None) == 'Running' or
cfg.get('PreprovisionedVMType', None) == 'Running' or
self._ppstype_from_imds(imds_md) == "Running" or
os.path.isfile(path)):
if not os.path.isfile(path):
LOG.info("Creating a marker file to poll imds: %s",
Expand Down Expand Up @@ -2239,7 +2260,7 @@ def _generate_network_config_from_fallback_config() -> dict:
@azure_ds_telemetry_reporter
def get_metadata_from_imds(fallback_nic,
retries,
md_type=metadata_type.compute,
md_type=metadata_type.all,
api_version=IMDS_VER_MIN,
exc_cb=retry_on_url_exc,
infinite=False):
Expand Down Expand Up @@ -2280,11 +2301,16 @@ def get_metadata_from_imds(fallback_nic,
def _get_metadata_from_imds(
retries,
exc_cb,
md_type=metadata_type.compute,
md_type=metadata_type.all,
api_version=IMDS_VER_MIN,
infinite=False):
url = "{}?api-version={}".format(md_type.value, api_version)
headers = {"Metadata": "true"}

# support for extended metadata begins with 2021-03-01
if api_version >= IMDS_EXTENDED_VER_MIN and md_type == metadata_type.all:
url = url + "&extended=true"

try:
response = readurl(
url, timeout=IMDS_TIMEOUT_IN_SECONDS, headers=headers,
Expand Down
83 changes: 78 additions & 5 deletions tests/unittests/test_datasource/test_azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,16 +434,16 @@ def test_get_metadata_does_not_dhcp_if_network_is_up(
@mock.patch(MOCKPATH + 'readurl', autospec=True)
@mock.patch(MOCKPATH + 'EphemeralDHCPv4')
@mock.patch(MOCKPATH + 'net.is_up')
def test_get_compute_metadata_uses_compute_url(
def test_get_metadata_uses_instance_url(
self, m_net_is_up, m_dhcp, m_readurl):
"""Make sure readurl is called with the correct url when accessing
network metadata"""
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)
'eth0', retries=3, md_type=dsaz.metadata_type.all)
m_readurl.assert_called_with(
"http://169.254.169.254/metadata/instance?api-version="
"2019-06-01", exception_cb=mock.ANY,
Expand Down Expand Up @@ -472,10 +472,10 @@ def test_get_network_metadata_uses_network_url(
@mock.patch(MOCKPATH + 'readurl', autospec=True)
@mock.patch(MOCKPATH + 'EphemeralDHCPv4')
@mock.patch(MOCKPATH + 'net.is_up')
def test_get_default_metadata_uses_compute_url(
def test_get_default_metadata_uses_instance_url(
self, m_net_is_up, m_dhcp, m_readurl):
"""Make sure readurl is called with the correct url when accessing
network metadata"""
metadata"""
m_net_is_up.return_value = True
m_readurl.return_value = url_helper.StringResponse(
json.dumps(IMDS_NETWORK_METADATA).encode('utf-8'))
Expand All @@ -488,6 +488,26 @@ def test_get_default_metadata_uses_compute_url(
headers=mock.ANY, retries=mock.ANY,
timeout=mock.ANY, infinite=False)

@mock.patch(MOCKPATH + 'readurl', autospec=True)
@mock.patch(MOCKPATH + 'EphemeralDHCPv4')
@mock.patch(MOCKPATH + 'net.is_up')
def test_get_metadata_uses_extended_url(
self, m_net_is_up, m_dhcp, m_readurl):
"""Make sure readurl is called with the correct url when accessing
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.all,
api_version="2021-08-01")
m_readurl.assert_called_with(
"http://169.254.169.254/metadata/instance?api-version="
"2021-08-01&extended=true", exception_cb=mock.ANY,
headers=mock.ANY, retries=mock.ANY,
timeout=mock.ANY, infinite=False)

@mock.patch(MOCKPATH + 'readurl', autospec=True)
@mock.patch(MOCKPATH + 'EphemeralDHCPv4WithReporting', autospec=True)
@mock.patch(MOCKPATH + 'net.is_up', autospec=True)
Expand Down Expand Up @@ -950,6 +970,43 @@ def test_crawl_metadata_raises_invalid_metadata_on_error(self):
dsrc.crawl_metadata()
self.assertEqual(str(cm.exception), error_msg)

def test_crawl_metadata_call_imds_once_no_reprovision(self):
"""If reprovisioning, report ready at the end"""
ovfenv = construct_valid_ovf_env(
platform_settings={"PreprovisionedVm": "False"}
)

data = {
'ovfcontent': ovfenv,
'sys_cfg': {}
}
dsrc = self._get_ds(data)
dsrc.crawl_metadata()
self.assertEqual(1, self.m_get_metadata_from_imds.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')
def test_crawl_metadata_call_imds_twice_with_reprovision(
self, poll_imds_func, m_report_ready, m_write, m_dhcp
):
"""If reprovisioning, imds metadata will be fetched twice"""
ovfenv = construct_valid_ovf_env(
platform_settings={"PreprovisionedVm": "True"}
)

data = {
'ovfcontent': ovfenv,
'sys_cfg': {}
}
dsrc = self._get_ds(data)
poll_imds_func.return_value = ovfenv
dsrc.crawl_metadata()
self.assertEqual(2, self.m_get_metadata_from_imds.call_count)

@mock.patch(
'cloudinit.sources.DataSourceAzure.EphemeralDHCPv4WithReporting')
@mock.patch('cloudinit.sources.DataSourceAzure.util.write_file')
Expand Down Expand Up @@ -2638,6 +2695,22 @@ def test__should_reprovision_returns_false(self, isfile):
dsa = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths)
self.assertFalse(dsa._should_reprovision((None, None, {}, None)))

@mock.patch(MOCKPATH + 'util.write_file', autospec=True)
def test__should_reprovision_uses_imds_md(self, write_file, isfile):
"""The _should_reprovision method should be able to
retrieve the preprovisioning VM type from imds metadata"""
isfile.return_value = False
dsa = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths)
self.assertTrue(dsa._should_reprovision(
(None, None, {}, None),
{'extended': {'compute': {'ppsType': 'Running'}}}))
self.assertFalse(dsa._should_reprovision(
(None, None, {}, None),
{}))
self.assertFalse(dsa._should_reprovision(
(None, None, {}, None),
{'extended': {'compute': {"hasCustomData": False}}}))

@mock.patch(MOCKPATH + 'DataSourceAzure._poll_imds')
def test_reprovision_calls__poll_imds(self, _poll_imds, isfile):
"""_reprovision will poll IMDS."""
Expand Down

0 comments on commit 48467aa

Please sign in to comment.