summaryrefslogtreecommitdiff
path: root/cloudinit/sources/azure/imds.py
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit/sources/azure/imds.py')
-rw-r--r--cloudinit/sources/azure/imds.py156
1 files changed, 156 insertions, 0 deletions
diff --git a/cloudinit/sources/azure/imds.py b/cloudinit/sources/azure/imds.py
new file mode 100644
index 00000000..54fc9a05
--- /dev/null
+++ b/cloudinit/sources/azure/imds.py
@@ -0,0 +1,156 @@
+# Copyright (C) 2022 Microsoft Corporation.
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import functools
+from typing import Dict
+
+import requests
+
+from cloudinit import log as logging
+from cloudinit import util
+from cloudinit.sources.helpers.azure import report_diagnostic_event
+from cloudinit.url_helper import UrlError, readurl, retry_on_url_exc
+
+LOG = logging.getLogger(__name__)
+
+IMDS_URL = "http://169.254.169.254/metadata"
+
+_readurl_exception_callback = functools.partial(
+ retry_on_url_exc,
+ retry_codes=(
+ 404, # not found (yet)
+ 410, # gone / unavailable (yet)
+ 429, # rate-limited/throttled
+ 500, # server error
+ ),
+ retry_instances=(
+ requests.ConnectionError,
+ requests.Timeout,
+ ),
+)
+
+
+def _fetch_url(
+ url: str, *, log_response: bool = True, retries: int = 10, timeout: int = 2
+) -> bytes:
+ """Fetch URL from IMDS.
+
+ :raises UrlError: on error fetching metadata.
+ """
+
+ try:
+ response = readurl(
+ url,
+ exception_cb=_readurl_exception_callback,
+ headers={"Metadata": "true"},
+ infinite=False,
+ log_req_resp=log_response,
+ retries=retries,
+ timeout=timeout,
+ )
+ except UrlError as error:
+ report_diagnostic_event(
+ "Failed to fetch metadata from IMDS: %s" % error,
+ logger_func=LOG.warning,
+ )
+ raise
+
+ return response.contents
+
+
+def _fetch_metadata(
+ url: str,
+) -> Dict:
+ """Fetch IMDS metadata.
+
+ :raises UrlError: on error fetching metadata.
+ :raises ValueError: on error parsing metadata.
+ """
+ metadata = _fetch_url(url)
+
+ try:
+ return util.load_json(metadata)
+ except ValueError as error:
+ report_diagnostic_event(
+ "Failed to parse metadata from IMDS: %s" % error,
+ logger_func=LOG.warning,
+ )
+ raise
+
+
+def fetch_metadata_with_api_fallback() -> Dict:
+ """Fetch extended metadata, falling back to non-extended as required.
+
+ :raises UrlError: on error fetching metadata.
+ :raises ValueError: on error parsing metadata.
+ """
+ try:
+ url = IMDS_URL + "/instance?api-version=2021-08-01&extended=true"
+ return _fetch_metadata(url)
+ except UrlError as error:
+ if error.code == 400:
+ report_diagnostic_event(
+ "Falling back to IMDS api-version: 2019-06-01",
+ logger_func=LOG.warning,
+ )
+ url = IMDS_URL + "/instance?api-version=2019-06-01"
+ return _fetch_metadata(url)
+ raise
+
+
+def fetch_reprovision_data() -> bytes:
+ """Fetch extended metadata, falling back to non-extended as required.
+
+ :raises UrlError: on error.
+ """
+ url = IMDS_URL + "/reprovisiondata?api-version=2019-06-01"
+
+ logging_threshold = 1
+ poll_counter = 0
+
+ def exception_callback(msg, exception):
+ nonlocal logging_threshold
+ nonlocal poll_counter
+
+ poll_counter += 1
+ if not isinstance(exception, UrlError):
+ report_diagnostic_event(
+ "Polling IMDS failed with unexpected exception: %r"
+ % (exception),
+ logger_func=LOG.warning,
+ )
+ return False
+
+ log = True
+ retry = False
+ if exception.code in (404, 410):
+ retry = True
+ if poll_counter >= logging_threshold:
+ # Exponential back-off on logging.
+ logging_threshold *= 2
+ else:
+ log = False
+
+ if log:
+ report_diagnostic_event(
+ "Polling IMDS failed with exception: %r count: %d"
+ % (exception, poll_counter),
+ logger_func=LOG.info,
+ )
+ return retry
+
+ response = readurl(
+ url,
+ exception_cb=exception_callback,
+ headers={"Metadata": "true"},
+ infinite=True,
+ log_req_resp=False,
+ timeout=2,
+ )
+
+ report_diagnostic_event(
+ f"Polled IMDS {poll_counter+1} time(s)",
+ logger_func=LOG.debug,
+ )
+ return response.contents