160 lines
5.7 KiB
Python
160 lines
5.7 KiB
Python
import json
|
|
import logging
|
|
from typing import Any, Dict
|
|
|
|
from uaclient import exceptions, http, secret_manager, system, util
|
|
from uaclient.clouds import PublicCloudAutoAttachInstance
|
|
|
|
IMDS_IPV4_ADDRESS = "169.254.169.254"
|
|
IMDS_IPV6_ADDRESS = "[fd00:ec2::254]"
|
|
|
|
IMDS_IP_ADDRESS = (IMDS_IPV4_ADDRESS, IMDS_IPV6_ADDRESS)
|
|
IMDS_V2_TOKEN_URL = "http://{}/latest/api/token"
|
|
IMDS_URL = "http://{}/latest/dynamic/instance-identity/pkcs7"
|
|
_IMDS_IID_URL = "http://{}/latest/dynamic/instance-identity/document"
|
|
|
|
SYS_HYPERVISOR_PRODUCT_UUID = "/sys/hypervisor/uuid"
|
|
DMI_PRODUCT_SERIAL = "/sys/class/dmi/id/product_serial"
|
|
DMI_PRODUCT_UUID = "/sys/class/dmi/id/product_uuid"
|
|
|
|
AWS_TOKEN_TTL_SECONDS = "21600"
|
|
AWS_TOKEN_PUT_HEADER = "X-aws-ec2-metadata-token"
|
|
AWS_TOKEN_REQ_HEADER = AWS_TOKEN_PUT_HEADER + "-ttl-seconds"
|
|
|
|
LOG = logging.getLogger(util.replace_top_level_logger_name(__name__))
|
|
|
|
|
|
class AWSAutoAttachInstance(PublicCloudAutoAttachInstance):
|
|
_api_token = None
|
|
_ip_address = None
|
|
|
|
def _get_imds_url_response(self, url: str, headers):
|
|
response = http.readurl(url, headers=headers, timeout=1)
|
|
if response.code == 200:
|
|
return response.body
|
|
else:
|
|
raise exceptions.CloudMetadataError(
|
|
code=response.code, body=response.body
|
|
)
|
|
|
|
# mypy does not handle @property around inner decorators
|
|
# https://github.com/python/mypy/issues/1362
|
|
@property # type: ignore
|
|
@util.retry(exceptions.CloudMetadataError, retry_sleeps=[0.5, 1, 1])
|
|
def identity_doc(self) -> Dict[str, Any]:
|
|
headers = self._request_imds_v2_token_headers()
|
|
url = IMDS_URL.format(self._ip_address)
|
|
imds_url_response = self._get_imds_url_response(url, headers=headers)
|
|
secret_manager.secrets.add_secret(imds_url_response)
|
|
return {"pkcs7": imds_url_response}
|
|
|
|
@util.retry(exceptions.CloudMetadataError, retry_sleeps=[0.5, 1, 1])
|
|
def _get_ii_doc(self) -> Dict:
|
|
"""
|
|
Get the instance identity doc associated with the current instance.
|
|
|
|
See
|
|
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/retrieve-iid.html
|
|
for more context.
|
|
|
|
@return: Dict containing the instance identity document.
|
|
"""
|
|
headers = self._request_imds_v2_token_headers()
|
|
url = _IMDS_IID_URL.format(self._ip_address)
|
|
try:
|
|
ii_doc = json.loads(
|
|
self._get_imds_url_response(url, headers=headers)
|
|
)
|
|
except json.JSONDecodeError as e:
|
|
LOG.debug("Error decoding instance identity document: %s", e)
|
|
return {}
|
|
return ii_doc
|
|
|
|
@property # type: ignore
|
|
def is_likely_pro(self) -> bool:
|
|
"""
|
|
Determines if the instance is likely Ubuntu Pro.
|
|
|
|
Criteria: if any billing-product or marketplace-product-code is
|
|
present, then is likely a Pro instance.
|
|
|
|
@return: Boolean indicating if the instance is likely pro or not.
|
|
"""
|
|
ii_doc = self._get_ii_doc()
|
|
billing_products = ii_doc.get("billingProducts", None)
|
|
marketplace_product_codes = ii_doc.get("marketplaceProductCodes", None)
|
|
return bool(billing_products) or bool(marketplace_product_codes)
|
|
|
|
def _request_imds_v2_token_headers(self):
|
|
for address in IMDS_IP_ADDRESS:
|
|
try:
|
|
headers = self._get_imds_v2_token_headers(ip_address=address)
|
|
except Exception as e:
|
|
LOG.warning(
|
|
"Could not reach AWS IMDS at http://%s: %s\n",
|
|
address,
|
|
getattr(e, "reason", ""),
|
|
)
|
|
else:
|
|
self._ip_address = address
|
|
break
|
|
if self._ip_address is None:
|
|
raise exceptions.AWSNoValidIMDS(
|
|
addresses=", ".join(IMDS_IP_ADDRESS)
|
|
)
|
|
return headers
|
|
|
|
@util.retry(exceptions.CloudMetadataError, retry_sleeps=[1, 2, 5])
|
|
def _get_imds_v2_token_headers(self, ip_address):
|
|
if self._api_token == "IMDSv1":
|
|
return None
|
|
elif self._api_token:
|
|
return {AWS_TOKEN_PUT_HEADER: self._api_token}
|
|
|
|
response = http.readurl(
|
|
IMDS_V2_TOKEN_URL.format(ip_address),
|
|
method="PUT",
|
|
headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS},
|
|
timeout=1,
|
|
)
|
|
if response.code == 200:
|
|
self._api_token = response.body
|
|
secret_manager.secrets.add_secret(self._api_token)
|
|
return {AWS_TOKEN_PUT_HEADER: self._api_token}
|
|
if response.code == 404:
|
|
self._api_token = "IMDSv1"
|
|
return None
|
|
|
|
raise exceptions.CloudMetadataError(
|
|
code=response.code, body=response.body
|
|
)
|
|
|
|
@property
|
|
def cloud_type(self) -> str:
|
|
return "aws"
|
|
|
|
@property
|
|
def is_viable(self) -> bool:
|
|
"""This machine is a viable AWSInstance"""
|
|
try:
|
|
hypervisor_uuid = system.load_file(SYS_HYPERVISOR_PRODUCT_UUID)
|
|
if "ec2" == hypervisor_uuid[0:3]:
|
|
return True
|
|
except FileNotFoundError:
|
|
# SYS_HYPERVISOR_PRODUCT_UUID isn't present on all EC2 instance
|
|
# types, fall through
|
|
pass
|
|
# Both DMI product_uuid and product_serial start with 'ec2'
|
|
dmi_uuid = system.load_file(DMI_PRODUCT_UUID).lower()
|
|
dmi_serial = system.load_file(DMI_PRODUCT_SERIAL).lower()
|
|
if "ec2" == dmi_uuid[0:3] == dmi_serial[0:3]:
|
|
return True
|
|
return False
|
|
|
|
def should_poll_for_pro_license(self) -> bool:
|
|
"""Unsupported"""
|
|
return False
|
|
|
|
def is_pro_license_present(self, *, wait_for_change: bool) -> bool:
|
|
raise exceptions.InPlaceUpgradeNotSupportedError()
|