def test__get_imds_v2_token_headers_none_on_404(self, readurl): """A 404 on private AWS regions indicates lack IMDSv2 support.""" readurl.side_effect = HTTPError("http://me", 404, "No IMDSv2 support", None, BytesIO()) instance = UAAutoAttachAWSInstance() assert None is instance._get_imds_v2_token_headers( ip_address=IMDS_IPV4_ADDRESS) assert "IMDSv1" == instance._api_token # No retries on 404. It is a permanent indication of no IMDSv2 support. instance._get_imds_v2_token_headers(ip_address=IMDS_IPV4_ADDRESS) assert 1 == readurl.call_count
def test_retry_backoff_on_failed_identity_doc( self, readurl, sleep, fail_count, exception, caplog_text ): """Retry backoff is attempted before failing to get AWS.identity_doc""" def fake_someurlerrors(url): if readurl.call_count <= fail_count: raise HTTPError( "http://me", 700 + readurl.call_count, "funky error msg", None, BytesIO(), ) return "pkcs7WOOT!==", {"header": "stuff"} readurl.side_effect = fake_someurlerrors instance = UAAutoAttachAWSInstance() if exception: with pytest.raises(HTTPError) as excinfo: instance.identity_doc assert 704 == excinfo.value.code else: assert "pkcs7WOOT!==" == instance.identity_doc expected_sleep_calls = [mock.call(1), mock.call(2), mock.call(5)] assert expected_sleep_calls == sleep.call_args_list expected_logs = [ "HTTP Error 701: funky error msg Retrying 3 more times.", "HTTP Error 702: funky error msg Retrying 2 more times.", "HTTP Error 703: funky error msg Retrying 1 more times.", ] logs = caplog_text() for log in expected_logs: assert log in logs
def test_identity_doc_from_aws_url_pkcs7(self, readurl): """Return pkcs7 content from IMDS as AWS' identity doc""" readurl.return_value = "pkcs7WOOT!==", {"header": "stuff"} instance = UAAutoAttachAWSInstance() assert "pkcs7WOOT!==" == instance.identity_doc url = "http://169.254.169.254/latest/dynamic/instance-identity/pkcs7" assert [mock.call(url)] == readurl.call_args_list
def test__get_imds_v2_token_headers_caches_response(self, readurl): """Return API token headers for IMDSv2 access. Response is cached.""" instance = UAAutoAttachAWSInstance() url = "http://169.254.169.254/latest/api/token" readurl.return_value = "somebase64token==", {"header": "stuff"} assert { AWS_TOKEN_PUT_HEADER: "somebase64token==" } == instance._get_imds_v2_token_headers(ip_address=IMDS_IPV4_ADDRESS) instance._get_imds_v2_token_headers(ip_address=IMDS_IPV4_ADDRESS) assert "somebase64token==" == instance._api_token assert [ mock.call( url, method="PUT", headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS}, timeout=1, ) ] == readurl.call_args_list
def test_retry_backoff_on__get_imds_v2_token_headers_caches_response( self, readurl, sleep, fail_count, exception, caplog_text): """Retry backoff before failing _get_imds_v2_token_headers.""" def fake_someurlerrors(url, method=None, headers=None, timeout=1): if readurl.call_count <= fail_count: raise HTTPError( "http://me", 700 + readurl.call_count, "funky error msg", None, BytesIO(), ) return "base64token==", {"header": "stuff"} readurl.side_effect = fake_someurlerrors instance = UAAutoAttachAWSInstance() if exception: with pytest.raises(HTTPError) as excinfo: instance._get_imds_v2_token_headers( ip_address=IMDS_IPV4_ADDRESS) assert 704 == excinfo.value.code else: assert { AWS_TOKEN_PUT_HEADER: "base64token==" } == instance._get_imds_v2_token_headers( ip_address=IMDS_IPV4_ADDRESS) expected_sleep_calls = [mock.call(1), mock.call(2), mock.call(5)] assert expected_sleep_calls == sleep.call_args_list expected_logs = [ "HTTP Error 701: funky error msg Retrying 3 more times.", "HTTP Error 702: funky error msg Retrying 2 more times.", "HTTP Error 703: funky error msg Retrying 1 more times.", ] logs = caplog_text() for log in expected_logs: assert log in logs
def test_is_viable_based_on_sys_product_serial_and_uuid( self, load_file, hypervisor_uuid, prod_uuid, prod_serial, viable): """Platform is viable when product serial and uuid start with ec2""" def fake_load_file(f_name): if f_name == "/sys/hypervisor/uuid": if hypervisor_uuid is not None: return hypervisor_uuid raise FileNotFoundError() if f_name == "/sys/class/dmi/id/product_uuid": return prod_uuid if f_name == "/sys/class/dmi/id/product_serial": return prod_serial raise AssertionError("Invalid load_file of {}".format(f_name)) load_file.side_effect = fake_load_file instance = UAAutoAttachAWSInstance() assert viable is instance.is_viable
def test_identity_doc_from_aws_url_pkcs7(self, readurl): """Return pkcs7 content from IMDS as AWS' identity doc""" readurl.return_value = "pkcs7WOOT!==", {"header": "stuff"} instance = UAAutoAttachAWSInstance() assert {"pkcs7": "pkcs7WOOT!=="} == instance.identity_doc url = "http://169.254.169.254/latest/dynamic/instance-identity/pkcs7" token_url = "http://169.254.169.254/latest/api/token" assert [ mock.call( token_url, method="PUT", headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS}, timeout=1, ), mock.call(url, headers={AWS_TOKEN_PUT_HEADER: "pkcs7WOOT!=="}, timeout=1), ] == readurl.call_args_list
def test_identity_doc_default_to_ipv6_if_ipv4_fail(self, readurl, caplog_text): instance = UAAutoAttachAWSInstance() ipv4_address = IMDS_IPV4_ADDRESS ipv6_address = IMDS_IPV6_ADDRESS def fake_someurlerrors(url, method=None, headers=None, timeout=1): if ipv4_address in url: raise Exception("IPv4 exception") if url == IMDS_V2_TOKEN_URL.format(ipv6_address): return "base64token==", {"header": "stuff"} if url == IMDS_URL.format(ipv6_address): return "pkcs7WOOT!==", {"header": "stuff"} readurl.side_effect = fake_someurlerrors assert {"pkcs7": "pkcs7WOOT!=="} == instance.identity_doc expected = [ mock.call( IMDS_V2_TOKEN_URL.format(ipv4_address), method="PUT", headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS}, timeout=1, ), mock.call( IMDS_V2_TOKEN_URL.format(ipv6_address), method="PUT", headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS}, timeout=1, ), mock.call( IMDS_URL.format(ipv6_address), headers={AWS_TOKEN_PUT_HEADER: "base64token=="}, timeout=1, ), ] assert expected == readurl.call_args_list expected_log = "Could not reach AWS IMDS at http://169.254.169.254:" assert expected_log in caplog_text()
def test_identity_doc_logs_error_if_both_ipv4_and_ipv6_fails( self, readurl, caplog_text): instance = UAAutoAttachAWSInstance() ipv4_address = IMDS_IPV4_ADDRESS ipv6_address = IMDS_IPV6_ADDRESS readurl.side_effect = Exception("Exception") expected_error = ("No valid AWS IMDS endpoint discovered at " "addresses: {}, {}".format(IMDS_IPV4_ADDRESS, IMDS_IPV6_ADDRESS)) with pytest.raises(exceptions.UserFacingError, match=re.escape(expected_error)): instance.identity_doc expected = [ mock.call( IMDS_V2_TOKEN_URL.format(ipv4_address), method="PUT", headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS}, timeout=1, ), mock.call( IMDS_V2_TOKEN_URL.format(ipv6_address), method="PUT", headers={AWS_TOKEN_REQ_HEADER: AWS_TOKEN_TTL_SECONDS}, timeout=1, ), ] assert expected == readurl.call_args_list expected_logs = [ "Could not reach AWS IMDS at http://169.254.169.254:", "Could not reach AWS IMDS at http://[fd00:ec2::254]:", ] for expected_log in expected_logs: assert expected_log in caplog_text()
class TestPollForProLicense: @pytest.mark.parametrize( "is_config_value_true," "is_attached," "is_current_series_lts," "cloud_instance," "should_poll," "is_pro_license_present," "cfg_poll_for_pro_licenses," "expected_log_debug_calls," "expected_is_pro_license_present_calls," "expected_attempt_auto_attach_calls", [ ( True, None, None, None, None, None, None, [mock.call("Configured to not auto attach, shutting down")], [], [], ), ( False, True, None, None, None, None, None, [mock.call("Already attached, shutting down")], [], [], ), ( False, False, False, None, None, None, None, [mock.call("Not on LTS, shutting down")], [], [], ), ( False, False, True, exceptions.CloudFactoryError("none"), None, None, None, [mock.call("Not on cloud, shutting down")], [], [], ), ( False, False, True, UAAutoAttachAWSInstance(), None, None, None, [mock.call("Not on gcp, shutting down")], [], [], ), ( False, False, True, UAAutoAttachGCPInstance(), False, None, None, [mock.call("Not on supported instance, shutting down")], [], [], ), ( False, False, True, UAAutoAttachGCPInstance(), True, True, None, [], [mock.call(wait_for_change=False)], [mock.call(mock.ANY, mock.ANY)], ), ( False, False, True, UAAutoAttachGCPInstance(), True, exceptions.CancelProLicensePolling(), None, [mock.call("Cancelling polling")], [mock.call(wait_for_change=False)], [], ), ( False, False, True, UAAutoAttachGCPInstance(), True, False, False, [ mock.call( "Configured to not poll for pro license, shutting down" ) ], [mock.call(wait_for_change=False)], [], ), ( False, False, True, UAAutoAttachGCPInstance(), True, False, False, [ mock.call( "Configured to not poll for pro license, shutting down" ) ], [mock.call(wait_for_change=False)], [], ), ], ) def test_before_polling_loop_checks( self, m_is_config_value_true, m_is_current_series_lts, m_cloud_instance_factory, m_should_poll, m_is_pro_license_present, m_attempt_auto_attach, m_time, m_sleep, m_log_debug, is_config_value_true, is_attached, is_current_series_lts, cloud_instance, should_poll, is_pro_license_present, cfg_poll_for_pro_licenses, expected_log_debug_calls, expected_is_pro_license_present_calls, expected_attempt_auto_attach_calls, FakeConfig, ): if is_attached: cfg = FakeConfig.for_attached_machine() else: cfg = FakeConfig() cfg.cfg.update( {"ua_config": { "poll_for_pro_license": cfg_poll_for_pro_licenses }}) m_is_config_value_true.return_value = is_config_value_true m_is_current_series_lts.return_value = is_current_series_lts m_cloud_instance_factory.side_effect = [cloud_instance] m_should_poll.return_value = should_poll m_is_pro_license_present.side_effect = [is_pro_license_present] poll_for_pro_license(cfg) assert expected_log_debug_calls == m_log_debug.call_args_list assert (expected_is_pro_license_present_calls == m_is_pro_license_present.call_args_list) assert (expected_attempt_auto_attach_calls == m_attempt_auto_attach.call_args_list) @pytest.mark.parametrize( "is_pro_license_present_side_effect," "time_side_effect," "expected_is_pro_license_present_calls," "expected_attempt_auto_attach_calls," "expected_log_debug_calls," "expected_sleep_calls", [ ( [False, True], time_mock_side_effect_increment_by(100), [ mock.call(wait_for_change=False), mock.call(wait_for_change=True), ], [mock.call(mock.ANY, mock.ANY)], [], [], ), ( [False, False, False, False, False, True], time_mock_side_effect_increment_by(100), [ mock.call(wait_for_change=False), mock.call(wait_for_change=True), mock.call(wait_for_change=True), mock.call(wait_for_change=True), mock.call(wait_for_change=True), mock.call(wait_for_change=True), ], [mock.call(mock.ANY, mock.ANY)], [], [], ), ( [False, False, True], time_mock_side_effect_increment_by(1), [ mock.call(wait_for_change=False), mock.call(wait_for_change=True), mock.call(wait_for_change=True), ], [mock.call(mock.ANY, mock.ANY)], [ mock.call( "wait_for_change returned quickly and no pro license" " present. Waiting 123 seconds before polling again") ], [mock.call(123)], ), ( [False, False, False, False, False, True], time_mock_side_effect_increment_by(1), [ mock.call(wait_for_change=False), mock.call(wait_for_change=True), mock.call(wait_for_change=True), mock.call(wait_for_change=True), mock.call(wait_for_change=True), mock.call(wait_for_change=True), ], [mock.call(mock.ANY, mock.ANY)], [ mock.call(mock.ANY), mock.call(mock.ANY), mock.call(mock.ANY), mock.call(mock.ANY), ], [ mock.call(123), mock.call(123), mock.call(123), mock.call(123), ], ), ( [ False, False, exceptions.DelayProLicensePolling(), False, exceptions.DelayProLicensePolling(), True, ], time_mock_side_effect_increment_by(100), [ mock.call(wait_for_change=False), mock.call(wait_for_change=True), mock.call(wait_for_change=True), mock.call(wait_for_change=True), mock.call(wait_for_change=True), mock.call(wait_for_change=True), ], [mock.call(mock.ANY, mock.ANY)], [], [mock.call(123), mock.call(123)], ), ( [False, False, exceptions.CancelProLicensePolling()], time_mock_side_effect_increment_by(100), [ mock.call(wait_for_change=False), mock.call(wait_for_change=True), mock.call(wait_for_change=True), ], [], [mock.call("Cancelling polling")], [], ), ], ) def test_polling_loop( self, m_is_config_value_true, m_is_current_series_lts, m_cloud_instance_factory, m_should_poll, m_is_pro_license_present, m_attempt_auto_attach, m_time, m_sleep, m_log_debug, is_pro_license_present_side_effect, time_side_effect, expected_is_pro_license_present_calls, expected_attempt_auto_attach_calls, expected_log_debug_calls, expected_sleep_calls, FakeConfig, ): cfg = FakeConfig() cfg.cfg.update({ "ua_config": { "poll_for_pro_license": True, "polling_error_retry_delay": 123, } }) m_is_config_value_true.return_value = False m_is_current_series_lts.return_value = True m_cloud_instance_factory.return_value = UAAutoAttachGCPInstance() m_should_poll.return_value = True m_is_pro_license_present.side_effect = ( is_pro_license_present_side_effect) m_time.side_effect = time_side_effect poll_for_pro_license(cfg) assert expected_sleep_calls == m_sleep.call_args_list assert expected_log_debug_calls == m_log_debug.call_args_list assert (expected_is_pro_license_present_calls == m_is_pro_license_present.call_args_list) assert (expected_attempt_auto_attach_calls == m_attempt_auto_attach.call_args_list)
def test_is_viable_based_on_sys_hypervisor_uuid(self, load_file, uuid): """Viable ec2 platform is determined by /sys/hypervisor/uuid prefix""" load_file.return_value = uuid instance = UAAutoAttachAWSInstance() assert True is instance.is_viable
def test_cloud_type(self): instance = UAAutoAttachAWSInstance() assert "aws" == instance.cloud_type
def test_unsupported_is_pro_license_present(self): """Unsupported""" instance = UAAutoAttachAWSInstance() with pytest.raises(exceptions.InPlaceUpgradeNotSupportedError): instance.is_pro_license_present(wait_for_change=False)
def test_unsupported_should_poll_for_pro_license(self): """Unsupported""" instance = UAAutoAttachAWSInstance() assert not instance.should_poll_for_pro_license()