From cd4e896980b04ee6067b047cc98e685c7506b9f9 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 12 Apr 2022 16:37:21 -0700 Subject: [PATCH 01/58] Update test_update.py to use mock_update_handler (#2547) Co-authored-by: narrieta --- tests/ga/mocks.py | 36 ++++++++--- tests/ga/test_update.py | 133 +++++++++++++--------------------------- 2 files changed, 68 insertions(+), 101 deletions(-) diff --git a/tests/ga/mocks.py b/tests/ga/mocks.py index 2563d402fe..3b93bc8bc7 100644 --- a/tests/ga/mocks.py +++ b/tests/ga/mocks.py @@ -28,20 +28,25 @@ def mock_update_handler(protocol, iterations=1, on_new_iteration=lambda _: None, exthandlers_handler=None, remote_access_handler=None, enable_agent_updates=False): """ Creates a mock UpdateHandler that executes its main loop for the given 'iterations'. - If 'on_new_iteration' is given, it is invoked at the beginning of each iteration passing the iteration number as argument. - Network requests (e.g. requests for the goal state) are done using the given 'protocol'. - The mock UpdateHandler uses mock no-op ExtHandlersHandler and RemoteAccessHandler, unless they are given by 'exthandlers_handler' and 'remote_access_handler'. - Agent updates are disabled, unless specified otherwise with 'enable_agent_updates'. - Background threads (monitor, env, telemetry, etc) are not started. + * If 'on_new_iteration' is given, it is invoked at the beginning of each iteration passing the iteration number as argument. + * Network requests (e.g. requests for the goal state) are done using the given 'protocol'. + * The mock UpdateHandler uses mock no-op ExtHandlersHandler and RemoteAccessHandler, unless they are given by 'exthandlers_handler' and 'remote_access_handler'. + * Agent updates are disabled, unless specified otherwise with 'enable_agent_updates'. + * Background threads (monitor, env, telemetry, etc) are not started. + * The UpdateHandler is augmented with these extra functions: + * get_exit_code() - returns the code passed to sys.exit() when the handler exits + * get_iterations() - returns the number of iterations executed by the main loop """ iteration_count = [0] def is_running(*args): # mock for property UpdateHandler.is_running, which controls the main loop if len(args) == 0: # getter - iteration_count[0] += 1 - on_new_iteration(iteration_count[0]) - return iteration_count[0] <= iterations + enter_loop = iteration_count[0] < iterations + if enter_loop: + iteration_count[0] += 1 + on_new_iteration(iteration_count[0]) + return enter_loop else: # setter return None @@ -60,9 +65,22 @@ def is_running(*args): # mock for property UpdateHandler.is_running, which cont with patch.object(UpdateHandler, "_start_threads"): with patch.object(UpdateHandler, "_check_threads_running"): with patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)): - with patch('sys.exit', side_effect=lambda _: 0): + with patch('sys.exit', side_effect=lambda _: 0) as mock_exit: + def get_exit_code(): + if mock_exit.call_count == 0: + raise Exception("The UpdateHandler did not exit") + if mock_exit.call_count != 1: + raise Exception("The UpdateHandler exited multiple times ({0})".format(mock_exit.call_count)) + args, _ = mock_exit.call_args + return args[0] + + def get_iterations(): + return iteration_count[0] + update_handler = get_update_handler() update_handler.protocol_util.get_protocol = Mock(return_value=protocol) + update_handler.get_exit_code = get_exit_code + update_handler.get_iterations = get_iterations yield update_handler diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index 8a648a8c41..8cc4d59828 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -26,8 +26,6 @@ _ORIGINAL_POPEN = subprocess.Popen -from mock import PropertyMock - from azurelinuxagent.common import conf from azurelinuxagent.common.event import EVENTS_DIRECTORY, WALAEventOperation from azurelinuxagent.common.exception import ProtocolError, UpdateError, ResourceGoneError, HttpError @@ -56,7 +54,7 @@ from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol.mockwiredata import DATA_FILE, DATA_FILE_MULTIPLE_EXT from tests.tools import AgentTestCase, AgentTestCaseWithGetVmSizeMock, data_dir, DEFAULT, patch, load_bin_data, Mock, MagicMock, \ - clear_singleton_instances, mock_sleep + clear_singleton_instances from tests.protocol import mockwiredata from tests.protocol.HttpRequestPredicates import HttpRequestPredicates @@ -103,59 +101,22 @@ def faux_logger(): @contextlib.contextmanager -def _get_update_handler(iterations=1, test_data=None): +def _get_update_handler(iterations=1, test_data=None, protocol=None): """ This function returns a mocked version of the UpdateHandler object to be used for testing. It will only run the main loop [iterations] no of times. - To reuse the same object, be sure to reset the iterations by using the update_handler.set_iterations() function. - :param iterations: No of times the UpdateHandler.run() method should run. - :return: Mocked object of UpdateHandler() class and object of the MockWireProtocol(). """ - def _set_iterations(iterations_): - # This will reset the current iteration and the max iterations to run for this test object. - update_handler._cur_iteration = 0 - update_handler._iterations = iterations_ - - def check_running(*val, **__): - # This method will determine if the current UpdateHandler object is supposed to run or not. - - # There can be scenarios where the UpdateHandler.is_running.setter is called, in that case, return the first - # value of the tuple and not increment the cur_iteration - if len(val) > 0: - return val[0] - - if update_handler._cur_iteration < update_handler._iterations: - update_handler._cur_iteration += 1 - return True - return False - test_data = DATA_FILE if test_data is None else test_data - with mock_wire_protocol(test_data) as protocol: - protocol_util = MagicMock() - protocol_util.get_protocol = Mock(return_value=protocol) - with patch("azurelinuxagent.ga.update.get_protocol_util", return_value=protocol_util): - with patch("azurelinuxagent.common.conf.get_autoupdate_enabled", return_value=False): - with patch.object(HostPluginProtocol, "is_default_channel", False): - update_handler = get_update_handler() - # Setup internal state for the object required for testing - update_handler._cur_iteration = 0 - update_handler._iterations = 0 - update_handler.set_iterations = _set_iterations - update_handler.get_iterations = lambda: update_handler._cur_iteration - type(update_handler).is_running = PropertyMock(side_effect=check_running) - with patch("time.sleep", side_effect=lambda _: mock_sleep(0.001)): - with patch('sys.exit') as exit_mock: - # Setup the initial number of iterations - update_handler.set_iterations(iterations) - update_handler.exit_mock = exit_mock - try: - yield update_handler, protocol - finally: - # Since PropertyMock requires us to mock the type(ClassName).property of the object, - # reverting it back to keep the state of the test clean - type(update_handler).is_running = True + with patch.object(HostPluginProtocol, "is_default_channel", False): + if protocol is None: + with mock_wire_protocol(test_data) as mock_protocol: + with mock_update_handler(mock_protocol, iterations=iterations, enable_agent_updates=True) as update_handler: + yield update_handler, mock_protocol + else: + with mock_update_handler(protocol, iterations=iterations, enable_agent_updates=True) as update_handler: + yield update_handler, protocol class UpdateTestCase(AgentTestCaseWithGetVmSizeMock): @@ -1585,9 +1546,8 @@ def test_it_should_recreate_handler_env_on_service_startup(self): # re-runnning the update handler. Then,ensure that the HandlerEnvironment file is recreated with eventsFolder # flag in HandlerEnvironment.json file. self._add_write_permission_to_goal_state_files() - with _get_update_handler(iterations) as (update_handler, protocol): + with _get_update_handler(iterations=1) as (update_handler, protocol): with patch("azurelinuxagent.common.agent_supported_feature._ETPFeature.is_supported", True): - update_handler.set_iterations(1) update_handler.run(debug=True) self.assertGreater(os.path.getmtime(handler_env_file), last_modification_time, @@ -1614,9 +1574,7 @@ def _mock_popen(cmd, *args, **kwargs): with patch("azurelinuxagent.common.logger.warn") as patch_warn: update_handler.run(debug=True) - self.assertTrue(update_handler.exit_mock.called, "The process should have exited") - exit_args, _ = update_handler.exit_mock.call_args - self.assertEqual(exit_args[0], 0, "Exit code should be 0; List of all warnings logged by the agent: {0}".format( + self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0; List of all warnings logged by the agent: {0}".format( patch_warn.call_args_list)) self.assertEqual(0, len(executed_firewall_commands), "firewall-cmd should not be called at all") self.assertTrue(any( @@ -1640,9 +1598,7 @@ def _mock_popen(cmd, *args, **kwargs): with patch("azurelinuxagent.common.logger.warn") as patch_warn: update_handler.run(debug=True) - self.assertTrue(update_handler.exit_mock.called, "The process should have exited") - exit_args, _ = update_handler.exit_mock.call_args - self.assertEqual(exit_args[0], 0, "Exit code should be 0; List of all warnings logged by the agent: {0}".format( + self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0; List of all warnings logged by the agent: {0}".format( patch_warn.call_args_list)) # Firewall-cmd should only be called 4 times - 1st to check if running, 2nd, 3rd and 4th for the QueryPassThrough cmd @@ -1836,7 +1792,7 @@ def test_it_should_recreate_extension_event_directories_for_existing_extensions_ self.assertTrue(os.path.exists(ext_dir), "Extension directory {0} should exist!".format(ext_dir)) def test_it_should_report_update_status_in_status_blob(self): - with _get_update_handler(iterations=1) as (update_handler, protocol): + with mock_wire_protocol(DATA_FILE) as protocol: with patch.object(conf, "get_enable_ga_versioning", return_value=True): with patch.object(conf, "get_autoupdate_gafamily", return_value="Prod"): with patch("azurelinuxagent.common.logger.warn") as patch_warn: @@ -1855,11 +1811,9 @@ def update_goal_state_and_run_handler(): protocol.incarnation += 1 protocol.mock_wire_data.set_incarnation(protocol.incarnation) self._add_write_permission_to_goal_state_files() - update_handler.set_iterations(1) - update_handler.run(debug=True) - self.assertTrue(update_handler.exit_mock.called, "The process should have exited") - exit_args, _ = update_handler.exit_mock.call_args - self.assertEqual(exit_args[0], 0, + with _get_update_handler(iterations=1, protocol=protocol) as (update_handler, _): + update_handler.run(debug=True) + self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0; List of all warnings logged by the agent: {0}".format( patch_warn.call_args_list)) @@ -1912,9 +1866,7 @@ def get_handler(url, **kwargs): protocol.set_http_handlers(http_get_handler=get_handler) update_handler.run(debug=True) - self.assertTrue(update_handler.exit_mock.called, "The process should have exited") - exit_args, _ = update_handler.exit_mock.call_args - self.assertEqual(exit_args[0], 0, "Exit code should be 0; List of all warnings logged by the agent: {0}".format( + self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0; List of all warnings logged by the agent: {0}".format( patch_warn.call_args_list)) warn_msgs = [args[0] for (args, _) in patch_warn.call_args_list if "An error occurred while retrieving the goal state" in args[0]] @@ -1951,14 +1903,13 @@ class TestAgentUpgrade(UpdateTestCase): def create_conf_mocks(self, hotfix_frequency, normal_frequency): # Disabling extension processing to speed up tests as this class deals with testing agent upgrades with patch("azurelinuxagent.common.conf.get_extensions_enabled", return_value=False): - with patch("azurelinuxagent.common.conf.get_autoupdate_enabled", return_value=True): - with patch("azurelinuxagent.common.conf.get_autoupdate_frequency", return_value=0.001): - with patch("azurelinuxagent.common.conf.get_hotfix_upgrade_frequency", - return_value=hotfix_frequency): - with patch("azurelinuxagent.common.conf.get_normal_upgrade_frequency", - return_value=normal_frequency): - with patch("azurelinuxagent.common.conf.get_autoupdate_gafamily", return_value="Prod"): - yield + with patch("azurelinuxagent.common.conf.get_autoupdate_frequency", return_value=0.001): + with patch("azurelinuxagent.common.conf.get_hotfix_upgrade_frequency", + return_value=hotfix_frequency): + with patch("azurelinuxagent.common.conf.get_normal_upgrade_frequency", + return_value=normal_frequency): + with patch("azurelinuxagent.common.conf.get_autoupdate_gafamily", return_value="Prod"): + yield @contextlib.contextmanager def __get_update_handler(self, iterations=1, test_data=None, hotfix_frequency=1.0, normal_frequency=2.0, @@ -1993,10 +1944,8 @@ def put_handler(url, *args, **_): update_handler._protocol = protocol yield update_handler, mock_telemetry - def __assert_exit_code_successful(self, exit_mock): - self.assertTrue(exit_mock.called, "The process should have exited") - exit_args, _ = exit_mock.call_args - self.assertEqual(exit_args[0], 0, "Exit code should be 0") + def __assert_exit_code_successful(self, update_handler): + self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0") def __assert_upgrade_telemetry_emitted_for_requested_version(self, mock_telemetry, upgrade=True, version="99999.0.0.0"): upgrade_event_msgs = [kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if @@ -2044,7 +1993,7 @@ def test_it_should_upgrade_agent_on_process_start_if_auto_upgrade_enabled(self): update_handler.run(debug=True) - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.assertEqual(1, update_handler.get_iterations(), "Update handler should've exited after the first run") self.__assert_agent_directories_available(versions=["99999.0.0.0"]) self.__assert_upgrade_telemetry_emitted(mock_telemetry) @@ -2072,7 +2021,7 @@ def reload_conf(url, protocol): update_handler.run(debug=True) self.assertGreater(reload_conf.call_count, 0, "Ensure the conf reload was called") - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.assertEqual(no_of_iterations, update_handler.get_iterations(), "Update handler should've run its course") # Ensure the new agent versions were also downloaded once the manifest was updated self.__assert_agent_directories_available(versions=["2.0.0", "2.1.0", "99999.0.0.0"]) @@ -2102,7 +2051,7 @@ def reload_conf(url, protocol): diff = time.time() - start_time self.assertGreater(reload_conf.call_count, 0, "Ensure the conf reload was called") - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.assertGreaterEqual(update_handler.get_iterations(), 3, "Update handler should've run at least until the new GA was available") # A bare-bone check to ensure that the agent waited for the new agent at least for the preset frequency time @@ -2115,7 +2064,7 @@ def test_it_should_not_auto_upgrade_if_auto_update_disabled(self): with patch("azurelinuxagent.common.conf.get_autoupdate_enabled", return_value=False): update_handler.run(debug=True) - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.assertGreaterEqual(update_handler.get_iterations(), 10, "Update handler should've run 10 times") self.__assert_no_agent_upgrade_telemetry(mock_telemetry) self.assertFalse(os.path.exists(self.agent_dir("99999.0.0.0")), @@ -2143,7 +2092,7 @@ def reload_conf(url, protocol): update_handler.run(debug=True) self.assertGreater(reload_conf.call_count, 0, "Ensure the conf reload was called") - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.assertEqual(no_of_iterations, update_handler.get_iterations(), "Update handler didn't run completely") self.__assert_no_agent_upgrade_telemetry(mock_telemetry) upgrade_event_msgs = [kwarg['message'] for _, kwarg in mock_telemetry.call_args_list if @@ -2159,7 +2108,7 @@ def test_it_should_download_only_requested_version_if_available(self): with patch.object(conf, "get_enable_ga_versioning", return_value=True): update_handler.run(debug=True) - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted_for_requested_version(mock_telemetry, version="9.9.9.10") self.__assert_agent_directories_exist_and_others_dont_exist(versions=["9.9.9.10"]) @@ -2175,7 +2124,7 @@ def test_it_should_cleanup_all_agents_except_requested_version_and_current_versi with patch.object(conf, "get_enable_ga_versioning", return_value=True): update_handler.run(debug=True) - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted_for_requested_version(mock_telemetry, version="9.9.9.10") self.__assert_agent_directories_exist_and_others_dont_exist(versions=["9.9.9.10", str(CURRENT_VERSION)]) @@ -2186,7 +2135,7 @@ def test_it_should_not_update_if_requested_version_not_found_in_manifest(self): with patch.object(conf, "get_enable_ga_versioning", return_value=True): update_handler.run(debug=True) - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.__assert_no_agent_upgrade_telemetry(mock_telemetry) agent_msgs = [kwarg for _, kwarg in mock_telemetry.call_args_list if kwarg['op'] in (WALAEventOperation.AgentUpgrade, WALAEventOperation.Download)] @@ -2239,7 +2188,7 @@ def reload_conf(url, protocol): update_handler.run(debug=True) self.assertGreaterEqual(reload_conf.call_count, 1, "Reload conf not updated as expected") - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted_for_requested_version(mock_telemetry) self.__assert_agent_directories_exist_and_others_dont_exist(versions=["99999.0.0.0", str(CURRENT_VERSION)]) self.assertEqual(update_handler._protocol.mock_wire_data.call_counts['agentArtifact'], 1, @@ -2286,7 +2235,7 @@ def reload_conf(url, protocol): update_handler.run(debug=True) self.assertGreater(reload_conf.call_count, 0, "Reload conf not updated") - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted(mock_telemetry) self.__assert_agent_directories_exist_and_others_dont_exist( versions=["1.0.0", "1.1.0", "1.2.0", "2.0.0", "2.1.0", "9.9.9.10", "99999.0.0.0", str(CURRENT_VERSION)]) @@ -2305,7 +2254,7 @@ def test_it_should_not_download_anything_if_requested_version_is_current_version update_handler._protocol.mock_wire_data.set_incarnation(2) update_handler.run(debug=True) - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.__assert_no_agent_upgrade_telemetry(mock_telemetry) self.__assert_agent_directories_exist_and_others_dont_exist(versions=[str(CURRENT_VERSION)]) @@ -2341,7 +2290,7 @@ def reload_conf(url, protocol): self.assertGreater(reload_conf.call_count, 0, "Reload conf not updated") self.assertLess(update_handler.get_iterations(), no_of_iterations, "The code should've exited as soon as requested version was found") - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted_for_requested_version(mock_telemetry, version="9.9.9.10") def test_it_should_blacklist_current_agent_on_downgrade(self): @@ -2364,7 +2313,7 @@ def test_it_should_blacklist_current_agent_on_downgrade(self): finally: os.environ.pop(DAEMON_VERSION_ENV_VARIABLE) - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) self.__assert_upgrade_telemetry_emitted_for_requested_version(mock_telemetry, upgrade=False, version=downgraded_version) current_agent = next(agent for agent in self.agents() if agent.version == CURRENT_VERSION) @@ -2387,7 +2336,7 @@ def test_it_should_not_downgrade_below_daemon_version(self): finally: os.environ.pop(DAEMON_VERSION_ENV_VARIABLE) - self.__assert_exit_code_successful(update_handler.exit_mock) + self.__assert_exit_code_successful(update_handler) upgrade_msgs = [kwarg for _, kwarg in mock_telemetry.call_args_list if kwarg['op'] == WALAEventOperation.AgentUpgrade] # This will throw if corresponding message not found so not asserting on that From 713633ad94ee3a4fa3d71dc06401c87897050394 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 13 Apr 2022 14:36:18 -0700 Subject: [PATCH 02/58] Update HGAP minimum required version for FastTrack (#2549) (#2551) Co-authored-by: narrieta (cherry picked from commit 50eaac1de1d707e4e0fcd74616cfef80ae54d463) --- azurelinuxagent/common/protocol/hostplugin.py | 5 ++--- .../vm_settings-difference_in_required_features.json | 2 +- tests/data/hostgaplugin/vm_settings-empty_depends_on.json | 2 +- tests/data/hostgaplugin/vm_settings-invalid_blob_type.json | 2 +- .../hostgaplugin/vm_settings-no_extension_manifests.json | 2 +- .../data/hostgaplugin/vm_settings-no_status_upload_blob.json | 2 +- tests/data/hostgaplugin/vm_settings-out-of-sync.json | 2 +- tests/data/hostgaplugin/vm_settings-parse_error.json | 2 +- tests/data/hostgaplugin/vm_settings-requested_version.json | 2 +- tests/data/hostgaplugin/vm_settings.json | 2 +- 10 files changed, 11 insertions(+), 12 deletions(-) diff --git a/azurelinuxagent/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py index cf254be304..81b9062566 100644 --- a/azurelinuxagent/common/protocol/hostplugin.py +++ b/azurelinuxagent/common/protocol/hostplugin.py @@ -547,9 +547,8 @@ def format_message(msg): logger.info(message) add_event(op=WALAEventOperation.HostPlugin, message=message, is_success=True) - # Don't support HostGAPlugin versions older than 123 - # TODO: update the minimum version to 1.0.8.123 before release - if vm_settings.host_ga_plugin_version < FlexibleVersion("1.0.8.117"): + # Don't support HostGAPlugin versions older than 124 + if vm_settings.host_ga_plugin_version < FlexibleVersion("1.0.8.124"): raise_not_supported() self._supports_vm_settings = True diff --git a/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json b/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json index 9cfb42752c..9fd2e4f722 100644 --- a/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json +++ b/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.123", + "hostGAPluginVersion": "1.0.8.124", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-empty_depends_on.json b/tests/data/hostgaplugin/vm_settings-empty_depends_on.json index 6fa93452cf..94d9f0eb1f 100644 --- a/tests/data/hostgaplugin/vm_settings-empty_depends_on.json +++ b/tests/data/hostgaplugin/vm_settings-empty_depends_on.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.123", + "hostGAPluginVersion": "1.0.8.124", "vmSettingsSchemaVersion": "0.0", "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9", "correlationId": "1bef4c48-044e-4225-8f42-1d1eac1eb158", diff --git a/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json b/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json index 62314a403d..e7945845ac 100644 --- a/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json +++ b/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.123", + "hostGAPluginVersion": "1.0.8.124", "vmSettingsSchemaVersion": "0.0", "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9", "correlationId": "1bef4c48-044e-4225-8f42-1d1eac1eb158", diff --git a/tests/data/hostgaplugin/vm_settings-no_extension_manifests.json b/tests/data/hostgaplugin/vm_settings-no_extension_manifests.json index 7deff8d5eb..b084900b68 100644 --- a/tests/data/hostgaplugin/vm_settings-no_extension_manifests.json +++ b/tests/data/hostgaplugin/vm_settings-no_extension_manifests.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.123", + "hostGAPluginVersion": "1.0.8.124", "vmSettingsSchemaVersion": "0.0", "activityId": "89d50bf1-fa55-4257-8af3-3db0c9f81ab4", "correlationId": "c143f8f0-a66b-4881-8c06-1efd278b0b02", diff --git a/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json b/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json index 27ebebcefe..2f70b55762 100644 --- a/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json +++ b/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.123", + "hostGAPluginVersion": "1.0.8.124", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-out-of-sync.json b/tests/data/hostgaplugin/vm_settings-out-of-sync.json index 737350d698..0aae82031c 100644 --- a/tests/data/hostgaplugin/vm_settings-out-of-sync.json +++ b/tests/data/hostgaplugin/vm_settings-out-of-sync.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.123", + "hostGAPluginVersion": "1.0.8.124", "vmSettingsSchemaVersion": "0.0", "activityId": "AAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE", "correlationId": "EEEEEEEE-DDDD-CCCC-BBBB-AAAAAAAAAAAA", diff --git a/tests/data/hostgaplugin/vm_settings-parse_error.json b/tests/data/hostgaplugin/vm_settings-parse_error.json index e817a1e888..bae5de4cb8 100644 --- a/tests/data/hostgaplugin/vm_settings-parse_error.json +++ b/tests/data/hostgaplugin/vm_settings-parse_error.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.123", + "hostGAPluginVersion": "1.0.8.124", "vmSettingsSchemaVersion": THIS_IS_A_SYNTAX_ERROR, "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-requested_version.json b/tests/data/hostgaplugin/vm_settings-requested_version.json index 1b5023b117..49e6a27780 100644 --- a/tests/data/hostgaplugin/vm_settings-requested_version.json +++ b/tests/data/hostgaplugin/vm_settings-requested_version.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.123", + "hostGAPluginVersion": "1.0.8.124", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings.json b/tests/data/hostgaplugin/vm_settings.json index b67ee0a23b..96836e7663 100644 --- a/tests/data/hostgaplugin/vm_settings.json +++ b/tests/data/hostgaplugin/vm_settings.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.123", + "hostGAPluginVersion": "1.0.8.124", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", From ae5cff8576e16e23772a993ad5859841527b5fc5 Mon Sep 17 00:00:00 2001 From: Laveesh Rohra Date: Mon, 18 Apr 2022 22:13:45 +0000 Subject: [PATCH 03/58] Fix No known host error message for DCR v2 (#2554) --- dcr/templates/setup-vm-and-execute-tests.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dcr/templates/setup-vm-and-execute-tests.yml b/dcr/templates/setup-vm-and-execute-tests.yml index b4e9ef32b1..b5779d7dbe 100644 --- a/dcr/templates/setup-vm-and-execute-tests.yml +++ b/dcr/templates/setup-vm-and-execute-tests.yml @@ -32,7 +32,7 @@ jobs: displayName: 'Install SSH Key to agent' name: "InstallKey" inputs: - knownHostsEntry: '$(SSH_PUBLIC)' + knownHostsEntry: 'github.com $(SSH_PUBLIC)' # Adding a dummy known host for github.com as leaving it empty is not allowed by this task sshPublicKey: '$(SSH_PUBLIC)' sshKeySecureFile: 'id_rsa' @@ -52,9 +52,8 @@ jobs: architecture: 'x64' - script: | - rm -rf ~/.ssh/known_hosts mkdir -p "$(Build.ArtifactStagingDirectory)/harvest" - displayName: "Clear known host keys and create directories" + displayName: "Create harvest directories" - bash: $(Build.SourcesDirectory)/dcr/scripts/build_agent_zip.sh displayName: "Build Agent Zip" @@ -205,4 +204,4 @@ jobs: - publish: $(Build.ArtifactStagingDirectory)/harvest artifact: $(rgName)-harvest condition: and(failed(), eq(variables.executeTests, 'true')) - displayName: 'Publish Harvest logs' \ No newline at end of file + displayName: 'Publish Harvest logs' From 02db57bc1c9a37ad0e330373a9cb47f10b0245f8 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Mon, 18 Apr 2022 16:23:34 -0700 Subject: [PATCH 04/58] fix depends-on scenario test (#2548) Co-authored-by: Norberto Arrieta --- tests/ga/test_extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ga/test_extension.py b/tests/ga/test_extension.py index d487fdc28d..e115748c36 100644 --- a/tests/ga/test_extension.py +++ b/tests/ga/test_extension.py @@ -1453,7 +1453,7 @@ def mock_popen(cmd, *args, **kwargs): return original_popen(["echo", "Yes"], *args, **kwargs) with patch('azurelinuxagent.common.cgroupapi.subprocess.Popen', side_effect=mock_popen): - with patch('azurelinuxagent.ga.exthandlers._DEFAULT_EXT_TIMEOUT_MINUTES', 0.001): + with patch('azurelinuxagent.ga.exthandlers._DEFAULT_EXT_TIMEOUT_MINUTES', 0.01): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() From 362df3fdec429fa1813202a41b5c587fdb245a70 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Mon, 18 Apr 2022 17:40:58 -0700 Subject: [PATCH 05/58] check agent cg after goal state processed and handle extensions to starts in extension slice (#2546) * check agent cg after goal state processed * fix PR comments * fix UT * fix default value * address comment Co-authored-by: Laveesh Rohra --- azurelinuxagent/common/cgroupconfigurator.py | 47 +++++++++++--------- azurelinuxagent/ga/exthandlers.py | 5 +-- azurelinuxagent/ga/update.py | 4 ++ tests/common/test_cgroupconfigurator.py | 5 ++- 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index f37f08d938..866e4f866f 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -17,6 +17,7 @@ import os import re import subprocess +import threading from azurelinuxagent.common import conf from azurelinuxagent.common import logger @@ -131,6 +132,7 @@ def __init__(self): self._cgroups_api = None self._agent_cpu_cgroup_path = None self._agent_memory_cgroup_path = None + self._check_cgroups_lock = threading.RLock() # Protect the check_cgroups which is called from Monitor thread and main loop. def initialize(self): try: @@ -541,32 +543,37 @@ def __try_set_cpu_quota(quota): return True def check_cgroups(self, cgroup_metrics): - if not self.enabled(): - return + self._check_cgroups_lock.acquire() + try: + if not self.enabled(): + return - errors = [] + errors = [] - process_check_success = False - try: - self._check_processes_in_agent_cgroup() - process_check_success = True - except CGroupsException as exception: - errors.append(exception) + process_check_success = False + try: + self._check_processes_in_agent_cgroup() + process_check_success = True + except CGroupsException as exception: + errors.append(exception) - quota_check_success = False - try: - self._check_agent_throttled_time(cgroup_metrics) - quota_check_success = True - except CGroupsException as exception: - errors.append(exception) + quota_check_success = False + try: + if cgroup_metrics: + self._check_agent_throttled_time(cgroup_metrics) + quota_check_success = True + except CGroupsException as exception: + errors.append(exception) - reason = "Check on cgroups failed:\n{0}".format("\n".join([ustr(e) for e in errors])) + reason = "Check on cgroups failed:\n{0}".format("\n".join([ustr(e) for e in errors])) - if not process_check_success and conf.get_cgroup_disable_on_process_check_failure(): - self.disable(reason, DisableCgroups.ALL) + if not process_check_success and conf.get_cgroup_disable_on_process_check_failure(): + self.disable(reason, DisableCgroups.ALL) - if not quota_check_success and conf.get_cgroup_disable_on_quota_check_failure(): - self.disable(reason, DisableCgroups.AGENT) + if not quota_check_success and conf.get_cgroup_disable_on_quota_check_failure(): + self.disable(reason, DisableCgroups.AGENT) + finally: + self._check_cgroups_lock.release() def _check_processes_in_agent_cgroup(self): """ diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py index cc9d0afc32..13d0ca3e09 100644 --- a/azurelinuxagent/ga/exthandlers.py +++ b/azurelinuxagent/ga/exthandlers.py @@ -1411,9 +1411,8 @@ def _enable_extension(self, extension, uninstall_exit_code): env = { ExtCommandEnvVariable.UninstallReturnCode: uninstall_exit_code } - # This check to call the setup if AzureMonitorLinuxAgent extension already installed and not called setup before - if self.is_azuremonitorlinuxagent(self.get_full_name()) and \ - not CGroupConfigurator.get_instance().is_extension_resource_limits_setup_completed(self.get_full_name()): + # This check to call the setup if extension already installed and not called setup before + if not CGroupConfigurator.get_instance().is_extension_resource_limits_setup_completed(self.get_full_name()): self.set_extension_resource_limits() self.set_operation(WALAEventOperation.Enable) diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index 2118ca6831..1bdda2fc44 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -614,6 +614,10 @@ def _process_goal_state(self, exthandlers_handler, remote_access_handler): self._extensions_summary = ExtensionsSummary() exthandlers_handler.run() + # check cgroup and disable if any extension started in agent cgroup after goal state processed. + # Note: Monitor thread periodically checks this in addition to here. + CGroupConfigurator.get_instance().check_cgroups(cgroup_metrics=[]) + # always report status, even if the goal state did not change # do it before processing the remote access, since that operation can take a long time self._report_status(exthandlers_handler) diff --git a/tests/common/test_cgroupconfigurator.py b/tests/common/test_cgroupconfigurator.py index fcae952bd2..ae856f25c1 100644 --- a/tests/common/test_cgroupconfigurator.py +++ b/tests/common/test_cgroupconfigurator.py @@ -859,7 +859,10 @@ def test_check_cgroups_should_disable_cgroups_when_a_check_fails(self): with patch("azurelinuxagent.common.cgroupconfigurator.add_event") as add_event: configurator.enable() - configurator.check_cgroups([]) + tracked_metrics = [ + MetricValue(MetricsCategory.CPU_CATEGORY, MetricsCounter.PROCESSOR_PERCENT_TIME, "test", + 10)] + configurator.check_cgroups(tracked_metrics) if method_to_fail == "_check_processes_in_agent_cgroup": self.assertFalse(configurator.enabled(), "An error in {0} should have disabled cgroups".format(method_to_fail)) else: From 7fd0d81f132638d52e83f3a72ecd4079df3eef69 Mon Sep 17 00:00:00 2001 From: Kevin Clark Date: Tue, 19 Apr 2022 11:27:44 -0700 Subject: [PATCH 06/58] Quick fix to handle intermittent test error. (#2556) --- tests/common/test_cgroupconfigurator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/common/test_cgroupconfigurator.py b/tests/common/test_cgroupconfigurator.py index ae856f25c1..56aa3d48ed 100644 --- a/tests/common/test_cgroupconfigurator.py +++ b/tests/common/test_cgroupconfigurator.py @@ -552,7 +552,13 @@ def test_start_extension_command_should_capture_only_the_last_subprocess_output( def mock_popen(command, *args, **kwargs): # Inject a syntax error to the call - systemd_command = command.replace('systemd-run', 'systemd-run syntax_error') + + # Popen can accept both strings and lists, handle both here. + if isinstance(command, str): + systemd_command = command.replace('systemd-run', 'systemd-run syntax_error') + elif isinstance(command, list) and command[0] == 'systemd-run': + systemd_command = ['systemd-run', 'syntax_error'] + command[1:] + return original_popen(systemd_command, *args, **kwargs) expected_output = "[stdout]\n{0}\n\n\n[stderr]\n" From 1da0ef6e3c0be655a890f621e38b97b0820f4971 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 19 Apr 2022 20:37:40 -0700 Subject: [PATCH 07/58] Updated tests in test_update.py to use mock_update_handler (#2552) Co-authored-by: narrieta --- tests/ga/mocks.py | 97 ++++++++++++++++-------- tests/ga/test_update.py | 158 ++++++++++++++++++++-------------------- 2 files changed, 146 insertions(+), 109 deletions(-) diff --git a/tests/ga/mocks.py b/tests/ga/mocks.py index 3b93bc8bc7..6fbc63d7da 100644 --- a/tests/ga/mocks.py +++ b/tests/ga/mocks.py @@ -1,4 +1,4 @@ -# Copyright 2018 Microsoft Corporation + # Copyright 2018 Microsoft Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,17 +25,29 @@ @contextlib.contextmanager -def mock_update_handler(protocol, iterations=1, on_new_iteration=lambda _: None, exthandlers_handler=None, remote_access_handler=None, enable_agent_updates=False): +def mock_update_handler(protocol, + iterations=1, + on_new_iteration=lambda _: None, + exthandlers_handler=None, + remote_access_handler=None, + autoupdate_enabled=False, + check_daemon_running=False, + start_background_threads=False, + check_background_threads=False + ): """ Creates a mock UpdateHandler that executes its main loop for the given 'iterations'. * If 'on_new_iteration' is given, it is invoked at the beginning of each iteration passing the iteration number as argument. * Network requests (e.g. requests for the goal state) are done using the given 'protocol'. * The mock UpdateHandler uses mock no-op ExtHandlersHandler and RemoteAccessHandler, unless they are given by 'exthandlers_handler' and 'remote_access_handler'. - * Agent updates are disabled, unless specified otherwise with 'enable_agent_updates'. - * Background threads (monitor, env, telemetry, etc) are not started. + * Agent updates are disabled, unless specified otherwise by 'autoupdate_enabled'. + * The check for the daemon is skipped, unless specified otherwise by 'check_daemon_running' + * Background threads (monitor, env, telemetry, etc) are not started, unless specified otherwise by 'start_background_threads' + * The check for background threads is skipped, unless specified otherwise by 'check_background_threads' * The UpdateHandler is augmented with these extra functions: - * get_exit_code() - returns the code passed to sys.exit() when the handler exits - * get_iterations() - returns the number of iterations executed by the main loop + * get_exit_code() - returns the code passed to sys.exit() when the handler exits + * get_iterations() - returns the number of iterations executed by the main loop + * get_iterations_completed() - returns the number of iterations of the main loop that completed execution (i.e. were not interrupted by an exception, return, etc) """ iteration_count = [0] @@ -53,34 +65,55 @@ def is_running(*args): # mock for property UpdateHandler.is_running, which cont if exthandlers_handler is None: exthandlers_handler = ExtHandlersHandler(protocol) + else: + exthandlers_handler.protocol = protocol if remote_access_handler is None: remote_access_handler = RemoteAccessHandler(protocol) - with patch("azurelinuxagent.ga.exthandlers.get_exthandlers_handler", return_value=exthandlers_handler): - with patch("azurelinuxagent.ga.remoteaccess.get_remote_access_handler", return_value=remote_access_handler): - with patch("azurelinuxagent.common.conf.get_autoupdate_enabled", return_value=enable_agent_updates): - with patch.object(UpdateHandler, "is_running", PropertyMock(side_effect=is_running)): - with patch.object(UpdateHandler, "_check_daemon_running"): - with patch.object(UpdateHandler, "_start_threads"): - with patch.object(UpdateHandler, "_check_threads_running"): - with patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)): - with patch('sys.exit', side_effect=lambda _: 0) as mock_exit: - def get_exit_code(): - if mock_exit.call_count == 0: - raise Exception("The UpdateHandler did not exit") - if mock_exit.call_count != 1: - raise Exception("The UpdateHandler exited multiple times ({0})".format(mock_exit.call_count)) - args, _ = mock_exit.call_args - return args[0] - - def get_iterations(): - return iteration_count[0] - - update_handler = get_update_handler() - update_handler.protocol_util.get_protocol = Mock(return_value=protocol) - update_handler.get_exit_code = get_exit_code - update_handler.get_iterations = get_iterations - - yield update_handler + cleanup_functions = [] + + def patch_object(target, attribute): + p = patch.object(target, attribute) + p.start() + cleanup_functions.insert(0, p.stop) + + try: + with patch("azurelinuxagent.ga.exthandlers.get_exthandlers_handler", return_value=exthandlers_handler): + with patch("azurelinuxagent.ga.remoteaccess.get_remote_access_handler", return_value=remote_access_handler): + with patch("azurelinuxagent.common.conf.get_autoupdate_enabled", return_value=autoupdate_enabled): + with patch.object(UpdateHandler, "is_running", PropertyMock(side_effect=is_running)): + with patch('azurelinuxagent.ga.update.time.sleep', side_effect=lambda _: mock_sleep(0.001)) as sleep: + with patch('sys.exit', side_effect=lambda _: 0) as mock_exit: + if not check_daemon_running: + patch_object(UpdateHandler, "_check_daemon_running") + if not start_background_threads: + patch_object(UpdateHandler, "_start_threads") + if not check_background_threads: + patch_object(UpdateHandler, "_check_threads_running") + + def get_exit_code(): + if mock_exit.call_count == 0: + raise Exception("The UpdateHandler did not exit") + if mock_exit.call_count != 1: + raise Exception("The UpdateHandler exited multiple times ({0})".format(mock_exit.call_count)) + args, _ = mock_exit.call_args + return args[0] + + def get_iterations(): + return iteration_count[0] + + def get_iterations_completed(): + return sleep.call_count + + update_handler = get_update_handler() + update_handler.protocol_util.get_protocol = Mock(return_value=protocol) + update_handler.get_exit_code = get_exit_code + update_handler.get_iterations = get_iterations + update_handler.get_iterations_completed = get_iterations_completed + + yield update_handler + finally: + for f in cleanup_functions: + f() diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index 8cc4d59828..664c78e205 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -112,10 +112,10 @@ def _get_update_handler(iterations=1, test_data=None, protocol=None): with patch.object(HostPluginProtocol, "is_default_channel", False): if protocol is None: with mock_wire_protocol(test_data) as mock_protocol: - with mock_update_handler(mock_protocol, iterations=iterations, enable_agent_updates=True) as update_handler: + with mock_update_handler(mock_protocol, iterations=iterations, autoupdate_enabled=True) as update_handler: yield update_handler, mock_protocol else: - with mock_update_handler(protocol, iterations=iterations, enable_agent_updates=True) as update_handler: + with mock_update_handler(protocol, iterations=iterations, autoupdate_enabled=True) as update_handler: yield update_handler, protocol @@ -1294,77 +1294,6 @@ def test_get_latest_agent_should_return_latest_agent_even_on_bad_error_json(self latest_agent = self.update_handler.get_latest_agent_greater_than_daemon() self.assertEqual(latest_agent.version, dst_ver, "Latest agent version is invalid") - def _test_run(self, invocations=1, calls=1, enable_updates=False, sleep_interval=(6,)): - conf.get_autoupdate_enabled = Mock(return_value=enable_updates) - - def iterator(*_, **__): - iterator.count += 1 - if iterator.count <= invocations: - return True - return False - iterator.count = 0 - - fileutil.write_file(conf.get_agent_pid_file_path(), ustr(42)) - - with patch('azurelinuxagent.ga.exthandlers.get_exthandlers_handler') as mock_handler: - mock_handler.run_ext_handlers = Mock() - with patch('azurelinuxagent.ga.update.get_monitor_handler') as mock_monitor: - with patch.object(UpdateHandler, 'is_running') as mock_is_running: - mock_is_running.__get__ = Mock(side_effect=iterator) - with patch('azurelinuxagent.ga.remoteaccess.get_remote_access_handler') as mock_ra_handler: - with patch('azurelinuxagent.ga.update.get_env_handler') as mock_env: - with patch('azurelinuxagent.ga.update.get_collect_logs_handler') as mock_collect_logs: - with patch('azurelinuxagent.ga.update.get_send_telemetry_events_handler') as mock_telemetry_send_events: - with patch('azurelinuxagent.ga.update.get_collect_telemetry_events_handler') as mock_event_collector: - with patch('azurelinuxagent.ga.update.initialize_event_logger_vminfo_common_parameters'): - with patch('azurelinuxagent.ga.update.is_log_collection_allowed', return_value=True): - with patch.object(self.update_handler, "_processing_new_extensions_goal_state", return_value=True): - with patch('time.sleep') as sleep_mock: - with patch('sys.exit') as mock_exit: - if isinstance(os.getppid, MagicMock): - self.update_handler.run() - else: - with patch('os.getppid', return_value=42): - self.update_handler.run() - - self.assertEqual(1, mock_handler.call_count) - self.assertEqual(calls, len([c for c in [call[0] for call in mock_handler.return_value.method_calls] if c == 'run'])) - self.assertEqual(1, mock_ra_handler.call_count) - self.assertEqual(calls, len(mock_ra_handler.return_value.method_calls)) - if calls > 0: - self.assertEqual(sleep_interval, sleep_mock.call_args[0]) - self.assertEqual(1, mock_monitor.call_count) - self.assertEqual(1, mock_env.call_count) - self.assertEqual(1, mock_collect_logs.call_count) - self.assertEqual(1, mock_telemetry_send_events.call_count) - self.assertEqual(1, mock_event_collector.call_count) - self.assertEqual(1, mock_exit.call_count) - - def test_run(self): - self._test_run() - - def test_run_stops_if_update_available(self): - self.update_handler._download_agent_if_upgrade_available = Mock(return_value=True) - self._test_run(invocations=0, calls=0, enable_updates=True) - - def test_run_stops_if_orphaned(self): - with patch('os.getppid', return_value=1): - self._test_run(invocations=0, calls=0, enable_updates=True) - - def test_run_clears_sentinel_on_successful_exit(self): - self._test_run() - self.assertFalse(os.path.isfile(self.update_handler._sentinel_file_path())) - - def test_run_leaves_sentinel_on_unsuccessful_exit(self): - self.update_handler._download_agent_if_upgrade_available = Mock(side_effect=Exception) - self._test_run(invocations=1, calls=0, enable_updates=True) - self.assertTrue(os.path.isfile(self.update_handler._sentinel_file_path())) - - def test_run_emits_restart_event(self): - self.update_handler._emit_restart_event = Mock() - self._test_run() - self.assertEqual(1, self.update_handler._emit_restart_event.call_count) - def test_set_agents_sets_agents(self): self.prepare_agents() @@ -1510,15 +1439,19 @@ def test_write_pid_file_ignores_exceptions(self): self.assertEqual(0, len(pid_files)) self.assertEqual(None, pid_file) - @patch('azurelinuxagent.common.conf.get_extensions_enabled', return_value=False) - def test_update_happens_when_extensions_disabled(self, _): + def test_update_happens_when_extensions_disabled(self): """ Although the extension enabled config will not get checked before an update is found, this test attempts to ensure that behavior never changes. """ - self.update_handler._download_agent_if_upgrade_available = Mock(return_value=True) - self._test_run(invocations=0, calls=0, enable_updates=True, sleep_interval=(300,)) + with patch('azurelinuxagent.common.conf.get_extensions_enabled', return_value=False): + with patch('azurelinuxagent.ga.update.UpdateHandler._download_agent_if_upgrade_available', return_value=True) as download_agent: + with mock_wire_protocol(DATA_FILE) as protocol: + with mock_update_handler(protocol, autoupdate_enabled=True) as update_handler: + update_handler.run() + + self.assertEqual(1, download_agent.call_count, "Agent update did not execute (no attempts to download the agent") @staticmethod def _get_test_ext_handler_instance(protocol, name="OSTCExtensions.ExampleHandlerLinux", version="1.0.0"): @@ -1897,6 +1830,77 @@ def test_it_should_reset_legacy_blacklisted_agents_on_process_start(self): self.assertFalse(agent.is_blacklisted, "Legacy Agent should not be blacklisted") +class UpdateHandlerRunTestCase(AgentTestCase): + def _test_run(self, autoupdate_enabled=False, check_daemon_running=False, expected_exit_code=0, emit_restart_event=None): + fileutil.write_file(conf.get_agent_pid_file_path(), ustr(42)) + + with patch('azurelinuxagent.ga.update.get_monitor_handler') as mock_monitor: + with patch('azurelinuxagent.ga.remoteaccess.get_remote_access_handler') as mock_ra_handler: + with patch('azurelinuxagent.ga.update.get_env_handler') as mock_env: + with patch('azurelinuxagent.ga.update.get_collect_logs_handler') as mock_collect_logs: + with patch('azurelinuxagent.ga.update.get_send_telemetry_events_handler') as mock_telemetry_send_events: + with patch('azurelinuxagent.ga.update.get_collect_telemetry_events_handler') as mock_event_collector: + with patch('azurelinuxagent.ga.update.initialize_event_logger_vminfo_common_parameters'): + with patch('azurelinuxagent.ga.update.is_log_collection_allowed', return_value=True): + with mock_wire_protocol(DATA_FILE) as protocol: + mock_exthandlers_handler = Mock() + with mock_update_handler( + protocol, + exthandlers_handler=mock_exthandlers_handler, + remote_access_handler=mock_ra_handler, + autoupdate_enabled=autoupdate_enabled, + check_daemon_running=check_daemon_running + ) as update_handler: + + if emit_restart_event is not None: + update_handler._emit_restart_event = emit_restart_event + + if isinstance(os.getppid, MagicMock): + update_handler.run() + else: + with patch('os.getppid', return_value=42): + update_handler.run() + + self.assertEqual(1, mock_monitor.call_count) + self.assertEqual(1, mock_env.call_count) + self.assertEqual(1, mock_collect_logs.call_count) + self.assertEqual(1, mock_telemetry_send_events.call_count) + self.assertEqual(1, mock_event_collector.call_count) + self.assertEqual(expected_exit_code, update_handler.get_exit_code()) + + if update_handler.get_iterations_completed() > 0: # some test cases exit before executing extensions or remote access + self.assertEqual(1, mock_exthandlers_handler.run.call_count) + self.assertEqual(1, mock_ra_handler.run.call_count) + + return update_handler + + def test_run(self): + self._test_run() + + def test_run_stops_if_update_available(self): + with patch('azurelinuxagent.ga.update.UpdateHandler._download_agent_if_upgrade_available', return_value=True): + update_handler = self._test_run(autoupdate_enabled=True) + self.assertEqual(0, update_handler.get_iterations_completed()) + + def test_run_stops_if_orphaned(self): + with patch('os.getppid', return_value=1): + update_handler = self._test_run(check_daemon_running=True) + self.assertEqual(0, update_handler.get_iterations_completed()) + + def test_run_clears_sentinel_on_successful_exit(self): + update_handler = self._test_run() + self.assertFalse(os.path.isfile(update_handler._sentinel_file_path())) + + def test_run_leaves_sentinel_on_unsuccessful_exit(self): + with patch('azurelinuxagent.ga.update.UpdateHandler._download_agent_if_upgrade_available', side_effect=Exception): + update_handler = self._test_run(autoupdate_enabled=True,expected_exit_code=1) + self.assertTrue(os.path.isfile(update_handler._sentinel_file_path())) + + def test_run_emits_restart_event(self): + update_handler = self._test_run(emit_restart_event=Mock()) + self.assertEqual(1, update_handler._emit_restart_event.call_count) + + class TestAgentUpgrade(UpdateTestCase): @contextlib.contextmanager From a2ce6c80f3d7251cf2242c6d2b05967eb80fe5ef Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Wed, 20 Apr 2022 16:49:32 -0700 Subject: [PATCH 08/58] enforce extension cpu limits (#2555) * enforce ext cpu limits * fix UTs --- azurelinuxagent/common/cgroupconfigurator.py | 22 ++++++++++++++++---- azurelinuxagent/ga/exthandlers.py | 6 +++--- tests/common/test_cgroupconfigurator.py | 22 +++++++++++++++++--- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index 866e4f866f..7dc0a80a99 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -56,6 +56,7 @@ Before=slices.target [Slice] CPUAccounting=yes +CPUQuota={cpu_quota} """ LOGCOLLECTOR_SLICE = "azure-walinuxagent-logcollector.slice" # More info on resource limits properties in systemd here: @@ -762,21 +763,22 @@ def start_extension_command(self, extension_name, command, cmd_name, timeout, sh process = subprocess.Popen(command, shell=shell, cwd=cwd, env=env, stdout=stdout, stderr=stderr, preexec_fn=os.setsid) # pylint: disable=W1509 return handle_process_completion(process=process, command=command, timeout=timeout, stdout=stdout, stderr=stderr, error_code=error_code) - def setup_extension_slice(self, extension_name): + def setup_extension_slice(self, extension_name, cpu_quota): """ Each extension runs under its own slice (Ex "Microsoft.CPlat.Extension.slice"). All the slices for extensions are grouped under "azure-vmextensions.slice. This method ensures that the extension slice is created. Setup should create under /lib/systemd/system if it is not exist. - TODO: set cpu and memory quotas + TODO: set memory quotas """ if self.enabled(): unit_file_install_path = systemd.get_unit_file_install_path() extension_slice_path = os.path.join(unit_file_install_path, SystemdCgroupsApi.get_extension_slice_name(extension_name)) try: - slice_contents = _EXTENSION_SLICE_CONTENTS.format(extension_name=extension_name) + cpu_quota = str(cpu_quota) + "%" if cpu_quota is not None else "" + slice_contents = _EXTENSION_SLICE_CONTENTS.format(extension_name=extension_name, cpu_quota=cpu_quota) CGroupConfigurator._Impl.__create_unit_file(extension_slice_path, slice_contents) except Exception as exception: _log_cgroup_warning("Failed to create unit files for the extension slice: {0}", ustr(exception)) @@ -800,7 +802,7 @@ def set_extension_services_cpu_memory_quota(self, services_list): Each extension service will have name, systemd path and it's quotas. This method ensures that drop-in files are created under service.d folder if quotas given. ex: /lib/systemd/system/extension.service.d/11-CPUAccounting.conf - TODO: set cpu and memory quotas + TODO: set memory quotas """ if self.enabled() and services_list is not None: for service in services_list: @@ -813,6 +815,13 @@ def set_extension_services_cpu_memory_quota(self, services_list): _DROP_IN_FILE_CPU_ACCOUNTING) files_to_create.append((drop_in_file_cpu_accounting, _DROP_IN_FILE_CPU_ACCOUNTING_CONTENTS)) + cpu_quota = service.get('cpuQuotaPercentage', None) + if cpu_quota is not None: + cpu_quota = str(cpu_quota) + "%" + drop_in_file_cpu_quota = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_QUOTA) + cpu_quota_contents = _DROP_IN_FILE_CPU_QUOTA_CONTENTS_FORMAT.format(cpu_quota) + files_to_create.append((drop_in_file_cpu_quota, cpu_quota_contents)) + self.__create_all_files(files_to_create) # reload the systemd configuration; the new unit will be used once the service restarts @@ -836,6 +845,11 @@ def remove_extension_services_drop_in_files(self, services_list): drop_in_file_cpu_accounting = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_ACCOUNTING) files_to_cleanup.append(drop_in_file_cpu_accounting) + cpu_quota = service.get('cpuQuotaPercentage', None) + if cpu_quota is not None: + drop_in_file_cpu_quota = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_QUOTA) + files_to_cleanup.append(drop_in_file_cpu_quota) + CGroupConfigurator._Impl.__cleanup_all_files(files_to_cleanup) _log_cgroup_info("Drop in files removed for {0}".format(service_name)) diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py index 13d0ca3e09..edbb9c3db0 100644 --- a/azurelinuxagent/ga/exthandlers.py +++ b/azurelinuxagent/ga/exthandlers.py @@ -1361,7 +1361,7 @@ def set_extension_resource_limits(self): man = self.load_manifest() resource_limits = man.get_resource_limits(extension_name, self.ext_handler.version) CGroupConfigurator.get_instance().setup_extension_slice( - extension_name=extension_name) + extension_name=extension_name, cpu_quota=resource_limits.get_extension_slice_cpu_quota()) CGroupConfigurator.get_instance().set_extension_services_cpu_memory_quota(resource_limits.get_service_list()) def create_status_file_if_not_exist(self, extension, status, code, operation, message): @@ -2318,12 +2318,12 @@ def __init__(self, data): def get_extension_slice_cpu_quota(self): if self.data is not None: - return self.data.get('cpuQuota', None) + return self.data.get('cpuQuotaPercentage', None) return None def get_extension_slice_memory_quota(self): if self.data is not None: - return self.data.get('memoryQuota', None) + return self.data.get('memoryQuotaInMB', None) return None def get_service_list(self): diff --git a/tests/common/test_cgroupconfigurator.py b/tests/common/test_cgroupconfigurator.py index 56aa3d48ed..6a945c5e18 100644 --- a/tests/common/test_cgroupconfigurator.py +++ b/tests/common/test_cgroupconfigurator.py @@ -186,14 +186,18 @@ def test_setup_extension_slice_should_create_unit_files(self): # get the paths to the mocked files extension_slice_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.extensionslice) - configurator.setup_extension_slice(extension_name="Microsoft.CPlat.Extension") + configurator.setup_extension_slice(extension_name="Microsoft.CPlat.Extension", cpu_quota=5) expected_cpu_accounting = "CPUAccounting=yes" + expected_cpu_quota_percentage = "5%" self.assertTrue(os.path.exists(extension_slice_unit_file), "{0} was not created".format(extension_slice_unit_file)) self.assertTrue(fileutil.findre_in_file(extension_slice_unit_file, expected_cpu_accounting), "CPUAccounting was not set correctly. Expected: {0}. Got:\n{1}".format(expected_cpu_accounting, fileutil.read_file( extension_slice_unit_file))) + self.assertTrue(fileutil.findre_in_file(extension_slice_unit_file, expected_cpu_quota_percentage), + "CPUQuota was not set correctly. Expected: {0}. Got:\n{1}".format(expected_cpu_quota_percentage, fileutil.read_file( + extension_slice_unit_file))) def test_remove_extension_slice_should_remove_unit_files(self): with self._get_cgroup_configurator() as configurator: @@ -583,15 +587,18 @@ def mock_popen(command, *args, **kwargs): def test_it_should_set_extension_services_cpu_memory_quota(self): service_list = [ { - "name": "extension.service" + "name": "extension.service", + "cpuQuotaPercentage": 5 } ] with self._get_cgroup_configurator() as configurator: # get the paths to the mocked files extension_service_cpu_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_accounting) + extension_service_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_quota) configurator.set_extension_services_cpu_memory_quota(service_list) expected_cpu_accounting = "CPUAccounting=yes" + expected_cpu_quota_percentage = "CPUQuota=5%" # create drop in files to set those properties self.assertTrue(os.path.exists(extension_service_cpu_accounting), "{0} was not created".format(extension_service_cpu_accounting)) @@ -599,6 +606,11 @@ def test_it_should_set_extension_services_cpu_memory_quota(self): fileutil.findre_in_file(extension_service_cpu_accounting, expected_cpu_accounting), "CPUAccounting was not enabled. Expected: {0}. Got:\n{1}".format(expected_cpu_accounting, fileutil.read_file(extension_service_cpu_accounting))) + self.assertTrue(os.path.exists(extension_service_cpu_quota), "{0} was not created".format(extension_service_cpu_quota)) + self.assertTrue( + fileutil.findre_in_file(extension_service_cpu_quota, expected_cpu_quota_percentage), + "CPUQuota was not set. Expected: {0}. Got:\n{1}".format(expected_cpu_quota_percentage, fileutil.read_file(extension_service_cpu_quota))) + def test_it_should_set_extension_services_when_quotas_not_defined(self): service_list = [ { @@ -654,15 +666,19 @@ def test_it_should_stop_tracking_extension_services_cgroups(self): def test_it_should_remove_extension_services_drop_in_files(self): service_list = [ { - "name": "extension.service" + "name": "extension.service", + "cpuQuotaPercentage": 5 } ] with self._get_cgroup_configurator() as configurator: extension_service_cpu_accounting = configurator.mocks.get_mapped_path( UnitFilePaths.extension_service_cpu_accounting) + extension_service_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_quota) configurator.remove_extension_services_drop_in_files(service_list) self.assertFalse(os.path.exists(extension_service_cpu_accounting), "{0} should not have been created".format(extension_service_cpu_accounting)) + self.assertFalse(os.path.exists(extension_service_cpu_quota), + "{0} should not have been created".format(extension_service_cpu_quota)) def test_it_should_start_tracking_unit_cgroups(self): From d6678b4055b525d919ee5e37d666e62503bfa8d3 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 26 Apr 2022 11:55:03 -0700 Subject: [PATCH 09/58] Improvements in waagent.log (#2558) (#2564) * Improvements in waagent.log * fix function call * update comment * typo Co-authored-by: narrieta (cherry picked from commit fa8e3708328de50c899e68b6cfc519c1bda40361) --- .../extensions_goal_state_from_vm_settings.py | 2 +- azurelinuxagent/common/protocol/goal_state.py | 29 +++++--- azurelinuxagent/common/utils/archive.py | 9 ++- azurelinuxagent/daemon/main.py | 2 +- azurelinuxagent/ga/exthandlers.py | 3 +- azurelinuxagent/ga/update.py | 36 ++++++---- .../ext_conf-requested_version.xml | 4 +- tests/data/hostgaplugin/ext_conf.xml | 4 +- ...tings-difference_in_required_features.json | 4 +- .../vm_settings-missing_cert.json | 68 +++++++++++++++++++ .../hostgaplugin/vm_settings-out-of-sync.json | 2 +- .../vm_settings-requested_version.json | 4 +- tests/data/hostgaplugin/vm_settings.json | 6 +- tests/protocol/test_goal_state.py | 22 +++++- tests/utils/test_archive.py | 3 +- 15 files changed, 154 insertions(+), 44 deletions(-) create mode 100644 tests/data/hostgaplugin/vm_settings-missing_cert.json diff --git a/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py index ce99a26079..38cca48f1b 100644 --- a/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py +++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py @@ -289,7 +289,7 @@ def _parse_extensions(self, vm_settings): # "settingsSeqNo": 0, # "settings": [ # { - # "protectedSettingsCertThumbprint": "4C4F304667711036E64AF4894B76EB208A863BD4", + # "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", # "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", # "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" # } diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index ae01e4d22c..6ec0a5ab64 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -18,6 +18,7 @@ import os import re import time +import json import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger @@ -145,6 +146,7 @@ def update(self): return if vm_settings_updated: + logger.info('') logger.info("Fetched new vmSettings [HostGAPlugin correlation ID: {0} eTag: {1} source: {2}]", vm_settings.hostga_plugin_correlation_id, vm_settings.etag, vm_settings.source) # Ignore the vmSettings if their source is Fabric (processing a Fabric goal state may require the tenant certificate and the vmSettings don't include it.) if vm_settings is not None and vm_settings.source == GoalStateSource.Fabric: @@ -184,6 +186,17 @@ def update(self): if self._extensions_goal_state is None or most_recent.created_on_timestamp > self._extensions_goal_state.created_on_timestamp: self._extensions_goal_state = most_recent + # For Fast Track goal states, verify that the required certificates are in the goal state + if self.extensions_goal_state.source == GoalStateSource.FastTrack: + for extension in self.extensions_goal_state.extensions: + for settings in extension.settings: + if settings.protectedSettings is None: + continue + certificates = self.certs.summary + if not any(settings.certificateThumbprint == c['thumbprint'] for c in certificates): + message = "Certificate {0} needed by {1} is missing from the goal state".format(settings.certificateThumbprint, extension.name) + add_event(op=WALAEventOperation.VmSettings, message=message, is_success=False) + def _restore_wire_server_goal_state(self, incarnation, xml_text, xml_doc, vm_settings_support_stopped_error): logger.info('The HGAP stopped supporting vmSettings; will fetched the goal state from the WireServer.') self._history = GoalStateHistory(timeutil.create_timestamp(), incarnation) @@ -270,6 +283,7 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): Returns the value of ExtensionsConfig. """ try: + logger.info('') logger.info('Fetching full goal state from the WireServer [incarnation {0}]', incarnation) role_instance = find(xml_doc, "RoleInstance") @@ -300,9 +314,10 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): certs = None certs_uri = findtext(xml_doc, "Certificates") if certs_uri is not None: - # Note that we do not save the certificates to the goal state history xml_text = self._wire_client.fetch_config(certs_uri, self._wire_client.get_header_for_cert()) certs = Certificates(xml_text) + # Save the certificate summary, which includes only the thumbprint but not the certificate itself, to the goal state history + self._history.save_certificates(json.dumps(certs.summary)) remote_access = None remote_access_uri = findtext(container, "RemoteAccessInfo") @@ -349,6 +364,7 @@ def __init__(self, xml_text): class Certificates(object): def __init__(self, xml_text): self.cert_list = CertList() + self.summary = [] # debugging info # Save the certificates local_file = os.path.join(conf.get_lib_dir(), CERTS_FILE_NAME) @@ -428,18 +444,15 @@ def __init__(self, xml_text): tmp_file = prvs[pubkey] prv = "{0}.prv".format(thumbprint) os.rename(tmp_file, os.path.join(conf.get_lib_dir(), prv)) - logger.info("Found private key matching thumbprint {0}".format(thumbprint)) else: # Since private key has *no* matching certificate, # it will not be named correctly logger.warn("Found NO matching cert/thumbprint for private key!") - # Log if any certificates were found without matching private keys - # This can happen (rarely), and is useful to know for debugging - for pubkey in thumbprints: - if not pubkey in prvs: - msg = "Certificate with thumbprint {0} has no matching private key." - logger.info(msg.format(thumbprints[pubkey])) + for pubkey, thumbprint in thumbprints.items(): + has_private_key = pubkey in prvs + logger.info("Downloaded certificate with thumbprint {0} (has private key: {1})".format(thumbprint, has_private_key)) + self.summary.append({"thumbprint": thumbprint, "hasPrivateKey": has_private_key}) for v1_cert in v1_cert_list: cert = Cert() diff --git a/azurelinuxagent/common/utils/archive.py b/azurelinuxagent/common/utils/archive.py index ed8122e970..880a23a119 100644 --- a/azurelinuxagent/common/utils/archive.py +++ b/azurelinuxagent/common/utils/archive.py @@ -42,13 +42,14 @@ _MAX_ARCHIVED_STATES = 50 _CACHE_PATTERNS = [ - re.compile(r"^VmSettings.\d+\.json$"), + re.compile(r"^VmSettings\.\d+\.json$"), re.compile(r"^(.*)\.(\d+)\.(agentsManifest)$", re.IGNORECASE), re.compile(r"^(.*)\.(\d+)\.(manifest\.xml)$", re.IGNORECASE), re.compile(r"^(.*)\.(\d+)\.(xml)$", re.IGNORECASE), re.compile(r"^SharedConfig\.xml$", re.IGNORECASE), re.compile(r"^HostingEnvironmentConfig\.xml$", re.IGNORECASE), - re.compile(r"^RemoteAccess\.xml$", re.IGNORECASE) + re.compile(r"^RemoteAccess\.xml$", re.IGNORECASE), + re.compile(r"^waagent_status\.\d+\.json$"), ] # @@ -69,6 +70,7 @@ _GOAL_STATE_FILE_NAME = "GoalState.xml" _VM_SETTINGS_FILE_NAME = "VmSettings.json" +_CERTIFICATES_FILE_NAME = "Certificates.json" _HOSTING_ENV_FILE_NAME = "HostingEnvironmentConfig.xml" _SHARED_CONF_FILE_NAME = "SharedConfig.xml" _REMOTE_ACCESS_FILE_NAME = "RemoteAccess.xml" @@ -239,6 +241,9 @@ def save_vm_settings(self, text): def save_remote_access(self, text): self.save(text, _REMOTE_ACCESS_FILE_NAME) + def save_certificates(self, text): + self.save(text, _CERTIFICATES_FILE_NAME) + def save_hosting_env(self, text): self.save(text, _HOSTING_ENV_FILE_NAME) diff --git a/azurelinuxagent/daemon/main.py b/azurelinuxagent/daemon/main.py index 91685bc641..c608768a67 100644 --- a/azurelinuxagent/daemon/main.py +++ b/azurelinuxagent/daemon/main.py @@ -64,7 +64,7 @@ def run(self, child_args=None): # # Be aware that telemetry events emitted before that will not include the Container ID. # - logger.info("{0} Version:{1}", AGENT_LONG_NAME, AGENT_VERSION) + logger.info("{0} Version: {1}", AGENT_LONG_NAME, AGENT_VERSION) logger.info("OS: {0} {1}", DISTRO_NAME, DISTRO_VERSION) logger.info("Python: {0}.{1}.{2}", PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO) diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py index edbb9c3db0..b7da7b4f14 100644 --- a/azurelinuxagent/ga/exthandlers.py +++ b/azurelinuxagent/ga/exthandlers.py @@ -308,6 +308,7 @@ def run(self): error = None message = "ProcessExtensionsGoalState started [{0} channel: {1} source: {2} activity: {3} correlation {4} created: {5}]".format( egs.id, egs.channel, egs.source, egs.activity_id, egs.correlation_id, egs.created_on_timestamp) + logger.info('') logger.info(message) add_event(op=WALAEventOperation.ExtensionProcessing, message=message) @@ -319,7 +320,7 @@ def run(self): finally: duration = elapsed_milliseconds(utc_start) if error is None: - message = 'ProcessExtensionsGoalState completed [{0} {1} ms]'.format(egs.id, duration) + message = 'ProcessExtensionsGoalState completed [{0} {1} ms]\n'.format(egs.id, duration) logger.info(message) else: message = 'ProcessExtensionsGoalState failed [{0} {1} ms]\n{2}'.format(egs.id, duration, error) diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index 1bdda2fc44..d8b879ce42 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -54,7 +54,7 @@ from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.networkutil import AddFirewallRules from azurelinuxagent.common.utils.shellutil import CommandError -from azurelinuxagent.common.version import AGENT_NAME, AGENT_DIR_PATTERN, CURRENT_AGENT, \ +from azurelinuxagent.common.version import AGENT_LONG_NAME, AGENT_NAME, AGENT_DIR_PATTERN, CURRENT_AGENT, \ CURRENT_VERSION, DISTRO_NAME, DISTRO_VERSION, get_lis_version, \ has_logrotate, PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO, get_daemon_version from azurelinuxagent.ga.collect_logs import get_collect_logs_handler, is_log_collection_allowed @@ -324,20 +324,11 @@ def run(self, debug=False): """ try: + logger.info("{0} Version: {1}", AGENT_LONG_NAME, CURRENT_AGENT) + logger.info("OS: {0} {1}", DISTRO_NAME, DISTRO_VERSION) + logger.info("Python: {0}.{1}.{2}", PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO) logger.info(u"Agent {0} is running as the goal state agent", CURRENT_AGENT) - # - # Initialize the goal state; some components depend on information provided by the goal state and this - # call ensures the required info is initialized (e.g. telemetry depends on the container ID.) - # - protocol = self.protocol_util.get_protocol() - - self._initialize_goal_state(protocol) - - # Initialize the common parameters for telemetry events - initialize_event_logger_vminfo_common_parameters(protocol) - - # Log OS-specific info. os_info_msg = u"Distro: {dist_name}-{dist_ver}; "\ u"OSUtil: {util_name}; AgentService: {service_name}; "\ u"Python: {py_major}.{py_minor}.{py_micro}; "\ @@ -351,8 +342,20 @@ def run(self, debug=False): py_micro=PY_VERSION_MICRO, systemd=systemd.is_systemd(), lis_ver=get_lis_version(), has_logrotate=has_logrotate() ) - logger.info(os_info_msg) + + # + # Initialize the goal state; some components depend on information provided by the goal state and this + # call ensures the required info is initialized (e.g. telemetry depends on the container ID.) + # + protocol = self.protocol_util.get_protocol() + + self._initialize_goal_state(protocol) + + # Initialize the common parameters for telemetry events + initialize_event_logger_vminfo_common_parameters(protocol) + + # Send telemetry for the OS-specific info. add_event(AGENT_NAME, op=WALAEventOperation.OSInfo, message=os_info_msg) # @@ -738,7 +741,7 @@ def forward_signal(self, signum, frame): return logger.info( - u"Agent {0} forwarding signal {1} to {2}", + u"Agent {0} forwarding signal {1} to {2}\n", CURRENT_AGENT, signum, self.child_agent.name if self.child_agent is not None else CURRENT_AGENT) @@ -827,6 +830,9 @@ def log_if_op_disabled(name, value): if conf.get_autoupdate_enabled(): log_if_int_changed_from_default("Autoupdate.Frequency", conf.get_autoupdate_frequency()) + if conf.get_enable_fast_track(): + log_if_op_disabled("Debug.EnableFastTrack", conf.get_enable_fast_track()) + if conf.get_lib_dir() != "/var/lib/waagent": log_event("lib dir is in an unexpected location: {0}".format(conf.get_lib_dir())) diff --git a/tests/data/hostgaplugin/ext_conf-requested_version.xml b/tests/data/hostgaplugin/ext_conf-requested_version.xml index c3bd928236..bbb8a20feb 100644 --- a/tests/data/hostgaplugin/ext_conf-requested_version.xml +++ b/tests/data/hostgaplugin/ext_conf-requested_version.xml @@ -60,7 +60,7 @@ "runtimeSettings": [ { "handlerSettings": { - "protectedSettingsCertThumbprint": "4C4F304667711036E64AF4894B76EB208A863BD4", + "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": {"GCS_AUTO_CONFIG":true} } @@ -73,7 +73,7 @@ "runtimeSettings": [ { "handlerSettings": { - "protectedSettingsCertThumbprint": "4C4F304667711036E64AF4894B76EB208A863BD4", + "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": {"enableGenevaUpload":true} } diff --git a/tests/data/hostgaplugin/ext_conf.xml b/tests/data/hostgaplugin/ext_conf.xml index eac5d63647..ebd90aa0b2 100644 --- a/tests/data/hostgaplugin/ext_conf.xml +++ b/tests/data/hostgaplugin/ext_conf.xml @@ -58,7 +58,7 @@ "runtimeSettings": [ { "handlerSettings": { - "protectedSettingsCertThumbprint": "4C4F304667711036E64AF4894B76EB208A863BD4", + "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent==", "publicSettings": {"GCS_AUTO_CONFIG":true} } @@ -71,7 +71,7 @@ "runtimeSettings": [ { "handlerSettings": { - "protectedSettingsCertThumbprint": "4C4F304667711036E64AF4894B76EB208A863BD4", + "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent==", "publicSettings": {"enableGenevaUpload":true} } diff --git a/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json b/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json index 9fd2e4f722..5601268706 100644 --- a/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json +++ b/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json @@ -56,7 +56,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4C4F304667711036E64AF4894B76EB208A863BD4", + "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } @@ -76,7 +76,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4C4F304667711036E64AF4894B76EB208A863BD4", + "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"enableGenevaUpload\":true}" } diff --git a/tests/data/hostgaplugin/vm_settings-missing_cert.json b/tests/data/hostgaplugin/vm_settings-missing_cert.json new file mode 100644 index 0000000000..a7192e942d --- /dev/null +++ b/tests/data/hostgaplugin/vm_settings-missing_cert.json @@ -0,0 +1,68 @@ +{ + "hostGAPluginVersion": "1.0.8.124", + "vmSettingsSchemaVersion": "0.0", + "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", + "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", + "inSvdSeqNo": 1, + "extensionsLastModifiedTickCount": 637726657706205299, + "extensionGoalStatesSource": "FastTrack", + "onHold": true, + "statusUploadBlob": { + "statusBlobType": "BlockBlob", + "value": "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" + }, + "inVMMetadata": { + "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", + "resourceGroupName": "rg-dc-86fjzhp", + "vmName": "edp0plkw2b", + "location": "CentralUSEUAP", + "vmId": "86f4ae0a-61f8-48ae-9199-40f402d56864", + "vmSize": "Standard_B2s", + "osType": "Linux" + }, + "requiredFeatures": [ + { + "name": "MultipleExtensionsPerHandler" + } + ], + "gaFamilies": [ + { + "name": "Prod", + "uris": [ + "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", + "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" + ] + }, + { + "name": "Test", + "uris": [ + "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", + "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" + ] + } + ], + "extensionGoalStates": [ + { + "name": "Microsoft.OSTCExtensions.VMAccessForLinux", + "version": "1.5.11", + "location": "https://umsasc25p0kjg0c1dg4b.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml", + "failoverlocation": "https://umsamfwlmfshvxx2lsjm.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml", + "additionalLocations": [ + "https://umsah3cwjlctnmhsvzqv.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml" + ], + "state": "enabled", + "autoUpgrade": false, + "runAsStartupTask": false, + "isJson": true, + "useExactVersion": true, + "settingsSeqNo": 0, + "isMultiConfig": false, + "settings": [ + { + "protectedSettingsCertThumbprint": "59A10F50FFE2A0408D3F03FE336C8FD5716CF25C", + "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpddesZQewdDBgegkxNzA1BgoJkgergres/Microsoft.OSTCExtensions.VMAccessForLinux==" + } + ] + } + ] +} diff --git a/tests/data/hostgaplugin/vm_settings-out-of-sync.json b/tests/data/hostgaplugin/vm_settings-out-of-sync.json index 0aae82031c..1f369ae5bc 100644 --- a/tests/data/hostgaplugin/vm_settings-out-of-sync.json +++ b/tests/data/hostgaplugin/vm_settings-out-of-sync.json @@ -56,7 +56,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4C4F304667711036E64AF4894B76EB208A863BD4", + "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } diff --git a/tests/data/hostgaplugin/vm_settings-requested_version.json b/tests/data/hostgaplugin/vm_settings-requested_version.json index 49e6a27780..98959dd4ec 100644 --- a/tests/data/hostgaplugin/vm_settings-requested_version.json +++ b/tests/data/hostgaplugin/vm_settings-requested_version.json @@ -56,7 +56,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4C4F304667711036E64AF4894B76EB208A863BD4", + "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } @@ -74,7 +74,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4C4F304667711036E64AF4894B76EB208A863BD4", + "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"enableGenevaUpload\":true}" } diff --git a/tests/data/hostgaplugin/vm_settings.json b/tests/data/hostgaplugin/vm_settings.json index 96836e7663..a4ef0f785f 100644 --- a/tests/data/hostgaplugin/vm_settings.json +++ b/tests/data/hostgaplugin/vm_settings.json @@ -56,7 +56,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4C4F304667711036E64AF4894B76EB208A863BD4", + "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent==", "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } @@ -76,7 +76,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4C4F304667711036E64AF4894B76EB208A863BD4", + "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent==", "publicSettings": "{\"enableGenevaUpload\":true}" } @@ -192,7 +192,7 @@ "isMultiConfig": false, "settings": [ { - "protectedSettingsCertThumbprint": "59A10F50FFE2A0408D3F03FE336C8FD5716CF25C", + "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpddesZQewdDBgegkxNzA1BgoJkgergres/Microsoft.OSTCExtensions.VMAccessForLinux==" } ] diff --git a/tests/protocol/test_goal_state.py b/tests/protocol/test_goal_state.py index 65d050d862..8cfa4842a8 100644 --- a/tests/protocol/test_goal_state.py +++ b/tests/protocol/test_goal_state.py @@ -8,6 +8,7 @@ import re import time +from azurelinuxagent.common.event import WALAEventOperation from azurelinuxagent.common.future import httpclient from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateSource, GoalStateChannel from azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config import ExtensionsGoalStateFromExtensionsConfig @@ -105,7 +106,7 @@ def test_instantiating_goal_state_should_save_the_goal_state_to_the_history_dire self._assert_directory_contents( self._find_history_subdirectory("999-888"), - ["GoalState.xml", "ExtensionsConfig.xml", "VmSettings.json", "SharedConfig.xml", "HostingEnvironmentConfig.xml"]) + ["GoalState.xml", "ExtensionsConfig.xml", "VmSettings.json", "Certificates.json", "SharedConfig.xml", "HostingEnvironmentConfig.xml"]) def _find_history_subdirectory(self, tag): matches = glob.glob(os.path.join(self.tmp_dir, ARCHIVE_DIRECTORY_NAME, "*_{0}".format(tag))) @@ -128,7 +129,7 @@ def test_update_should_create_new_history_subdirectories(self): goal_state = GoalState(protocol.client) self._assert_directory_contents( self._find_history_subdirectory("123-654"), - ["GoalState.xml", "ExtensionsConfig.xml", "VmSettings.json", "SharedConfig.xml", "HostingEnvironmentConfig.xml"]) + ["GoalState.xml", "ExtensionsConfig.xml", "VmSettings.json", "Certificates.json", "SharedConfig.xml", "HostingEnvironmentConfig.xml"]) def http_get_handler(url, *_, **__): if HttpRequestPredicates.is_host_plugin_vm_settings_request(url): @@ -140,7 +141,7 @@ def http_get_handler(url, *_, **__): goal_state.update() self._assert_directory_contents( self._find_history_subdirectory("234-654"), - ["GoalState.xml", "ExtensionsConfig.xml", "SharedConfig.xml", "HostingEnvironmentConfig.xml"]) + ["GoalState.xml", "ExtensionsConfig.xml", "Certificates.json", "SharedConfig.xml", "HostingEnvironmentConfig.xml"]) protocol.mock_wire_data.set_etag(987) protocol.set_http_handlers(http_get_handler=None) @@ -358,3 +359,18 @@ def http_get_handler(url, *_, **__): self._assert_goal_state(goal_state, initial_incarnation, channel=GoalStateChannel.WireServer, source=GoalStateSource.Fabric) self.assertEqual(initial_timestamp, goal_state.extensions_goal_state.created_on_timestamp, "The timestamp of the updated goal state is incorrect") self.assertTrue(goal_state.extensions_goal_state.is_outdated, "The updated goal state should be marked as outdated") + + def test_it_should_report_missing_certificates(self): + data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() + data_file["vm_settings"] = "hostgaplugin/vm_settings-missing_cert.json" + + with mock_wire_protocol(data_file) as protocol: + with patch("azurelinuxagent.common.protocol.goal_state.add_event") as add_event: + _ = GoalState(protocol.client) + + expected_message = "Certificate 59A10F50FFE2A0408D3F03FE336C8FD5716CF25C needed by Microsoft.OSTCExtensions.VMAccessForLinux is missing from the goal state" + events = [kwargs for _, kwargs in add_event.call_args_list if kwargs['op'] == WALAEventOperation.VmSettings and kwargs['message'] == expected_message] + + self.assertTrue( + len(events) == 1, + "Missing certificate 59A10F50FFE2A0408D3F03FE336C8FD5716CF25C was note reported. Telemetry: {0}".format([kwargs['message'] for _, kwargs in add_event.call_args_list])) diff --git a/tests/utils/test_archive.py b/tests/utils/test_archive.py index 466d674c79..61214558c5 100644 --- a/tests/utils/test_archive.py +++ b/tests/utils/test_archive.py @@ -135,7 +135,8 @@ def test_purge_legacy_goal_state_history(self): 'Microsoft.Azure.Extensions.CustomScript.1.xml', 'SharedConfig.xml', 'HostingEnvironmentConfig.xml', - 'RemoteAccess.xml' + 'RemoteAccess.xml', + 'waagent_status.1.json' ] legacy_files = [os.path.join(self.tmp_dir, f) for f in legacy_files] for f in legacy_files: From 03df04a07c64439b51a20626471fc0ceabecc6a5 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 26 Apr 2022 12:57:14 -0700 Subject: [PATCH 10/58] Change format of history items (#2560) (#2565) * Change format of history directory * Update message; fix typo * py2 compat * py2 compat Co-authored-by: narrieta (cherry picked from commit 93a2564fd6509a93ddab1417507a61f40ba56424) --- azurelinuxagent/common/protocol/goal_state.py | 9 +++++---- azurelinuxagent/common/utils/archive.py | 19 +++++++++++-------- azurelinuxagent/common/utils/timeutil.py | 11 ++++++++++- azurelinuxagent/ga/update.py | 5 ++--- tests/protocol/test_goal_state.py | 2 +- tests/utils/test_archive.py | 10 ++++++++-- 6 files changed, 37 insertions(+), 19 deletions(-) diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index 6ec0a5ab64..da0de9a032 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -15,6 +15,7 @@ # limitations under the License. # # Requires Python 2.6+ and Openssl 1.0+ +import datetime import os import re import time @@ -31,7 +32,7 @@ from azurelinuxagent.common.protocol.extensions_goal_state import VmSettingsParseError, GoalStateSource from azurelinuxagent.common.protocol.hostplugin import VmSettingsNotSupported, VmSettingsSupportStopped from azurelinuxagent.common.protocol.restapi import Cert, CertList, RemoteAccessUser, RemoteAccessUsersList -from azurelinuxagent.common.utils import fileutil, timeutil +from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils.archive import GoalStateHistory from azurelinuxagent.common.utils.cryptutil import CryptUtil from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext, getattrib @@ -131,7 +132,7 @@ def update(self): # # Fetch the goal state from both the HGAP and the WireServer # - timestamp = timeutil.create_timestamp() + timestamp = datetime.datetime.utcnow() incarnation, xml_text, xml_doc = GoalState._fetch_goal_state(self._wire_client) goal_state_updated = incarnation != self._incarnation @@ -199,7 +200,7 @@ def update(self): def _restore_wire_server_goal_state(self, incarnation, xml_text, xml_doc, vm_settings_support_stopped_error): logger.info('The HGAP stopped supporting vmSettings; will fetched the goal state from the WireServer.') - self._history = GoalStateHistory(timeutil.create_timestamp(), incarnation) + self._history = GoalStateHistory(datetime.datetime.utcnow(), incarnation) self._history.save_goal_state(xml_text) self._extensions_goal_state = self._fetch_full_wire_server_goal_state(incarnation, xml_doc) if self._extensions_goal_state.created_on_timestamp < vm_settings_support_stopped_error.timestamp: @@ -270,7 +271,7 @@ def _fetch_vm_settings(wire_client): except VmSettingsParseError as exception: # ensure we save the vmSettings if there were parsing errors, but save them only once per ETag if not GoalStateHistory.tag_exists(exception.etag): - GoalStateHistory(timeutil.create_timestamp(), exception.etag).save_vm_settings(exception.vm_settings_text) + GoalStateHistory(datetime.datetime.utcnow(), exception.etag).save_vm_settings(exception.vm_settings_text) raise return vm_settings, vm_settings_updated diff --git a/azurelinuxagent/common/utils/archive.py b/azurelinuxagent/common/utils/archive.py index 880a23a119..6123fdb0d2 100644 --- a/azurelinuxagent/common/utils/archive.py +++ b/azurelinuxagent/common/utils/archive.py @@ -9,7 +9,7 @@ import azurelinuxagent.common.logger as logger import azurelinuxagent.common.conf as conf -from azurelinuxagent.common.utils import fileutil +from azurelinuxagent.common.utils import fileutil, timeutil # pylint: disable=W0105 @@ -58,13 +58,15 @@ # 2018-04-06T08:21:37.142697.zip # 2018-04-06T08:21:37.142697_incarnation_N # 2018-04-06T08:21:37.142697_incarnation_N.zip +# 2018-04-06T08:21:37.142697_N-M +# 2018-04-06T08:21:37.142697_N-M.zip # # Current names # -# 2018-04-06T08:21:37.142697_N-M -# 2018-04-06T08:21:37.142697_N-M.zip +# 2018-04-06T08-21-37__N-M +# 2018-04-06T08-21-37__N-M.zip # -_ARCHIVE_BASE_PATTERN = r"\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}\.\d+((_incarnation)?_(\d+|status)(-\d+)?)?" +_ARCHIVE_BASE_PATTERN = r"\d{4}\-\d{2}\-\d{2}T\d{2}[:-]\d{2}[:-]\d{2}(\.\d+)?((_incarnation)?_+(\d+|status)(-\d+)?)?" _ARCHIVE_PATTERNS_DIRECTORY = re.compile(r'^{0}$'.format(_ARCHIVE_BASE_PATTERN)) _ARCHIVE_PATTERNS_ZIP = re.compile(r'^{0}\.zip$'.format(_ARCHIVE_BASE_PATTERN)) @@ -163,7 +165,6 @@ def purge(self): newest ones. Also, clean up any legacy history files. """ states = self._get_archive_states() - states.sort(reverse=True) for state in states[_MAX_ARCHIVED_STATES:]: state.delete() @@ -184,7 +185,6 @@ def purge_legacy_goal_state_history(): def archive(self): states = self._get_archive_states() - states.sort(reverse=True) if len(states) > 0: # Skip the most recent goal state, since it may still be in use @@ -203,13 +203,16 @@ def _get_archive_states(self): if match is not None: states.append(StateZip(full_path, match.group(0))) + states.sort(key=lambda state: os.path.getctime(state._path), reverse=True) + return states class GoalStateHistory(object): - def __init__(self, timestamp, tag): + def __init__(self, time, tag): self._errors = False - self._root = os.path.join(conf.get_lib_dir(), ARCHIVE_DIRECTORY_NAME, "{0}_{1}".format(timestamp, tag) if tag is not None else timestamp) + timestamp = timeutil.create_history_timestamp(time) + self._root = os.path.join(conf.get_lib_dir(), ARCHIVE_DIRECTORY_NAME, "{0}__{1}".format(timestamp, tag) if tag is not None else timestamp) @staticmethod def tag_exists(tag): diff --git a/azurelinuxagent/common/utils/timeutil.py b/azurelinuxagent/common/utils/timeutil.py index c4dd755a0c..c8fa37647c 100644 --- a/azurelinuxagent/common/utils/timeutil.py +++ b/azurelinuxagent/common/utils/timeutil.py @@ -5,7 +5,7 @@ def create_timestamp(dt=None): """ - Returns a string with the given datetime iso format. If no datetime is given as parameter, it + Returns a string with the given datetime in iso format. If no datetime is given as parameter, it uses datetime.utcnow(). """ if dt is None: @@ -13,6 +13,15 @@ def create_timestamp(dt=None): return dt.isoformat() +def create_history_timestamp(dt=None): + """ + Returns a string with the given datetime formatted as a timestamp for the agent's history folder + """ + if dt is None: + dt = datetime.datetime.utcnow() + return dt.strftime('%Y-%m-%dT%H-%M-%S') + + def datetime_to_ticks(dt): """ Converts 'dt', a datetime, to the number of ticks (1 tick == 1/10000000 sec) since datetime.min (0001-01-01 00:00:00). diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index d8b879ce42..1a453c8c05 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -54,7 +54,7 @@ from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.networkutil import AddFirewallRules from azurelinuxagent.common.utils.shellutil import CommandError -from azurelinuxagent.common.version import AGENT_LONG_NAME, AGENT_NAME, AGENT_DIR_PATTERN, CURRENT_AGENT, \ +from azurelinuxagent.common.version import AGENT_LONG_NAME, AGENT_NAME, AGENT_DIR_PATTERN, CURRENT_AGENT, AGENT_VERSION, \ CURRENT_VERSION, DISTRO_NAME, DISTRO_VERSION, get_lis_version, \ has_logrotate, PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO, get_daemon_version from azurelinuxagent.ga.collect_logs import get_collect_logs_handler, is_log_collection_allowed @@ -324,10 +324,9 @@ def run(self, debug=False): """ try: - logger.info("{0} Version: {1}", AGENT_LONG_NAME, CURRENT_AGENT) + logger.info("{0} (Goal State Agent version {1})", AGENT_LONG_NAME, AGENT_VERSION) logger.info("OS: {0} {1}", DISTRO_NAME, DISTRO_VERSION) logger.info("Python: {0}.{1}.{2}", PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO) - logger.info(u"Agent {0} is running as the goal state agent", CURRENT_AGENT) os_info_msg = u"Distro: {dist_name}-{dist_ver}; "\ u"OSUtil: {util_name}; AgentService: {service_name}; "\ diff --git a/tests/protocol/test_goal_state.py b/tests/protocol/test_goal_state.py index 8cfa4842a8..c54a65f9f7 100644 --- a/tests/protocol/test_goal_state.py +++ b/tests/protocol/test_goal_state.py @@ -373,4 +373,4 @@ def test_it_should_report_missing_certificates(self): self.assertTrue( len(events) == 1, - "Missing certificate 59A10F50FFE2A0408D3F03FE336C8FD5716CF25C was note reported. Telemetry: {0}".format([kwargs['message'] for _, kwargs in add_event.call_args_list])) + "Missing certificate 59A10F50FFE2A0408D3F03FE336C8FD5716CF25C was not reported. Telemetry: {0}".format([kwargs['message'] for _, kwargs in add_event.call_args_list])) diff --git a/tests/utils/test_archive.py b/tests/utils/test_archive.py index 61214558c5..0c649c9e24 100644 --- a/tests/utils/test_archive.py +++ b/tests/utils/test_archive.py @@ -67,7 +67,10 @@ def test_archive_should_zip_all_but_the_latest_goal_state_in_the_history_folder( test_directories.append(directory) test_subject = StateArchiver(self.tmp_dir) - test_subject.archive() + # NOTE: StateArchiver sorts the state directories by creation time, but the test files are created too fast and the + # time resolution is too coarse, so instead we mock getctime to simply return the path of the file + with patch("azurelinuxagent.common.utils.archive.os.path.getctime", side_effect=lambda path: path): + test_subject.archive() for directory in test_directories[0:2]: zip_file = directory + ".zip" @@ -110,7 +113,10 @@ def test_archive02(self): self.assertEqual(total, len(os.listdir(self.history_dir))) test_subject = StateArchiver(self.tmp_dir) - test_subject.purge() + # NOTE: StateArchiver sorts the state directories by creation time, but the test files are created too fast and the + # time resolution is too coarse, so instead we mock getctime to simply return the path of the file + with patch("azurelinuxagent.common.utils.archive.os.path.getctime", side_effect=lambda path: path): + test_subject.purge() archived_entries = os.listdir(self.history_dir) self.assertEqual(_MAX_ARCHIVED_STATES, len(archived_entries)) From 1b3123b023a42c7d1b9fbe65a09a79bb15368655 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 26 Apr 2022 16:18:21 -0700 Subject: [PATCH 11/58] Update log messages (#2567) Co-authored-by: narrieta --- dcr/scenario_utils/check_waagent_log.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dcr/scenario_utils/check_waagent_log.py b/dcr/scenario_utils/check_waagent_log.py index bff1a3b295..cfc6668bcd 100644 --- a/dcr/scenario_utils/check_waagent_log.py +++ b/dcr/scenario_utils/check_waagent_log.py @@ -96,23 +96,23 @@ def check_waagent_log_for_errors(waagent_log=AGENT_LOG_FILE, ignore=None): 'if': lambda log_line: log_line.level == "WARNING" and log_line.who == "Daemon" }, # - # 2022-02-09T04:50:37.384810Z WARNING ExtHandler ExtHandler An error occurred while retrieving the goal state: [ProtocolError] GET vmSettings [correlation ID: 2bed9b62-188e-4668-b1a8-87c35cfa4927 eTag: 7031887032544600793]: [Internal error in HostGAPlugin] [HTTP Failed] [502: Bad Gateway] b'{ "errorCode": "VMArtifactsProfileBlobContentNotFound", "message": "VM artifacts profile blob has no content in it.", "details": ""}' + # 2022-02-09T04:50:37.384810Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] GET vmSettings [correlation ID: 2bed9b62-188e-4668-b1a8-87c35cfa4927 eTag: 7031887032544600793]: [Internal error in HostGAPlugin] [HTTP Failed] [502: Bad Gateway] b'{ "errorCode": "VMArtifactsProfileBlobContentNotFound", "message": "VM artifacts profile blob has no content in it.", "details": ""}' # # Fetching the goal state may catch the HostGAPlugin in the process of computing the vmSettings. This can be ignored, if the issue persist the log would include additional instances. # { 'message': r"\[ProtocolError\] GET vmSettings.*VMArtifactsProfileBlobContentNotFound", - 'if': lambda log_line: log_line.level == "WARNING" + 'if': lambda log_line: log_line.level == "ERROR" }, # - # 2021-12-29T06:50:49.904601Z WARNING ExtHandler ExtHandler An error occurred while retrieving the goal state: [ProtocolError] Error fetching goal state: [ProtocolError] Error fetching goal state Inner error: [ResourceGoneError] [HTTP Failed] [410: Gone] The page you requested was removed. - # 2022-03-21T02:44:03.770017Z WARNING ExtHandler ExtHandler An error occurred while retrieving the goal state: [ProtocolError] Error fetching goal state Inner error: [ResourceGoneError] Resource is gone + # 2021-12-29T06:50:49.904601Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] Error fetching goal state Inner error: [ResourceGoneError] [HTTP Failed] [410: Gone] The page you requested was removed. + # 2022-03-21T02:44:03.770017Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] Error fetching goal state Inner error: [ResourceGoneError] Resource is gone # 2022-02-16T04:46:50.477315Z WARNING Daemon Daemon Fetching the goal state failed: [ResourceGoneError] [HTTP Failed] [410: Gone] b'\n\n ResourceNotAvailable\n The resource requested is no longer available. Please refresh your cache.\n
\n
' # # ResourceGone can happen if we are fetching one of the URIs in the goal state and a new goal state arrives { - 'message': r"(?s)(An error occurred while retrieving the goal state|Fetching the goal state failed|Error fetching goal state).*(\[ResourceGoneError\]|\[410: Gone\]|Resource is gone)", - 'if': lambda log_line: log_line.level == "WARNING" + 'message': r"(?s)(Fetching the goal state failed|Error fetching goal state|Error fetching the goal state).*(\[ResourceGoneError\]|\[410: Gone\]|Resource is gone)", + 'if': lambda log_line: log_line.level in ("WARNING", "ERROR") }, # # 2022-03-08T03:03:23.036161Z WARNING ExtHandler ExtHandler Fetch failed from [http://168.63.129.16:32526/extensionArtifact]: [HTTP Failed] [400: Bad Request] b'' From a9213e449799ce1178285bb08b36343e827a8a67 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 27 Apr 2022 17:00:20 -0700 Subject: [PATCH 12/58] Refresh goal state when certificates are missing (#2562) (#2566) * Refresh goal state when certificates are missing * Improve error reporting * Fix assert message Co-authored-by: narrieta (cherry picked from commit cd03ff2c22c572fcafb74d645ed5a437a2c3bf5d) --- azurelinuxagent/common/logger.py | 4 + azurelinuxagent/common/protocol/goal_state.py | 100 ++++++++++++------ azurelinuxagent/common/protocol/hostplugin.py | 4 +- azurelinuxagent/common/protocol/wire.py | 12 +-- azurelinuxagent/ga/update.py | 52 +++++---- tests/data/wire/certs-2.xml | 85 +++++++++++++++ tests/data/wire/goal_state.xml | 12 +-- tests/data/wire/goal_state_no_ext.xml | 10 +- tests/data/wire/goal_state_remote_access.xml | 13 +-- tests/ga/test_update.py | 50 ++++----- tests/protocol/HttpRequestPredicates.py | 16 +++ tests/protocol/mockwiredata.py | 28 ++--- tests/protocol/test_goal_state.py | 56 ++++++++-- tests/protocol/test_hostplugin.py | 12 +-- tests/protocol/test_wire.py | 2 +- 15 files changed, 325 insertions(+), 131 deletions(-) create mode 100644 tests/data/wire/certs-2.xml diff --git a/azurelinuxagent/common/logger.py b/azurelinuxagent/common/logger.py index 07e3f23936..3d0dc617d3 100644 --- a/azurelinuxagent/common/logger.py +++ b/azurelinuxagent/common/logger.py @@ -45,6 +45,7 @@ def __init__(self, logger=None, prefix=None): self.logger = self if logger is None else logger self.periodic_messages = {} self.prefix = prefix + self.silent = False def reset_periodic(self): self.logger.periodic_messages = {} @@ -124,6 +125,9 @@ def write_log(log_appender): # pylint: disable=W0612 finally: log_appender.appender_lock = False + if self.silent: + return + # if msg_format is not unicode convert it to unicode if type(msg_format) is not ustr: msg_format = ustr(msg_format, errors="backslashreplace") diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index da0de9a032..edfd9d14f7 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -48,8 +48,16 @@ _GET_GOAL_STATE_MAX_ATTEMPTS = 6 +class GoalStateInconsistentError(ProtocolError): + """ + Indicates an inconsistency in the goal state (e.g. missing tenant certificate) + """ + def __init__(self, msg, inner=None): + super(GoalStateInconsistentError, self).__init__(msg, inner) + + class GoalState(object): - def __init__(self, wire_client): + def __init__(self, wire_client, silent=False): """ Fetches the goal state using the given wire client. @@ -64,6 +72,8 @@ def __init__(self, wire_client): self._wire_client = wire_client self._history = None self._extensions_goal_state = None # populated from vmSettings or extensionsConfig + self.logger = logger.Logger(logger.DEFAULT_LOGGER) + self.logger.silent = silent # These properties hold the goal state from the WireServer and are initialized by self._fetch_full_wire_server_goal_state() self._incarnation = None @@ -75,8 +85,10 @@ def __init__(self, wire_client): self._certs = None self._remote_access = None - self.update() + self.update(silent=silent) + except ProtocolError: + raise except Exception as exception: # We don't log the error here since fetching the goal state is done every few seconds raise ProtocolError(msg="Error fetching goal state", inner=exception) @@ -125,34 +137,47 @@ def update_host_plugin_headers(wire_client): # Fetching the goal state updates the HostGAPlugin so simply trigger the request GoalState._fetch_goal_state(wire_client) - def update(self): + def update(self, silent=False): """ Updates the current GoalState instance fetching values from the WireServer/HostGAPlugin as needed """ + self.logger.silent = silent + + try: + self._update(force_update=False) + except GoalStateInconsistentError as e: + self.logger.warn("Detected an inconsistency in the goal state: {0}", ustr(e)) + self._update(force_update=True) + self.logger.info("The goal state is consistent") + + def _update(self, force_update): # # Fetch the goal state from both the HGAP and the WireServer # timestamp = datetime.datetime.utcnow() + if force_update: + self.logger.info("Refreshing goal state and vmSettings") + incarnation, xml_text, xml_doc = GoalState._fetch_goal_state(self._wire_client) - goal_state_updated = incarnation != self._incarnation + goal_state_updated = force_update or incarnation != self._incarnation if goal_state_updated: - logger.info('Fetched a new incarnation for the WireServer goal state [incarnation {0}]', incarnation) + self.logger.info('Fetched a new incarnation for the WireServer goal state [incarnation {0}]', incarnation) vm_settings, vm_settings_updated = None, False try: - vm_settings, vm_settings_updated = GoalState._fetch_vm_settings(self._wire_client) + vm_settings, vm_settings_updated = GoalState._fetch_vm_settings(self._wire_client, force_update=force_update) except VmSettingsSupportStopped as exception: # If the HGAP stopped supporting vmSettings, we need to use the goal state from the WireServer self._restore_wire_server_goal_state(incarnation, xml_text, xml_doc, exception) return if vm_settings_updated: - logger.info('') - logger.info("Fetched new vmSettings [HostGAPlugin correlation ID: {0} eTag: {1} source: {2}]", vm_settings.hostga_plugin_correlation_id, vm_settings.etag, vm_settings.source) + self.logger.info('') + self.logger.info("Fetched new vmSettings [HostGAPlugin correlation ID: {0} eTag: {1} source: {2}]", vm_settings.hostga_plugin_correlation_id, vm_settings.etag, vm_settings.source) # Ignore the vmSettings if their source is Fabric (processing a Fabric goal state may require the tenant certificate and the vmSettings don't include it.) if vm_settings is not None and vm_settings.source == GoalStateSource.Fabric: if vm_settings_updated: - logger.info("The vmSettings originated via Fabric; will ignore them.") + self.logger.info("The vmSettings originated via Fabric; will ignore them.") vm_settings, vm_settings_updated = None, False # If neither goal state has changed we are done with the update @@ -187,19 +212,30 @@ def update(self): if self._extensions_goal_state is None or most_recent.created_on_timestamp > self._extensions_goal_state.created_on_timestamp: self._extensions_goal_state = most_recent - # For Fast Track goal states, verify that the required certificates are in the goal state + # + # For Fast Track goal states, verify that the required certificates are in the goal state. + # + # Some scenarios can produce inconsistent goal states. For example, during hibernation/resume, the Fabric goal state changes (the + # tenant certificate is re-generated when the VM is restarted) *without* the incarnation necessarily changing (e.g. if the incarnation + # is 1 before the hibernation; on resume the incarnation is set to 1 even though the goal state has a new certificate). If a Fast + # Track goal state comes after that, the extensions will need the new certificate. The Agent needs to refresh the goal state in that + # case, to ensure it fetches the new certificate. + # if self.extensions_goal_state.source == GoalStateSource.FastTrack: - for extension in self.extensions_goal_state.extensions: - for settings in extension.settings: - if settings.protectedSettings is None: - continue - certificates = self.certs.summary - if not any(settings.certificateThumbprint == c['thumbprint'] for c in certificates): - message = "Certificate {0} needed by {1} is missing from the goal state".format(settings.certificateThumbprint, extension.name) - add_event(op=WALAEventOperation.VmSettings, message=message, is_success=False) + self._check_certificates() + + def _check_certificates(self): + for extension in self.extensions_goal_state.extensions: + for settings in extension.settings: + if settings.protectedSettings is None: + continue + certificates = self.certs.summary + if not any(settings.certificateThumbprint == c['thumbprint'] for c in certificates): + message = "Certificate {0} needed by {1} is missing from the goal state".format(settings.certificateThumbprint, extension.name) + raise GoalStateInconsistentError(message) def _restore_wire_server_goal_state(self, incarnation, xml_text, xml_doc, vm_settings_support_stopped_error): - logger.info('The HGAP stopped supporting vmSettings; will fetched the goal state from the WireServer.') + self.logger.info('The HGAP stopped supporting vmSettings; will fetched the goal state from the WireServer.') self._history = GoalStateHistory(datetime.datetime.utcnow(), incarnation) self._history.save_goal_state(xml_text) self._extensions_goal_state = self._fetch_full_wire_server_goal_state(incarnation, xml_doc) @@ -207,7 +243,7 @@ def _restore_wire_server_goal_state(self, incarnation, xml_text, xml_doc, vm_set self._extensions_goal_state.is_outdated = True msg = "Fetched a Fabric goal state older than the most recent FastTrack goal state; will skip it.\nFabric: {0}\nFastTrack: {1}".format( self._extensions_goal_state.created_on_timestamp, vm_settings_support_stopped_error.timestamp) - logger.info(msg) + self.logger.info(msg) add_event(op=WALAEventOperation.VmSettings, message=msg, is_success=True) def save_to_history(self, data, file_name): @@ -249,7 +285,7 @@ def _fetch_goal_state(wire_client): return incarnation, xml_text, xml_doc @staticmethod - def _fetch_vm_settings(wire_client): + def _fetch_vm_settings(wire_client, force_update=False): """ Issues an HTTP request (HostGAPlugin) for the vm settings and returns the response as an ExtensionsGoalState. """ @@ -258,11 +294,11 @@ def _fetch_vm_settings(wire_client): if conf.get_enable_fast_track(): try: try: - vm_settings, vm_settings_updated = wire_client.get_host_plugin().fetch_vm_settings() + vm_settings, vm_settings_updated = wire_client.get_host_plugin().fetch_vm_settings(force_update=force_update) except ResourceGoneError: # retry after refreshing the HostGAPlugin GoalState.update_host_plugin_headers(wire_client) - vm_settings, vm_settings_updated = wire_client.get_host_plugin().fetch_vm_settings() + vm_settings, vm_settings_updated = wire_client.get_host_plugin().fetch_vm_settings(force_update=force_update) except VmSettingsSupportStopped: raise @@ -284,8 +320,8 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): Returns the value of ExtensionsConfig. """ try: - logger.info('') - logger.info('Fetching full goal state from the WireServer [incarnation {0}]', incarnation) + self.logger.info('') + self.logger.info('Fetching full goal state from the WireServer [incarnation {0}]', incarnation) role_instance = find(xml_doc, "RoleInstance") role_instance_id = findtext(role_instance, "InstanceId") @@ -317,7 +353,11 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): if certs_uri is not None: xml_text = self._wire_client.fetch_config(certs_uri, self._wire_client.get_header_for_cert()) certs = Certificates(xml_text) - # Save the certificate summary, which includes only the thumbprint but not the certificate itself, to the goal state history + # Log and save the certificates summary (i.e. the thumbprint but not the certificate itself) to the goal state history + for c in certs.summary: + logger.info("Downloaded certificate {0}".format(c)) + if len(certs.warnings) > 0: + logger.warn(certs.warnings) self._history.save_certificates(json.dumps(certs.summary)) remote_access = None @@ -339,10 +379,10 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): return extensions_config except Exception as exception: - logger.warn("Fetching the goal state failed: {0}", ustr(exception)) + self.logger.warn("Fetching the goal state failed: {0}", ustr(exception)) raise ProtocolError(msg="Error fetching goal state", inner=exception) finally: - logger.info('Fetch goal state completed') + self.logger.info('Fetch goal state completed') class HostingEnv(object): @@ -366,6 +406,7 @@ class Certificates(object): def __init__(self, xml_text): self.cert_list = CertList() self.summary = [] # debugging info + self.warnings = [] # Save the certificates local_file = os.path.join(conf.get_lib_dir(), CERTS_FILE_NAME) @@ -448,11 +489,10 @@ def __init__(self, xml_text): else: # Since private key has *no* matching certificate, # it will not be named correctly - logger.warn("Found NO matching cert/thumbprint for private key!") + self.warnings.append("Found NO matching cert/thumbprint for private key!") for pubkey, thumbprint in thumbprints.items(): has_private_key = pubkey in prvs - logger.info("Downloaded certificate with thumbprint {0} (has private key: {1})".format(thumbprint, has_private_key)) self.summary.append({"thumbprint": thumbprint, "hasPrivateKey": has_private_key}) for v1_cert in v1_cert_list: diff --git a/azurelinuxagent/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py index 81b9062566..5f795dca3e 100644 --- a/azurelinuxagent/common/protocol/hostplugin.py +++ b/azurelinuxagent/common/protocol/hostplugin.py @@ -453,7 +453,7 @@ def get_fast_track_timestamp(): HostPluginProtocol._get_fast_track_state_file(), ustr(e)) return timeutil.create_timestamp(datetime.datetime.utcnow()) - def fetch_vm_settings(self): + def fetch_vm_settings(self, force_update=False): """ Queries the vmSettings from the HostGAPlugin and returns an (ExtensionsGoalState, bool) tuple with the vmSettings and a boolean indicating if they are an updated (True) or a cached value (False). @@ -491,7 +491,7 @@ def format_message(msg): # Raise VmSettingsNotSupported directly instead of using raise_not_supported() to avoid resetting the timestamp for the next check raise VmSettingsNotSupported() - etag = None if self._cached_vm_settings is None else self._cached_vm_settings.etag + etag = None if force_update or self._cached_vm_settings is None else self._cached_vm_settings.etag correlation_id = str(uuid.uuid4()) self._vm_settings_error_reporter.report_request() diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py index 40e58cc0f5..7923bea75d 100644 --- a/azurelinuxagent/common/protocol/wire.py +++ b/azurelinuxagent/common/protocol/wire.py @@ -83,8 +83,8 @@ def detect(self): logger.info('Initializing goal state during protocol detection') self.client.update_goal_state(force_update=True) - def update_goal_state(self): - self.client.update_goal_state() + def update_goal_state(self, silent=False): + self.client.update_goal_state(silent=silent) def update_host_plugin_from_goal_state(self): self.client.update_host_plugin_from_goal_state() @@ -759,18 +759,18 @@ def update_host_plugin(self, container_id, role_config_name): self._host_plugin.update_container_id(container_id) self._host_plugin.update_role_config_name(role_config_name) - def update_goal_state(self, force_update=False): + def update_goal_state(self, force_update=False, silent=False): """ Updates the goal state if the incarnation or etag changed or if 'force_update' is True """ try: if force_update: - logger.info("Forcing an update of the goal state..") + logger.info("Forcing an update of the goal state.") if self._goal_state is None or force_update: - self._goal_state = GoalState(self) + self._goal_state = GoalState(self, silent=silent) else: - self._goal_state.update() + self._goal_state.update(silent=silent) except ProtocolError: raise diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index 1a453c8c05..c800ce4700 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -168,7 +168,8 @@ def __init__(self): # these members are used to avoid reporting errors too frequently self._heartbeat_update_goal_state_error_count = 0 - self._last_try_update_goal_state_failed = False + self._update_goal_state_error_count = 0 + self._update_goal_state_last_error_report = datetime.min self._report_status_last_failed_goal_state = None # incarnation of the last goal state that has been fully processed @@ -481,13 +482,16 @@ def _try_update_goal_state(self, protocol): Attempts to update the goal state and returns True on success or False on failure, sending telemetry events about the failures. """ try: - protocol.update_goal_state() + max_errors_to_log = 3 + + protocol.update_goal_state(silent=self._update_goal_state_error_count >= max_errors_to_log) self._goal_state = protocol.get_goal_state() - if self._last_try_update_goal_state_failed: - self._last_try_update_goal_state_failed = False - message = u"Retrieving the goal state recovered from previous errors" + if self._update_goal_state_error_count > 0: + self._update_goal_state_error_count = 0 + message = u"Fetching the goal state recovered from previous errors. Fetched {0} (certificates: {1})".format( + self._goal_state.extensions_goal_state.id, self._goal_state.certs.summary) add_event(AGENT_NAME, op=WALAEventOperation.FetchGoalState, version=CURRENT_VERSION, is_success=True, message=message, log_event=False) logger.info(message) @@ -497,15 +501,21 @@ def _try_update_goal_state(self, protocol): self._supports_fast_track = False except Exception as e: - if not self._last_try_update_goal_state_failed: - self._last_try_update_goal_state_failed = True - message = u"An error occurred while retrieving the goal state: {0}".format(textutil.format_exception(e)) - logger.warn(message) - add_event(AGENT_NAME, op=WALAEventOperation.FetchGoalState, version=CURRENT_VERSION, is_success=False, message=message, log_event=False) - message = u"Attempts to retrieve the goal state are failing: {0}".format(ustr(e)) - logger.periodic_warn(logger.EVERY_SIX_HOURS, "[PERIODIC] {0}".format(message)) + self._update_goal_state_error_count += 1 self._heartbeat_update_goal_state_error_count += 1 + if self._update_goal_state_error_count <= max_errors_to_log: + message = u"Error fetching the goal state: {0}".format(textutil.format_exception(e)) + logger.error(message) + add_event(op=WALAEventOperation.FetchGoalState, is_success=False, message=message, log_event=False) + self._update_goal_state_last_error_report = datetime.now() + else: + if self._update_goal_state_last_error_report + timedelta(hours=6) > datetime.now(): + self._update_goal_state_last_error_report = datetime.now() + message = u"Fetching the goal state is still failing: {0}".format(textutil.format_exception(e)) + logger.error(message) + add_event(op=WALAEventOperation.FetchGoalState, is_success=False, message=message, log_event=False) return False + return True def __update_guest_agent(self, protocol): @@ -559,8 +569,8 @@ def handle_updates_for_requested_version(): raise AgentUpgradeExitException( "Exiting current process to {0} to the request Agent version {1}".format(prefix, requested_version)) - # Ignore new agents if updating is disabled - if not conf.get_autoupdate_enabled(): + # Skip the update if there is no goal state yet or auto-update is disabled + if self._goal_state is None or not conf.get_autoupdate_enabled(): return False if self._download_agent_if_upgrade_available(protocol): @@ -600,11 +610,14 @@ def _process_goal_state(self, exthandlers_handler, remote_access_handler): protocol = exthandlers_handler.protocol # update self._goal_state - self._try_update_goal_state(protocol) - - # Update the Guest Agent if a new version is available - if self._goal_state is not None: + if not self._try_update_goal_state(protocol): + # agent updates and status reporting should be done even when the goal state is not updated self.__update_guest_agent(protocol) + self._report_status(exthandlers_handler) + return + + # check for agent updates + self.__update_guest_agent(protocol) if self._processing_new_extensions_goal_state(): if not self._extensions_summary.converged: @@ -620,8 +633,7 @@ def _process_goal_state(self, exthandlers_handler, remote_access_handler): # Note: Monitor thread periodically checks this in addition to here. CGroupConfigurator.get_instance().check_cgroups(cgroup_metrics=[]) - # always report status, even if the goal state did not change - # do it before processing the remote access, since that operation can take a long time + # report status before processing the remote access, since that operation can take a long time self._report_status(exthandlers_handler) if self._processing_new_incarnation(): diff --git a/tests/data/wire/certs-2.xml b/tests/data/wire/certs-2.xml new file mode 100644 index 0000000000..66a231ee87 --- /dev/null +++ b/tests/data/wire/certs-2.xml @@ -0,0 +1,85 @@ + + + 2012-11-30 + 5 + Pkcs7BlobWithPfxContents + MIIOgwYJKoZIhvcNAQcDoIIOdDCCDnACAQIxggEwMIIBLAIBAoAUiF8ZYMs9mMa8 +QOEMxDaIhGza+0IwDQYJKoZIhvcNAQEBBQAEggEAQW7GyeRVEhHSU1/dzV0IndH0 +rDQk+27MvlsWTcpNcgGFtfRYxu5bzmp0+DoimX3pRBlSFOpMJ34jpg4xs78EsSWH +FRhCf3EGuEUBHo6yR8FhXDTuS7kZ0UmquiCI2/r8j8gbaGBNeP8IRizcAYrPMA5S +E8l1uCrw7DHuLscbVni/7UglGaTfFS3BqS5jYbiRt2Qh3p+JPUfm51IG3WCIw/WS +2QHebmHxvMFmAp8AiBWSQJizQBEJ1lIfhhBMN4A7NadMWAe6T2DRclvdrQhJX32k +amOiogbW4HJsL6Hphn7Frrw3CENOdWMAvgQBvZ3EjAXgsJuhBA1VIrwofzlDljCC +DTUGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIxcvw9qx4y0qAgg0QrINXpC23BWT2 +Fb9N8YS3Be9eO3fF8KNdM6qGf0kKR16l/PWyP2L+pZxCcCPk83d070qPdnJK9qpJ +6S1hI80Y0oQnY9VBFrdfkc8fGZHXqm5jNS9G32v/AxYpJJC/qrAQnWuOdLtOZaGL +94GEh3XRagvz1wifv8SRI8B1MzxrpCimeMxHkL3zvJFg9FjLGdrak868feqhr6Nb +pqH9zL7bMq8YP788qTRELUnL72aDzGAM7HEj7V4yu2uD3i3Ryz3bqWaj9IF38Sa0 +6rACBkiNfZBPgExoMUm2GNVyx8hTis2XKRgz4NLh29bBkKrArK9sYDncE9ocwrrX +AQ99yn03Xv6TH8bRp0cSj4jzBXc5RFsUQG/LxzJVMjvnkDbwNE41DtFiYz5QVcv1 +cMpTH16YfzSL34a479eNq/4+JAs/zcb2wjBskJipMUU4hNx5fhthvfKwDOQbLTqN +HcP23iPQIhjdUXf6gpu5RGu4JZ0dAMHMHFKvNL6TNejwx/H6KAPp6rCRsYi6QhAb +42SXdZmhAyQsFpGD9U5ieJApqeCHfj9Xhld61GqLJA9+WLVhDPADjqHoAVvrOkKH +OtPegId/lWnCB7p551klAjiEA2/DKxFBIAEhqZpiLl+juZfMXovkdmGxMP4gvNNF +gbS2k5A0IJ8q51gZcH1F56smdAmi5kvhPnFdy/9gqeI/F11F1SkbPVLImP0mmrFi +zQD5JGfEu1psUYvhpOdaYDkmAK5qU5xHSljqZFz5hXNt4ebvSlurHAhunJb2ln3g +AJUHwtZnVBrtYMB0w6fdwYqMxXi4vLeqUiHtIQtbOq32zlSryNPQqG9H0iP9l/G1 +t7oUfr9woI/B0kduaY9jd5Qtkqs1DoyfNMSaPNohUK/CWOTD51qOadzSvK0hJ+At +033PFfv9ilaX6GmzHdEVEanrn9a+BoBCnGnuysHk/8gdswj9OzeCemyIFJD7iObN +rNex3SCf3ucnAejJOA0awaLx88O1XTteUjcFn26EUji6DRK+8JJiN2lXSyQokNeY +ox6Z4hFQDmw/Q0k/iJqe9/Dq4zA0l3Krkpra0DZoWh5kzYUA0g5+Yg6GmRNRa8YG +tuuD6qK1SBEzmCYff6ivjgsXV5+vFBSjEpx2dPEaKdYxtHMOjkttuTi1mr+19dVf +hSltbzfISbV9HafX76dhwZJ0QwsUx+aOW6OrnK8zoQc5AFOXpe9BrrOuEX01qrM0 +KX5tS8Zx5HqDLievjir194oi3r+nAiG14kYlGmOTHshu7keGCgJmzJ0iVG/i+TnV +ZSLyd8OqV1F6MET1ijgR3OPL3kt81Zy9lATWk/DgKbGBkkKAnXO2HUw9U34JFyEy +vEc81qeHci8sT5QKSFHiP3r8EcK8rT5k9CHpnbFmg7VWSMVD0/wRB/C4BiIw357a +xyJ/q1NNvOZVAyYzIzf9TjwREtyeHEo5kS6hyWSn7fbFf3sNGO2I30veWOvE6kFA +HMtF3NplOrTYcM7fAK5zJCBK20oU645TxI8GsICMog7IFidFMdRn4MaXpwAjEZO4 +44m2M+4XyeRCAZhp1Fu4mDiHGqgd44mKtwvLACVF4ygWZnACDpI17X88wMnwL4uU +vgehLZdAE89gvukSCsET1inVBnn/hVenCRbbZ++IGv2XoYvRfeezfOoNUcJXyawQ +JFqN0CRB5pliuCesTO2urn4HSwGGoeBd507pGWZmOAjbNjGswlJJXF0NFnNW/zWw +UFYy+BI9axuhWTSnCXbNbngdNQKHznKe1Lwit6AI3U9jS33pM3W+pwUAQegVdtpG +XT01YgiMCBX+b8B/xcWTww0JbeUwKXudzKsPhQmaA0lubAo04JACMfON8jSZCeRV +TyIzgacxGU6YbEKH4PhYTGl9srcWIT9iGSYD53V7Kyvjumd0Y3Qc3JLnuWZT6Oe3 +uJ4xz9jJtoaTDvPJQNK3igscjZnWZSP8XMJo1/f7vbvD57pPt1Hqdirp1EBQNshk +iX9CUh4fuGFFeHf6MtGxPofbXmvA2GYcFsOez4/2eOTEmo6H3P4Hrya97XHS0dmD +zFSAjzAlacTrn1uuxtxFTikdOwvdmQJJEfyYWCB1lqWOZi97+7nzqyXMLvMgmwug +ZF/xHFMhFTR8Wn7puuwf36JpPQiM4oQ/Lp66zkS4UlKrVsmSXIXudLMg8SQ5WqK8 +DjevEZwsHHaMtfDsnCAhAdRc2jCpyHKKnmhCDdkcdJJEymWKILUJI5PJ3XtiMHnR +Sa35OOICS0lTq4VwhUdkGwGjRoY1GsriPHd6LOt1aom14yJros1h7ta604hSCn4k +zj9p7wY9gfgkXWXNfmarrZ9NNwlHxzgSva+jbJcLmE4GMX5OFHHGlRj/9S1xC2Wf +MY9orzlooGM74NtmRi4qNkFj3dQCde8XRR4wh2IvPUCsr4j+XaoCoc3R5Rn/yNJK +zIkccJ2K14u9X/A0BLXHn5Gnd0tBYcVOqP6dQlW9UWdJC/Xooh7+CVU5cZIxuF/s +Vvg+Xwiv3XqekJRu3cMllJDp5rwe5EWZSmnoAiGKjouKAIszlevaRiD/wT6Zra3c +Wn/1U/sGop6zRscHR7pgI99NSogzpVGThUs+ez7otDBIdDbLpMjktahgWoi1Vqhc +fNZXjA6ob4zTWY/16Ys0YWxHO+MtyWTMP1dnsqePDfYXGUHe8yGxylbcjfrsVYta +4H6eYR86eU3eXB+MpS/iA4jBq4QYWR9QUkd6FDfmRGgWlMXhisPv6Pfnj384NzEV +Emeg7tW8wzWR64EON9iGeGYYa2BBl2FVaayMEoUhthhFcDM1r3/Mox5xF0qnlys4 +goWkMzqbzA2t97bC0KDGzkcHT4wMeiJBLDZ7S2J2nDAEhcTLY0P2zvOB4879pEWx +Bd15AyG1DvNssA5ooaDzKi/Li6NgDuMJ8W7+tmsBwDvwuf2N3koqBeXfKhR4rTqu +Wg1k9fX3+8DzDf0EjtDZJdfWZAynONi1PhZGbNbaMKsQ+6TflkCACInRdOADR5GM +rL7JtrgF1a9n0HD9vk2WGZqKI71tfS8zODkOZDD8aAusD2DOSmVZl48HX/t4i4Wc +3dgi/gkCMrfK3wOujb8tL4zjnlVkM7kzKk0MgHuA1w81zFjeMFvigHes4IWhQVcz +ek3l4bGifI2kzU7bGIi5e/019ppJzGsVcrOE/3z4GS0DJVk6fy7MEMIFx0LhJPlL +T+9HMH85sSYb97PTiMWpfBvNw3FSC7QQT9FC3L8d/XtMY3NvZoc7Fz7cSGaj7NXG +1OgVnAzMunPa3QaduoxMF9346s+4a+FrpRxL/3bb4skojjmmLqP4dsbD1uz0fP9y +xSifnTnrtjumYWMVi+pEb5kR0sTHl0XS7qKRi3SEfv28uh72KdvcufonIA5rnEb5 ++yqAZiqW2OxVsRoVLVODPswP4VIDiun2kCnfkQygPzxlZUeDZur0mmZ3vwC81C1Q +dZcjlukZcqUaxybUloUilqfNeby+2Uig0krLh2+AM4EqR63LeZ/tk+zCitHeRBW0 +wl3Bd7ShBFg6kN5tCJlHf/G6suIJVr+A9BXfwekO9+//CutKakCwmJTUiNWbQbtN +q3aNCnomyD3WjvUbitVO0CWYjZrmMLIsPtzyLQydpT7tjXpHgvwm5GYWdUGnNs4y +NbA262sUl7Ku/GDw1CnFYXbxl+qxbucLtCdSIFR2xUq3rEO1MXlD/txdTxn6ANax +hi9oBg8tHzuGYJFiCDCvbVVTHgWUSnm/EqfclpJzGmxt8g7vbaohW7NMmMQrLBFP +G6qBypgvotx1iJWaHVLNNiXvyqQwTtelNPAUweRoNawBp/5KTwwy/tHeF0gsVQ7y +mFX4umub9YT34Lpe7qUPKNxXzFcUgAf1SA6vyZ20UI7p42S2OT2PrahJ+uO6LQVD ++REhtN0oyS3G6HzAmKkBgw7LcV3XmAr39iSR7mdmoHSJuI9bjveAPhniK+N6uuln +xf17Qnw5NWfr9MXcLli7zqwMglU/1bNirkwVqf/ogi/zQ3JYCo6tFGf/rnGQAORJ +hvOq2SEYXnizPPIH7VrpE16+jUXwgpiQ8TDyeLPmpZVuhXTXiCaJO5lIwmLQqkmg +JqNiT9V44sksNFTGNKgZo5O9rEqfqX4dLjfv6pGJL+MFXD9if4f1JQiXJfhcRcDh +Ff9B6HukgbJ1H96eLUUNj8sL1+WPOqawkS4wg7tVaERE8CW7mqk15dCysn9shSut +I+7JU7+dZsxpj0ownrxuPAFuT8ZlcBPrFzPUwTlW1G0CbuEco8ijfy5IfbyGCn5s +K/0bOfAuNVGoOpLZ1dMki2bGdBwQOQlkLKhAxYcCVQ0/urr1Ab+VXU9kBsIU8ssN +GogKngYpuUV0PHmpzmobielOHLjNqA2v9vQSV3Ed48wRy5OCwLX1+vYmYlggMDGt +wfl+7QbXYf+k5WnELf3IqYvh8ZWexa0= + + \ No newline at end of file diff --git a/tests/data/wire/goal_state.xml b/tests/data/wire/goal_state.xml index 579b5e87ad..0ccff211c9 100644 --- a/tests/data/wire/goal_state.xml +++ b/tests/data/wire/goal_state.xml @@ -15,12 +15,12 @@ b61f93d0-e1ed-40b2-b067-22c243233448.MachineRole_IN_0 Started - http://168.63.129.16:80/hostingenvuri/ - http://168.63.129.16:80/sharedconfiguri/ - http://168.63.129.16:80/certificatesuri/ - http://168.63.129.16:80/extensionsconfiguri/ - http://168.63.129.16:80/fullconfiguri/ - b61f93d0-e1ed-40b2-b067-22c243233448.1.b61f93d0-e1ed-40b2-b067-22c243233448.2.MachineRole_IN_0.xml + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=hostingEnvironmentConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=sharedConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=extensionsConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=fullConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=certificates&incarnation=1 + bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5.bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5._canary.1.xml diff --git a/tests/data/wire/goal_state_no_ext.xml b/tests/data/wire/goal_state_no_ext.xml index ef7e3989e6..e9048daf6e 100644 --- a/tests/data/wire/goal_state_no_ext.xml +++ b/tests/data/wire/goal_state_no_ext.xml @@ -15,11 +15,11 @@ b61f93d0-e1ed-40b2-b067-22c243233448.MachineRole_IN_0 Started - http://168.63.129.16:80/hostingenvuri/ - http://168.63.129.16:80/sharedconfiguri/ - http://168.63.129.16:80/certificatesuri/ - http://168.63.129.16:80/fullconfiguri/ - b61f93d0-e1ed-40b2-b067-22c243233448.1.b61f93d0-e1ed-40b2-b067-22c243233448.2.MachineRole_IN_0.xml + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=hostingEnvironmentConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=sharedConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=fullConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=certificates&incarnation=1 + bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5.bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5._canary.1.xml diff --git a/tests/data/wire/goal_state_remote_access.xml b/tests/data/wire/goal_state_remote_access.xml index c2840645fd..279006f219 100644 --- a/tests/data/wire/goal_state_remote_access.xml +++ b/tests/data/wire/goal_state_remote_access.xml @@ -17,12 +17,13 @@ b61f93d0-e1ed-40b2-b067-22c243233448.MachineRole_IN_0 Started - http://168.63.129.16:80/hostingenvuri/ - http://168.63.129.16:80/sharedconfiguri/ - http://168.63.129.16:80/certificatesuri/ - http://168.63.129.16:80/extensionsconfiguri/ - http://168.63.129.16:80/fullconfiguri/ - b61f93d0-e1ed-40b2-b067-22c243233448.1.b61f93d0-e1ed-40b2-b067-22c243233448.2.MachineRole_IN_0.xml + b61f93d0-e1ed-40b2-b067-22c243233448.1.b61f93d0-e1ed-40b2-b067-22c243233448.2.MachineRole_IN_0.xml + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=hostingEnvironmentConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=sharedConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=extensionsConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=fullConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=certificates&incarnation=1 + bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5.bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5._canary.1.xml diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index 664c78e205..f301a8a7de 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -1785,7 +1785,7 @@ def update_goal_state_and_run_handler(): def test_it_should_wait_to_fetch_first_goal_state(self): with _get_update_handler() as (update_handler, protocol): - with patch("azurelinuxagent.common.logger.warn") as patch_warn: + with patch("azurelinuxagent.common.logger.error") as patch_error: with patch("azurelinuxagent.common.logger.info") as patch_info: # Fail GS fetching for the 1st 5 times the agent asks for it update_handler._fail_gs_count = 5 @@ -1799,13 +1799,15 @@ def get_handler(url, **kwargs): protocol.set_http_handlers(http_get_handler=get_handler) update_handler.run(debug=True) - self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0; List of all warnings logged by the agent: {0}".format( - patch_warn.call_args_list)) - warn_msgs = [args[0] for (args, _) in patch_warn.call_args_list if - "An error occurred while retrieving the goal state" in args[0]] - self.assertTrue(len(warn_msgs) > 0, "Error should've been reported when failed to retrieve GS") + self.assertEqual(0, update_handler.get_exit_code(), "Exit code should be 0; List of all errors logged by the agent: {0}".format( + patch_error.call_args_list)) + + error_msgs = [args[0] for (args, _) in patch_error.call_args_list if + "Error fetching the goal state" in args[0]] + self.assertTrue(len(error_msgs) > 0, "Error should've been reported when failed to retrieve GS") + info_msgs = [args[0] for (args, _) in patch_info.call_args_list if - "Retrieving the goal state recovered from previous errors" in args[0]] + "Fetching the goal state recovered from previous errors." in args[0]] self.assertTrue(len(info_msgs) > 0, "Agent should've logged a message when recovered from GS errors") def test_it_should_reset_legacy_blacklisted_agents_on_process_start(self): @@ -2637,9 +2639,9 @@ def create_log_and_telemetry_mocks(): calls_to_strings = lambda calls: (str(c) for c in calls) filter_calls = lambda calls, regex=None: (c for c in calls_to_strings(calls) if regex is None or re.match(regex, c)) logger_calls = lambda regex=None: [m for m in filter_calls(logger.method_calls, regex)] # pylint: disable=used-before-assignment,unnecessary-comprehension - warnings = lambda: logger_calls(r'call.warn\(.*An error occurred while retrieving the goal state.*') - periodic_warnings = lambda: logger_calls(r'call.periodic_warn\(.*Attempts to retrieve the goal state are failing.*') - success_messages = lambda: logger_calls(r'call.info\(.*Retrieving the goal state recovered from previous errors.*') + errors = lambda: logger_calls(r'call.error\(.*Error fetching the goal state.*') + periodic_errors = lambda: logger_calls(r'call.error\(.*Fetching the goal state is still failing*') + success_messages = lambda: logger_calls(r'call.info\(.*Fetching the goal state recovered from previous errors.*') telemetry_calls = lambda regex=None: [m for m in filter_calls(add_event.mock_calls, regex)] # pylint: disable=used-before-assignment,unnecessary-comprehension goal_state_events = lambda: telemetry_calls(r".*op='FetchGoalState'.*") @@ -2664,10 +2666,8 @@ def create_log_and_telemetry_mocks(): with create_log_and_telemetry_mocks() as (logger, add_event): update_handler._try_update_goal_state(protocol) - w = warnings() - pw = periodic_warnings() - self.assertEqual(1, len(w), "A failure should have produced a warning: [{0}]".format(w)) - self.assertEqual(1, len(pw), "A failure should have produced a periodic warning: [{0}]".format(pw)) + e = errors() + self.assertEqual(1, len(e), "A failure should have produced an error: [{0}]".format(e)) gs = goal_state_events() self.assertTrue(len(gs) == 1 and 'is_success=False' in gs[0], "A failure should produce a telemetry event (success=false): [{0}]".format(gs)) @@ -2676,17 +2676,17 @@ def create_log_and_telemetry_mocks(): # ... and errors continue happening... # with create_log_and_telemetry_mocks() as (logger, add_event): - update_handler._try_update_goal_state(protocol) - update_handler._try_update_goal_state(protocol) - update_handler._try_update_goal_state(protocol) + for _ in range(5): + update_handler._update_goal_state_last_error_report = datetime.now() + timedelta(days=1) + update_handler._try_update_goal_state(protocol) - w = warnings() - pw = periodic_warnings() - self.assertTrue(len(w) == 0, "Subsequent failures should not produce warnings: [{0}]".format(w)) - self.assertEqual(len(pw), 3, "Subsequent failures should produce periodic warnings: [{0}]".format(pw)) + e = errors() + pe = periodic_errors() + self.assertEqual(2, len(e), "Two additional errors should have been reported: [{0}]".format(e)) + self.assertEqual(len(pe), 3, "Subsequent failures should produce periodic errors: [{0}]".format(pe)) tc = telemetry_calls() - self.assertTrue(len(tc) == 0, "Subsequent failures should not produce any telemetry events: [{0}]".format(tc)) + self.assertTrue(len(tc) == 5, "The failures should have produced telemetry events. Got: [{0}]".format(tc)) # # ... until we finally succeed @@ -2696,10 +2696,10 @@ def create_log_and_telemetry_mocks(): update_handler._try_update_goal_state(protocol) s = success_messages() - w = warnings() - pw = periodic_warnings() + e = errors() + pe = periodic_errors() self.assertEqual(len(s), 1, "Recovering after failures should have produced an info message: [{0}]".format(s)) - self.assertTrue(len(w) == 0 and len(pw) == 0, "Recovering after failures should have not produced any warnings: [{0}] [{1}]".format(w, pw)) + self.assertTrue(len(e) == 0 and len(pe) == 0, "Recovering after failures should have not produced any errors: [{0}] [{1}]".format(e, pe)) gs = goal_state_events() self.assertTrue(len(gs) == 1 and 'is_success=True' in gs[0], "Recovering after failures should produce a telemetry event (success=true): [{0}]".format(gs)) diff --git a/tests/protocol/HttpRequestPredicates.py b/tests/protocol/HttpRequestPredicates.py index 39243d5431..db3ab8b2a6 100644 --- a/tests/protocol/HttpRequestPredicates.py +++ b/tests/protocol/HttpRequestPredicates.py @@ -11,6 +11,22 @@ class HttpRequestPredicates(object): def is_goal_state_request(url): return url.lower() == 'http://{0}/machine/?comp=goalstate'.format(restutil.KNOWN_WIRESERVER_IP) + @staticmethod + def is_certificates_request(url): + return re.match(r'http://{0}(:80)?/machine/.*?comp=certificates'.format(restutil.KNOWN_WIRESERVER_IP), url, re.IGNORECASE) + + @staticmethod + def is_extensions_config_request(url): + return re.match(r'http://{0}(:80)?/machine/.*?comp=config&type=extensionsConfig'.format(restutil.KNOWN_WIRESERVER_IP), url, re.IGNORECASE) + + @staticmethod + def is_hosting_environment_config_request(url): + return re.match(r'http://{0}(:80)?/machine/.*?comp=config&type=hostingEnvironmentConfig'.format(restutil.KNOWN_WIRESERVER_IP), url, re.IGNORECASE) + + @staticmethod + def is_shared_config_request(url): + return re.match(r'http://{0}(:80)?/machine/.*?comp=config&type=sharedConfig'.format(restutil.KNOWN_WIRESERVER_IP), url, re.IGNORECASE) + @staticmethod def is_telemetry_request(url): return url.lower() == 'http://{0}/machine?comp=telemetrydata'.format(restutil.KNOWN_WIRESERVER_IP) diff --git a/tests/protocol/mockwiredata.py b/tests/protocol/mockwiredata.py index 218bd29377..7ec311af46 100644 --- a/tests/protocol/mockwiredata.py +++ b/tests/protocol/mockwiredata.py @@ -135,10 +135,10 @@ def __init__(self, data_files=None): "/HealthService": 0, "/vmAgentLog": 0, "goalstate": 0, - "hostingenvuri": 0, - "sharedconfiguri": 0, - "certificatesuri": 0, - "extensionsconfiguri": 0, + "hostingEnvironmentConfig": 0, + "sharedConfig": 0, + "certificates": 0, + "extensionsConfig": 0, "remoteaccessinfouri": 0, "extensionArtifact": 0, "agentArtifact": 0, @@ -198,6 +198,10 @@ def reload(self): if in_vm_artifacts_profile_file is not None: self.in_vm_artifacts_profile = load_data(in_vm_artifacts_profile_file) + def reset_call_counts(self): + for counter in self.call_counts: + self.call_counts[counter] = 0 + def mock_http_get(self, url, *_, **kwargs): content = '' response_headers = [] @@ -217,18 +221,18 @@ def mock_http_get(self, url, *_, **kwargs): elif "goalstate" in url: content = self.goal_state self.call_counts["goalstate"] += 1 - elif "hostingenvuri" in url: + elif HttpRequestPredicates.is_hosting_environment_config_request(url): content = self.hosting_env - self.call_counts["hostingenvuri"] += 1 - elif "sharedconfiguri" in url: + self.call_counts["hostingEnvironmentConfig"] += 1 + elif HttpRequestPredicates.is_shared_config_request(url): content = self.shared_config - self.call_counts["sharedconfiguri"] += 1 - elif "certificatesuri" in url: + self.call_counts["sharedConfig"] += 1 + elif HttpRequestPredicates.is_certificates_request(url): content = self.certs - self.call_counts["certificatesuri"] += 1 - elif "extensionsconfiguri" in url: + self.call_counts["certificates"] += 1 + elif HttpRequestPredicates.is_extensions_config_request(url): content = self.ext_conf - self.call_counts["extensionsconfiguri"] += 1 + self.call_counts["extensionsConfig"] += 1 elif "remoteaccessinfouri" in url: content = self.remote_access self.call_counts["remoteaccessinfouri"] += 1 diff --git a/tests/protocol/test_goal_state.py b/tests/protocol/test_goal_state.py index c54a65f9f7..c774171595 100644 --- a/tests/protocol/test_goal_state.py +++ b/tests/protocol/test_goal_state.py @@ -8,13 +8,12 @@ import re import time -from azurelinuxagent.common.event import WALAEventOperation from azurelinuxagent.common.future import httpclient from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateSource, GoalStateChannel from azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config import ExtensionsGoalStateFromExtensionsConfig from azurelinuxagent.common.protocol.extensions_goal_state_from_vm_settings import ExtensionsGoalStateFromVmSettings from azurelinuxagent.common.protocol import hostplugin -from azurelinuxagent.common.protocol.goal_state import GoalState, _GET_GOAL_STATE_MAX_ATTEMPTS +from azurelinuxagent.common.protocol.goal_state import GoalState, GoalStateInconsistentError, _GET_GOAL_STATE_MAX_ATTEMPTS from azurelinuxagent.common.exception import ProtocolError from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils.archive import ARCHIVE_DIRECTORY_NAME @@ -360,17 +359,54 @@ def http_get_handler(url, *_, **__): self.assertEqual(initial_timestamp, goal_state.extensions_goal_state.created_on_timestamp, "The timestamp of the updated goal state is incorrect") self.assertTrue(goal_state.extensions_goal_state.is_outdated, "The updated goal state should be marked as outdated") - def test_it_should_report_missing_certificates(self): + def test_it_should_raise_when_the_tenant_certificate_is_missing(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() - data_file["vm_settings"] = "hostgaplugin/vm_settings-missing_cert.json" with mock_wire_protocol(data_file) as protocol: - with patch("azurelinuxagent.common.protocol.goal_state.add_event") as add_event: + data_file["vm_settings"] = "hostgaplugin/vm_settings-missing_cert.json" + protocol.mock_wire_data.reload() + + with self.assertRaises(GoalStateInconsistentError) as context: _ = GoalState(protocol.client) - expected_message = "Certificate 59A10F50FFE2A0408D3F03FE336C8FD5716CF25C needed by Microsoft.OSTCExtensions.VMAccessForLinux is missing from the goal state" - events = [kwargs for _, kwargs in add_event.call_args_list if kwargs['op'] == WALAEventOperation.VmSettings and kwargs['message'] == expected_message] + expected_message = "Certificate 59A10F50FFE2A0408D3F03FE336C8FD5716CF25C needed by Microsoft.OSTCExtensions.VMAccessForLinux is missing from the goal state" + self.assertIn(expected_message, str(context.exception)) + + def test_it_should_refresh_the_goal_state_when_it_is_inconsistent(self): + # + # Some scenarios can produce inconsistent goal states. For example, during hibernation/resume, the Fabric goal state changes (the + # tenant certificate is re-generated when the VM is restarted) *without* the incarnation changing. If a Fast Track goal state + # comes after that, the extensions will need the new certificate. This test simulates that scenario by mocking the certificates + # request and returning first a set of certificates (certs-2.xml) that do not match those needed by the extensions, and then a + # set (certs.xml) that does match. The test then ensures that the goal state was refreshed and the correct certificates were + # fetched. + # + data_files = [ + "wire/certs-2.xml", + "wire/certs.xml" + ] + + def http_get_handler(url, *_, **__): + if HttpRequestPredicates.is_certificates_request(url): + http_get_handler.certificate_requests += 1 + if http_get_handler.certificate_requests < len(data_files): + data = load_data(data_files[http_get_handler.certificate_requests - 1]) + return MockHttpResponse(status=200, body=data.encode('utf-8')) + return None + http_get_handler.certificate_requests = 0 + + with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: + protocol.set_http_handlers(http_get_handler=http_get_handler) + protocol.mock_wire_data.reset_call_counts() + + goal_state = GoalState(protocol.client) + + self.assertEqual(2, protocol.mock_wire_data.call_counts['goalstate'], "There should have been exactly 2 requests for the goal state (original + refresh)") + self.assertEqual(2, http_get_handler.certificate_requests, "There should have been exactly 2 requests for the goal state certificates (original + refresh)") + + thumbprints = [c.thumbprint for c in goal_state.certs.cert_list.certificates] - self.assertTrue( - len(events) == 1, - "Missing certificate 59A10F50FFE2A0408D3F03FE336C8FD5716CF25C was not reported. Telemetry: {0}".format([kwargs['message'] for _, kwargs in add_event.call_args_list])) + for extension in goal_state.extensions_goal_state.extensions: + for settings in extension.settings: + if settings.protectedSettings is not None: + self.assertIn(settings.certificateThumbprint, thumbprints, "Certificate is missing from the goal state.") diff --git a/tests/protocol/test_hostplugin.py b/tests/protocol/test_hostplugin.py index 16bb7ef0b6..9f96f7d554 100644 --- a/tests/protocol/test_hostplugin.py +++ b/tests/protocol/test_hostplugin.py @@ -257,9 +257,8 @@ def test_default_channel(self, patch_put, patch_upload, _): # assert host plugin route is called self.assertEqual(1, patch_put.call_count, "Host plugin was not used") - # assert update goal state is only called once, non-forced + # assert update goal state is only called once self.assertEqual(1, wire_protocol.client.update_goal_state.call_count, "Unexpected call count") - self.assertEqual(0, len(wire_protocol.client.update_goal_state.call_args[1]), "Unexpected parameters") # ensure the correct url is used self.assertEqual(sas_url, patch_put.call_args[0][0]) @@ -291,9 +290,8 @@ def test_fallback_channel_503(self, patch_put, patch_upload, _): # assert host plugin route is called self.assertEqual(1, patch_put.call_count, "Host plugin was not used") - # assert update goal state is only called once, non-forced + # assert update goal state is only called once self.assertEqual(1, wire_protocol.client.update_goal_state.call_count, "Update goal state unexpected call count") - self.assertEqual(0, len(wire_protocol.client.update_goal_state.call_args[1]), "Update goal state unexpected call count") # ensure the correct url is used self.assertEqual(sas_url, patch_put.call_args[0][0]) @@ -326,9 +324,8 @@ def test_fallback_channel_410(self, patch_refresh_host_plugin, patch_put, patch_ # assert host plugin route is called self.assertEqual(1, patch_put.call_count, "Host plugin was not used") - # assert update goal state is called with no arguments (forced=False), then update_host_plugin_from_goal_state is called + # assert update goal state is called, then update_host_plugin_from_goal_state is called self.assertEqual(1, wire_protocol.client.update_goal_state.call_count, "Update goal state unexpected call count") - self.assertEqual(0, len(wire_protocol.client.update_goal_state.call_args[1]), "Update goal state unexpected argument count") self.assertEqual(1, patch_refresh_host_plugin.call_count, "Refresh host plugin unexpected call count") # ensure the correct url is used @@ -361,9 +358,8 @@ def test_fallback_channel_failure(self, patch_put, patch_upload, _): # assert host plugin route is called self.assertEqual(1, patch_put.call_count, "Host plugin was not used") - # assert update goal state is called twice, forced=True on the second + # assert update goal state is called twice self.assertEqual(1, wire_protocol.client.update_goal_state.call_count, "Update goal state unexpected call count") - self.assertEqual(0, len(wire_protocol.client.update_goal_state.call_args[1]), "Update goal state unexpected call count") # ensure the correct url is used self.assertEqual(sas_url, patch_put.call_args[0][0]) diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py index 0cc8a01e9a..c564af7217 100644 --- a/tests/protocol/test_wire.py +++ b/tests/protocol/test_wire.py @@ -160,7 +160,7 @@ def test_getters_with_stale_goal_state(self, patch_report, *args): # -- Tracking calls to retrieve GoalState is problematic since it is # fetched often; however, the dependent documents, such as the # HostingEnvironmentConfig, will be retrieved the expected number - self.assertEqual(1, test_data.call_counts["hostingenvuri"]) + self.assertEqual(1, test_data.call_counts["hostingEnvironmentConfig"]) self.assertEqual(1, patch_report.call_count) def test_call_storage_kwargs(self, *args): # pylint: disable=unused-argument From 10c49a11c7c2a03b8f7fbd03f492b0b4ea57eda2 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Fri, 29 Apr 2022 10:41:01 -0700 Subject: [PATCH 13/58] Do not mark goal state as processed when goal state fails to update (#2569) (#2571) Co-authored-by: narrieta (cherry picked from commit 4bc921590409e610d225387afb13f9b7c821777a) --- azurelinuxagent/common/protocol/goal_state.py | 4 ++-- azurelinuxagent/ga/update.py | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index edfd9d14f7..3301d783da 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -209,7 +209,7 @@ def _update(self, force_update): else: # vm_settings_updated most_recent = vm_settings - if self._extensions_goal_state is None or most_recent.created_on_timestamp > self._extensions_goal_state.created_on_timestamp: + if self._extensions_goal_state is None or most_recent.created_on_timestamp >= self._extensions_goal_state.created_on_timestamp: self._extensions_goal_state = most_recent # @@ -221,7 +221,7 @@ def _update(self, force_update): # Track goal state comes after that, the extensions will need the new certificate. The Agent needs to refresh the goal state in that # case, to ensure it fetches the new certificate. # - if self.extensions_goal_state.source == GoalStateSource.FastTrack: + if self._extensions_goal_state.source == GoalStateSource.FastTrack: self._check_certificates() def _check_certificates(self): diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index c800ce4700..eb14c6bac1 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -606,19 +606,19 @@ def _processing_new_extensions_goal_state(self): return self._goal_state is not None and egs.id != self._last_extensions_gs_id and not egs.is_outdated def _process_goal_state(self, exthandlers_handler, remote_access_handler): - try: - protocol = exthandlers_handler.protocol - - # update self._goal_state - if not self._try_update_goal_state(protocol): - # agent updates and status reporting should be done even when the goal state is not updated - self.__update_guest_agent(protocol) - self._report_status(exthandlers_handler) - return + protocol = exthandlers_handler.protocol - # check for agent updates + # update self._goal_state + if not self._try_update_goal_state(protocol): + # agent updates and status reporting should be done even when the goal state is not updated self.__update_guest_agent(protocol) + self._report_status(exthandlers_handler) + return + + # check for agent updates + self.__update_guest_agent(protocol) + try: if self._processing_new_extensions_goal_state(): if not self._extensions_summary.converged: message = "A new goal state was received, but not all the extensions in the previous goal state have completed: {0}".format(self._extensions_summary) From f0c0fb8214ef12b5279fa1e1847c44aab9bbee80 Mon Sep 17 00:00:00 2001 From: Shantanu Mishra Date: Sat, 30 Apr 2022 03:13:31 +0530 Subject: [PATCH 14/58] Debian - string conversion for systemd service (#2574) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 56f7f54f11..f929435852 100755 --- a/setup.py +++ b/setup.py @@ -251,7 +251,7 @@ def get_data_files(name, version, fullname): # pylint: disable=R0912 def debian_has_systemd(): try: return subprocess.check_output( - ['cat', '/proc/1/comm']).strip() == 'systemd' + ['cat', '/proc/1/comm']).strip().decode() == 'systemd' except subprocess.CalledProcessError: return False From 685348dcba4a59c49a9c8e553c162075b6a5d0e2 Mon Sep 17 00:00:00 2001 From: Kevin Clark Date: Wed, 11 May 2022 09:49:35 -0700 Subject: [PATCH 15/58] Bug fix for fetching a goal state with empty certificates property (#2575) (#2585) (cherry picked from commit 8e9c1b5b514ffccb10f664f9f1b7c9edee1ddced) --- azurelinuxagent/common/protocol/goal_state.py | 9 +++- tests/data/wire/goal_state_no_certs.xml | 27 +++++++++++ tests/ga/test_update.py | 47 +++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 tests/data/wire/goal_state_no_certs.xml diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index 3301d783da..8b508f61ad 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -82,7 +82,7 @@ def __init__(self, wire_client, silent=False): self._container_id = None self._hosting_env = None self._shared_conf = None - self._certs = None + self._certs = EmptyCertificates() self._remote_access = None self.update(silent=silent) @@ -348,7 +348,7 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): shared_conf = SharedConfig(xml_text) self._history.save_shared_conf(xml_text) - certs = None + certs = EmptyCertificates() certs_uri = findtext(xml_doc, "Certificates") if certs_uri is not None: xml_text = self._wire_client.fetch_config(certs_uri, self._wire_client.get_header_for_cert()) @@ -506,6 +506,11 @@ def _write_to_tmp_file(index, suffix, buf): fileutil.write_file(file_name, "".join(buf)) return file_name +class EmptyCertificates: + def __init__(self): + self.cert_list = CertList() + self.summary = [] # debugging info + self.warnings = [] class RemoteAccess(object): """ diff --git a/tests/data/wire/goal_state_no_certs.xml b/tests/data/wire/goal_state_no_certs.xml new file mode 100644 index 0000000000..1ab7fa2172 --- /dev/null +++ b/tests/data/wire/goal_state_no_certs.xml @@ -0,0 +1,27 @@ + + + 2010-12-15 + 1 + + Started + + 16001 + + + + c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2 + + + b61f93d0-e1ed-40b2-b067-22c243233448.MachineRole_IN_0 + Started + + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=hostingEnvironmentConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=sharedConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=extensionsConfig&incarnation=1 + http://168.63.129.16:80/machine/865a6683-91d8-450f-99ae/bc8b9d47%2Db5ed%2D4704%2D85d9%2Dfd74cc967ec2.%5Fcanary?comp=config&type=fullConfig&incarnation=1 + bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5.bc8b9d47-b5ed-4704-85d9-fd74cc967ec2.5._canary.1.xml + + + + + diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index f301a8a7de..92bd858374 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -1458,6 +1458,53 @@ def _get_test_ext_handler_instance(protocol, name="OSTCExtensions.ExampleHandler eh = Extension(name=name) eh.version = version return ExtHandlerInstance(eh, protocol) + + def test_update_handler_recovers_from_error_with_no_certs(self): + data = DATA_FILE.copy() + data['goal_state'] = 'wire/goal_state_no_certs.xml' + + def fail_gs_fetch(url, *_, **__): + if HttpRequestPredicates.is_goal_state_request(url): + return MockHttpResponse(status=500) + return None + + with mock_wire_protocol(data) as protocol: + + def fail_fetch_on_second_iter(iteration): + if iteration == 2: + protocol.set_http_handlers(http_get_handler=fail_gs_fetch) + if iteration > 2: # Zero out the fail handler for subsequent iterations. + protocol.set_http_handlers(http_get_handler=None) + + with mock_update_handler(protocol, 3, on_new_iteration=fail_fetch_on_second_iter) as update_handler: + with patch("azurelinuxagent.ga.update.logger.error") as patched_error: + with patch("azurelinuxagent.ga.update.logger.info") as patched_info: + def match_unexpected_errors(): + unexpected_msg_fragment = "Error fetching the goal state:" + + matching_errors = [] + for (args, _) in filter(lambda a: len(a) > 0, patched_error.call_args_list): + if unexpected_msg_fragment in args[0]: + matching_errors.append(args[0]) + + if len(matching_errors) > 1: + self.fail("Guest Agent did not recover, with new error(s): {}"\ + .format(matching_errors[1:])) + + def match_expected_info(): + expected_msg_fragment = "Fetching the goal state recovered from previous errors" + + for (call_args, _) in filter(lambda a: len(a) > 0, patched_info.call_args_list): + if expected_msg_fragment in call_args[0]: + break + else: + self.fail("Expected the guest agent to recover with '{}', but it didn't"\ + .format(expected_msg_fragment)) + + update_handler.run(debug=True) + match_unexpected_errors() # Match on errors first, they can provide more info. + match_expected_info() + def test_it_should_recreate_handler_env_on_service_startup(self): iterations = 5 From a867dffdb50fb569dc53759d491eb4cca003377b Mon Sep 17 00:00:00 2001 From: Kevin Clark Date: Mon, 16 May 2022 10:27:26 -0700 Subject: [PATCH 16/58] Move error counter reset down to end of block. (#2576) (#2586) (cherry picked from commit dee5fef9b98bf3e50b8fce213f67bd0d0b288c21) --- azurelinuxagent/ga/update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index eb14c6bac1..fd11c4ca09 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -489,11 +489,11 @@ def _try_update_goal_state(self, protocol): self._goal_state = protocol.get_goal_state() if self._update_goal_state_error_count > 0: - self._update_goal_state_error_count = 0 message = u"Fetching the goal state recovered from previous errors. Fetched {0} (certificates: {1})".format( self._goal_state.extensions_goal_state.id, self._goal_state.certs.summary) add_event(AGENT_NAME, op=WALAEventOperation.FetchGoalState, version=CURRENT_VERSION, is_success=True, message=message, log_event=False) logger.info(message) + self._update_goal_state_error_count = 0 try: self._supports_fast_track = conf.get_enable_fast_track() and protocol.client.get_host_plugin().check_vm_settings_support() From 81563a6be32ca2f028ff2adb4fbe3b23bc686b63 Mon Sep 17 00:00:00 2001 From: Kevin Clark Date: Mon, 16 May 2022 11:48:34 -0700 Subject: [PATCH 17/58] Bug Fix: Change fast track timestamp default from None to datetime.min (#2577) (#2587) (cherry picked from commit 35fed83afb5da1e93d0e207f4b54a863f188ec6c) --- azurelinuxagent/common/protocol/hostplugin.py | 4 +- .../vm_settings-fabric-no_thumbprints.json | 192 ++++++++++++++++++ tests/ga/test_update.py | 55 ++++- 3 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json diff --git a/azurelinuxagent/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py index 5f795dca3e..f79076f8ef 100644 --- a/azurelinuxagent/common/protocol/hostplugin.py +++ b/azurelinuxagent/common/protocol/hostplugin.py @@ -95,7 +95,7 @@ def __init__(self, endpoint): if not os.path.exists(self._get_fast_track_state_file()): self._supports_vm_settings = False self._supports_vm_settings_next_check = datetime.datetime.now() - self._fast_track_timestamp = None + self._fast_track_timestamp = timeutil.create_timestamp(datetime.datetime.min) else: self._supports_vm_settings = True self._supports_vm_settings_next_check = datetime.datetime.now() @@ -443,7 +443,7 @@ def get_fast_track_timestamp(): goal state was Fabric or fetch_vm_settings() has not been invoked. """ if not os.path.exists(HostPluginProtocol._get_fast_track_state_file()): - return None + return timeutil.create_timestamp(datetime.datetime.min) try: with open(HostPluginProtocol._get_fast_track_state_file(), "r") as file_: diff --git a/tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json b/tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json new file mode 100644 index 0000000000..bbd9459336 --- /dev/null +++ b/tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json @@ -0,0 +1,192 @@ +{ + "hostGAPluginVersion": "1.0.8.124", + "vmSettingsSchemaVersion": "0.0", + "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", + "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", + "inSvdSeqNo": 1, + "extensionsLastModifiedTickCount": 637726657706205299, + "extensionGoalStatesSource": "Fabric", + "onHold": true, + "statusUploadBlob": { + "statusBlobType": "BlockBlob", + "value": "https://dcrcl3a0xs.blob.core.windows.net/$system/edp0plkw2b.86f4ae0a-61f8-48ae-9199-40f402d56864.status?sv=2018-03-28&sr=b&sk=system-1&sig=KNWgC2%3d&se=9999-01-01T00%3a00%3a00Z&sp=w" + }, + "inVMMetadata": { + "subscriptionId": "8e037ad4-618f-4466-8bc8-5099d41ac15b", + "resourceGroupName": "rg-dc-86fjzhp", + "vmName": "edp0plkw2b", + "location": "CentralUSEUAP", + "vmId": "86f4ae0a-61f8-48ae-9199-40f402d56864", + "vmSize": "Standard_B2s", + "osType": "Linux" + }, + "requiredFeatures": [ + { + "name": "MultipleExtensionsPerHandler" + } + ], + "gaFamilies": [ + { + "name": "Prod", + "uris": [ + "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml", + "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" + ] + }, + { + "name": "Test", + "uris": [ + "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml", + "https://ardfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Test_uscentraleuap_manifest.xml" + ] + } + ], + "extensionGoalStates": [ + { + "name": "Microsoft.Azure.Monitor.AzureMonitorLinuxAgent", + "version": "1.9.1", + "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", + "failoverlocation": "https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml", + "additionalLocations": ["https://zrdfepirv2cbn09pr02a.blob.core.windows.net/a47f0806d764480a8d989d009c75007d/Microsoft.Azure.Monitor_AzureMonitorLinuxAgent_useast2euap_manifest.xml"], + "state": "enabled", + "autoUpgrade": true, + "runAsStartupTask": false, + "isJson": true, + "useExactVersion": true, + "settingsSeqNo": 0, + "settings": [ + { + "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" + } + ] + }, + { + "name": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent", + "version": "2.15.112", + "location": "https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml", + "failoverlocation": "https://zrdfepirv2cbz06prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml", + "additionalLocations": ["https://zrdfepirv2cbn06prdstr01a.blob.core.windows.net/4ef06ad957494df49c807a5334f2b5d2/Microsoft.Azure.Security.Monitoring_AzureSecurityLinuxAgent_useast2euap_manifest.xml"], + "state": "enabled", + "autoUpgrade": true, + "runAsStartupTask": false, + "isJson": true, + "useExactVersion": true, + "settingsSeqNo": 0, + "settings": [ + { + "publicSettings": "{\"enableGenevaUpload\":true}" + } + ] + }, + { + "name": "Microsoft.Azure.Extensions.CustomScript", + "version": "2.1.6", + "location": "https://umsavwggj2v40kvqhc0w.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", + "failoverlocation": "https://umsafwzhkbm1rfrhl0ws.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml", + "additionalLocations": [ + "https://umsanh4b5rfz0q0p4pwm.blob.core.windows.net/5237dd14-0aad-f051-0fad-1e33e1b63091/5237dd14-0aad-f051-0fad-1e33e1b63091_manifest.xml" + ], + "state": "enabled", + "autoUpgrade": true, + "runAsStartupTask": false, + "isJson": true, + "useExactVersion": true, + "settingsSeqNo": 0, + "isMultiConfig": false, + "settings": [ + { + "publicSettings": "{\"commandToExecute\":\"echo 'cee174d4-4daa-4b07-9958-53b9649445c2'\"}" + } + ], + "dependsOn": [ + { + "DependsOnExtension": [ + { + "handler": "Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent" + } + ], + "dependencyLevel": 1 + } + ] + }, + { + "name": "Microsoft.CPlat.Core.RunCommandHandlerLinux", + "version": "1.2.0", + "location": "https://umsavbvncrpzbnxmxzmr.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", + "failoverlocation": "https://umsajbjtqrb3zqjvgb2z.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml", + "additionalLocations": [ + "https://umsawqtlsshtn5v2nfgh.blob.core.windows.net/f4086d41-69f9-3103-78e0-8a2c7e789d0f/f4086d41-69f9-3103-78e0-8a2c7e789d0f_manifest.xml" + ], + "state": "enabled", + "autoUpgrade": true, + "runAsStartupTask": false, + "isJson": true, + "useExactVersion": true, + "settingsSeqNo": 0, + "isMultiConfig": true, + "settings": [ + { + "publicSettings": "{\"source\":{\"script\":\"echo '4abb1e88-f349-41f8-8442-247d9fdfcac5'\"}}", + "seqNo": 0, + "extensionName": "MCExt1", + "extensionState": "enabled" + }, + { + "publicSettings": "{\"source\":{\"script\":\"echo 'e865c9bc-a7b3-42c6-9a79-cfa98a1ee8b3'\"}}", + "seqNo": 0, + "extensionName": "MCExt2", + "extensionState": "enabled" + }, + { + "publicSettings": "{\"source\":{\"script\":\"echo 'f923e416-0340-485c-9243-8b84fb9930c6'\"}}", + "seqNo": 0, + "extensionName": "MCExt3", + "extensionState": "enabled" + } + ], + "dependsOn": [ + { + "dependsOnExtension": [ + { + "extension": "...", + "handler": "..." + }, + { + "extension": "...", + "handler": "..." + } + ], + "dependencyLevel": 2, + "name": "MCExt1" + }, + { + "dependsOnExtension": [ + { + "extension": "...", + "handler": "..." + } + ], + "dependencyLevel": 1, + "name": "MCExt2" + } + ] + }, + { + "name": "Microsoft.OSTCExtensions.VMAccessForLinux", + "version": "1.5.11", + "location": "https://umsasc25p0kjg0c1dg4b.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml", + "failoverlocation": "https://umsamfwlmfshvxx2lsjm.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml", + "additionalLocations": [ + "https://umsah3cwjlctnmhsvzqv.blob.core.windows.net/2bbece4f-0283-d415-b034-cc0adc6997a1/2bbece4f-0283-d415-b034-cc0adc6997a1_manifest.xml" + ], + "state": "enabled", + "autoUpgrade": false, + "runAsStartupTask": false, + "isJson": true, + "useExactVersion": true, + "settingsSeqNo": 0, + "isMultiConfig": false, + "settings": [ ] + } + ] +} diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index 92bd858374..edcf669d62 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -38,7 +38,7 @@ VMAgentUpdateStatuses from azurelinuxagent.common.protocol.util import ProtocolUtil from azurelinuxagent.common.protocol.wire import WireProtocol -from azurelinuxagent.common.utils import fileutil, restutil, textutil +from azurelinuxagent.common.utils import fileutil, restutil, textutil, timeutil from azurelinuxagent.common.utils.archive import ARCHIVE_DIRECTORY_NAME, AGENT_STATUS_FILE from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.networkutil import FirewallCmdDirectCommands, AddFirewallRules @@ -52,7 +52,7 @@ READONLY_FILE_GLOBS, ExtensionsSummary, AgentUpgradeType from tests.ga.mocks import mock_update_handler from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse -from tests.protocol.mockwiredata import DATA_FILE, DATA_FILE_MULTIPLE_EXT +from tests.protocol.mockwiredata import DATA_FILE, DATA_FILE_MULTIPLE_EXT, DATA_FILE_VM_SETTINGS from tests.tools import AgentTestCase, AgentTestCaseWithGetVmSizeMock, data_dir, DEFAULT, patch, load_bin_data, Mock, MagicMock, \ clear_singleton_instances from tests.protocol import mockwiredata @@ -2874,7 +2874,7 @@ def test_it_should_mark_outdated_goal_states_on_service_restart_when_host_ga_plu def test_it_should_clear_the_timestamp_for_the_most_recent_fast_track_goal_state(self): data_file = self._prepare_fast_track_goal_state() - if HostPluginProtocol.get_fast_track_timestamp() is None: + if HostPluginProtocol.get_fast_track_timestamp() == timeutil.create_timestamp(datetime.min): raise Exception("The test setup did not save the Fast Track state") with patch("azurelinuxagent.common.conf.get_enable_fast_track", return_value=False): @@ -2882,8 +2882,55 @@ def test_it_should_clear_the_timestamp_for_the_most_recent_fast_track_goal_state with mock_update_handler(protocol) as update_handler: update_handler.run() - self.assertIsNone(HostPluginProtocol.get_fast_track_timestamp(), "The Fast Track state was not cleared") + self.assertEqual(HostPluginProtocol.get_fast_track_timestamp(), timeutil.create_timestamp(datetime.min), + "The Fast Track state was not cleared") + + def test_it_should_default_fast_track_timestamp_to_datetime_min(self): + data = DATA_FILE_VM_SETTINGS.copy() + # TODO: Currently, there's a limitation in the mocks where bumping the incarnation but the goal + # state will cause the agent to error out while trying to write the certificates to disk. These + # files have no dependencies on certs, so using them does not present that issue. + # + # Note that the scenario this test is representing does not depend on certificates at all, and + # can be changed to use the default files when the above limitation is addressed. + data["vm_settings"] = "hostgaplugin/vm_settings-fabric-no_thumbprints.json" + data['goal_state'] = 'wire/goal_state_no_certs.xml' + + def vm_settings_no_change(url, *_, **__): + if HttpRequestPredicates.is_host_plugin_vm_settings_request(url): + return MockHttpResponse(httpclient.NOT_MODIFIED) + return None + + def vm_settings_not_supported(url, *_, **__): + if HttpRequestPredicates.is_host_plugin_vm_settings_request(url): + return MockHttpResponse(404) + return None + + with mock_wire_protocol(data) as protocol: + + def mock_live_migration(iteration): + if iteration == 1: + protocol.mock_wire_data.set_incarnation(2) + protocol.set_http_handlers(http_get_handler=vm_settings_no_change) + elif iteration == 2: + protocol.mock_wire_data.set_incarnation(3) + protocol.set_http_handlers(http_get_handler=vm_settings_not_supported) + + with mock_update_handler(protocol, 3, on_new_iteration=mock_live_migration) as update_handler: + with patch("azurelinuxagent.ga.update.logger.error") as patched_error: + def check_for_errors(): + msg_fragment = "Error fetching the goal state:" + + for (args, _) in filter(lambda a: len(a) > 0, patched_error.call_args_list): + if msg_fragment in args[0]: + self.fail("Found error: {}".format(args[0])) + update_handler.run(debug=True) + check_for_errors() + + timestamp = protocol.client.get_host_plugin()._fast_track_timestamp + self.assertEqual(timestamp, timeutil.create_timestamp(datetime.min), + "Expected fast track time stamp to be set to {0}, got {1}".format(datetime.min, timestamp)) class HeartbeatTestCase(AgentTestCase): From e4ec4c96497ac2e7d9869d54829562cbd07febbe Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Mon, 16 May 2022 16:07:36 -0700 Subject: [PATCH 18/58] Update CODEOWNERS (#2590) Co-authored-by: narrieta --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9801efc186..e85dfc8d3d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -20,4 +20,4 @@ # # Linux Agent team # -* @narrieta @larohra @kevinclark19a @ZhidongPeng @dhivyaganesan @nagworld9 +* @narrieta @kevinclark19a @ZhidongPeng @dhivyaganesan @nagworld9 From aa6d82c97082705ee47875fb4429152a3c53f898 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Tue, 17 May 2022 12:21:19 -0700 Subject: [PATCH 19/58] ext cpu throttling scenarios (#2581) * ext cpu throttling scenarios * address comments * remove unused import --- azurelinuxagent/common/cgroup.py | 14 ++- azurelinuxagent/common/cgroupapi.py | 6 +- azurelinuxagent/common/cgroupconfigurator.py | 103 +++++++++++++++--- .../common/utils/extensionprocessutil.py | 30 ++++- tests/common/test_cgroupconfigurator.py | 54 ++++++++- tests/common/test_cgroups.py | 2 +- tests/common/test_cgroupstelemetry.py | 2 +- tests/utils/test_extension_process_util.py | 50 ++++++++- 8 files changed, 223 insertions(+), 38 deletions(-) diff --git a/azurelinuxagent/common/cgroup.py b/azurelinuxagent/common/cgroup.py index 900f4f5b46..5fadf6bfb1 100644 --- a/azurelinuxagent/common/cgroup.py +++ b/azurelinuxagent/common/cgroup.py @@ -171,7 +171,7 @@ def _get_cpu_ticks(self, allow_no_such_file_or_directory_error=False): return cpu_ticks - def _get_throttled_time(self): + def get_throttled_time(self): try: with open(os.path.join(self.path, 'cpu.stat')) as cpu_stat: # @@ -205,7 +205,7 @@ def initialize_cpu_usage(self): raise CGroupsException("initialize_cpu_usage() should be invoked only once") self._current_cgroup_cpu = self._get_cpu_ticks(allow_no_such_file_or_directory_error=True) self._current_system_cpu = self._osutil.get_total_cpu_ticks_since_boot() - self._current_throttled_time = self._get_throttled_time() + self._current_throttled_time = self.get_throttled_time() def get_cpu_usage(self): """ @@ -229,16 +229,20 @@ def get_cpu_usage(self): return round(100.0 * self._osutil.get_processor_cores() * float(cgroup_delta) / float(system_delta), 3) - def get_throttled_time(self): + def get_cpu_throttled_time(self, read_previous_throttled_time=True): """ Computes the throttled time (in seconds) since the last call to this function. NOTE: initialize_cpu_usage() must be invoked before calling this function + Compute only current throttled time if read_previous_throttled_time set to False """ + if not read_previous_throttled_time: + return float(self.get_throttled_time() / 1E9) + if not self._cpu_usage_initialized(): raise CGroupsException("initialize_cpu_usage() must be invoked before the first call to get_throttled_time()") self._previous_throttled_time = self._current_throttled_time - self._current_throttled_time = self._get_throttled_time() + self._current_throttled_time = self.get_throttled_time() return float(self._current_throttled_time - self._previous_throttled_time) / 1E9 @@ -249,7 +253,7 @@ def get_tracked_metrics(self, **kwargs): tracked.append(MetricValue(MetricsCategory.CPU_CATEGORY, MetricsCounter.PROCESSOR_PERCENT_TIME, self.name, cpu_usage)) if 'track_throttled_time' in kwargs and kwargs['track_throttled_time']: - throttled_time = self.get_throttled_time() + throttled_time = self.get_cpu_throttled_time() if cpu_usage >= float(0) and throttled_time >= float(0): tracked.append(MetricValue(MetricsCategory.CPU_CATEGORY, MetricsCounter.THROTTLED_TIME, self.name, throttled_time)) diff --git a/azurelinuxagent/common/cgroupapi.py b/azurelinuxagent/common/cgroupapi.py index f69b1591c0..7b7e688a1a 100644 --- a/azurelinuxagent/common/cgroupapi.py +++ b/azurelinuxagent/common/cgroupapi.py @@ -280,6 +280,7 @@ def start_extension_command(self, extension_name, command, cmd_name, timeout, sh logger.info("Started extension in unit '{0}'", scope_name) + cpu_cgroup = None try: cgroup_relative_path = os.path.join('azure.slice/azure-vmextensions.slice', extension_slice_name) @@ -289,7 +290,8 @@ def start_extension_command(self, extension_name, command, cmd_name, timeout, sh logger.info("The CPU controller is not mounted; will not track resource usage") else: cpu_cgroup_path = os.path.join(cpu_cgroup_mountpoint, cgroup_relative_path) - CGroupsTelemetry.track_cgroup(CpuCgroup(extension_name, cpu_cgroup_path)) + cpu_cgroup = CpuCgroup(extension_name, cpu_cgroup_path) + CGroupsTelemetry.track_cgroup(cpu_cgroup) except IOError as e: if e.errno == 2: # 'No such file or directory' @@ -301,7 +303,7 @@ def start_extension_command(self, extension_name, command, cmd_name, timeout, sh # Wait for process completion or timeout try: return handle_process_completion(process=process, command=command, timeout=timeout, stdout=stdout, - stderr=stderr, error_code=error_code) + stderr=stderr, error_code=error_code, cpu_cgroup=cpu_cgroup) except ExtensionError as e: # The extension didn't terminate successfully. Determine whether it was due to systemd errors or # extension errors. diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index 7dc0a80a99..308838a731 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -14,6 +14,8 @@ # limitations under the License. # # Requires Python 2.6+ and Openssl 1.0+ +import glob +import json import os import re import subprocess @@ -358,12 +360,16 @@ def __setup_azure_slice(): CGroupConfigurator._Impl.__cleanup_unit_file(unit_file) return - # reload the systemd configuration; the new slices will be used once the agent's service restarts - try: - logger.info("Executing systemctl daemon-reload...") - shellutil.run_command(["systemctl", "daemon-reload"]) - except Exception as exception: - _log_cgroup_warning("daemon-reload failed (create azure slice): {0}", ustr(exception)) + CGroupConfigurator._Impl.__reload_systemd_config() + + @staticmethod + def __reload_systemd_config(): + # reload the systemd configuration; the new slices will be used once the agent's service restarts + try: + logger.info("Executing systemctl daemon-reload...") + shellutil.run_command(["systemctl", "daemon-reload"]) + except Exception as exception: + _log_cgroup_warning("daemon-reload failed (create azure slice): {0}", ustr(exception)) @staticmethod def __create_unit_file(path, contents): @@ -479,18 +485,23 @@ def enable(self): self.__set_cpu_quota(conf.get_agent_cpu_quota()) def disable(self, reason, disable_cgroups): - # Todo: disable/reset extension when ext quotas introduced if disable_cgroups == DisableCgroups.ALL: # disable all - self._agent_cgroups_enabled = False - self._extensions_cgroups_enabled = False + # Reset quotas self.__reset_agent_cpu_quota() + extension_services = self.get_extension_services_list() + for extension in extension_services: + logger.info("Resetting extension : {0} and it's services: {1} CPUQuota".format(extension, extension_services[extension])) + self.__reset_extension_cpu_quota(extension_name=extension) + self.__reset_extension_services_cpu_quota(extension_services[extension]) + self.__reload_systemd_config() + CGroupsTelemetry.reset() + self._agent_cgroups_enabled = False + self._extensions_cgroups_enabled = False elif disable_cgroups == DisableCgroups.AGENT: # disable agent self._agent_cgroups_enabled = False self.__reset_agent_cpu_quota() CGroupsTelemetry.stop_tracking(CpuCgroup(AGENT_NAME_TELEMETRY, self._agent_cpu_cgroup_path)) - elif disable_cgroups == DisableCgroups.EXTENSIONS: # disable extensions - self._extensions_cgroups_enabled = False message = "[CGW] Disabling resource usage monitoring. Reason: {0}".format(reason) logger.info(message) # log as INFO for now, in the future it should be logged as WARNING @@ -519,7 +530,6 @@ def __reset_agent_cpu_quota(): """ logger.info("Resetting agent's CPUQuota") if CGroupConfigurator._Impl.__try_set_cpu_quota(''): # setting an empty value resets to the default (infinity) - CGroupsTelemetry.set_track_throttled_time(False) _log_cgroup_info('CPUQuota: {0}', systemd.get_unit_property(systemd.get_agent_unit_name(), "CPUQuotaPerSecUSec")) @staticmethod @@ -763,6 +773,16 @@ def start_extension_command(self, extension_name, command, cmd_name, timeout, sh process = subprocess.Popen(command, shell=shell, cwd=cwd, env=env, stdout=stdout, stderr=stderr, preexec_fn=os.setsid) # pylint: disable=W1509 return handle_process_completion(process=process, command=command, timeout=timeout, stdout=stdout, stderr=stderr, error_code=error_code) + def __reset_extension_cpu_quota(self, extension_name): + """ + Removes any CPUQuota on the extension + + NOTE: This resets the quota on the extension's slice; any local overrides on the VM will take precedence + over this setting. + """ + if self.enabled(): + self.setup_extension_slice(extension_name, cpu_quota=None) + def setup_extension_slice(self, extension_name, cpu_quota): """ Each extension runs under its own slice (Ex "Microsoft.CPlat.Extension.slice"). All the slices for @@ -777,11 +797,11 @@ def setup_extension_slice(self, extension_name, cpu_quota): extension_slice_path = os.path.join(unit_file_install_path, SystemdCgroupsApi.get_extension_slice_name(extension_name)) try: - cpu_quota = str(cpu_quota) + "%" if cpu_quota is not None else "" + cpu_quota = str(cpu_quota) + "%" if cpu_quota is not None else "" # setting an empty value resets to the default (infinity) slice_contents = _EXTENSION_SLICE_CONTENTS.format(extension_name=extension_name, cpu_quota=cpu_quota) CGroupConfigurator._Impl.__create_unit_file(extension_slice_path, slice_contents) except Exception as exception: - _log_cgroup_warning("Failed to create unit files for the extension slice: {0}", ustr(exception)) + _log_cgroup_warning("Failed to set the extension {0} slice and quotas: {1}", extension_name, ustr(exception)) CGroupConfigurator._Impl.__cleanup_unit_file(extension_slice_path) def remove_extension_slice(self, extension_name): @@ -823,13 +843,35 @@ def set_extension_services_cpu_memory_quota(self, services_list): files_to_create.append((drop_in_file_cpu_quota, cpu_quota_contents)) self.__create_all_files(files_to_create) + self.__reload_systemd_config() + + def __reset_extension_services_cpu_quota(self, services_list): + """ + Removes any CPUQuota on the extension service - # reload the systemd configuration; the new unit will be used once the service restarts + NOTE: This resets the quota on the extension service's default dropin file; any local overrides on the VM will take precedence + over this setting. + """ + if self.enabled() and services_list is not None: try: - logger.info("Executing systemctl daemon-reload...") - shellutil.run_command(["systemctl", "daemon-reload"]) + service_name = None + for service in services_list: + service_name = service.get('name', None) + unit_file_path = systemd.get_unit_file_install_path() + if service_name is not None and unit_file_path is not None: + files_to_create = [] + drop_in_path = os.path.join(unit_file_path, "{0}.d".format(service_name)) + cpu_quota = "" # setting an empty value resets to the default (infinity) + drop_in_file_cpu_quota = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_QUOTA) + cpu_quota_contents = _DROP_IN_FILE_CPU_QUOTA_CONTENTS_FORMAT.format(cpu_quota) + if os.path.exists(drop_in_file_cpu_quota): + with open(drop_in_file_cpu_quota, "r") as file_: + if file_.read() == cpu_quota_contents: + return + files_to_create.append((drop_in_file_cpu_quota, cpu_quota_contents)) + self.__create_all_files(files_to_create) except Exception as exception: - _log_cgroup_warning("daemon-reload failed (create service unit files): {0}", ustr(exception)) + _log_cgroup_warning('Failed to reset CPUQuota for {0} : {1}', service_name, ustr(exception)) def remove_extension_services_drop_in_files(self, services_list): """ @@ -873,6 +915,31 @@ def start_tracking_extension_services_cgroups(self, services_list): if service_name is not None: self.start_tracking_unit_cgroups(service_name) + @staticmethod + def get_extension_services_list(): + """ + ResourceLimits for extensions are coming from /HandlerManifest.json file. + Use this pattern to determine all the installed extension HandlerManifest files and + read the extension services if ResourceLimits are present. + """ + extensions_services = {} + for manifest_path in glob.iglob(os.path.join(conf.get_lib_dir(), "*/HandlerManifest.json")): + match = re.search("(?P[\\w+\\.-]+).HandlerManifest\\.json", manifest_path) + if match is not None: + extensions_name = match.group('extname') + if not extensions_name.startswith('WALinuxAgent'): + try: + data = json.loads(fileutil.read_file(manifest_path)) + resource_limits = data[0].get('resourceLimits', None) + services = resource_limits.get('services') if resource_limits else None + extensions_services[extensions_name] = services + except (IOError, OSError) as e: + _log_cgroup_warning( + 'Failed to load manifest file ({0}): {1}'.format(manifest_path, e.strerror)) + except ValueError: + _log_cgroup_warning('Malformed manifest file ({0}).'.format(manifest_path)) + return extensions_services + # unique instance for the singleton _instance = None diff --git a/azurelinuxagent/common/utils/extensionprocessutil.py b/azurelinuxagent/common/utils/extensionprocessutil.py index dda83fda29..9038f6145c 100644 --- a/azurelinuxagent/common/utils/extensionprocessutil.py +++ b/azurelinuxagent/common/utils/extensionprocessutil.py @@ -21,13 +21,14 @@ import signal import time +from azurelinuxagent.common import logger from azurelinuxagent.common.exception import ExtensionErrorCodes, ExtensionOperationError, ExtensionError from azurelinuxagent.common.future import ustr TELEMETRY_MESSAGE_MAX_LEN = 3200 -def wait_for_process_completion_or_timeout(process, timeout): +def wait_for_process_completion_or_timeout(process, timeout, cpu_cgroup): """ Utility function that waits for the process to complete within the given time frame. This function will terminate the process if when the given time frame elapses. @@ -40,18 +41,20 @@ def wait_for_process_completion_or_timeout(process, timeout): timeout -= 1 return_code = None + throttled_time = 0 if timeout == 0: + throttled_time = get_cpu_throttled_time(cpu_cgroup) os.killpg(os.getpgid(process.pid), signal.SIGKILL) else: # process completed or forked; sleep 1 sec to give the child process (if any) a chance to start time.sleep(1) return_code = process.wait() - return timeout == 0, return_code + return timeout == 0, return_code, throttled_time -def handle_process_completion(process, command, timeout, stdout, stderr, error_code): +def handle_process_completion(process, command, timeout, stdout, stderr, error_code, cpu_cgroup=None): """ Utility function that waits for process completion and retrieves its output (stdout and stderr) if it completed before the timeout period. Otherwise, the process will get killed and an ExtensionError will be raised. @@ -62,13 +65,18 @@ def handle_process_completion(process, command, timeout, stdout, stderr, error_c :param stdout: Must be a file since we seek on it when parsing the subprocess output :param stderr: Must be a file since we seek on it when parsing the subprocess outputs :param error_code: The error code to set if we raise an ExtensionError + :param cpu_cgroup: Reference the cpu cgroup name and path :return: """ # Wait for process completion or timeout - timed_out, return_code = wait_for_process_completion_or_timeout(process, timeout) + timed_out, return_code, throttled_time = wait_for_process_completion_or_timeout(process, timeout, cpu_cgroup) process_output = read_output(stdout, stderr) if timed_out: + if cpu_cgroup is not None:# Report CPUThrottledTime when timeout happens + raise ExtensionError("Timeout({0});CPUThrottledTime({1}secs): {2}\n{3}".format(timeout, throttled_time, command, process_output), + code=ExtensionErrorCodes.PluginHandlerScriptTimedout) + raise ExtensionError("Timeout({0}): {1}\n{2}".format(timeout, command, process_output), code=ExtensionErrorCodes.PluginHandlerScriptTimedout) @@ -141,3 +149,17 @@ def to_s(captured_stdout, stdout_offset, captured_stderr, stderr_offset): return to_s(stdout, -1*stdout_len, stderr, 0) else: return to_s(stdout, -1*max_len_each, stderr, -1*max_len_each) + + +def get_cpu_throttled_time(cpu_cgroup): + """ + return the throttled time for the given cgroup. + """ + throttled_time = 0 + if cpu_cgroup is not None: + try: + throttled_time = cpu_cgroup.get_cpu_throttled_time(read_previous_throttled_time=False) + except Exception as e: + logger.warn("Failed to get cpu throttled time for the extension: {0}", ustr(e)) + + return throttled_time diff --git a/tests/common/test_cgroupconfigurator.py b/tests/common/test_cgroupconfigurator.py index 6a945c5e18..e2f06a67ac 100644 --- a/tests/common/test_cgroupconfigurator.py +++ b/tests/common/test_cgroupconfigurator.py @@ -254,7 +254,7 @@ def test_enable_should_not_track_throttled_time_when_setting_the_cpu_quota_fails self.assertFalse(CGroupsTelemetry.get_track_throttled_time(), "Throttle time should not be tracked") - def test_disable_should_reset_cpu_quota_and_tracked_cgroups(self): + def test_disable_should_reset_cpu_quota(self): with self._get_cgroup_configurator() as configurator: if len(CGroupsTelemetry._tracked) == 0: raise Exception("Test setup should have started tracking at least 1 cgroup (the agent's)") @@ -269,7 +269,55 @@ def test_disable_should_reset_cpu_quota_and_tracked_cgroups(self): fileutil.findre_in_file(agent_drop_in_file_cpu_quota, "^CPUQuota=$"), "CPUQuota was not set correctly. Expected an empty value. Got:\n{0}".format(fileutil.read_file(agent_drop_in_file_cpu_quota))) self.assertEqual(len(CGroupsTelemetry._tracked), 0, "No cgroups should be tracked after disable. Tracking: {0}".format(CGroupsTelemetry._tracked)) - self.assertFalse(CGroupsTelemetry._track_throttled_time, "Throttle Time should not be tracked after disable") + + def test_disable_should_reset_cpu_quota_for_all_cgroups(self): + service_list = [ + { + "name": "extension.service", + "cpuQuotaPercentage": 5 + } + ] + extension_name = "Microsoft.CPlat.Extension" + extension_services = {extension_name: service_list} + with self._get_cgroup_configurator() as configurator: + with patch.object(configurator, "get_extension_services_list", return_value=extension_services): + # get the paths to the mocked files + agent_drop_in_file_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.cpu_quota) + extension_slice_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.extensionslice) + extension_service_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_quota) + + configurator.setup_extension_slice(extension_name=extension_name, cpu_quota=5) + configurator.set_extension_services_cpu_memory_quota(service_list) + CGroupsTelemetry._tracked['/sys/fs/cgroup/cpu,cpuacct/system.slice/extension.service'] = \ + CpuCgroup('extension.service', '/sys/fs/cgroup/cpu,cpuacct/system.slice/extension.service') + CGroupsTelemetry._tracked['/sys/fs/cgroup/cpu,cpuacct/azure.slice/azure-vmextensions.slice/' \ + 'azure-vmextensions-Microsoft.CPlat.Extension.slice'] = \ + CpuCgroup('Microsoft.CPlat.Extension', + '/sys/fs/cgroup/cpu,cpuacct/azure.slice/azure-vmextensions.slice/azure-vmextensions-Microsoft.CPlat.Extension.slice') + + configurator.disable("UNIT TEST", DisableCgroups.ALL) + + self.assertTrue(os.path.exists(agent_drop_in_file_cpu_quota), + "{0} was not created".format(agent_drop_in_file_cpu_quota)) + self.assertTrue( + fileutil.findre_in_file(agent_drop_in_file_cpu_quota, "^CPUQuota=$"), + "CPUQuota was not set correctly. Expected an empty value. Got:\n{0}".format( + fileutil.read_file(agent_drop_in_file_cpu_quota))) + self.assertTrue(os.path.exists(extension_slice_unit_file), + "{0} was not created".format(extension_slice_unit_file)) + self.assertTrue( + fileutil.findre_in_file(extension_slice_unit_file, "^CPUQuota=$"), + "CPUQuota was not set correctly. Expected an empty value. Got:\n{0}".format( + fileutil.read_file(extension_slice_unit_file))) + self.assertTrue(os.path.exists(extension_service_cpu_quota), + "{0} was not created".format(extension_service_cpu_quota)) + self.assertTrue( + fileutil.findre_in_file(extension_service_cpu_quota, "^CPUQuota=$"), + "CPUQuota was not set correctly. Expected an empty value. Got:\n{0}".format( + fileutil.read_file(extension_service_cpu_quota))) + self.assertEqual(len(CGroupsTelemetry._tracked), 0, + "No cgroups should be tracked after disable. Tracking: {0}".format( + CGroupsTelemetry._tracked)) @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_not_use_systemd_when_cgroups_are_not_enabled(self, _): @@ -529,7 +577,7 @@ def test_start_extension_command_should_not_use_fallback_option_if_extension_tim with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: with patch("azurelinuxagent.common.utils.extensionprocessutil.wait_for_process_completion_or_timeout", - return_value=[True, None]): + return_value=[True, None, 0]): with patch("azurelinuxagent.common.cgroupapi.SystemdCgroupsApi._is_systemd_failure", return_value=False): with self.assertRaises(ExtensionError) as context_manager: diff --git a/tests/common/test_cgroups.py b/tests/common/test_cgroups.py index 1f994b8593..61c3178546 100644 --- a/tests/common/test_cgroups.py +++ b/tests/common/test_cgroups.py @@ -183,7 +183,7 @@ def test_get_throttled_time_should_return_the_value_since_its_last_invocation(se cgroup.initialize_cpu_usage() shutil.copyfile(os.path.join(data_dir, "cgroups", "cpu.stat_t1"), test_file) # throttled_time = 2075541442327 - throttled_time = cgroup.get_throttled_time() + throttled_time = cgroup.get_cpu_throttled_time() self.assertEqual(throttled_time, float(2075541442327 - 50) / 1E9, "The value of throttled_time is incorrect") diff --git a/tests/common/test_cgroupstelemetry.py b/tests/common/test_cgroupstelemetry.py index 9d96a1d825..dc612244d8 100644 --- a/tests/common/test_cgroupstelemetry.py +++ b/tests/common/test_cgroupstelemetry.py @@ -375,7 +375,7 @@ def test_extension_telemetry_not_sent_for_empty_perf_metrics(self, *args): # py self.assertEqual(0, len(metrics)) @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") - @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_throttled_time") + @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_throttled_time") @patch("azurelinuxagent.common.cgroup.CGroup.is_active") def test_cgroup_telemetry_should_not_report_cpu_negative_value(self, patch_is_active, path_get_throttled_time, patch_get_cpu_usage): diff --git a/tests/utils/test_extension_process_util.py b/tests/utils/test_extension_process_util.py index e950338fa6..a74c4ff733 100644 --- a/tests/utils/test_extension_process_util.py +++ b/tests/utils/test_extension_process_util.py @@ -15,14 +15,16 @@ # Requires Python 2.6+ and Openssl 1.0+ # import os +import shutil import subprocess import tempfile +from azurelinuxagent.common.cgroup import CpuCgroup from azurelinuxagent.common.exception import ExtensionError, ExtensionErrorCodes from azurelinuxagent.common.future import ustr from azurelinuxagent.common.utils.extensionprocessutil import format_stdout_stderr, read_output, \ wait_for_process_completion_or_timeout, handle_process_completion -from tests.tools import AgentTestCase, patch +from tests.tools import AgentTestCase, patch, data_dir class TestProcessUtils(AgentTestCase): @@ -50,7 +52,7 @@ def test_wait_for_process_completion_or_timeout_should_terminate_cleanly(self): stdout=subprocess.PIPE, stderr=subprocess.PIPE) - timed_out, ret = wait_for_process_completion_or_timeout(process=process, timeout=5) + timed_out, ret, _ = wait_for_process_completion_or_timeout(process=process, timeout=5, cpu_cgroup=None) self.assertEqual(timed_out, False) self.assertEqual(ret, 0) @@ -68,7 +70,7 @@ def test_wait_for_process_completion_or_timeout_should_kill_process_on_timeout(s # We don't actually mock the kill, just wrap it so we can assert its call count with patch('azurelinuxagent.common.utils.extensionprocessutil.os.killpg', wraps=os.killpg) as patch_kill: with patch('time.sleep') as mock_sleep: - timed_out, ret = wait_for_process_completion_or_timeout(process=process, timeout=timeout) + timed_out, ret, _ = wait_for_process_completion_or_timeout(process=process, timeout=timeout, cpu_cgroup=None) # We're mocking sleep to avoid prolonging the test execution time, but we still want to make sure # we're "waiting" the correct amount of time before killing the process @@ -87,7 +89,7 @@ def test_handle_process_completion_should_return_nonzero_when_process_fails(self stdout=subprocess.PIPE, stderr=subprocess.PIPE) - timed_out, ret = wait_for_process_completion_or_timeout(process=process, timeout=5) + timed_out, ret, _ = wait_for_process_completion_or_timeout(process=process, timeout=5, cpu_cgroup=None) self.assertEqual(timed_out, False) self.assertEqual(ret, 2) @@ -143,6 +145,46 @@ def test_handle_process_completion_should_raise_on_timeout(self): self.assertEqual(context_manager.exception.code, ExtensionErrorCodes.PluginHandlerScriptTimedout) self.assertIn("Timeout({0})".format(timeout), ustr(context_manager.exception)) + self.assertNotIn("CPUThrottledTime({0}secs)".format(timeout), ustr(context_manager.exception)) #Extension not started in cpuCgroup + + + def test_handle_process_completion_should_log_throttled_time_on_timeout(self): + command = "sleep 1m" + timeout = 20 + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout: + with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr: + with patch('time.sleep') as mock_sleep: + with self.assertRaises(ExtensionError) as context_manager: + test_file = os.path.join(self.tmp_dir, "cpu.stat") + shutil.copyfile(os.path.join(data_dir, "cgroups", "cpu.stat_t0"), + test_file) # throttled_time = 50 + cgroup = CpuCgroup("test", self.tmp_dir) + process = subprocess.Popen(command, # pylint: disable=subprocess-popen-preexec-fn + shell=True, + cwd=self.tmp_dir, + env={}, + stdout=stdout, + stderr=stderr, + preexec_fn=os.setsid) + + handle_process_completion(process=process, + command=command, + timeout=timeout, + stdout=stdout, + stderr=stderr, + error_code=42, + cpu_cgroup=cgroup) + + # We're mocking sleep to avoid prolonging the test execution time, but we still want to make sure + # we're "waiting" the correct amount of time before killing the process and raising an exception + # Due to an extra call to sleep at some point in the call stack which only happens sometimes, + # we are relaxing this assertion to allow +/- 2 sleep calls. + self.assertTrue(abs(mock_sleep.call_count - timeout) <= 2) + + self.assertEqual(context_manager.exception.code, ExtensionErrorCodes.PluginHandlerScriptTimedout) + self.assertIn("Timeout({0})".format(timeout), ustr(context_manager.exception)) + throttled_time = float(50 / 1E9) + self.assertIn("CPUThrottledTime({0}secs)".format(throttled_time), ustr(context_manager.exception)) def test_handle_process_completion_should_raise_on_nonzero_exit_code(self): command = "ls folder_does_not_exist" From b8ca4323d91fd2fac54fbcb7aab0f6988b4cd0e5 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Tue, 17 May 2022 14:27:46 -0700 Subject: [PATCH 20/58] fix network interface restart in RHEL9 (#2592) --- azurelinuxagent/common/osutil/factory.py | 5 ++++- azurelinuxagent/common/osutil/redhat.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/azurelinuxagent/common/osutil/factory.py b/azurelinuxagent/common/osutil/factory.py index 2ed4be78ba..61c2e6d20d 100644 --- a/azurelinuxagent/common/osutil/factory.py +++ b/azurelinuxagent/common/osutil/factory.py @@ -34,7 +34,7 @@ from .nsbsd import NSBSDOSUtil from .openbsd import OpenBSDOSUtil from .openwrt import OpenWRTOSUtil -from .redhat import RedhatOSUtil, Redhat6xOSUtil +from .redhat import RedhatOSUtil, Redhat6xOSUtil, RedhatOSModernUtil from .suse import SUSEOSUtil, SUSE11OSUtil from .photonos import PhotonOSUtil from .ubuntu import UbuntuOSUtil, Ubuntu12OSUtil, Ubuntu14OSUtil, \ @@ -107,6 +107,9 @@ def _get_osutil(distro_name, distro_code_name, distro_version, distro_full_name) if Version(distro_version) < Version("7"): return Redhat6xOSUtil() + if Version(distro_version) == Version("8.6") or Version(distro_version) > Version("9"): + return RedhatOSModernUtil() + return RedhatOSUtil() if distro_name == "euleros": diff --git a/azurelinuxagent/common/osutil/redhat.py b/azurelinuxagent/common/osutil/redhat.py index 9759d1136b..312dd16084 100644 --- a/azurelinuxagent/common/osutil/redhat.py +++ b/azurelinuxagent/common/osutil/redhat.py @@ -142,3 +142,25 @@ def get_dhcp_lease_endpoint(self): endpoint = self.get_endpoint_from_leases_path('/var/lib/NetworkManager/dhclient-*.lease') return endpoint + + +class RedhatOSModernUtil(RedhatOSUtil): + def __init__(self): # pylint: disable=W0235 + super(RedhatOSModernUtil, self).__init__() + + def restart_if(self, ifname, retries=3, wait=5): + """ + Restart an interface by bouncing the link. systemd-networkd observes + this event, and forces a renew of DHCP. + """ + retry_limit = retries + 1 + for attempt in range(1, retry_limit): + return_code = shellutil.run("ip link set {0} down && ip link set {0} up".format(ifname)) + if return_code == 0: + return + logger.warn("failed to restart {0}: return code {1}".format(ifname, return_code)) + if attempt < retry_limit: + logger.info("retrying in {0} seconds".format(wait)) + time.sleep(wait) + else: + logger.warn("exceeded restart retries") From cd3b6d21b0fdc22aeee9e0c089819acf653e32fb Mon Sep 17 00:00:00 2001 From: peter9370 Date: Wed, 18 May 2022 17:24:32 +0100 Subject: [PATCH 21/58] Added support for devuan linux distribution (#2553) * Added support for distro Devuan (Will only work with Devuan >= 4 (Chimaera) - reason is that in Devuan < 4, the platform.linux_distribution module is used to get distro details - and this module is unable to distinguish between Debian and Devuan. In Chimaera, python3 is 3.9, and in this, platform.linux_distribution has been removed, so that distro details are obtained using distro.linux_distribution - which is able to distinguish between Debian and Devuan) Details: - added azurelinuxagent/common/osutil/devuan.py - modified azurelinuxagent/common/osutil/factory.py to use devuan.py - added init and config files for devuan - modified setup.py for devuan support - modified tests/common/osutil/test_factory.py to test devuan support * waagent.conf - changed default for OS.EnableFirewall to y * devuan.py - removed unused module imports * init/devuan/walinuxagent: removed all operations on and references to pidfile, and simplified status to reflect this * init/devuan/walinuxagent: removed commented-out code lines, and tidied up some comments Co-authored-by: Norberto Arrieta --- azurelinuxagent/common/osutil/devuan.py | 52 ++++ azurelinuxagent/common/osutil/factory.py | 11 + config/devuan/waagent.conf | 130 +++++++++ init/devuan/default/walinuxagent | 2 + init/devuan/walinuxagent | 344 +++++++++++++++++++++++ setup.py | 10 + tests/common/osutil/test_factory.py | 9 + 7 files changed, 558 insertions(+) create mode 100644 azurelinuxagent/common/osutil/devuan.py create mode 100644 config/devuan/waagent.conf create mode 100644 init/devuan/default/walinuxagent create mode 100644 init/devuan/walinuxagent diff --git a/azurelinuxagent/common/osutil/devuan.py b/azurelinuxagent/common/osutil/devuan.py new file mode 100644 index 0000000000..a482cd05a0 --- /dev/null +++ b/azurelinuxagent/common/osutil/devuan.py @@ -0,0 +1,52 @@ +# +# Copyright 2018 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.6+ and Openssl 1.0+ +# + +import azurelinuxagent.common.logger as logger +import azurelinuxagent.common.utils.shellutil as shellutil +from azurelinuxagent.common.osutil.default import DefaultOSUtil + + +class DevuanOSUtil(DefaultOSUtil): + + def __init__(self): + super(DevuanOSUtil, self).__init__() + self.jit_enabled = True + + def restart_ssh_service(self): + logger.info("DevuanOSUtil::restart_ssh_service - trying to restart sshd") + return shellutil.run("/usr/sbin/service restart ssh", chk_err=False) + + def stop_agent_service(self): + logger.info("DevuanOSUtil::stop_agent_service - trying to stop waagent") + return shellutil.run("/usr/sbin/service walinuxagent stop", chk_err=False) + + def start_agent_service(self): + logger.info("DevuanOSUtil::start_agent_service - trying to start waagent") + return shellutil.run("/usr/sbin/service walinuxagent start", chk_err=False) + + def start_network(self): + pass + + def remove_rules_files(self, rules_files=""): + pass + + def restore_rules_files(self, rules_files=""): + pass + + def get_dhcp_lease_endpoint(self): + return self.get_endpoint_from_leases_path('/var/lib/dhcp/dhclient.*.leases') diff --git a/azurelinuxagent/common/osutil/factory.py b/azurelinuxagent/common/osutil/factory.py index 61c2e6d20d..e799ddcd99 100644 --- a/azurelinuxagent/common/osutil/factory.py +++ b/azurelinuxagent/common/osutil/factory.py @@ -27,6 +27,7 @@ from .coreos import CoreOSUtil from .debian import DebianOSBaseUtil, DebianOSModernUtil from .default import DefaultOSUtil +from .devuan import DevuanOSUtil from .freebsd import FreeBSDOSUtil from .gaia import GaiaOSUtil from .iosxe import IosxeOSUtil @@ -102,6 +103,16 @@ def _get_osutil(distro_name, distro_code_name, distro_version, distro_full_name) return DebianOSBaseUtil() + # Devuan support only works with v4+ + # Reason is that Devuan v4 (Chimaera) uses python v3.9, in which the + # platform.linux_distribution module has been removed. This was unable + # to distinguish between debian and devuan. The new distro.linux_distribution module + # is able to distinguish between the two. + + if distro_name == "devuan" and Version(distro_version) >= Version("4"): + return DevuanOSUtil() + + if distro_name in ("redhat", "rhel", "centos", "oracle", "almalinux", "cloudlinux", "rocky"): if Version(distro_version) < Version("7"): diff --git a/config/devuan/waagent.conf b/config/devuan/waagent.conf new file mode 100644 index 0000000000..be80edbd42 --- /dev/null +++ b/config/devuan/waagent.conf @@ -0,0 +1,130 @@ +# +# Microsoft Azure Linux Agent Configuration +# + +# Enable extension handling. Do not disable this unless you do not need password reset, +# backup, monitoring, or any extension handling whatsoever. +Extensions.Enabled=y + +# Which provisioning agent to use. Supported values are "auto" (default), "waagent", +# "cloud-init", or "disabled". +Provisioning.Agent=auto + +# Password authentication for root account will be unavailable. +Provisioning.DeleteRootPassword=y + +# Generate fresh host key pair. +Provisioning.RegenerateSshHostKeyPair=y + +# Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto". +# The "auto" option is supported on OpenSSH 5.9 (2011) and later. +Provisioning.SshHostKeyPairType=auto + +# Monitor host name changes and publish changes via DHCP requests. +Provisioning.MonitorHostName=y + +# Decode CustomData from Base64. +Provisioning.DecodeCustomData=n + +# Execute CustomData after provisioning. +Provisioning.ExecuteCustomData=n + +# Algorithm used by crypt when generating password hash. +#Provisioning.PasswordCryptId=6 + +# Length of random salt used when generating password hash. +#Provisioning.PasswordCryptSaltLength=10 + +# Allow reset password of sys user +Provisioning.AllowResetSysUser=n + +# Format if unformatted. If 'n', resource disk will not be mounted. +ResourceDisk.Format=y + +# File system on the resource disk +# Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. +ResourceDisk.Filesystem=ext4 + +# Mount point for the resource disk +ResourceDisk.MountPoint=/mnt/resource + +# Create and use swapfile on resource disk. +ResourceDisk.EnableSwap=n + +# Size of the swapfile. +ResourceDisk.SwapSizeMB=0 + +# Comma-separated list of mount options. See mount(8) for valid options. +ResourceDisk.MountOptions=None + +# Enable verbose logging (y|n) +Logs.Verbose=n + +# Enable Console logging, default is y +# Logs.Console=y + +# Is FIPS enabled +OS.EnableFIPS=n + +# Root device timeout in seconds. +OS.RootDeviceScsiTimeout=300 + +# If "None", the system default version is used. +OS.OpensslPath=None + +# Set the SSH ClientAliveInterval +# OS.SshClientAliveInterval=180 + +# Set the path to SSH keys and configuration files +OS.SshDir=/etc/ssh + +# If set, agent will use proxy server to access internet +#HttpProxy.Host=None +#HttpProxy.Port=None + +# Detect Scvmm environment, default is n +# DetectScvmmEnv=n + +# +# Lib.Dir=/var/lib/waagent + +# +# DVD.MountPoint=/mnt/cdrom/secure + +# +# Pid.File=/var/run/waagent.pid + +# +# Extension.LogDir=/var/log/azure + +# +# Home.Dir=/home + +# Enable RDMA management and set up, should only be used in HPC images +# OS.EnableRDMA=y + +# Enable or disable goal state processing auto-update, default is enabled +# AutoUpdate.Enabled=y + +# Determine the update family, this should not be changed +# AutoUpdate.GAFamily=Prod + +# Determine if the overprovisioning feature is enabled. If yes, hold extension +# handling until inVMArtifactsProfile.OnHold is false. +# Default is enabled +# EnableOverProvisioning=y + +# Allow fallback to HTTP if HTTPS is unavailable +# Note: Allowing HTTP (vs. HTTPS) may cause security risks +# OS.AllowHTTP=n + +# Add firewall rules to protect access to Azure host node services +# Note: +# - The default is false to protect the state of existing VMs +OS.EnableFirewall=y + +# Enforce control groups limits on the agent and extensions +CGroups.EnforceLimits=n + +# CGroups which are excluded from limits, comma separated +CGroups.Excluded=customscript,runcommand diff --git a/init/devuan/default/walinuxagent b/init/devuan/default/walinuxagent new file mode 100644 index 0000000000..025320250e --- /dev/null +++ b/init/devuan/default/walinuxagent @@ -0,0 +1,2 @@ +# To disable the Microsoft Azure Agent, set WALINUXAGENT_ENABLED=0 +WALINUXAGENT_ENABLED=1 diff --git a/init/devuan/walinuxagent b/init/devuan/walinuxagent new file mode 100644 index 0000000000..0d967c0cc8 --- /dev/null +++ b/init/devuan/walinuxagent @@ -0,0 +1,344 @@ +#!/bin/bash +# walinuxagent +# script to start and stop the waagent daemon. +# +# This script takes into account the possibility that both daemon and +# non-daemon instances of waagent may be running concurrently, +# and attempts to ensure that any non-daemon instances are preserved +# when the daemon instance is stopped. +# +### BEGIN INIT INFO +# Provides: walinuxagent +# Required-Start: $remote_fs $syslog $network +# Required-Stop: $remote_fs +# X-Start-Before: cloud-init +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Microsoft Azure Linux Agent +### END INIT INFO + +DESC="Microsoft Azure Linux Agent" +INTERPRETER="/usr/bin/python3" +DAEMON='/usr/sbin/waagent' +DAEMON_ARGS='-daemon' +START_ARGS='--background' +NAME='waagent' +# set to 1 to enable a lot of debugging output +DEBUG=0 + +. /lib/lsb/init-functions + +debugmsg() { + # output a console message if DEBUG is set + # (can be enabled dynamically by giving "debug" as an extra argument) + if [ "x${DEBUG}" == "x1" ] ; then + echo "[debug]: $1" >&2 + fi + return 0 +} + +check_non_daemon_instances() { + # check if there are any non-daemon instances of waagent running + local NDPIDLIST i NDPIDCT + declare -a NDPIDLIST + debugmsg "check_non_daemon_instance: after init, #NDPIDLIST=${#NDPIDLIST[*]}" + readarray -t NDPIDLIST < <( ps ax | + grep "${INTERPRETER}" | + grep "${DAEMON}" | + grep -v -- "${DAEMON_ARGS}" | + grep -v "grep" | + awk '{ print $1 }') + NDPIDCT=${#NDPIDLIST[@]} + debugmsg "check_non_daemon_instances: NDPIDCT=${NDPIDCT}" + debugmsg "check_non_daemon_instances: NDPIDLIST[0] = ${NDPIDLIST[0]}" + if [ ${NDPIDCT} -gt 0 ] ; then + debugmsg "check_non_daemon_instances: WARNING: non-daemon instances of waagent exist" + else + debugmsg "check_non_daemon_instances: no non-daemon instances of waagent are currently running" + fi + for (( i = 0 ; i < ${NDPIDCT} ; i++ )) ; do + debugmsg "check_non_daemon_instances: WARNING: process ${NDPIDLIST[${i}]} is a non-daemon waagent instance" + done + return 0 +} + +get_daemon_pid() { + # (re)create PIDLIST, return the first entry + local PID + create_pidlist + PID=${PIDLIST[0]} + if [ -z "${PID}" ] ; then + debugmsg "get_daemon_pid: : WARNING: no waagent daemon process found" + fi + echo "${PID}" +} + +recheck_status() { + # after an attempt to stop the daemon, re-check the status + # and take any further actions required. + # (NB: at the moment, we only re-check once. Possible improvement + # would be to iterate the re-check up to a given maximum tries). + local STATUS NEWSTATUS + get_status + STATUS=$? + debugmsg "stop_waagent: status is now ${STATUS}" + # ideal if stop has been successful: STATUS=1 - no daemon process + case ${STATUS} in + 0) + # stop didn't work + # what to do? maybe try kill -9 ? + debugmsg "recheck_status: ERROR: unable to stop waagent" + debugmsg "recheck_status: trying again with kill -9" + kill_daemon_from_pid 1 + # probably need to check status again? + get_status + NEW_STATUS=$? + if [ "x${NEW_STATUS}" == "x1" ] ; then + debugmsg "recheck_status: successfully stopped." + log_end_msg 0 || true + else + # could probably do something more productive here + debugmsg "recheck_status: unable to stop daemon - giving up" + log_end_msg 1 || true + exit 1 + fi + ;; + 1) + # THIS IS THE EXPECTED CASE: daemon is no longer running and + debugmsg "recheck_status: waagent daemon stopped successfully." + log_end_msg 0 || true + ;; + 2) + # so weird that we can't figure out what's going on + debugmsg "recheck_status: ERROR: unable to determine waagent status" + debugmsg "recheck_status: manual intervention required" + log_end_msg 1 || true + exit 1 + ;; + esac +} + +start_waagent() { + # we use start-stop-daemon for starting waagent + local STATUS + get_status + STATUS=$? + # check the status value - take appropriate action + debugmsg "start_waagent: STATUS=${STATUS}" + case "${STATUS}" in + 0) + debugmsg "start_waagent: waagent is already running" + log_daemon_msg "waagent is already running" + log_end_msg 0 || true + ;; + 1) + # not running (we ignore presence/absence of pidfile) + # just start waagent + debugmsg "start_waagent: waagent is not currently running" + log_daemon_msg "Starting ${NAME} daemon" + start-stop-daemon --start --quiet --background --name "${NAME}" --exec ${INTERPRETER} -- ${DAEMON} ${DAEMON_ARGS} + log_end_msg $? || true + ;; + 2) + # get_status can't figure out what's going on. + # try doing a stop to clean up, then attempt to start waagent + # will probably require manual intervention + debugmsg "start_waagent: unable to determine current status" + debugmsg "start_waagent: trying to stop waagent first, and then start it" + stop_waagent + log_daemon_msg "Starting ${NAME} daemon" + start-stop-daemon --start --quiet --background --name ${NAME} --exec ${INTERPRETER} -- ${DAEMON} ${DAEMON_ARGS} + log_end_msg $? || true + ;; + esac +} + +kill_daemon_from_pidlist() { + # check the pidlist for at least one waagent daemon process + # if found, kill it directly from the entry in the pidlist + # Ignore any pidfile. Avoid killing any non-daemon + # waagent processes. + # If called with "1" as first argument, use kill -9 rather than + # normal kill + local i PIDCT FORCE + FORCE=0 + if [ "x${1}" == "x1" ] ; then + debugmsg "kill_daemon_from_pidlist: WARNING: using kill -9" + FORCE=1 + fi + debugmsg "kill_daemon_from_pidlist: killing daemon using pid(s) in PIDLIST" + PIDCT=${#PIDLIST[*]} + if [ "${PIDCT}" -eq 0 ] ; then + debugmsg "kill_daemon_from_pidlist: ERROR: no pids in PIDLIST" + return 1 + fi + for (( i=0 ; i < ${PIDCT} ; i++ )) ; do + debugmsg "kill_daemon_from_pidlist: killing waagent daemon process ${PIDLIST[${i}]}" + if [ "x${FORCE}" == "x1" ] ; then + kill -9 ${PIDLIST[${i}]} + else + kill ${PIDLIST[${i}]} + fi + done + return 0 +} + +stop_waagent() { + # check the current status and if the waagent daemon is running, attempt + # to stop it. + # start-stop-daemon is avoided here + local STATUS PID RC + get_status + STATUS=$? + debugmsg "stop_waagent: current status = ${STATUS}" + case "${STATUS}" in + 0) + # - ignore any pidfile - kill directly from process list + log_daemon_msg "Stopping ${NAME} daemon (using process list)" + kill_daemon_from_pidlist + recheck_status + ;; + 1) + # not running - we ignore any pidfile + # REVISIT: should we check for a pidfile and remove if found? + debugmsg "waagent is not running" + log_daemon_msg "waagent is already stopped" + log_end_msg 0 || true + ;; + 2) + # weirdness - call for help + debugmsg "ERROR: unable to determine waagent status - manual intervention required" + log_daemon_msg "WARNING: unable to determine status of waagent daemon - manual intervention required" + log_end_msg 1 || true + ;; + esac +} + +check_daemons() { + # check for running waagent daemon processes + local ENTRY + ps ax | + grep "${INTERPRETER}" | + grep "${DAEMON}" | + grep -- "${DAEMON_ARGS}" | + grep -v 'grep' | + while read ENTRY ; do + debugmsg "check_daemons(): ENTRY='${ENTRY}'" + done + return 0 +} + +create_pidlist() { + # initialise the list of waagent daemon processes + # NB: there should only be one - both this script and waagent itself + # attempt to avoid starting more than one daemon process. + # However, we use an array just in case. + readarray -t PIDLIST < <( ps ax | + grep "${INTERPRETER}" | + grep "${DAEMON}" | + grep -- "${DAEMON_ARGS}" | + grep -v 'grep' | + awk '{ print $1 }') + if [ "${#PIDLIST[*]}" -eq 0 ] ; then + debugmsg "create_pidlist: WARNING: no waagent daemons found" + elif [ "${#PIDLIST[*]}" -gt 1 ] ; then + debugmsg "create_pidlist: WARNING: multiple waagent daemons running" + fi + return 0 +} + +get_status() { + # simplified status - ignoring any pidfile + # Possibilities: + # 0 - waagent daemon running + # 1 - waagent daemon not running + # 2 - status unclear + # (NB: if we find that multiple daemons exist, we just ignore the fact. + # It should be virtually impossible for this to happen) + local FOUND RPID ENTRY STATUS DAEMON_RUNNING PIDCT + PIDCT=0 + DAEMON_RUNNING= + RPID= + ENTRY= + # assume the worst + STATUS=2 + check_daemons + create_pidlist + # should only be one daemon running - but we check, just in case + PIDCT=${#PIDLIST[@]} + debugmsg "get_status: PIDCT=${PIDCT}" + if [ ${PIDCT} -eq 0 ] ; then + # not running + STATUS=1 + else + # at least one daemon process is running + if [ ${PIDCT} -gt 1 ] ; then + debugmsg "get_status: WARNING: more than one waagent daemon running" + debugmsg "get_status: (should not happen)" + else + debugmsg "get_status: only one daemon instance running - as expected" + fi + STATUS=0 + fi + return ${STATUS} +} + +waagent_status() { + # get the current status of the waagent daemon, and return it + local STATUS + get_status + STATUS=$? + debugmsg "waagent status = ${STATUS}" + case ${STATUS} in + 0) + log_daemon_msg "waagent is running" + ;; + 1) + log_daemon_msg "WARNING: waagent is not running" + ;; + 2) + log_daemon_msg "WARNING: waagent status cannot be determined" + ;; + esac + log_end_msg 0 || true + return 0 +} + + +######################################################################### +# MAINLINE +# Usage: "service [scriptname] [ start | stop | status | restart ] [ debug ] +# (specifying debug as extra argument enables debugging output) +######################################################################### + +export PATH="${PATH}:+$PATH:}/usr/sbin:/sbin" + +declare -a PIDLIST + +if [ ! -z "$2" -a "$2" == "debug" ] ; then + DEBUG=1 +fi + +# pre-check for non-daemon (e.g. console) instances of waagent +check_non_daemon_instances + +case "$1" in + start) + start_waagent + ;; + + stop) + stop_waagent + ;; + + status) + waagent_status + ;; + + restart) + stop_waagent + start_waagent + ;; + +esac +exit 0 diff --git a/setup.py b/setup.py index f929435852..f90ea70e4a 100755 --- a/setup.py +++ b/setup.py @@ -219,6 +219,16 @@ def get_data_files(name, version, fullname): # pylint: disable=R0912 set_udev_files(data_files, dest="/lib/udev/rules.d") if debian_has_systemd(): set_systemd_files(data_files, dest=systemd_dir_path) + elif name == 'devuan': + set_bin_files(data_files, dest=agent_bin_path, + src=["bin/py3/waagent", "bin/waagent2.0"]) + set_files(data_files, dest="/etc/init.d", + src=['init/devuan/walinuxagent']) + set_files(data_files, dest="/etc/default", + src=['init/devuan/default/walinuxagent']) + set_conf_files(data_files, src=['config/devuan/waagent.conf']) + set_logrotate_files(data_files) + set_udev_files(data_files, dest="/lib/udev/rules.d") elif name == 'iosxe': set_bin_files(data_files, dest=agent_bin_path) set_conf_files(data_files, src=["config/iosxe/waagent.conf"]) diff --git a/tests/common/osutil/test_factory.py b/tests/common/osutil/test_factory.py index 5007242733..7bd729c3b3 100644 --- a/tests/common/osutil/test_factory.py +++ b/tests/common/osutil/test_factory.py @@ -21,6 +21,7 @@ from azurelinuxagent.common.osutil.clearlinux import ClearLinuxUtil from azurelinuxagent.common.osutil.coreos import CoreOSUtil from azurelinuxagent.common.osutil.debian import DebianOSBaseUtil, DebianOSModernUtil +from azurelinuxagent.common.osutil.devuan import DevuanOSUtil from azurelinuxagent.common.osutil.default import DefaultOSUtil from azurelinuxagent.common.osutil.factory import _get_osutil from azurelinuxagent.common.osutil.freebsd import FreeBSDOSUtil @@ -187,6 +188,14 @@ def test_get_osutil_it_should_return_debian(self): self.assertTrue(isinstance(ret, DebianOSModernUtil)) self.assertEqual(ret.get_service_name(), "walinuxagent") + def test_get_osutil_it_should_return_devuan(self): + ret = _get_osutil(distro_name="devuan", + distro_code_name="", + distro_full_name="", + distro_version="4") + self.assertTrue(isinstance(ret, DevuanOSUtil)) + self.assertEqual(ret.get_service_name(), "waagent") + def test_get_osutil_it_should_return_redhat(self): ret = _get_osutil(distro_name="redhat", distro_code_name="", From 215cde7433c242ab070821fd99fab066a647aec5 Mon Sep 17 00:00:00 2001 From: yinbing Date: Thu, 19 May 2022 14:33:31 -0700 Subject: [PATCH 22/58] CGroup: set Agent CPU to 50% (#2591) Co-authored-by: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> --- azurelinuxagent/common/conf.py | 4 ++-- tests/test_agent.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/azurelinuxagent/common/conf.py b/azurelinuxagent/common/conf.py index bd101f617f..f5caf3ce72 100644 --- a/azurelinuxagent/common/conf.py +++ b/azurelinuxagent/common/conf.py @@ -184,7 +184,7 @@ def load_conf_from_file(conf_file_path, conf=__conf__): # versions of the Agent. # "Debug.CgroupCheckPeriod": 300, - "Debug.AgentCpuQuota": 75, + "Debug.AgentCpuQuota": 50, "Debug.AgentCpuThrottledTimeThreshold": 120, "Debug.EtpCollectionPeriod": 300, "Debug.AutoUpdateHotfixFrequency": 14400, @@ -543,7 +543,7 @@ def get_agent_cpu_quota(conf=__conf__): NOTE: This option is experimental and may be removed in later versions of the Agent. """ - return conf.get_int("Debug.AgentCpuQuota", 75) + return conf.get_int("Debug.AgentCpuQuota", 50) def get_agent_cpu_throttled_time_threshold(conf=__conf__): diff --git a/tests/test_agent.py b/tests/test_agent.py index ceaa858273..1b14c9d169 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -29,7 +29,7 @@ AutoUpdate.GAFamily = Prod Autoupdate.Frequency = 3600 DVD.MountPoint = /mnt/cdrom/secure -Debug.AgentCpuQuota = 75 +Debug.AgentCpuQuota = 50 Debug.AgentCpuThrottledTimeThreshold = 120 Debug.AutoUpdateHotfixFrequency = 14400 Debug.AutoUpdateNormalFrequency = 86400 From 1d434f92e08a3d77c68703f468813545910fdedd Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Fri, 20 May 2022 10:32:30 -0700 Subject: [PATCH 23/58] Create placeholder GoalState.*.xml file (#2594) (#2596) Co-authored-by: narrieta (cherry picked from commit 7b92617d5fb155b4c56a03313b00a77382db4d3c) --- azurelinuxagent/common/utils/archive.py | 22 ++++++++++++++++++++++ tests/utils/test_archive.py | 8 ++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/azurelinuxagent/common/utils/archive.py b/azurelinuxagent/common/utils/archive.py index 6123fdb0d2..0be1544c57 100644 --- a/azurelinuxagent/common/utils/archive.py +++ b/azurelinuxagent/common/utils/archive.py @@ -39,6 +39,10 @@ ARCHIVE_DIRECTORY_NAME = 'history' +# TODO: See comment in GoalStateHistory._save_placeholder and remove this code when no longer needed +_PLACEHOLDER_FILE_NAME = 'GoalState.1.xml' +# END TODO + _MAX_ARCHIVED_STATES = 50 _CACHE_PATTERNS = [ @@ -173,6 +177,10 @@ def purge(self): def purge_legacy_goal_state_history(): lib_dir = conf.get_lib_dir() for current_file in os.listdir(lib_dir): + # TODO: See comment in GoalStateHistory._save_placeholder and remove this code when no longer needed + if current_file == _PLACEHOLDER_FILE_NAME: + return + # END TODO full_path = os.path.join(lib_dir, current_file) for pattern in _CACHE_PATTERNS: match = pattern.match(current_file) @@ -232,8 +240,22 @@ def save(self, data, file_name): self._errors = True logger.warn("Failed to save {0} to the goal state history: {1} [no additional errors saving the goal state will be reported]".format(file_name, e)) + @staticmethod + def _save_placeholder(): + """ + Some internal components took a dependency in the legacy GoalState.*.xml file. We create it here while those components are updated to remove the dependency. + When removing this code, also remove the check in StateArchiver.purge_legacy_goal_state_history, and the definition of _PLACEHOLDER_FILE_NAME + """ + try: + placeholder = os.path.join(conf.get_lib_dir(), _PLACEHOLDER_FILE_NAME) + with open(placeholder, "w") as handle: + handle.write("empty placeholder file") + except Exception as e: + logger.warn("Failed to save placeholder file ({0}): {1}".format(_PLACEHOLDER_FILE_NAME, e)) + def save_goal_state(self, text): self.save(text, _GOAL_STATE_FILE_NAME) + self._save_placeholder() def save_extensions_config(self, text): self.save(text, _EXT_CONF_FILE_NAME) diff --git a/tests/utils/test_archive.py b/tests/utils/test_archive.py index 0c649c9e24..5eee67c7da 100644 --- a/tests/utils/test_archive.py +++ b/tests/utils/test_archive.py @@ -134,10 +134,10 @@ def test_archive02(self): def test_purge_legacy_goal_state_history(self): with patch("azurelinuxagent.common.conf.get_lib_dir", return_value=self.tmp_dir): legacy_files = [ - 'GoalState.1.xml', - 'VmSettings.1.json', - 'Prod.1.manifest.xml', - 'ExtensionsConfig.1.xml', + 'GoalState.2.xml', + 'VmSettings.2.json', + 'Prod.2.manifest.xml', + 'ExtensionsConfig.2.xml', 'Microsoft.Azure.Extensions.CustomScript.1.xml', 'SharedConfig.xml', 'HostingEnvironmentConfig.xml', From dca7b21118d176b51c6327a5ac8788d046ccfde3 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Thu, 26 May 2022 14:13:08 -0700 Subject: [PATCH 24/58] Do not mark VmSettingsSummary as a failure (#2599) Co-authored-by: narrieta --- azurelinuxagent/common/protocol/hostplugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurelinuxagent/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py index f79076f8ef..313a626391 100644 --- a/azurelinuxagent/common/protocol/hostplugin.py +++ b/azurelinuxagent/common/protocol/hostplugin.py @@ -624,7 +624,7 @@ def report_error(self, error, category): self._error_count += 1 if self._error_count <= _VmSettingsErrorReporter._MaxErrors: - add_event(op=WALAEventOperation.VmSettings, message="[{0}] {1}".format(category, error), is_success=False, log_event=False) + add_event(op=WALAEventOperation.VmSettings, message="[{0}] {1}".format(category, error), is_success=True, log_event=False) if category == _VmSettingsError.ClientError: self._client_error_count += 1 From 48158a8b56ba89730be93c7211e76cd0373d2690 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Thu, 26 May 2022 17:07:17 -0700 Subject: [PATCH 25/58] onboard rhel9 distro (#2598) --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f90ea70e4a..e7e52663e8 100755 --- a/setup.py +++ b/setup.py @@ -97,7 +97,7 @@ def get_data_files(name, version, fullname): # pylint: disable=R0912 agent_bin_path = osutil.get_agent_bin_path() if name in ('redhat', 'centos', 'almalinux', 'cloudlinux', 'rocky'): - if version.startswith("8"): + if version.startswith("8") or version.startswith("9"): # redhat8+ default to py3 set_bin_files(data_files, dest=agent_bin_path, src=["bin/py3/waagent", "bin/waagent2.0"]) @@ -106,7 +106,7 @@ def get_data_files(name, version, fullname): # pylint: disable=R0912 set_conf_files(data_files) set_logrotate_files(data_files) set_udev_files(data_files) - if version.startswith("8"): + if version.startswith("8") or version.startswith("9"): # redhat 8+ uses systemd and python3 set_systemd_files(data_files, dest=systemd_dir_path, src=["init/redhat/waagent.service", From 8320fee03cd4264563c11c908902ca19e8acd5e3 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Thu, 2 Jun 2022 09:26:46 -0700 Subject: [PATCH 26/58] Monitor RAM usage for VMAgent (#2597) * Monitor RAM usage for VMAgent * address comments * fix Unit test * address new comments * refactor metricvalue * fix tests * fix monitor UTS * fix pylint warning * update periodic report * addressed few more comments * fix ut errors * pylint warnings * pylint error * fix tests * address private members * log counter found error * fix test errors --- azurelinuxagent/common/cgroup.py | 134 +++++++++++++++--- azurelinuxagent/common/cgroupconfigurator.py | 26 +++- azurelinuxagent/ga/monitor.py | 7 +- init/redhat/py2/waagent.service | 1 + init/redhat/waagent.service | 1 + init/ubuntu/walinuxagent.service | 1 + tests/common/mock_cgroup_environment.py | 1 + tests/common/test_cgroupconfigurator.py | 18 ++- tests/common/test_cgroups.py | 21 ++- tests/common/test_cgroupstelemetry.py | 106 +++++++------- tests/common/test_event.py | 5 +- tests/data/cgroups/memory_mount/memory.stat | 36 +++++ .../memory_mount/memory.usage_in_bytes | 1 - .../missing_memory_counters/memory.stat | 34 +++++ tests/data/init/walinuxagent.service | 1 + tests/ga/test_monitor.py | 51 ++----- 16 files changed, 317 insertions(+), 127 deletions(-) create mode 100644 tests/data/cgroups/memory_mount/memory.stat delete mode 100644 tests/data/cgroups/memory_mount/memory.usage_in_bytes create mode 100644 tests/data/cgroups/missing_memory_counters/memory.stat diff --git a/azurelinuxagent/common/cgroup.py b/azurelinuxagent/common/cgroup.py index 5fadf6bfb1..6d6b7bde83 100644 --- a/azurelinuxagent/common/cgroup.py +++ b/azurelinuxagent/common/cgroup.py @@ -13,21 +13,60 @@ # limitations under the License. # # Requires Python 2.6+ and Openssl 1.0+ -from collections import namedtuple import errno import os import re +from datetime import timedelta -from azurelinuxagent.common import logger +from azurelinuxagent.common import logger, conf from azurelinuxagent.common.exception import CGroupsException from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.utils import fileutil +_REPORT_EVERY_HOUR = timedelta(hours=1) +_DEFAULT_REPORT_PERIOD = timedelta(seconds=conf.get_cgroup_check_period()) + AGENT_NAME_TELEMETRY = "walinuxagent.service" # Name used for telemetry; it needs to be consistent even if the name of the service changes -MetricValue = namedtuple('Metric', ['category', 'counter', 'instance', 'value']) + +class CounterNotFound(Exception): + pass + + +class MetricValue(object): + + """ + Class for defining all the required metric fields to send telemetry. + """ + + def __init__(self, category, counter, instance, value, report_period=_DEFAULT_REPORT_PERIOD): + self._category = category + self._counter = counter + self._instance = instance + self._value = value + self._report_period = report_period + + @property + def category(self): + return self._category + + @property + def counter(self): + return self._counter + + @property + def instance(self): + return self._instance + + @property + def value(self): + return self._value + + @property + def report_period(self): + return self._report_period class MetricsCategory(object): @@ -40,6 +79,7 @@ class MetricsCounter(object): TOTAL_MEM_USAGE = "Total Memory Usage" MAX_MEM_USAGE = "Max Memory Usage" THROTTLED_TIME = "Throttled Time" + SWAP_MEM_USAGE = "Swap Memory Usage" re_user_system_times = re.compile(r'user (\d+)\nsystem (\d+)\n') @@ -166,7 +206,8 @@ def _get_cpu_ticks(self, allow_no_such_file_or_directory_error=False): # match = re_user_system_times.match(cpuacct_stat) if not match: - raise CGroupsException("The contents of {0} are invalid: {1}".format(self._get_cgroup_file('cpuacct.stat'), cpuacct_stat)) + raise CGroupsException( + "The contents of {0} are invalid: {1}".format(self._get_cgroup_file('cpuacct.stat'), cpuacct_stat)) cpu_ticks = int(match.groups()[0]) + int(match.groups()[1]) return cpu_ticks @@ -239,7 +280,8 @@ def get_cpu_throttled_time(self, read_previous_throttled_time=True): return float(self.get_throttled_time() / 1E9) if not self._cpu_usage_initialized(): - raise CGroupsException("initialize_cpu_usage() must be invoked before the first call to get_throttled_time()") + raise CGroupsException( + "initialize_cpu_usage() must be invoked before the first call to get_throttled_time()") self._previous_throttled_time = self._current_throttled_time self._current_throttled_time = self.get_throttled_time() @@ -250,53 +292,99 @@ def get_tracked_metrics(self, **kwargs): tracked = [] cpu_usage = self.get_cpu_usage() if cpu_usage >= float(0): - tracked.append(MetricValue(MetricsCategory.CPU_CATEGORY, MetricsCounter.PROCESSOR_PERCENT_TIME, self.name, cpu_usage)) + tracked.append( + MetricValue(MetricsCategory.CPU_CATEGORY, MetricsCounter.PROCESSOR_PERCENT_TIME, self.name, cpu_usage)) if 'track_throttled_time' in kwargs and kwargs['track_throttled_time']: throttled_time = self.get_cpu_throttled_time() if cpu_usage >= float(0) and throttled_time >= float(0): - tracked.append(MetricValue(MetricsCategory.CPU_CATEGORY, MetricsCounter.THROTTLED_TIME, self.name, throttled_time)) + tracked.append( + MetricValue(MetricsCategory.CPU_CATEGORY, MetricsCounter.THROTTLED_TIME, self.name, throttled_time)) return tracked class MemoryCgroup(CGroup): + def __init__(self, name, cgroup_path): + super(MemoryCgroup, self).__init__(name, cgroup_path) + + self._counter_not_found_error_count = 0 + + def _get_memory_stat_counter(self, counter_name): + try: + with open(os.path.join(self.path, 'memory.stat')) as memory_stat: + # cat /sys/fs/cgroup/memory/azure.slice/memory.stat + # cache 67178496 + # rss 42340352 + # rss_huge 6291456 + # swap 0 + for line in memory_stat: + re_memory_counter = r'{0}\s+(\d+)'.format(counter_name) + match = re.match(re_memory_counter, line) + if match is not None: + return int(match.groups()[0]) + except (IOError, OSError) as e: + if e.errno == errno.ENOENT: + raise + raise CGroupsException("Failed to read memory.stat: {0}".format(ustr(e))) + except Exception as e: + raise CGroupsException("Failed to read memory.stat: {0}".format(ustr(e))) + + raise CounterNotFound("Cannot find counter: {0}".format(counter_name)) + def get_memory_usage(self): """ - Collect memory.usage_in_bytes from the cgroup. + Collect RSS+CACHE from memory.stat cgroup. :return: Memory usage in bytes :rtype: int """ - usage = None - try: - usage = self._get_parameters('memory.usage_in_bytes', first_line_only=True) - except Exception as e: - if isinstance(e, (IOError, OSError)) and e.errno == errno.ENOENT: # pylint: disable=E1101 - raise - raise CGroupsException("Exception while attempting to read {0}".format("memory.usage_in_bytes"), e) - return int(usage) + cache = self._get_memory_stat_counter("cache") + rss = self._get_memory_stat_counter("rss") + return cache + rss + + def try_swap_memory_usage(self): + """ + Collect SWAP from memory.stat cgroup. + + :return: Memory usage in bytes + :rtype: int + Note: stat file is the only place to get the SWAP since other swap related file memory.memsw.usage_in_bytes is for total Memory+SWAP. + """ + try: + return self._get_memory_stat_counter("swap") + except CounterNotFound as e: + if self._counter_not_found_error_count < 1: + logger.periodic_warn(logger.EVERY_HALF_HOUR, + 'Could not find swap counter from "memory.stat" file in the cgroup: {0}.' + ' Internal error: {1}'.format(self.path, ustr(e))) + self._counter_not_found_error_count += 1 + return 0 def get_max_memory_usage(self): """ - Collect memory.usage_in_bytes from the cgroup. + Collect memory.max_usage_in_bytes from the cgroup. :return: Memory usage in bytes :rtype: int """ - usage = None + usage = 0 try: - usage = self._get_parameters('memory.max_usage_in_bytes', first_line_only=True) + usage = int(self._get_parameters('memory.max_usage_in_bytes', first_line_only=True)) except Exception as e: if isinstance(e, (IOError, OSError)) and e.errno == errno.ENOENT: # pylint: disable=E1101 raise - raise CGroupsException("Exception while attempting to read {0}".format("memory.usage_in_bytes"), e) + raise CGroupsException("Exception while attempting to read {0}".format("memory.max_usage_in_bytes"), e) - return int(usage) + return usage def get_tracked_metrics(self, **_): return [ - MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.TOTAL_MEM_USAGE, self.name, self.get_memory_usage()), - MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.MAX_MEM_USAGE, self.name, self.get_max_memory_usage()), + MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.TOTAL_MEM_USAGE, self.name, + self.get_memory_usage()), + MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.MAX_MEM_USAGE, self.name, + self.get_max_memory_usage(), _REPORT_EVERY_HOUR), + MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.SWAP_MEM_USAGE, self.name, + self.try_swap_memory_usage(), _REPORT_EVERY_HOUR) ] diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index 308838a731..6c16666eab 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -23,7 +23,7 @@ from azurelinuxagent.common import conf from azurelinuxagent.common import logger -from azurelinuxagent.common.cgroup import CpuCgroup, AGENT_NAME_TELEMETRY, MetricsCounter +from azurelinuxagent.common.cgroup import CpuCgroup, AGENT_NAME_TELEMETRY, MetricsCounter, MemoryCgroup from azurelinuxagent.common.cgroupapi import CGroupsApi, SystemdCgroupsApi, SystemdRunError, EXTENSION_SLICE_PREFIX from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry from azurelinuxagent.common.exception import ExtensionErrorCodes, CGroupsException @@ -98,6 +98,13 @@ [Service] CPUQuota={0} """ +_DROP_IN_FILE_MEMORY_ACCOUNTING = "13-MemoryAccounting.conf" +_DROP_IN_FILE_MEMORY_ACCOUNTING_CONTENTS = """ +# This drop-in unit file was created by the Azure VM Agent. +# Do not edit. +[Service] +MemoryAccounting=yes +""" class DisableCgroups(object): @@ -176,11 +183,18 @@ def initialize(self): cpu_controller_root, memory_controller_root) + if self._agent_cpu_cgroup_path is not None or self._agent_memory_cgroup_path is not None: + self.enable() + if self._agent_cpu_cgroup_path is not None: _log_cgroup_info("Agent CPU cgroup: {0}", self._agent_cpu_cgroup_path) - self.enable() + self.__set_cpu_quota(conf.get_agent_cpu_quota()) CGroupsTelemetry.track_cgroup(CpuCgroup(AGENT_NAME_TELEMETRY, self._agent_cpu_cgroup_path)) + if self._agent_memory_cgroup_path is not None: + _log_cgroup_info("Agent Memory cgroup: {0}", self._agent_memory_cgroup_path) + CGroupsTelemetry.track_cgroup(MemoryCgroup(AGENT_NAME_TELEMETRY, self._agent_memory_cgroup_path)) + _log_cgroup_info('Agent cgroups enabled: {0}', self._agent_cgroups_enabled) except Exception as exception: @@ -322,6 +336,7 @@ def __setup_azure_slice(): agent_drop_in_path = systemd.get_agent_drop_in_path() agent_drop_in_file_slice = os.path.join(agent_drop_in_path, _AGENT_DROP_IN_FILE_SLICE) agent_drop_in_file_cpu_accounting = os.path.join(agent_drop_in_path, _DROP_IN_FILE_CPU_ACCOUNTING) + agent_drop_in_file_memory_accounting = os.path.join(agent_drop_in_path, _DROP_IN_FILE_MEMORY_ACCOUNTING) files_to_create = [] @@ -349,6 +364,12 @@ def __setup_azure_slice(): if not os.path.exists(agent_drop_in_file_cpu_accounting): files_to_create.append((agent_drop_in_file_cpu_accounting, _DROP_IN_FILE_CPU_ACCOUNTING_CONTENTS)) + if fileutil.findre_in_file(agent_unit_file, r"MemoryAccounting=") is not None: + CGroupConfigurator._Impl.__cleanup_unit_file(agent_drop_in_file_memory_accounting) + else: + if not os.path.exists(agent_drop_in_file_memory_accounting): + files_to_create.append((agent_drop_in_file_memory_accounting, _DROP_IN_FILE_MEMORY_ACCOUNTING_CONTENTS)) + if len(files_to_create) > 0: # create the unit files, but if 1 fails remove all and return try: @@ -482,7 +503,6 @@ def enable(self): "Attempted to enable cgroups, but they are not supported on the current platform") self._agent_cgroups_enabled = True self._extensions_cgroups_enabled = True - self.__set_cpu_quota(conf.get_agent_cpu_quota()) def disable(self, reason, disable_cgroups): if disable_cgroups == DisableCgroups.ALL: # disable all diff --git a/azurelinuxagent/ga/monitor.py b/azurelinuxagent/ga/monitor.py index a4afea98ac..43ebd6888b 100644 --- a/azurelinuxagent/ga/monitor.py +++ b/azurelinuxagent/ga/monitor.py @@ -47,16 +47,21 @@ class PollResourceUsage(PeriodicOperation): Periodic operation to poll the tracked cgroups for resource usage data. It also checks whether there are processes in the agent's cgroup that should not be there. + """ def __init__(self): super(PollResourceUsage, self).__init__(conf.get_cgroup_check_period()) self.__log_metrics = conf.get_cgroup_log_metrics() + self.__periodic_metrics = {} def _operation(self): tracked_metrics = CGroupsTelemetry.poll_all_tracked() for metric in tracked_metrics: - report_metric(metric.category, metric.counter, metric.instance, metric.value, log_event=self.__log_metrics) + key = metric.category + metric.counter + metric.instance + if key not in self.__periodic_metrics or (self.__periodic_metrics[key] + metric.report_period) <= datetime.datetime.now(): + report_metric(metric.category, metric.counter, metric.instance, metric.value, log_event=self.__log_metrics) + self.__periodic_metrics[key] = datetime.datetime.now() CGroupConfigurator.get_instance().check_cgroups(tracked_metrics) diff --git a/init/redhat/py2/waagent.service b/init/redhat/py2/waagent.service index c6d15420ef..920e0ec73a 100644 --- a/init/redhat/py2/waagent.service +++ b/init/redhat/py2/waagent.service @@ -13,6 +13,7 @@ Restart=always RestartSec=5 Slice=azure.slice CPUAccounting=yes +MemoryAccounting=yes [Install] WantedBy=multi-user.target diff --git a/init/redhat/waagent.service b/init/redhat/waagent.service index dc11fbb1a7..2c6ac5d824 100644 --- a/init/redhat/waagent.service +++ b/init/redhat/waagent.service @@ -13,6 +13,7 @@ Restart=always RestartSec=5 Slice=azure.slice CPUAccounting=yes +MemoryAccounting=yes [Install] WantedBy=multi-user.target diff --git a/init/ubuntu/walinuxagent.service b/init/ubuntu/walinuxagent.service index f3887c5b4f..4b4588617a 100644 --- a/init/ubuntu/walinuxagent.service +++ b/init/ubuntu/walinuxagent.service @@ -19,6 +19,7 @@ ExecStart=/usr/bin/python3 -u /usr/sbin/waagent -daemon Restart=always Slice=azure.slice CPUAccounting=yes +MemoryAccounting=yes [Install] WantedBy=multi-user.target diff --git a/tests/common/mock_cgroup_environment.py b/tests/common/mock_cgroup_environment.py index d4caa0015b..ea64890ccf 100644 --- a/tests/common/mock_cgroup_environment.py +++ b/tests/common/mock_cgroup_environment.py @@ -97,6 +97,7 @@ class UnitFilePaths: slice = "/lib/systemd/system/walinuxagent.service.d/10-Slice.conf" cpu_accounting = "/lib/systemd/system/walinuxagent.service.d/11-CPUAccounting.conf" cpu_quota = "/lib/systemd/system/walinuxagent.service.d/12-CPUQuota.conf" + memory_accounting = "/lib/systemd/system/walinuxagent.service.d/13-MemoryAccounting.conf" extension_service_cpu_accounting = '/lib/systemd/system/extension.service.d/11-CPUAccounting.conf' extension_service_cpu_quota = '/lib/systemd/system/extension.service.d/12-CPUQuota.conf' extension_service_memory_accounting = '/lib/systemd/system/extension.service.d/13-MemoryAccounting.conf' diff --git a/tests/common/test_cgroupconfigurator.py b/tests/common/test_cgroupconfigurator.py index e2f06a67ac..ac35b0d199 100644 --- a/tests/common/test_cgroupconfigurator.py +++ b/tests/common/test_cgroupconfigurator.py @@ -77,6 +77,8 @@ def test_initialize_should_start_tracking_the_agent_cgroups(self): self.assertTrue(configurator.enabled(), "Cgroups should be enabled") self.assertTrue(any(cg for cg in tracked.values() if cg.name == AGENT_NAME_TELEMETRY and 'cpu' in cg.path), "The Agent's CPU is not being tracked. Tracked: {0}".format(tracked)) + self.assertTrue(any(cg for cg in tracked.values() if cg.name == AGENT_NAME_TELEMETRY and 'memory' in cg.path), + "The Agent's Memory is not being tracked. Tracked: {0}".format(tracked)) def test_initialize_should_start_tracking_other_controllers_when_one_is_not_present(self): command_mocks = [MockCommand(r"^mount -t cgroup$", @@ -147,6 +149,7 @@ def test_initialize_should_not_create_unit_files(self): extensions_slice_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.vmextensions) agent_drop_in_file_slice = configurator.mocks.get_mapped_path(UnitFilePaths.slice) agent_drop_in_file_cpu_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.cpu_accounting) + agent_drop_in_file_memory_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.memory_accounting) # The mock creates the slice unit files; delete them os.remove(azure_slice_unit_file) @@ -158,6 +161,7 @@ def test_initialize_should_not_create_unit_files(self): self.assertFalse(os.path.exists(extensions_slice_unit_file), "{0} should not have been created".format(extensions_slice_unit_file)) self.assertFalse(os.path.exists(agent_drop_in_file_slice), "{0} should not have been created".format(agent_drop_in_file_slice)) self.assertFalse(os.path.exists(agent_drop_in_file_cpu_accounting), "{0} should not have been created".format(agent_drop_in_file_cpu_accounting)) + self.assertFalse(os.path.exists(agent_drop_in_file_memory_accounting), "{0} should not have been created".format(agent_drop_in_file_memory_accounting)) def test_initialize_should_create_unit_files_when_the_agent_service_file_is_not_updated(self): with self._get_cgroup_configurator(initialize=False) as configurator: @@ -166,6 +170,7 @@ def test_initialize_should_create_unit_files_when_the_agent_service_file_is_not_ extensions_slice_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.vmextensions) agent_drop_in_file_slice = configurator.mocks.get_mapped_path(UnitFilePaths.slice) agent_drop_in_file_cpu_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.cpu_accounting) + agent_drop_in_file_memory_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.memory_accounting) # The mock creates the service and slice unit files; replace the former and delete the latter configurator.mocks.add_data_file(os.path.join(data_dir, 'init', "walinuxagent.service.previous"), UnitFilePaths.walinuxagent) @@ -180,6 +185,7 @@ def test_initialize_should_create_unit_files_when_the_agent_service_file_is_not_ self.assertTrue(os.path.exists(extensions_slice_unit_file), "{0} was not created".format(extensions_slice_unit_file)) self.assertTrue(os.path.exists(agent_drop_in_file_slice), "{0} was not created".format(agent_drop_in_file_slice)) self.assertTrue(os.path.exists(agent_drop_in_file_cpu_accounting), "{0} was not created".format(agent_drop_in_file_cpu_accounting)) + self.assertTrue(os.path.exists(agent_drop_in_file_memory_accounting), "{0} was not created".format(agent_drop_in_file_memory_accounting)) def test_setup_extension_slice_should_create_unit_files(self): with self._get_cgroup_configurator() as configurator: @@ -229,12 +235,12 @@ def test_enable_should_raise_cgroups_exception_when_cgroups_are_not_supported(se self.assertIn("Attempted to enable cgroups, but they are not supported on the current platform", str(context_manager.exception)) def test_enable_should_set_agent_cpu_quota_and_track_throttled_time(self): - with self._get_cgroup_configurator(enable=False) as configurator: + with self._get_cgroup_configurator(initialize=False) as configurator: agent_drop_in_file_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.cpu_quota) if os.path.exists(agent_drop_in_file_cpu_quota): raise Exception("{0} should not have been created during test setup".format(agent_drop_in_file_cpu_quota)) - configurator.enable() + configurator.initialize() expected_quota = "CPUQuota={0}%".format(conf.get_agent_cpu_quota()) self.assertTrue(os.path.exists(agent_drop_in_file_cpu_quota), "{0} was not created".format(agent_drop_in_file_cpu_quota)) @@ -244,13 +250,13 @@ def test_enable_should_set_agent_cpu_quota_and_track_throttled_time(self): self.assertTrue(CGroupsTelemetry.get_track_throttled_time(), "Throttle time should be tracked") def test_enable_should_not_track_throttled_time_when_setting_the_cpu_quota_fails(self): - with self._get_cgroup_configurator(enable=False) as configurator: + with self._get_cgroup_configurator(initialize=False) as configurator: if CGroupsTelemetry.get_track_throttled_time(): raise Exception("Test setup should not start tracking Throttle Time") configurator.mocks.add_file(UnitFilePaths.cpu_quota, Exception("A TEST EXCEPTION")) - configurator.enable() + configurator.initialize() self.assertFalse(CGroupsTelemetry.get_track_throttled_time(), "Throttle time should not be tracked") @@ -268,7 +274,9 @@ def test_disable_should_reset_cpu_quota(self): self.assertTrue( fileutil.findre_in_file(agent_drop_in_file_cpu_quota, "^CPUQuota=$"), "CPUQuota was not set correctly. Expected an empty value. Got:\n{0}".format(fileutil.read_file(agent_drop_in_file_cpu_quota))) - self.assertEqual(len(CGroupsTelemetry._tracked), 0, "No cgroups should be tracked after disable. Tracking: {0}".format(CGroupsTelemetry._tracked)) + self.assertEqual(len(CGroupsTelemetry._tracked), 1, "Memory cgroups should be tracked after disable. Tracking: {0}".format(CGroupsTelemetry._tracked)) + self.assertFalse(any(cg for cg in CGroupsTelemetry._tracked.values() if cg.name == 'walinuxagent.service' and 'cpu' in cg.path), + "The Agent's cpu should not be tracked. Tracked: {0}".format(CGroupsTelemetry._tracked)) def test_disable_should_reset_cpu_quota_for_all_cgroups(self): service_list = [ diff --git a/tests/common/test_cgroups.py b/tests/common/test_cgroups.py index 61c3178546..7f549e5b8d 100644 --- a/tests/common/test_cgroups.py +++ b/tests/common/test_cgroups.py @@ -22,7 +22,7 @@ import random import shutil -from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup, MetricsCounter +from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup, MetricsCounter, CounterNotFound from azurelinuxagent.common.exception import CGroupsException from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.utils import fileutil @@ -206,11 +206,14 @@ def test_get_metrics(self): test_mem_cg = MemoryCgroup("test_extension", os.path.join(data_dir, "cgroups", "memory_mount")) memory_usage = test_mem_cg.get_memory_usage() - self.assertEqual(100000, memory_usage) + self.assertEqual(150000, memory_usage) max_memory_usage = test_mem_cg.get_max_memory_usage() self.assertEqual(1000000, max_memory_usage) + swap_memory_usage = test_mem_cg.try_swap_memory_usage() + self.assertEqual(20000, swap_memory_usage) + def test_get_metrics_when_files_not_present(self): test_mem_cg = MemoryCgroup("test_extension", os.path.join(data_dir, "cgroups")) @@ -223,3 +226,17 @@ def test_get_metrics_when_files_not_present(self): test_mem_cg.get_max_memory_usage() self.assertEqual(e.exception.errno, errno.ENOENT) + + with self.assertRaises(IOError) as e: + test_mem_cg.try_swap_memory_usage() + + self.assertEqual(e.exception.errno, errno.ENOENT) + + def test_get_memory_usage_counters_not_found(self): + test_mem_cg = MemoryCgroup("test_extension", os.path.join(data_dir, "cgroups", "missing_memory_counters")) + + with self.assertRaises(CounterNotFound): + test_mem_cg.get_memory_usage() + + swap_memory_usage = test_mem_cg.try_swap_memory_usage() + self.assertEqual(0, swap_memory_usage) diff --git a/tests/common/test_cgroupstelemetry.py b/tests/common/test_cgroupstelemetry.py index dc612244d8..fe1ff299a3 100644 --- a/tests/common/test_cgroupstelemetry.py +++ b/tests/common/test_cgroupstelemetry.py @@ -116,18 +116,20 @@ def _assert_cgroups_are_tracked(self, num_extensions): self.assertTrue(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) self.assertTrue(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) - def _assert_polled_metrics_equal(self, metrics, cpu_metric_value, memory_metric_value, max_memory_metric_value): + def _assert_polled_metrics_equal(self, metrics, cpu_metric_value, memory_metric_value, max_memory_metric_value, swap_memory_value): for metric in metrics: self.assertIn(metric.category, ["CPU", "Memory"]) if metric.category == "CPU": self.assertEqual(metric.counter, "% Processor Time") self.assertEqual(metric.value, cpu_metric_value) if metric.category == "Memory": - self.assertIn(metric.counter, ["Total Memory Usage", "Max Memory Usage", "Memory Used by Process"]) + self.assertIn(metric.counter, ["Total Memory Usage", "Max Memory Usage", "Swap Memory Usage"]) if metric.counter == "Total Memory Usage": self.assertEqual(metric.value, memory_metric_value) elif metric.counter == "Max Memory Usage": self.assertEqual(metric.value, max_memory_metric_value) + elif metric.counter == "Swap Memory Usage": + self.assertEqual(metric.value, swap_memory_value) def test_telemetry_polling_with_active_cgroups(self, *args): # pylint: disable=unused-argument num_extensions = 3 @@ -136,27 +138,30 @@ def test_telemetry_polling_with_active_cgroups(self, *args): # pylint: disable= with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") as patch_get_memory_max_usage: with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: - with patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") as patch_get_cpu_usage: - with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: - patch_is_active.return_value = True - - current_cpu = 30 - current_memory = 209715200 - current_max_memory = 471859200 - - # 1 CPU metric + 1 Current Memory + 1 Max memory - num_of_metrics_per_extn_expected = 3 - patch_get_cpu_usage.return_value = current_cpu - patch_get_memory_usage.return_value = current_memory # example 200 MB - patch_get_memory_max_usage.return_value = current_max_memory # example 450 MB - num_polls = 10 - - for data_count in range(1, num_polls + 1): # pylint: disable=unused-variable - metrics = CGroupsTelemetry.poll_all_tracked() - - self.assertEqual(len(metrics), num_extensions * num_of_metrics_per_extn_expected) - self._assert_polled_metrics_equal(metrics, current_cpu, current_memory, current_max_memory) - + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.try_swap_memory_usage") as patch_try_swap_memory_usage: + with patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") as patch_get_cpu_usage: + with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: + patch_is_active.return_value = True + + current_cpu = 30 + current_memory = 209715200 + current_max_memory = 471859200 + current_swap_memory = 20971520 + + # 1 CPU metric + 1 Current Memory + 1 Max memory + 1 swap memory + num_of_metrics_per_extn_expected = 4 + patch_get_cpu_usage.return_value = current_cpu + patch_get_memory_usage.return_value = current_memory # example 200 MB + patch_get_memory_max_usage.return_value = current_max_memory # example 450 MB + patch_try_swap_memory_usage.return_value = current_swap_memory # example 20MB + num_polls = 12 + + for data_count in range(1, num_polls + 1): # pylint: disable=unused-variable + metrics = CGroupsTelemetry.poll_all_tracked() + + self.assertEqual(len(metrics), num_extensions * num_of_metrics_per_extn_expected) + self._assert_polled_metrics_equal(metrics, current_cpu, current_memory, current_max_memory, current_swap_memory) @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage", side_effect=raise_ioerror) @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage", side_effect=raise_ioerror) @@ -177,7 +182,6 @@ def test_telemetry_polling_with_inactive_cgroups(self, *_): self.assertEqual(len(metrics), 0) - @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") @@ -216,7 +220,6 @@ def test_telemetry_polling_with_changing_cgroups_state(self, patch_is_active, pa self.assertFalse(CGroupsTelemetry.is_tracked("dummy_cpu_path_{0}".format(i))) self.assertFalse(CGroupsTelemetry.is_tracked("dummy_memory_path_{0}".format(i))) - # mocking get_proc_stat to make it run on Mac and other systems. This test does not need to read the values of the # /proc/stat file on the filesystem. @patch("azurelinuxagent.common.logger.periodic_warn") @@ -238,7 +241,7 @@ def test_telemetry_polling_to_not_generate_transient_logs_ioerror_file_not_found @patch("azurelinuxagent.common.logger.periodic_warn") def test_telemetry_polling_to_generate_transient_logs_ioerror_permission_denied(self, patch_periodic_warn): num_extensions = 1 - num_controllers = 2 + num_controllers = 1 is_active_check_per_controller = 2 self._track_new_extension_cgroups(num_extensions) @@ -251,7 +254,7 @@ def test_telemetry_polling_to_generate_transient_logs_ioerror_permission_denied( with patch("azurelinuxagent.common.utils.fileutil.read_file", side_effect=io_error_3): poll_count = 1 expected_count_per_call = num_controllers + is_active_check_per_controller - # each collect per controller would generate a log statement, and each cgroup would invoke a + # get_max_memory_usage memory controller would generate a log statement, and each cgroup would invoke a # is active check raising an exception for data_count in range(poll_count, 10): # pylint: disable=unused-variable @@ -266,16 +269,18 @@ def test_telemetry_polling_to_generate_transient_logs_index_error(self): # Trying to invoke IndexError during the getParameter call with patch("azurelinuxagent.common.utils.fileutil.read_file", return_value=''): with patch("azurelinuxagent.common.logger.periodic_warn") as patch_periodic_warn: - expected_call_count = 2 # 1 periodic warning for the cpu cgroups, and 1 for memory + expected_call_count = 1 # 1 periodic warning for memory for data_count in range(1, 10): # pylint: disable=unused-variable CGroupsTelemetry.poll_all_tracked() self.assertEqual(expected_call_count, patch_periodic_warn.call_count) + @patch("azurelinuxagent.common.cgroup.MemoryCgroup.try_swap_memory_usage") @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") @patch("azurelinuxagent.common.cgroup.CGroup.is_active") - def test_telemetry_calculations(self, patch_is_active, patch_get_cpu_usage, patch_get_memory_usage, patch_get_memory_max_usage, *args): # pylint: disable=unused-argument + def test_telemetry_calculations(self, patch_is_active, patch_get_cpu_usage, patch_get_memory_usage, patch_get_memory_max_usage, patch_try_memory_swap_usage, + *args): # pylint: disable=unused-argument num_polls = 10 num_extensions = 1 @@ -284,6 +289,7 @@ def test_telemetry_calculations(self, patch_is_active, patch_get_cpu_usage, pat # only verifying calculations and not validity of the values. memory_usage_values = [random.randint(0, 8 * 1024 ** 3) for _ in range(num_polls)] max_memory_usage_values = [random.randint(0, 8 * 1024 ** 3) for _ in range(num_polls)] + swap_usage_values = [random.randint(0, 8 * 1024 ** 3) for _ in range(num_polls)] self._track_new_extension_cgroups(num_extensions) self.assertEqual(2 * num_extensions, len(CGroupsTelemetry._tracked)) @@ -291,14 +297,15 @@ def test_telemetry_calculations(self, patch_is_active, patch_get_cpu_usage, pat for i in range(num_polls): patch_is_active.return_value = True patch_get_cpu_usage.return_value = cpu_percent_values[i] - patch_get_memory_usage.return_value = memory_usage_values[i] # example 200 MB - patch_get_memory_max_usage.return_value = max_memory_usage_values[i] # example 450 MB + patch_get_memory_usage.return_value = memory_usage_values[i] + patch_get_memory_max_usage.return_value = max_memory_usage_values[i] + patch_try_memory_swap_usage.return_value = swap_usage_values[i] metrics = CGroupsTelemetry.poll_all_tracked() - # 1 CPU metric + 1 Current Memory + 1 Max memory - self.assertEqual(len(metrics), 3 * num_extensions) - self._assert_polled_metrics_equal(metrics, cpu_percent_values[i], memory_usage_values[i], max_memory_usage_values[i]) + # 1 CPU metric + 1 Current Memory + 1 Max memory + 1 swap memory + self.assertEqual(len(metrics), 4 * num_extensions) + self._assert_polled_metrics_equal(metrics, cpu_percent_values[i], memory_usage_values[i], max_memory_usage_values[i], swap_usage_values[i]) def test_cgroup_tracking(self, *args): # pylint: disable=unused-argument num_extensions = 5 @@ -332,8 +339,7 @@ def test_process_cgroup_metric_with_no_memory_cgroup_mounted(self, *args): # py metrics = CGroupsTelemetry.poll_all_tracked() self.assertEqual(len(metrics), num_extensions * 1) # Only CPU populated - self._assert_polled_metrics_equal(metrics, current_cpu, 0, 0) - + self._assert_polled_metrics_equal(metrics, current_cpu, 0, 0, 0) @patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage", side_effect=raise_ioerror) def test_process_cgroup_metric_with_no_cpu_cgroup_mounted(self, *args): # pylint: disable=unused-argument @@ -343,20 +349,23 @@ def test_process_cgroup_metric_with_no_cpu_cgroup_mounted(self, *args): # pylin with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") as patch_get_memory_max_usage: with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: - with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: - patch_is_active.return_value = True + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.try_swap_memory_usage") as patch_try_swap_memory_usage: + with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: + patch_is_active.return_value = True - current_memory = 209715200 - current_max_memory = 471859200 + current_memory = 209715200 + current_max_memory = 471859200 + current_swap_memory = 20971520 - patch_get_memory_usage.return_value = current_memory # example 200 MB - patch_get_memory_max_usage.return_value = current_max_memory # example 450 MB - num_polls = 10 - for data_count in range(1, num_polls + 1): # pylint: disable=unused-variable - metrics = CGroupsTelemetry.poll_all_tracked() - # Memory is only populated, CPU is not. Thus 2 metrics per cgroup. - self.assertEqual(len(metrics), num_extensions * 2) - self._assert_polled_metrics_equal(metrics, 0, current_memory, current_max_memory) + patch_get_memory_usage.return_value = current_memory # example 200 MB + patch_get_memory_max_usage.return_value = current_max_memory # example 450 MB + patch_try_swap_memory_usage.return_value = current_swap_memory # example 20MB + num_polls = 10 + for data_count in range(1, num_polls + 1): # pylint: disable=unused-variable + metrics = CGroupsTelemetry.poll_all_tracked() + # Memory is only populated, CPU is not. Thus 3 metrics for memory. + self.assertEqual(len(metrics), num_extensions * 3) + self._assert_polled_metrics_equal(metrics, 0, current_memory, current_max_memory, current_swap_memory) @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage", side_effect=raise_ioerror) @patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage", side_effect=raise_ioerror) @@ -408,4 +417,3 @@ def test_cgroup_telemetry_should_not_report_cpu_negative_value(self, patch_is_ac for metric in metrics: self.assertGreaterEqual(metric.value, 0, "telemetry should not report negative value") - diff --git a/tests/common/test_event.py b/tests/common/test_event.py index 46e923ef73..7191f3c30a 100644 --- a/tests/common/test_event.py +++ b/tests/common/test_event.py @@ -31,9 +31,10 @@ from azurelinuxagent.common.utils import textutil, fileutil from azurelinuxagent.common import event, logger from azurelinuxagent.common.AgentGlobals import AgentGlobals -from azurelinuxagent.common.event import add_event, add_periodic, add_log_event, elapsed_milliseconds, report_metric, \ +from azurelinuxagent.common.event import add_event, add_periodic, add_log_event, elapsed_milliseconds, \ WALAEventOperation, parse_xml_event, parse_json_event, AGENT_EVENT_FILE_EXTENSION, EVENTS_DIRECTORY, \ - TELEMETRY_EVENT_EVENT_ID, TELEMETRY_EVENT_PROVIDER_ID, TELEMETRY_LOG_EVENT_ID, TELEMETRY_LOG_PROVIDER_ID + TELEMETRY_EVENT_EVENT_ID, TELEMETRY_EVENT_PROVIDER_ID, TELEMETRY_LOG_EVENT_ID, TELEMETRY_LOG_PROVIDER_ID, \ + report_metric from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil from azurelinuxagent.common.telemetryevent import CommonTelemetryEventSchema, GuestAgentGenericLogsSchema, \ diff --git a/tests/data/cgroups/memory_mount/memory.stat b/tests/data/cgroups/memory_mount/memory.stat new file mode 100644 index 0000000000..945d561dc2 --- /dev/null +++ b/tests/data/cgroups/memory_mount/memory.stat @@ -0,0 +1,36 @@ +cache 50000 +rss 100000 +rss_huge 4194304 +shmem 8192 +mapped_file 540672 +dirty 0 +writeback 0 +swap 20000 +pgpgin 42584 +pgpgout 24188 +pgfault 71983 +pgmajfault 402 +inactive_anon 32854016 +active_anon 12288 +inactive_file 47472640 +active_file 1290240 +unevictable 0 +hierarchical_memory_limit 9223372036854771712 +hierarchical_memsw_limit 9223372036854771712 +total_cache 48771072 +total_rss 32845824 +total_rss_huge 4194304 +total_shmem 8192 +total_mapped_file 540672 +total_dirty 0 +total_writeback 0 +total_swap 0 +total_pgpgin 42584 +total_pgpgout 24188 +total_pgfault 71983 +total_pgmajfault 402 +total_inactive_anon 32854016 +total_active_anon 12288 +total_inactive_file 47472640 +total_active_file 1290240 +total_unevictable 0 \ No newline at end of file diff --git a/tests/data/cgroups/memory_mount/memory.usage_in_bytes b/tests/data/cgroups/memory_mount/memory.usage_in_bytes deleted file mode 100644 index 483fb82b6d..0000000000 --- a/tests/data/cgroups/memory_mount/memory.usage_in_bytes +++ /dev/null @@ -1 +0,0 @@ -100000 \ No newline at end of file diff --git a/tests/data/cgroups/missing_memory_counters/memory.stat b/tests/data/cgroups/missing_memory_counters/memory.stat new file mode 100644 index 0000000000..2f42670f0f --- /dev/null +++ b/tests/data/cgroups/missing_memory_counters/memory.stat @@ -0,0 +1,34 @@ +cache 50000 +rss_huge 4194304 +shmem 8192 +mapped_file 540672 +dirty 0 +writeback 0 +pgpgin 42584 +pgpgout 24188 +pgfault 71983 +pgmajfault 402 +inactive_anon 32854016 +active_anon 12288 +inactive_file 47472640 +active_file 1290240 +unevictable 0 +hierarchical_memory_limit 9223372036854771712 +hierarchical_memsw_limit 9223372036854771712 +total_cache 48771072 +total_rss 32845824 +total_rss_huge 4194304 +total_shmem 8192 +total_mapped_file 540672 +total_dirty 0 +total_writeback 0 +total_swap 0 +total_pgpgin 42584 +total_pgpgout 24188 +total_pgfault 71983 +total_pgmajfault 402 +total_inactive_anon 32854016 +total_active_anon 12288 +total_inactive_file 47472640 +total_active_file 1290240 +total_unevictable 0 \ No newline at end of file diff --git a/tests/data/init/walinuxagent.service b/tests/data/init/walinuxagent.service index cc1b71c910..e46bf99624 100644 --- a/tests/data/init/walinuxagent.service +++ b/tests/data/init/walinuxagent.service @@ -16,6 +16,7 @@ ExecStart=/usr/bin/python3 -u /usr/sbin/waagent -daemon Restart=always Slice=azure.slice CPUAccounting=yes +MemoryAccounting=yes [Install] WantedBy=multi-user.target diff --git a/tests/ga/test_monitor.py b/tests/ga/test_monitor.py index b2730a3eb2..9543f52848 100644 --- a/tests/ga/test_monitor.py +++ b/tests/ga/test_monitor.py @@ -21,7 +21,7 @@ import string from azurelinuxagent.common import event, logger -from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup, MetricValue +from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup, MetricValue, _REPORT_EVERY_HOUR from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry from azurelinuxagent.common.event import EVENTS_DIRECTORY from azurelinuxagent.common.protocol.healthservice import HealthService @@ -197,22 +197,23 @@ def tearDown(self): self.get_protocol.stop() @patch('azurelinuxagent.common.event.EventLogger.add_metric') - @patch('azurelinuxagent.common.event.EventLogger.add_event') @patch("azurelinuxagent.common.cgroupstelemetry.CGroupsTelemetry.poll_all_tracked") - def test_send_extension_metrics_telemetry(self, patch_poll_all_tracked, patch_add_event, # pylint: disable=unused-argument + def test_send_extension_metrics_telemetry(self, patch_poll_all_tracked, # pylint: disable=unused-argument patch_add_metric, *args): - patch_poll_all_tracked.return_value = [MetricValue("Process", "% Processor Time", 1, 1), - MetricValue("Memory", "Total Memory Usage", 1, 1), - MetricValue("Memory", "Max Memory Usage", 1, 1)] + patch_poll_all_tracked.return_value = [MetricValue("Process", "% Processor Time", "service", 1), + MetricValue("Memory", "Total Memory Usage", "service", 1), + MetricValue("Memory", "Max Memory Usage", "service", 1, _REPORT_EVERY_HOUR), + MetricValue("Memory", "Swap Memory Usage", "service", 1, _REPORT_EVERY_HOUR) + ] PollResourceUsage().run() self.assertEqual(1, patch_poll_all_tracked.call_count) - self.assertEqual(3, patch_add_metric.call_count) # Three metrics being sent. + self.assertEqual(4, patch_add_metric.call_count) # Four metrics being sent. @patch('azurelinuxagent.common.event.EventLogger.add_metric') @patch("azurelinuxagent.common.cgroupstelemetry.CGroupsTelemetry.poll_all_tracked") def test_send_extension_metrics_telemetry_for_empty_cgroup(self, patch_poll_all_tracked, # pylint: disable=unused-argument - patch_add_metric,*args): + patch_add_metric, *args): patch_poll_all_tracked.return_value = [] PollResourceUsage().run() @@ -245,41 +246,9 @@ def test_send_extension_metrics_telemetry_handling_cpu_cgroup_exceptions_errno2( ioerror.errno = 2 patch_cpu_usage.side_effect = ioerror - CGroupsTelemetry._tracked["/test/path"]= CpuCgroup("cgroup_name", "/test/path") + CGroupsTelemetry._tracked["/test/path"] = CpuCgroup("cgroup_name", "/test/path") PollResourceUsage().run() self.assertEqual(0, patch_periodic_warn.call_count) self.assertEqual(0, patch_add_metric.call_count) # No metrics should be sent. - def test_generate_extension_metrics_telemetry_dictionary(self, *args): # pylint: disable=unused-argument - num_polls = 10 - num_extensions = 1 - - cpu_percent_values = [random.randint(0, 100) for _ in range(num_polls)] - - # only verifying calculations and not validity of the values. - memory_usage_values = [random.randint(0, 8 * 1024 ** 3) for _ in range(num_polls)] - max_memory_usage_values = [random.randint(0, 8 * 1024 ** 3) for _ in range(num_polls)] - - # no need to initialize the CPU usage, since we mock get_cpu_usage() below - with patch("azurelinuxagent.common.cgroup.CpuCgroup.initialize_cpu_usage"): - for i in range(num_extensions): - dummy_cpu_cgroup = CpuCgroup("dummy_extension_{0}".format(i), "dummy_cpu_path_{0}".format(i)) - CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup) - - dummy_memory_cgroup = MemoryCgroup("dummy_extension_{0}".format(i), "dummy_memory_path_{0}".format(i)) - CGroupsTelemetry.track_cgroup(dummy_memory_cgroup) - - self.assertEqual(2 * num_extensions, len(CGroupsTelemetry._tracked)) - - with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_max_memory_usage") as patch_get_memory_max_usage: - with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_memory_usage") as patch_get_memory_usage: - with patch("azurelinuxagent.common.cgroup.CpuCgroup.get_cpu_usage") as patch_get_cpu_usage: - with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active: - for i in range(num_polls): - patch_is_active.return_value = True - patch_get_cpu_usage.return_value = cpu_percent_values[i] - patch_get_memory_usage.return_value = memory_usage_values[i] # example 200 MB - patch_get_memory_max_usage.return_value = max_memory_usage_values[i] # example 450 MB - CGroupsTelemetry.poll_all_tracked() - From a926e5454403a952f03c05da3a1c2dfbb43c89de Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Mon, 6 Jun 2022 14:18:42 -0700 Subject: [PATCH 27/58] Mark INFO messages as successful (#2604) Co-authored-by: narrieta --- azurelinuxagent/ga/update.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index fd11c4ca09..0cdd3b3b7f 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -722,11 +722,11 @@ def _report_extensions_summary(self, vm_status): self._extensions_summary = extensions_summary message = "Extension status: {0}".format(self._extensions_summary) logger.info(message) - add_event(op=WALAEventOperation.GoalState, message=message) + add_event(op=WALAEventOperation.GoalState, message=message, is_success=True) if self._extensions_summary.converged: message = "All extensions in the goal state have reached a terminal state: {0}".format(extensions_summary) logger.info(message) - add_event(op=WALAEventOperation.GoalState, message=message) + add_event(op=WALAEventOperation.GoalState, message=message, is_success=True) if self._is_initial_goal_state: self._on_initial_goal_state_completed(self._extensions_summary) except Exception as error: From 1884ec5fb9275692d1a0cd301f9a4cba60e9c635 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Mon, 6 Jun 2022 14:35:06 -0700 Subject: [PATCH 28/58] Do not mark VmSettingsSummary as a failure (#2605) * Do not mark VmSettingsSummary as a failure * Do not mark VmSettingsSummary as a failure Co-authored-by: narrieta --- azurelinuxagent/common/protocol/hostplugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurelinuxagent/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py index 313a626391..583b95ecf7 100644 --- a/azurelinuxagent/common/protocol/hostplugin.py +++ b/azurelinuxagent/common/protocol/hostplugin.py @@ -648,7 +648,7 @@ def report_summary(self): "failedRequests": self._request_failure_count } message = json.dumps(summary) - add_event(op=WALAEventOperation.VmSettingsSummary, message=message, is_success=False, log_event=False) + add_event(op=WALAEventOperation.VmSettingsSummary, message=message, is_success=True, log_event=False) if self._error_count > 0: logger.info("[VmSettingsSummary] {0}", message) From a560872bec103523e73ddcb5fb5705896545ffce Mon Sep 17 00:00:00 2001 From: Kevin Clark Date: Wed, 8 Jun 2022 12:31:46 -0700 Subject: [PATCH 29/58] Update Log Collector default in Comments and Readme (#2608) --- README.md | 2 +- config/debian/waagent.conf | 2 +- config/suse/waagent.conf | 2 +- config/ubuntu/waagent.conf | 2 +- config/waagent.conf | 2 +- tests/data/test_waagent.conf | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a1bfab5a58..996fafd5e9 100644 --- a/README.md +++ b/README.md @@ -455,7 +455,7 @@ _Default: y_ If set, agent logs will be periodically collected and uploaded to a secure location for improved supportability. -NOTE: This feature is only supported ubuntu 16.04+; this flag will not take effect on any other distro. +NOTE: This feature relies on the agent's resource usage features (cgroups); this flag will not take effect on any distro not supported. #### __Logs.CollectPeriod__ diff --git a/config/debian/waagent.conf b/config/debian/waagent.conf index 6bc3660462..dfd7afcd6d 100644 --- a/config/debian/waagent.conf +++ b/config/debian/waagent.conf @@ -63,7 +63,7 @@ Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y -# Enable periodic log collection, default is n +# Enable periodic log collection, default is y Logs.Collect=y # How frequently to collect logs, default is each hour diff --git a/config/suse/waagent.conf b/config/suse/waagent.conf index ac9f11a2d1..c617f9af8b 100644 --- a/config/suse/waagent.conf +++ b/config/suse/waagent.conf @@ -66,7 +66,7 @@ Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y -# Enable periodic log collection, default is n +# Enable periodic log collection, default is y Logs.Collect=y # How frequently to collect logs, default is each hour diff --git a/config/ubuntu/waagent.conf b/config/ubuntu/waagent.conf index 63635a81e2..19b56bae4a 100644 --- a/config/ubuntu/waagent.conf +++ b/config/ubuntu/waagent.conf @@ -66,7 +66,7 @@ Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y -# Enable periodic log collection, default is n +# Enable periodic log collection, default is y Logs.Collect=y # How frequently to collect logs, default is each hour diff --git a/config/waagent.conf b/config/waagent.conf index 0dd988840b..7316dc2da4 100644 --- a/config/waagent.conf +++ b/config/waagent.conf @@ -69,7 +69,7 @@ Logs.Verbose=n # Enable Console logging, default is y # Logs.Console=y -# Enable periodic log collection, default is n +# Enable periodic log collection, default is y Logs.Collect=y # How frequently to collect logs, default is each hour diff --git a/tests/data/test_waagent.conf b/tests/data/test_waagent.conf index a386228a09..cc60886e6e 100644 --- a/tests/data/test_waagent.conf +++ b/tests/data/test_waagent.conf @@ -67,7 +67,7 @@ ResourceDisk.MountOptions=None # Enable verbose logging (y|n) Logs.Verbose=n -# Enable periodic log collection, default is n +# Enable periodic log collection, default is y Logs.Collect=y # How frequently to collect logs, default is each hour From f73329503db5fa0ef075e7bf9d4df7831c65b6e9 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Wed, 15 Jun 2022 10:58:14 -0700 Subject: [PATCH 30/58] monitor system-wide memory metrics (#2610) * monitor system-wide memory metrics * address comments * fix uts --- azurelinuxagent/common/cgroup.py | 2 ++ azurelinuxagent/common/conf.py | 1 + azurelinuxagent/common/osutil/default.py | 22 ++++++++++++++++++++++ azurelinuxagent/ga/monitor.py | 23 +++++++++++++++++++++++ tests/common/osutil/test_default.py | 22 ++++++++++++++++++++++ tests/ga/test_monitor.py | 24 +++++++++++++++++++++++- 6 files changed, 93 insertions(+), 1 deletion(-) diff --git a/azurelinuxagent/common/cgroup.py b/azurelinuxagent/common/cgroup.py index 6d6b7bde83..28e65e24bd 100644 --- a/azurelinuxagent/common/cgroup.py +++ b/azurelinuxagent/common/cgroup.py @@ -80,6 +80,8 @@ class MetricsCounter(object): MAX_MEM_USAGE = "Max Memory Usage" THROTTLED_TIME = "Throttled Time" SWAP_MEM_USAGE = "Swap Memory Usage" + AVAILABLE_MEM = "Available MBytes" + USED_MEM = "Used MBytes" re_user_system_times = re.compile(r'user (\d+)\nsystem (\d+)\n') diff --git a/azurelinuxagent/common/conf.py b/azurelinuxagent/common/conf.py index f5caf3ce72..3c6e960fd0 100644 --- a/azurelinuxagent/common/conf.py +++ b/azurelinuxagent/common/conf.py @@ -563,6 +563,7 @@ def get_cgroup_monitor_expiry_time(conf=__conf__): """ return conf.get("Debug.CgroupMonitorExpiryTime", "2022-03-31") + def get_cgroup_monitor_extension_name (conf=__conf__): """ cgroups monitoring extension name diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py index d2ae36e834..056c50e07e 100644 --- a/azurelinuxagent/common/osutil/default.py +++ b/azurelinuxagent/common/osutil/default.py @@ -108,6 +108,7 @@ def get_firewall_delete_conntrack_drop_command(wait, destination): PACKET_PATTERN = "^\s*(\d+)\s+(\d+)\s+DROP\s+.*{0}[^\d]*$" # pylint: disable=W1401 ALL_CPUS_REGEX = re.compile('^cpu .*') +ALL_MEMS_REGEX = re.compile('^Mem.*') _enable_firewall = True @@ -1411,6 +1412,27 @@ def get_total_cpu_ticks_since_boot(): break return system_cpu + @staticmethod + def get_used_and_available_system_memory(): + """ + Get the contents of free -b in bytes. + # free -b + # total used free shared buff/cache available + # Mem: 8340144128 619352064 5236809728 1499136 2483982336 7426314240 + # Swap: 0 0 0 + + :return: used and available memory in megabytes + """ + used_mem = available_mem = 0 + free_cmd = ["free", "-b"] + memory = shellutil.run_command(free_cmd) + for line in memory.split("\n"): + if ALL_MEMS_REGEX.match(line): + mems = line.split() + used_mem = int(mems[2]) + available_mem = int(mems[6]) # see "man free" for a description of these fields + return used_mem/(1024 ** 2), available_mem/(1024 ** 2) + def get_nic_state(self, as_string=False): """ Capture NIC state (IPv4 and IPv6 addresses plus link state). diff --git a/azurelinuxagent/ga/monitor.py b/azurelinuxagent/ga/monitor.py index 43ebd6888b..e2744bc434 100644 --- a/azurelinuxagent/ga/monitor.py +++ b/azurelinuxagent/ga/monitor.py @@ -22,6 +22,7 @@ import azurelinuxagent.common.conf as conf import azurelinuxagent.common.logger as logger import azurelinuxagent.common.utils.networkutil as networkutil +from azurelinuxagent.common.cgroup import MetricValue, MetricsCategory, MetricsCounter from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry from azurelinuxagent.common.errorstate import ErrorState @@ -66,6 +67,27 @@ def _operation(self): CGroupConfigurator.get_instance().check_cgroups(tracked_metrics) +class PollSystemWideResourceUsage(PeriodicOperation): + def __init__(self): + super(PollSystemWideResourceUsage, self).__init__(datetime.timedelta(hours=1)) + self.__log_metrics = conf.get_cgroup_log_metrics() + self.osutil = get_osutil() + + def poll_system_memory_metrics(self): + used_mem, available_mem = self.osutil.get_used_and_available_system_memory() + return [ + MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.USED_MEM, "", + used_mem), + MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.AVAILABLE_MEM, "", + available_mem) + ] + + def _operation(self): + metrics = self.poll_system_memory_metrics() + for metric in metrics: + report_metric(metric.category, metric.counter, metric.instance, metric.value, log_event=self.__log_metrics) + + class ResetPeriodicLogMessages(PeriodicOperation): """ Periodic operation to clean up the hash-tables maintained by the loggers. For reference, please check @@ -274,6 +296,7 @@ def daemon(self): ResetPeriodicLogMessages(), ReportNetworkErrors(), PollResourceUsage(), + PollSystemWideResourceUsage(), SendHostPluginHeartbeat(protocol, health_service), SendImdsHeartbeat(protocol_util, health_service) ] diff --git a/tests/common/osutil/test_default.py b/tests/common/osutil/test_default.py index 2817a63481..ac55102430 100644 --- a/tests/common/osutil/test_default.py +++ b/tests/common/osutil/test_default.py @@ -963,6 +963,28 @@ def test_get_nic_state(self): as_string = osutil.DefaultOSUtil().get_nic_state(as_string=True) self.assertNotEqual(as_string, '') + def test_get_used_and_available_system_memory(self): + memory_table = "\ + total used free shared buff/cache available \n\ +Mem: 8340144128 619352064 5236809728 1499136 2483982336 7426314240 \n\ +Swap: 0 0 0 \n" + with patch.object(shellutil, 'run_command', return_value=memory_table): + used_mem, available_mem = osutil.DefaultOSUtil().get_used_and_available_system_memory() + + self.assertEqual(used_mem, 619352064/(1024**2), "The value didn't match") + self.assertEqual(available_mem, 7426314240/(1024**2), "The value didn't match") + + def test_get_used_and_available_system_memory_error(self): + msg = 'message' + exception = shellutil.CommandError("free -d", 1, "", msg) + + with patch.object(shellutil, 'run_command', + side_effect=exception) as patch_run: + with self.assertRaises(shellutil.CommandError) as context_manager: + osutil.DefaultOSUtil().get_used_and_available_system_memory() + self.assertEqual(patch_run.call_count, 1) + self.assertEqual(context_manager.exception.returncode, 1) + def test_get_dhcp_pid_should_return_a_list_of_pids(self): osutil_get_dhcp_pid_should_return_a_list_of_pids(self, osutil.DefaultOSUtil()) diff --git a/tests/ga/test_monitor.py b/tests/ga/test_monitor.py index 9543f52848..d5700bc91b 100644 --- a/tests/ga/test_monitor.py +++ b/tests/ga/test_monitor.py @@ -29,7 +29,7 @@ from azurelinuxagent.common.protocol.wire import WireProtocol from azurelinuxagent.ga.monitor import get_monitor_handler, PeriodicOperation, SendImdsHeartbeat, \ ResetPeriodicLogMessages, SendHostPluginHeartbeat, PollResourceUsage, \ - ReportNetworkErrors, ReportNetworkConfigurationChanges + ReportNetworkErrors, ReportNetworkConfigurationChanges, PollSystemWideResourceUsage from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.protocol.mockwiredata import DATA_FILE @@ -74,6 +74,7 @@ def periodic_operation_run(self): expected_operations = [ PollResourceUsage.__name__, + PollSystemWideResourceUsage.__name__, ReportNetworkErrors.__name__, ResetPeriodicLogMessages.__name__, SendHostPluginHeartbeat.__name__, @@ -252,3 +253,24 @@ def test_send_extension_metrics_telemetry_handling_cpu_cgroup_exceptions_errno2( self.assertEqual(0, patch_periodic_warn.call_count) self.assertEqual(0, patch_add_metric.call_count) # No metrics should be sent. + +class TestPollSystemWideResourceUsage(AgentTestCase): + + @patch('azurelinuxagent.common.event.EventLogger.add_metric') + @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_used_and_available_system_memory") + def test_send_system_memory_metrics(self, path_get_system_memory, patch_add_metric, *args): # pylint: disable=unused-argument + path_get_system_memory.return_value = (234.45, 123.45) + PollSystemWideResourceUsage().run() + + self.assertEqual(1, path_get_system_memory.call_count) + self.assertEqual(2, patch_add_metric.call_count) # 2 metrics being sent. + + @patch('azurelinuxagent.common.event.EventLogger.add_metric') + @patch("azurelinuxagent.ga.monitor.PollSystemWideResourceUsage.poll_system_memory_metrics") + def test_send_system_memory_metrics_empty(self, path_poll_system_memory_metrics, patch_add_metric, # pylint: disable=unused-argument + *args): + path_poll_system_memory_metrics.return_value = [] + PollSystemWideResourceUsage().run() + + self.assertEqual(1, path_poll_system_memory_metrics.call_count) + self.assertEqual(0, patch_add_metric.call_count) # Zero metrics being sent. \ No newline at end of file From 99d07d29b7843293588bec4b961e4ef2d1daabb2 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Fri, 17 Jun 2022 13:32:43 -0700 Subject: [PATCH 31/58] Parse missing agent manifests as empty (#2617) (cherry picked from commit 7e39ec80f27f6fa04b3780b23b7e4a4332c2d0b0) Co-authored-by: narrieta --- .../protocol/extensions_goal_state_from_vm_settings.py | 4 +++- ...on_manifests.json => vm_settings-no_manifests.json} | 7 +------ .../test_extensions_goal_state_from_vm_settings.py | 10 +++++++++- 3 files changed, 13 insertions(+), 8 deletions(-) rename tests/data/hostgaplugin/{vm_settings-no_extension_manifests.json => vm_settings-no_manifests.json} (83%) diff --git a/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py index 38cca48f1b..10e036c9c9 100644 --- a/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py +++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py @@ -266,7 +266,9 @@ def _parse_agent_manifests(self, vm_settings): for family in families: name = family["name"] version = family.get("version") - uris = family["uris"] + uris = family.get("uris") + if uris is None: + uris = [] manifest = VMAgentManifest(name, version) for u in uris: manifest.uris.append(u) diff --git a/tests/data/hostgaplugin/vm_settings-no_extension_manifests.json b/tests/data/hostgaplugin/vm_settings-no_manifests.json similarity index 83% rename from tests/data/hostgaplugin/vm_settings-no_extension_manifests.json rename to tests/data/hostgaplugin/vm_settings-no_manifests.json index b084900b68..7ec3a5c3d1 100644 --- a/tests/data/hostgaplugin/vm_settings-no_extension_manifests.json +++ b/tests/data/hostgaplugin/vm_settings-no_manifests.json @@ -27,12 +27,7 @@ }, "gaFamilies": [ { - "name": "Prod", - "uris": [ - "https://zrdfepirv2dz5prdstr07a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentral_manifest.xml", - "https://rdfepirv2dm1prdstr09.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentral_manifest.xml", - "https://zrdfepirv2dm5prdstr06a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentral_manifest.xml" - ] + "name": "Prod" } ], "extensionGoalStates": [ diff --git a/tests/protocol/test_extensions_goal_state_from_vm_settings.py b/tests/protocol/test_extensions_goal_state_from_vm_settings.py index 9bcba5ece4..8cdfa81bf9 100644 --- a/tests/protocol/test_extensions_goal_state_from_vm_settings.py +++ b/tests/protocol/test_extensions_goal_state_from_vm_settings.py @@ -69,9 +69,17 @@ def test_it_should_parse_missing_status_upload_blob_as_none(self): self.assertIsNone(extensions_goal_state.status_upload_blob, "Expected status upload blob to be None") self.assertEqual("BlockBlob", extensions_goal_state.status_upload_blob_type, "Expected status upload blob to be Block") + def test_it_should_parse_missing_agent_manifests_as_empty(self): + data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() + data_file["vm_settings"] = "hostgaplugin/vm_settings-no_manifests.json" + with mock_wire_protocol(data_file) as protocol: + extensions_goal_state = protocol.get_goal_state().extensions_goal_state + self.assertEqual(1, len(extensions_goal_state.agent_manifests), "Expected exactly one agent manifest. Got: {0}".format(extensions_goal_state.agent_manifests)) + self.assertListEqual([], extensions_goal_state.agent_manifests[0].uris, "Expected an empty list of agent manifests") + def test_it_should_parse_missing_extension_manifests_as_empty(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() - data_file["vm_settings"] = "hostgaplugin/vm_settings-no_extension_manifests.json" + data_file["vm_settings"] = "hostgaplugin/vm_settings-no_manifests.json" with mock_wire_protocol(data_file) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state From ca6bdd13aa1db96296d2f5d32bb11d50f0daf415 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:58:47 -0700 Subject: [PATCH 32/58] support rhel distro (#2620) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e7e52663e8..17d130867e 100755 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def get_data_files(name, version, fullname): # pylint: disable=R0912 systemd_dir_path = osutil.get_systemd_unit_file_install_path() agent_bin_path = osutil.get_agent_bin_path() - if name in ('redhat', 'centos', 'almalinux', 'cloudlinux', 'rocky'): + if name in ('redhat', 'rhel', 'centos', 'almalinux', 'cloudlinux', 'rocky'): if version.startswith("8") or version.startswith("9"): # redhat8+ default to py3 set_bin_files(data_files, dest=agent_bin_path, From e72862cf824540b485af6f4d921da7aa69f0f025 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 5 Jul 2022 14:30:18 -0700 Subject: [PATCH 33/58] Retry HGAP's extensionsArtifact requests on BAD_REQUEST status (#2621) (#2623) * Retry HGAP's extensionsArtifact requests on BAD_REQUEST status * python 2.6 compat Co-authored-by: narrieta (cherry picked from commit dbc82d3a948ff423529e8ea75d5ba9465709d67c) --- ...ensions_goal_state_from_extensions_config.py | 3 ++- azurelinuxagent/common/protocol/wire.py | 17 ++++++++++------- azurelinuxagent/common/utils/restutil.py | 9 +++++++++ azurelinuxagent/ga/update.py | 6 +++--- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py b/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py index c7e01dd207..8dce261ce6 100644 --- a/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py +++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py @@ -25,6 +25,7 @@ from azurelinuxagent.common.future import ustr from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState, GoalStateChannel, GoalStateSource from azurelinuxagent.common.protocol.restapi import ExtensionSettings, Extension, VMAgentManifest, ExtensionState, InVMGoalStateMetaData +from azurelinuxagent.common.utils import restutil from azurelinuxagent.common.utils.textutil import parse_doc, parse_json, findall, find, findtext, getattrib, gettext, format_exception, \ is_str_none_or_whitespace, is_str_empty @@ -99,7 +100,7 @@ def fetch_direct(): def fetch_through_host(): host = wire_client.get_host_plugin() uri, headers = host.get_artifact_request(artifacts_profile_blob) - content, _ = wire_client.fetch(uri, headers, use_proxy=False) + content, _ = wire_client.fetch(uri, headers, use_proxy=False, retry_codes=restutil.HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES) return content logger.verbose("Retrieving the artifacts profile") diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py index 7923bea75d..a57355e07a 100644 --- a/azurelinuxagent/common/protocol/wire.py +++ b/azurelinuxagent/common/protocol/wire.py @@ -130,7 +130,7 @@ def get_goal_state(self): def _download_ext_handler_pkg_through_host(self, uri, destination): host = self.client.get_host_plugin() uri, headers = host.get_artifact_request(uri, host.manifest_uri) - success = self.client.stream(uri, destination, headers=headers, use_proxy=False, max_retry=1) + success = self.client.stream(uri, destination, headers=headers, use_proxy=False, max_retry=1) # set max_retry to 1 because extension packages already have a retry loop (see ExtHandlerInstance.download()) return success def download_ext_handler_pkg(self, uri, destination, headers=None, use_proxy=True): # pylint: disable=W0613 @@ -626,7 +626,7 @@ def call_storage_service(http_req, *args, **kwargs): def fetch_manifest_through_host(self, uri): host = self.get_host_plugin() uri, headers = host.get_artifact_request(uri) - response, _ = self.fetch(uri, headers, use_proxy=False, max_retry=1) + response, _ = self.fetch(uri, headers, use_proxy=False, retry_codes=restutil.HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES) return response def fetch_manifest(self, version_uris, timeout_in_minutes=5, timeout_in_ms=0): @@ -649,9 +649,11 @@ def fetch_manifest(self, version_uris, timeout_in_minutes=5, timeout_in_ms=0): logger.verbose('The specified manifest URL is empty, ignored.') continue - direct_func = lambda: self.fetch(version_uri, max_retry=1)[0] # pylint: disable=W0640 + # Disable W0640: OK to use version_uri in a lambda within the loop's body + direct_func = lambda: self.fetch(version_uri)[0] # pylint: disable=W0640 # NOTE: the host_func may be called after refreshing the goal state, be careful about any goal state data # in the lambda. + # Disable W0640: OK to use version_uri in a lambda within the loop's body host_func = lambda: self.fetch_manifest_through_host(version_uri) # pylint: disable=W0640 try: @@ -690,7 +692,7 @@ def stream(self, uri, destination, headers=None, use_proxy=None, max_retry=None) return success - def fetch(self, uri, headers=None, use_proxy=None, decode=True, max_retry=None, ok_codes=None): + def fetch(self, uri, headers=None, use_proxy=None, decode=True, max_retry=None, retry_codes=None, ok_codes=None): """ max_retry indicates the maximum number of retries for the HTTP request; None indicates that the default value should be used @@ -699,14 +701,14 @@ def fetch(self, uri, headers=None, use_proxy=None, decode=True, max_retry=None, logger.verbose("Fetch [{0}] with headers [{1}]", uri, headers) content = None response_headers = None - response = self._fetch_response(uri, headers, use_proxy, max_retry=max_retry, ok_codes=ok_codes) + response = self._fetch_response(uri, headers, use_proxy, max_retry=max_retry, retry_codes=retry_codes, ok_codes=ok_codes) if response is not None and not restutil.request_failed(response, ok_codes=ok_codes): response_content = response.read() content = self.decode_config(response_content) if decode else response_content response_headers = response.getheaders() return content, response_headers - def _fetch_response(self, uri, headers=None, use_proxy=None, max_retry=None, ok_codes=None): + def _fetch_response(self, uri, headers=None, use_proxy=None, max_retry=None, retry_codes=None, ok_codes=None): """ max_retry indicates the maximum number of retries for the HTTP request; None indicates that the default value should be used """ @@ -717,7 +719,8 @@ def _fetch_response(self, uri, headers=None, use_proxy=None, max_retry=None, ok_ uri, headers=headers, use_proxy=use_proxy, - max_retry=max_retry) + max_retry=max_retry, + retry_codes=retry_codes) host_plugin = self.get_host_plugin() diff --git a/azurelinuxagent/common/utils/restutil.py b/azurelinuxagent/common/utils/restutil.py index 0c6d6d9ad0..8c2fc4e4e6 100644 --- a/azurelinuxagent/common/utils/restutil.py +++ b/azurelinuxagent/common/utils/restutil.py @@ -56,6 +56,15 @@ 429, # Request Rate Limit Exceeded ] +# +# Currently the HostGAPlugin has an issue its cache that may produce a BAD_REQUEST failure for valid URIs when using the extensionArtifact API. +# Add this status to the retryable codes, but use it only when requesting downloads via the HostGAPlugin. The retry logic in the download code +# would give enough time to the HGAP to refresh its cache. Once the fix to address that issue is deployed, consider removing the use of +# HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES. +# +HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES = RETRY_CODES[:] # make a copy of RETRY_CODES +HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES.append(httpclient.BAD_REQUEST) + RESOURCE_GONE_CODES = [ httpclient.GONE ] diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index 0cdd3b3b7f..e373e6398e 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -1618,7 +1618,7 @@ def _download(self): uri, headers = self.host.get_artifact_request(uri, self.host.manifest_uri) try: - if self._fetch(uri, headers=headers, use_proxy=False): + if self._fetch(uri, headers=headers, use_proxy=False, retry_codes=restutil.HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES): if not HostPluginProtocol.is_default_channel: logger.verbose("Setting host plugin as default channel") HostPluginProtocol.is_default_channel = True @@ -1645,12 +1645,12 @@ def _download(self): message=msg) raise UpdateError(msg) - def _fetch(self, uri, headers=None, use_proxy=True): + def _fetch(self, uri, headers=None, use_proxy=True, retry_codes=None): package = None try: is_healthy = True error_response = '' - resp = restutil.http_get(uri, use_proxy=use_proxy, headers=headers, max_retry=1) + resp = restutil.http_get(uri, use_proxy=use_proxy, headers=headers, max_retry=3, retry_codes=retry_codes) # Use only 3 retries, since there are usually 5 or 6 URIs and we try all of them if restutil.request_succeeded(resp): package = resp.read() fileutil.write_file(self.get_agent_pkg_path(), From 45018d776a473026c4ecae786402d6f0b5094d53 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Wed, 6 Jul 2022 13:27:58 -0700 Subject: [PATCH 34/58] fix if command in rhel v8.6+ (#2624) (#2626) (cherry picked from commit e54072819d985059caf40fc59816dbb81f80050f) --- azurelinuxagent/common/osutil/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurelinuxagent/common/osutil/factory.py b/azurelinuxagent/common/osutil/factory.py index e799ddcd99..d48c493477 100644 --- a/azurelinuxagent/common/osutil/factory.py +++ b/azurelinuxagent/common/osutil/factory.py @@ -118,7 +118,7 @@ def _get_osutil(distro_name, distro_code_name, distro_version, distro_full_name) if Version(distro_version) < Version("7"): return Redhat6xOSUtil() - if Version(distro_version) == Version("8.6") or Version(distro_version) > Version("9"): + if Version(distro_version) >= Version("8.6"): return RedhatOSModernUtil() return RedhatOSUtil() From 71edfe351f57a486d2394704d8e602e43508ce1f Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Thu, 14 Jul 2022 14:58:13 -0700 Subject: [PATCH 35/58] Monitor extension memory cgroups (#2632) * Monitor extension memory cgroups * disable the resource accounting * fix unit test cases --- azurelinuxagent/common/cgroup.py | 2 +- azurelinuxagent/common/cgroupapi.py | 16 ++++-- azurelinuxagent/common/cgroupconfigurator.py | 55 ++++++++++++++++---- azurelinuxagent/ga/exthandlers.py | 11 ++-- init/azure-vmextensions.slice | 1 + tests/common/mock_cgroup_environment.py | 2 +- tests/common/test_cgroupapi.py | 6 ++- tests/common/test_cgroupconfigurator.py | 45 +++++++++++++--- 8 files changed, 108 insertions(+), 30 deletions(-) diff --git a/azurelinuxagent/common/cgroup.py b/azurelinuxagent/common/cgroup.py index 28e65e24bd..2af4c0a117 100644 --- a/azurelinuxagent/common/cgroup.py +++ b/azurelinuxagent/common/cgroup.py @@ -358,7 +358,7 @@ def try_swap_memory_usage(self): return self._get_memory_stat_counter("swap") except CounterNotFound as e: if self._counter_not_found_error_count < 1: - logger.periodic_warn(logger.EVERY_HALF_HOUR, + logger.periodic_info(logger.EVERY_HALF_HOUR, 'Could not find swap counter from "memory.stat" file in the cgroup: {0}.' ' Internal error: {1}'.format(self.path, ustr(e))) self._counter_not_found_error_count += 1 diff --git a/azurelinuxagent/common/cgroupapi.py b/azurelinuxagent/common/cgroupapi.py index 7b7e688a1a..2935e2516a 100644 --- a/azurelinuxagent/common/cgroupapi.py +++ b/azurelinuxagent/common/cgroupapi.py @@ -23,7 +23,7 @@ import uuid from azurelinuxagent.common import logger -from azurelinuxagent.common.cgroup import CpuCgroup +from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry from azurelinuxagent.common.conf import get_agent_pid_file_path from azurelinuxagent.common.exception import CGroupsException, ExtensionErrorCodes, ExtensionError, \ @@ -265,7 +265,10 @@ def start_extension_command(self, extension_name, command, cmd_name, timeout, sh extension_slice_name = self.get_extension_slice_name(extension_name) with self._systemd_run_commands_lock: process = subprocess.Popen( # pylint: disable=W1509 - "systemd-run --unit={0} --scope --slice={1} {2}".format(scope, extension_slice_name, command), + # Some distros like ubuntu20 by default cpu and memory accounting enabled. Thus create nested cgroups under the extension slice + # So disabling CPU and Memory accounting prevents from creating nested cgroups, so that all the counters will be present in extension Cgroup + # since slice unit file configured with accounting enabled. + "systemd-run --property=CPUAccounting=no --property=MemoryAccounting=no --unit={0} --scope --slice={1} {2}".format(scope, extension_slice_name, command), shell=shell, cwd=cwd, stdout=stdout, @@ -284,7 +287,7 @@ def start_extension_command(self, extension_name, command, cmd_name, timeout, sh try: cgroup_relative_path = os.path.join('azure.slice/azure-vmextensions.slice', extension_slice_name) - cpu_cgroup_mountpoint, _ = self.get_cgroup_mount_points() + cpu_cgroup_mountpoint, memory_cgroup_mountpoint = self.get_cgroup_mount_points() if cpu_cgroup_mountpoint is None: logger.info("The CPU controller is not mounted; will not track resource usage") @@ -293,6 +296,13 @@ def start_extension_command(self, extension_name, command, cmd_name, timeout, sh cpu_cgroup = CpuCgroup(extension_name, cpu_cgroup_path) CGroupsTelemetry.track_cgroup(cpu_cgroup) + if memory_cgroup_mountpoint is None: + logger.info("The Memory controller is not mounted; will not track resource usage") + else: + memory_cgroup_path = os.path.join(memory_cgroup_mountpoint, cgroup_relative_path) + memory_cgroup = MemoryCgroup(extension_name, memory_cgroup_path) + CGroupsTelemetry.track_cgroup(memory_cgroup) + except IOError as e: if e.errno == 2: # 'No such file or directory' logger.info("The extension command already completed; will not track resource usage") diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index 6c16666eab..d79af493d6 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -50,6 +50,7 @@ Before=slices.target [Slice] CPUAccounting=yes +MemoryAccounting=yes """ _EXTENSION_SLICE_CONTENTS = """ [Unit] @@ -59,6 +60,7 @@ [Slice] CPUAccounting=yes CPUQuota={cpu_quota} +MemoryAccounting=yes """ LOGCOLLECTOR_SLICE = "azure-walinuxagent-logcollector.slice" # More info on resource limits properties in systemd here: @@ -142,7 +144,7 @@ def __init__(self): self._cgroups_api = None self._agent_cpu_cgroup_path = None self._agent_memory_cgroup_path = None - self._check_cgroups_lock = threading.RLock() # Protect the check_cgroups which is called from Monitor thread and main loop. + self._check_cgroups_lock = threading.RLock() # Protect the check_cgroups which is called from Monitor thread and main loop. def initialize(self): try: @@ -368,7 +370,8 @@ def __setup_azure_slice(): CGroupConfigurator._Impl.__cleanup_unit_file(agent_drop_in_file_memory_accounting) else: if not os.path.exists(agent_drop_in_file_memory_accounting): - files_to_create.append((agent_drop_in_file_memory_accounting, _DROP_IN_FILE_MEMORY_ACCOUNTING_CONTENTS)) + files_to_create.append( + (agent_drop_in_file_memory_accounting, _DROP_IN_FILE_MEMORY_ACCOUNTING_CONTENTS)) if len(files_to_create) > 0: # create the unit files, but if 1 fails remove all and return @@ -432,12 +435,18 @@ def __create_all_files(files_to_create): CGroupConfigurator._Impl.__cleanup_unit_file(unit_file) return - def is_extension_resource_limits_setup_completed(self, extension_name): + def is_extension_resource_limits_setup_completed(self, extension_name, cpu_quota=None): unit_file_install_path = systemd.get_unit_file_install_path() extension_slice_path = os.path.join(unit_file_install_path, SystemdCgroupsApi.get_extension_slice_name(extension_name)) + cpu_quota = str( + cpu_quota) + "%" if cpu_quota is not None else "" # setting an empty value resets to the default (infinity) + slice_contents = _EXTENSION_SLICE_CONTENTS.format(extension_name=extension_name, + cpu_quota=cpu_quota) if os.path.exists(extension_slice_path): - return True + with open(extension_slice_path, "r") as file_: + if file_.read() == slice_contents: + return True return False def __get_agent_cgroups(self, agent_slice, cpu_controller_root, memory_controller_root): @@ -550,7 +559,8 @@ def __reset_agent_cpu_quota(): """ logger.info("Resetting agent's CPUQuota") if CGroupConfigurator._Impl.__try_set_cpu_quota(''): # setting an empty value resets to the default (infinity) - _log_cgroup_info('CPUQuota: {0}', systemd.get_unit_property(systemd.get_agent_unit_name(), "CPUQuotaPerSecUSec")) + _log_cgroup_info('CPUQuota: {0}', + systemd.get_unit_property(systemd.get_agent_unit_name(), "CPUQuotaPerSecUSec")) @staticmethod def __try_set_cpu_quota(quota): @@ -722,13 +732,18 @@ def start_tracking_unit_cgroups(self, unit_name): TODO: Start tracking Memory Cgroups """ try: - cpu_cgroup_path, _ = self._cgroups_api.get_unit_cgroup_paths(unit_name) + cpu_cgroup_path, memory_cgroup_path = self._cgroups_api.get_unit_cgroup_paths(unit_name) if cpu_cgroup_path is None: logger.info("The CPU controller is not mounted; will not track resource usage") else: CGroupsTelemetry.track_cgroup(CpuCgroup(unit_name, cpu_cgroup_path)) + if memory_cgroup_path is None: + logger.info("The Memory controller is not mounted; will not track resource usage") + else: + CGroupsTelemetry.track_cgroup(MemoryCgroup(unit_name, memory_cgroup_path)) + except Exception as exception: logger.info("Failed to start tracking resource usage for the extension: {0}", ustr(exception)) @@ -737,11 +752,14 @@ def stop_tracking_unit_cgroups(self, unit_name): TODO: remove Memory cgroups from tracked list. """ try: - cpu_cgroup_path, _ = self._cgroups_api.get_unit_cgroup_paths(unit_name) + cpu_cgroup_path, memory_cgroup_path = self._cgroups_api.get_unit_cgroup_paths(unit_name) if cpu_cgroup_path is not None: CGroupsTelemetry.stop_tracking(CpuCgroup(unit_name, cpu_cgroup_path)) + if memory_cgroup_path is not None: + CGroupsTelemetry.stop_tracking(MemoryCgroup(unit_name, memory_cgroup_path)) + except Exception as exception: logger.info("Failed to stop tracking resource usage for the extension service: {0}", ustr(exception)) @@ -754,12 +772,16 @@ def stop_tracking_extension_cgroups(self, extension_name): cgroup_relative_path = os.path.join(_AZURE_VMEXTENSIONS_SLICE, extension_slice_name) - cpu_cgroup_mountpoint, _ = self._cgroups_api.get_cgroup_mount_points() + cpu_cgroup_mountpoint, memory_cgroup_mountpoint = self._cgroups_api.get_cgroup_mount_points() cpu_cgroup_path = os.path.join(cpu_cgroup_mountpoint, cgroup_relative_path) + memory_cgroup_path = os.path.join(memory_cgroup_mountpoint, cgroup_relative_path) if cpu_cgroup_path is not None: CGroupsTelemetry.stop_tracking(CpuCgroup(extension_name, cpu_cgroup_path)) + if memory_cgroup_path is not None: + CGroupsTelemetry.stop_tracking(MemoryCgroup(extension_name, memory_cgroup_path)) + except Exception as exception: logger.info("Failed to stop tracking resource usage for the extension service: {0}", ustr(exception)) @@ -818,10 +840,13 @@ def setup_extension_slice(self, extension_name, cpu_quota): SystemdCgroupsApi.get_extension_slice_name(extension_name)) try: cpu_quota = str(cpu_quota) + "%" if cpu_quota is not None else "" # setting an empty value resets to the default (infinity) - slice_contents = _EXTENSION_SLICE_CONTENTS.format(extension_name=extension_name, cpu_quota=cpu_quota) + _log_cgroup_info("Ensuring the {0}'s CPUQuota is {1}", extension_name, cpu_quota) + slice_contents = _EXTENSION_SLICE_CONTENTS.format(extension_name=extension_name, + cpu_quota=cpu_quota) CGroupConfigurator._Impl.__create_unit_file(extension_slice_path, slice_contents) except Exception as exception: - _log_cgroup_warning("Failed to set the extension {0} slice and quotas: {1}", extension_name, ustr(exception)) + _log_cgroup_warning("Failed to set the extension {0} slice and quotas: {1}", extension_name, + ustr(exception)) CGroupConfigurator._Impl.__cleanup_unit_file(extension_slice_path) def remove_extension_slice(self, extension_name): @@ -854,10 +879,15 @@ def set_extension_services_cpu_memory_quota(self, services_list): drop_in_file_cpu_accounting = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_ACCOUNTING) files_to_create.append((drop_in_file_cpu_accounting, _DROP_IN_FILE_CPU_ACCOUNTING_CONTENTS)) + drop_in_file_memory_accounting = os.path.join(drop_in_path, + _DROP_IN_FILE_MEMORY_ACCOUNTING) + files_to_create.append( + (drop_in_file_memory_accounting, _DROP_IN_FILE_MEMORY_ACCOUNTING_CONTENTS)) cpu_quota = service.get('cpuQuotaPercentage', None) if cpu_quota is not None: cpu_quota = str(cpu_quota) + "%" + _log_cgroup_info("Ensuring the {0}'s CPUQuota is {1}", service_name, cpu_quota) drop_in_file_cpu_quota = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_QUOTA) cpu_quota_contents = _DROP_IN_FILE_CPU_QUOTA_CONTENTS_FORMAT.format(cpu_quota) files_to_create.append((drop_in_file_cpu_quota, cpu_quota_contents)) @@ -873,8 +903,8 @@ def __reset_extension_services_cpu_quota(self, services_list): over this setting. """ if self.enabled() and services_list is not None: + service_name = None try: - service_name = None for service in services_list: service_name = service.get('name', None) unit_file_path = systemd.get_unit_file_install_path() @@ -907,6 +937,9 @@ def remove_extension_services_drop_in_files(self, services_list): drop_in_file_cpu_accounting = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_ACCOUNTING) files_to_cleanup.append(drop_in_file_cpu_accounting) + drop_in_file_memory_accounting = os.path.join(drop_in_path, + _DROP_IN_FILE_MEMORY_ACCOUNTING) + files_to_cleanup.append(drop_in_file_memory_accounting) cpu_quota = service.get('cpuQuotaPercentage', None) if cpu_quota is not None: drop_in_file_cpu_quota = os.path.join(drop_in_path, _DROP_IN_FILE_CPU_QUOTA) diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py index b7da7b4f14..21c3d7b2dd 100644 --- a/azurelinuxagent/ga/exthandlers.py +++ b/azurelinuxagent/ga/exthandlers.py @@ -1361,9 +1361,11 @@ def set_extension_resource_limits(self): # setup the resource limits for extension operations and it's services. man = self.load_manifest() resource_limits = man.get_resource_limits(extension_name, self.ext_handler.version) - CGroupConfigurator.get_instance().setup_extension_slice( - extension_name=extension_name, cpu_quota=resource_limits.get_extension_slice_cpu_quota()) - CGroupConfigurator.get_instance().set_extension_services_cpu_memory_quota(resource_limits.get_service_list()) + if not CGroupConfigurator.get_instance().is_extension_resource_limits_setup_completed(extension_name, + cpu_quota=resource_limits.get_extension_slice_cpu_quota()): + CGroupConfigurator.get_instance().setup_extension_slice( + extension_name=extension_name, cpu_quota=resource_limits.get_extension_slice_cpu_quota()) + CGroupConfigurator.get_instance().set_extension_services_cpu_memory_quota(resource_limits.get_service_list()) def create_status_file_if_not_exist(self, extension, status, code, operation, message): _, status_path = self.get_status_file_path(extension) @@ -1413,8 +1415,7 @@ def _enable_extension(self, extension, uninstall_exit_code): ExtCommandEnvVariable.UninstallReturnCode: uninstall_exit_code } # This check to call the setup if extension already installed and not called setup before - if not CGroupConfigurator.get_instance().is_extension_resource_limits_setup_completed(self.get_full_name()): - self.set_extension_resource_limits() + self.set_extension_resource_limits() self.set_operation(WALAEventOperation.Enable) man = self.load_manifest() diff --git a/init/azure-vmextensions.slice b/init/azure-vmextensions.slice index 87dd84b1fb..b90c0e82d6 100644 --- a/init/azure-vmextensions.slice +++ b/init/azure-vmextensions.slice @@ -4,3 +4,4 @@ DefaultDependencies=no Before=slices.target [Slice] CPUAccounting=yes +MemoryAccounting=yes diff --git a/tests/common/mock_cgroup_environment.py b/tests/common/mock_cgroup_environment.py index ea64890ccf..10d499077e 100644 --- a/tests/common/mock_cgroup_environment.py +++ b/tests/common/mock_cgroup_environment.py @@ -69,7 +69,7 @@ MockCommand(r"^systemctl stop ([^\s]+)"), - MockCommand(r"^systemd-run --unit=([^\s]+) --scope ([^\s]+)", + MockCommand(r"^systemd-run (.+) --unit=([^\s]+) --scope ([^\s]+)", ''' Running scope as unit: TEST_UNIT.scope Thu 28 May 2020 07:25:55 AM PDT diff --git a/tests/common/test_cgroupapi.py b/tests/common/test_cgroupapi.py index 14ed0129df..3b70214d39 100644 --- a/tests/common/test_cgroupapi.py +++ b/tests/common/test_cgroupapi.py @@ -144,7 +144,7 @@ def test_start_extension_command_should_return_the_command_output(self, _): original_popen = subprocess.Popen def mock_popen(command, *args, **kwargs): - if command.startswith('systemd-run --unit'): + if command.startswith('systemd-run --property'): command = "echo TEST_OUTPUT" return original_popen(command, *args, **kwargs) @@ -184,6 +184,10 @@ def test_start_extension_command_should_execute_the_command_in_a_cgroup(self, _) any(cg for cg in tracked.values() if cg.name == 'Microsoft.Compute.TestExtension-1.2.3' and 'cpu' in cg.path), "The extension's CPU is not being tracked") + self.assertTrue( + any(cg for cg in tracked.values() if cg.name == 'Microsoft.Compute.TestExtension-1.2.3' and 'memory' in cg.path), + "The extension's Memory is not being tracked") + @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_use_systemd_to_execute_the_command(self, _): with mock_cgroup_environment(self.tmp_dir): diff --git a/tests/common/test_cgroupconfigurator.py b/tests/common/test_cgroupconfigurator.py index ac35b0d199..62a7211a70 100644 --- a/tests/common/test_cgroupconfigurator.py +++ b/tests/common/test_cgroupconfigurator.py @@ -196,6 +196,7 @@ def test_setup_extension_slice_should_create_unit_files(self): expected_cpu_accounting = "CPUAccounting=yes" expected_cpu_quota_percentage = "5%" + expected_memory_accounting = "MemoryAccounting=yes" self.assertTrue(os.path.exists(extension_slice_unit_file), "{0} was not created".format(extension_slice_unit_file)) self.assertTrue(fileutil.findre_in_file(extension_slice_unit_file, expected_cpu_accounting), @@ -204,6 +205,9 @@ def test_setup_extension_slice_should_create_unit_files(self): self.assertTrue(fileutil.findre_in_file(extension_slice_unit_file, expected_cpu_quota_percentage), "CPUQuota was not set correctly. Expected: {0}. Got:\n{1}".format(expected_cpu_quota_percentage, fileutil.read_file( extension_slice_unit_file))) + self.assertTrue(fileutil.findre_in_file(extension_slice_unit_file, expected_memory_accounting), + "MemoryAccounting was not set correctly. Expected: {0}. Got:\n{1}".format(expected_memory_accounting, fileutil.read_file( + extension_slice_unit_file))) def test_remove_extension_slice_should_remove_unit_files(self): with self._get_cgroup_configurator() as configurator: @@ -224,6 +228,9 @@ def test_remove_extension_slice_should_remove_unit_files(self): self.assertFalse( any(cg for cg in tracked.values() if cg.name == 'Microsoft.CPlat.Extension' and 'cpu' in cg.path), "The extension's CPU is being tracked") + self.assertFalse( + any(cg for cg in tracked.values() if cg.name == 'Microsoft.CPlat.Extension' and 'memory' in cg.path), + "The extension's Memory is being tracked") self.assertFalse(os.path.exists(extension_slice_unit_file), "{0} should not be present".format(extension_slice_unit_file)) @@ -390,6 +397,9 @@ def test_start_extension_command_should_start_tracking_the_extension_cgroups(sel self.assertTrue( any(cg for cg in tracked.values() if cg.name == 'Microsoft.Compute.TestExtension-1.2.3' and 'cpu' in cg.path), "The extension's CPU is not being tracked") + self.assertTrue( + any(cg for cg in tracked.values() if cg.name == 'Microsoft.Compute.TestExtension-1.2.3' and 'memory' in cg.path), + "The extension's Memory is not being tracked") def test_start_extension_command_should_raise_an_exception_when_the_command_cannot_be_started(self): with self._get_cgroup_configurator() as configurator: @@ -418,17 +428,12 @@ def mock_popen(command_arg, *args, **kwargs): @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_disable_cgroups_and_invoke_the_command_directly_if_systemd_fails(self, _): with self._get_cgroup_configurator() as configurator: - original_popen = subprocess.Popen - def mock_popen(command, *args, **kwargs): - if 'systemd-run' in command: - # Inject a syntax error to the call - command = command.replace('systemd-run', 'systemd-run syntax_error') - return original_popen(command, *args, **kwargs) + configurator.mocks.add_command(MockCommand("systemd-run", return_value=1, stdout='', stderr='Failed to start transient scope unit: syntax error')) with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as output_file: with patch("azurelinuxagent.common.cgroupconfigurator.add_event") as mock_add_event: - with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen) as popen_patch: + with patch("subprocess.Popen", wraps=subprocess.Popen) as popen_patch: CGroupsTelemetry.reset() command = "echo TEST_OUTPUT" @@ -677,6 +682,8 @@ def test_it_should_set_extension_services_when_quotas_not_defined(self): # get the paths to the mocked files extension_service_cpu_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_accounting) extension_service_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_quota) + extension_service_memory_accounting = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_memory_accounting) + extension_service_memory_quota = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_memory_limit) configurator.set_extension_services_cpu_memory_quota(service_list) @@ -685,6 +692,11 @@ def test_it_should_set_extension_services_when_quotas_not_defined(self): self.assertFalse(os.path.exists(extension_service_cpu_quota), "{0} should not have been created during setup".format(extension_service_cpu_quota)) + self.assertTrue(os.path.exists(extension_service_memory_accounting), + "{0} was not created".format(extension_service_memory_accounting)) + self.assertFalse(os.path.exists(extension_service_memory_quota), + "{0} should not have been created during setup".format(extension_service_memory_quota)) + def test_it_should_start_tracking_extension_services_cgroups(self): service_list = [ { @@ -699,6 +711,9 @@ def test_it_should_start_tracking_extension_services_cgroups(self): self.assertTrue( any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'cpu' in cg.path), "The extension service's CPU is not being tracked") + self.assertTrue( + any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'memory' in cg.path), + "The extension service's Memory is not being tracked") def test_it_should_stop_tracking_extension_services_cgroups(self): service_list = [ @@ -718,6 +733,9 @@ def test_it_should_stop_tracking_extension_services_cgroups(self): self.assertFalse( any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'cpu' in cg.path), "The extension service's CPU is being tracked") + self.assertFalse( + any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'memory' in cg.path), + "The extension service's Memory is being tracked") def test_it_should_remove_extension_services_drop_in_files(self): service_list = [ @@ -730,11 +748,15 @@ def test_it_should_remove_extension_services_drop_in_files(self): extension_service_cpu_accounting = configurator.mocks.get_mapped_path( UnitFilePaths.extension_service_cpu_accounting) extension_service_cpu_quota = configurator.mocks.get_mapped_path(UnitFilePaths.extension_service_cpu_quota) + extension_service_memory_accounting = configurator.mocks.get_mapped_path( + UnitFilePaths.extension_service_memory_accounting) configurator.remove_extension_services_drop_in_files(service_list) self.assertFalse(os.path.exists(extension_service_cpu_accounting), "{0} should not have been created".format(extension_service_cpu_accounting)) self.assertFalse(os.path.exists(extension_service_cpu_quota), "{0} should not have been created".format(extension_service_cpu_quota)) + self.assertFalse(os.path.exists(extension_service_memory_accounting), + "{0} should not have been created".format(extension_service_memory_accounting)) def test_it_should_start_tracking_unit_cgroups(self): @@ -747,6 +769,10 @@ def test_it_should_start_tracking_unit_cgroups(self): any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'cpu' in cg.path), "The extension service's CPU is not being tracked") + self.assertTrue( + any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'memory' in cg.path), + "The extension service's Memory is not being tracked") + def test_it_should_stop_tracking_unit_cgroups(self): def side_effect(path): @@ -766,6 +792,9 @@ def side_effect(path): self.assertFalse( any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'cpu' in cg.path), "The extension service's CPU is being tracked") + self.assertFalse( + any(cg for cg in tracked.values() if cg.name == 'extension.service' and 'memory' in cg.path), + "The extension service's Memory is being tracked") def test_check_processes_in_agent_cgroup_should_raise_a_cgroups_exception_when_there_are_unexpected_processes_in_the_agent_cgroup(self): with self._get_cgroup_configurator() as configurator: @@ -833,7 +862,7 @@ def start_extension(): # Extensions are started using systemd-run; mock Popen to remove the call to systemd-run; the test script creates a couple of # child processes, which would simulate the extension's processes. def mock_popen(command, *args, **kwargs): - match = re.match(r"^systemd-run --unit=[^\s]+ --scope --slice=[^\s]+ (.+)", command) + match = re.match(r"^systemd-run --property=CPUAccounting=no --property=MemoryAccounting=no --unit=[^\s]+ --scope --slice=[^\s]+ (.+)", command) is_systemd_run = match is not None if is_systemd_run: command = match.group(1) From 4db3b44f0271f2ed825b001c2bdf4725fdcd1f72 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Thu, 21 Jul 2022 16:28:31 -0700 Subject: [PATCH 36/58] Restrict mode of test script (#2634) Co-authored-by: narrieta --- dcr/scenarios/agent-persist-firewall/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dcr/scenarios/agent-persist-firewall/setup.sh b/dcr/scenarios/agent-persist-firewall/setup.sh index 20c0d78356..73ef46419a 100644 --- a/dcr/scenarios/agent-persist-firewall/setup.sh +++ b/dcr/scenarios/agent-persist-firewall/setup.sh @@ -9,7 +9,7 @@ ipt=$(which iptables) username="dcr" script_dir=$(dirname "$0") cp "$script_dir/access_wire_ip.sh" "/usr/bin/" -chmod 777 "/usr/bin/access_wire_ip.sh" +chmod 700 "/usr/bin/access_wire_ip.sh" mkdir -p /home/$username || echo "this is only needed for Suse VMs for running cron jobs as non-root" # Setup Cron jobs echo "@reboot ($d --utc +\\%FT\\%T.\\%3NZ && /usr/bin/access_wire_ip.sh $ipt) > /var/tmp/reboot-cron-root.log 2>&1" | crontab -u root - From b2bca2cbd4572857224263ff84dd7919155af43a Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 2 Aug 2022 07:07:34 -0700 Subject: [PATCH 37/58] Cleanup history directory when creating new subdirectories (#2633) (#2639) * Cleanup history directory when creating new subdirectories * Review feedback Co-authored-by: narrieta (cherry picked from commit ac56d0e1395db01e23c5e4a3aeefcc702752dd84) --- azurelinuxagent/common/protocol/goal_state.py | 10 ++-- azurelinuxagent/common/protocol/wire.py | 10 ++-- azurelinuxagent/common/utils/archive.py | 51 ++++++++++++---- azurelinuxagent/ga/update.py | 7 +-- tests/utils/test_archive.py | 58 +++---------------- 5 files changed, 63 insertions(+), 73 deletions(-) diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index 8b508f61ad..4d354e5673 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -352,12 +352,12 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): certs_uri = findtext(xml_doc, "Certificates") if certs_uri is not None: xml_text = self._wire_client.fetch_config(certs_uri, self._wire_client.get_header_for_cert()) - certs = Certificates(xml_text) + certs = Certificates(xml_text, self.logger) # Log and save the certificates summary (i.e. the thumbprint but not the certificate itself) to the goal state history for c in certs.summary: - logger.info("Downloaded certificate {0}".format(c)) + self.logger.info("Downloaded certificate {0}".format(c)) if len(certs.warnings) > 0: - logger.warn(certs.warnings) + self.logger.warn(certs.warnings) self._history.save_certificates(json.dumps(certs.summary)) remote_access = None @@ -403,7 +403,7 @@ def __init__(self, xml_text): class Certificates(object): - def __init__(self, xml_text): + def __init__(self, xml_text, my_logger): self.cert_list = CertList() self.summary = [] # debugging info self.warnings = [] @@ -421,7 +421,7 @@ def __init__(self, xml_text): # if the certificates format is not Pkcs7BlobWithPfxContents do not parse it certificateFormat = findtext(xml_doc, "Format") if certificateFormat and certificateFormat != "Pkcs7BlobWithPfxContents": - logger.warn("The Format is not Pkcs7BlobWithPfxContents. Format is " + certificateFormat) + my_logger.warn("The Format is not Pkcs7BlobWithPfxContents. Format is " + certificateFormat) return cryptutil = CryptUtil(conf.get_openssl_cmd()) diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py index a57355e07a..b8b05c98b8 100644 --- a/azurelinuxagent/common/protocol/wire.py +++ b/azurelinuxagent/common/protocol/wire.py @@ -767,7 +767,7 @@ def update_goal_state(self, force_update=False, silent=False): Updates the goal state if the incarnation or etag changed or if 'force_update' is True """ try: - if force_update: + if force_update and not silent: logger.info("Forcing an update of the goal state.") if self._goal_state is None or force_update: @@ -970,11 +970,13 @@ def upload_status_blob(self): if extensions_goal_state.status_upload_blob is None: # the status upload blob is in ExtensionsConfig so force a full goal state refresh - self.update_goal_state(force_update=True) + self.update_goal_state(force_update=True, silent=True) extensions_goal_state = self.get_goal_state().extensions_goal_state - if extensions_goal_state.status_upload_blob is None: - raise ProtocolNotFoundError("Status upload uri is missing") + if extensions_goal_state.status_upload_blob is None: + raise ProtocolNotFoundError("Status upload uri is missing") + + logger.info("Refreshed the goal state to get the status upload blob. New Goal State ID: {0}", extensions_goal_state.id) blob_type = extensions_goal_state.status_upload_blob_type diff --git a/azurelinuxagent/common/utils/archive.py b/azurelinuxagent/common/utils/archive.py index 0be1544c57..b624d1742c 100644 --- a/azurelinuxagent/common/utils/archive.py +++ b/azurelinuxagent/common/utils/archive.py @@ -162,17 +162,6 @@ def __init__(self, lib_dir): if exception.errno != errno.EEXIST: logger.warn("{0} : {1}", self._source, exception.strerror) - def purge(self): - """ - Delete "old" archive directories and .zip archives. Old - is defined as any directories or files older than the X - newest ones. Also, clean up any legacy history files. - """ - states = self._get_archive_states() - - for state in states[_MAX_ARCHIVED_STATES:]: - state.delete() - @staticmethod def purge_legacy_goal_state_history(): lib_dir = conf.get_lib_dir() @@ -222,6 +211,8 @@ def __init__(self, time, tag): timestamp = timeutil.create_history_timestamp(time) self._root = os.path.join(conf.get_lib_dir(), ARCHIVE_DIRECTORY_NAME, "{0}__{1}".format(timestamp, tag) if tag is not None else timestamp) + GoalStateHistory._purge() + @staticmethod def tag_exists(tag): """ @@ -240,6 +231,44 @@ def save(self, data, file_name): self._errors = True logger.warn("Failed to save {0} to the goal state history: {1} [no additional errors saving the goal state will be reported]".format(file_name, e)) + _purge_error_count = 0 + + @staticmethod + def _purge(): + """ + Delete "old" history directories and .zip archives. Old is defined as any directories or files older than the X newest ones. + """ + try: + history_root = os.path.join(conf.get_lib_dir(), ARCHIVE_DIRECTORY_NAME) + + if not os.path.exists(history_root): + return + + items = [] + for current_item in os.listdir(history_root): + full_path = os.path.join(history_root, current_item) + items.append(full_path) + items.sort(key=os.path.getctime, reverse=True) + + for current_item in items[_MAX_ARCHIVED_STATES:]: + if os.path.isfile(current_item): + os.remove(current_item) + else: + shutil.rmtree(current_item) + + if GoalStateHistory._purge_error_count > 0: + GoalStateHistory._purge_error_count = 0 + # Log a success message when we are recovering from errors. + logger.info("Successfully cleaned up the goal state history directory") + + except Exception as e: + GoalStateHistory._purge_error_count += 1 + if GoalStateHistory._purge_error_count < 5: + logger.warn("Failed to clean up the goal state history directory: {0}".format(e)) + elif GoalStateHistory._purge_error_count == 5: + logger.warn("Failed to clean up the goal state history directory [will stop reporting these errors]: {0}".format(e)) + + @staticmethod def _save_placeholder(): """ diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index e373e6398e..c9887543dc 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -639,9 +639,9 @@ def _process_goal_state(self, exthandlers_handler, remote_access_handler): if self._processing_new_incarnation(): remote_access_handler.run() - # lastly, cleanup the goal state history (but do it only on new goal states - no need to do it on every iteration) + # lastly, archive the goal state history (but do it only on new goal states - no need to do it on every iteration) if self._processing_new_extensions_goal_state(): - UpdateHandler._cleanup_goal_state_history() + UpdateHandler._archive_goal_state_history() finally: if self._goal_state is not None: @@ -649,10 +649,9 @@ def _process_goal_state(self, exthandlers_handler, remote_access_handler): self._last_extensions_gs_id = self._goal_state.extensions_goal_state.id @staticmethod - def _cleanup_goal_state_history(): + def _archive_goal_state_history(): try: archiver = StateArchiver(conf.get_lib_dir()) - archiver.purge() archiver.archive() except Exception as exception: logger.warn("Error cleaning up the goal state history: {0}", ustr(exception)) diff --git a/tests/utils/test_archive.py b/tests/utils/test_archive.py index 5eee67c7da..ce97d65fde 100644 --- a/tests/utils/test_archive.py +++ b/tests/utils/test_archive.py @@ -6,8 +6,9 @@ from datetime import datetime, timedelta import azurelinuxagent.common.logger as logger +from azurelinuxagent.common import conf from azurelinuxagent.common.utils import fileutil, timeutil -from azurelinuxagent.common.utils.archive import StateArchiver, _MAX_ARCHIVED_STATES +from azurelinuxagent.common.utils.archive import GoalStateHistory, StateArchiver, _MAX_ARCHIVED_STATES, ARCHIVE_DIRECTORY_NAME from tests.tools import AgentTestCase, patch debug = False @@ -28,7 +29,7 @@ def setUp(self): self.tmp_dir = tempfile.mkdtemp(prefix=prefix) def _write_file(self, filename, contents=None): - full_name = os.path.join(self.tmp_dir, filename) + full_name = os.path.join(conf.get_lib_dir(), filename) fileutil.mkdir(os.path.dirname(full_name)) with open(full_name, 'w') as file_handler: @@ -38,7 +39,7 @@ def _write_file(self, filename, contents=None): @property def history_dir(self): - return os.path.join(self.tmp_dir, 'history') + return os.path.join(conf.get_lib_dir(), ARCHIVE_DIRECTORY_NAME) @staticmethod def _parse_archive_name(name): @@ -66,7 +67,7 @@ def test_archive_should_zip_all_but_the_latest_goal_state_in_the_history_folder( self._write_file(os.path.join(directory, current_file)) test_directories.append(directory) - test_subject = StateArchiver(self.tmp_dir) + test_subject = StateArchiver(conf.get_lib_dir()) # NOTE: StateArchiver sorts the state directories by creation time, but the test files are created too fast and the # time resolution is too coarse, so instead we mock getctime to simply return the path of the file with patch("azurelinuxagent.common.utils.archive.os.path.getctime", side_effect=lambda path: path): @@ -83,9 +84,9 @@ def test_archive_should_zip_all_but_the_latest_goal_state_in_the_history_folder( self.assertTrue(os.path.exists(test_directories[2]), "{0}, the latest goal state, should not have being removed".format(test_directories[2])) - def test_archive02(self): + def test_goal_state_history_init_should_purge_old_items(self): """ - StateArchiver should purge the MAX_ARCHIVED_STATES oldest files + GoalStateHistory.__init__ should _purge the MAX_ARCHIVED_STATES oldest files or directories. The oldest timestamps are purged first. This test case creates a mixture of archive files and directories. @@ -112,11 +113,10 @@ def test_archive02(self): self.assertEqual(total, len(os.listdir(self.history_dir))) - test_subject = StateArchiver(self.tmp_dir) - # NOTE: StateArchiver sorts the state directories by creation time, but the test files are created too fast and the + # NOTE: The purge method sorts the items by creation time, but the test files are created too fast and the # time resolution is too coarse, so instead we mock getctime to simply return the path of the file with patch("azurelinuxagent.common.utils.archive.os.path.getctime", side_effect=lambda path: path): - test_subject.purge() + GoalStateHistory(datetime.utcnow(), 'test') archived_entries = os.listdir(self.history_dir) self.assertEqual(_MAX_ARCHIVED_STATES, len(archived_entries)) @@ -153,46 +153,6 @@ def test_purge_legacy_goal_state_history(self): for f in legacy_files: self.assertFalse(os.path.exists(f), "Legacy file {0} was not removed".format(f)) - def test_archive03(self): - """ - All archives should be purged, both with the legacy naming (with incarnation number) and with the new naming. - """ - start = datetime.now() - timestamp1 = start + timedelta(seconds=5) - timestamp2 = start + timedelta(seconds=10) - timestamp3 = start + timedelta(seconds=10) - - dir_old = timestamp1.isoformat() - dir_new = "{0}_incarnation_1".format(timestamp2.isoformat()) - - archive_old = "{0}.zip".format(timestamp1.isoformat()) - archive_new = "{0}_incarnation_1.zip".format(timestamp2.isoformat()) - - status = "{0}.zip".format(timestamp3.isoformat()) - - self._write_file(os.path.join("history", dir_old, "Prod.manifest.xml")) - self._write_file(os.path.join("history", dir_new, "Prod.manifest.xml")) - self._write_file(os.path.join("history", archive_old)) - self._write_file(os.path.join("history", archive_new)) - self._write_file(os.path.join("history", status)) - - self.assertEqual(5, len(os.listdir(self.history_dir)), "Not all entries were archived!") - - test_subject = StateArchiver(self.tmp_dir) - with patch("azurelinuxagent.common.utils.archive._MAX_ARCHIVED_STATES", 0): - test_subject.purge() - - archived_entries = os.listdir(self.history_dir) - self.assertEqual(0, len(archived_entries), "Not all entries were purged!") - - def test_archive04(self): - """ - The archive directory is created if it does not exist. - - This failure was caught when .purge() was called before .archive(). - """ - test_subject = StateArchiver(os.path.join(self.tmp_dir, 'does-not-exist')) - test_subject.purge() @staticmethod def parse_isoformat(timestamp_str): From 05d44c8539a495b66352ffbb392020ce3f45ca9d Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 3 Aug 2022 07:03:10 -0700 Subject: [PATCH 38/58] Update Code Owners (#2641) Co-authored-by: narrieta --- CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index e85dfc8d3d..32cd27f227 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,3 +1,4 @@ +1 # See https://help.github.com/articles/about-codeowners/ # for more info about CODEOWNERS file @@ -20,4 +21,4 @@ # # Linux Agent team # -* @narrieta @kevinclark19a @ZhidongPeng @dhivyaganesan @nagworld9 +* @narrieta @ZhidongPeng @nagworld9 From aa9b0e15f82d1e06784956d230de7cf599664378 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Thu, 4 Aug 2022 05:45:35 -0700 Subject: [PATCH 39/58] SUSE: Fix valid values for DHCLIENT_HOSTNAME_OPTION (#2643) --- azurelinuxagent/common/osutil/suse.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azurelinuxagent/common/osutil/suse.py b/azurelinuxagent/common/osutil/suse.py index d7801e0482..52fd3ce565 100644 --- a/azurelinuxagent/common/osutil/suse.py +++ b/azurelinuxagent/common/osutil/suse.py @@ -110,7 +110,8 @@ def set_dhcp_hostname(self, hostname): ) if hostname_send_setting: value = hostname_send_setting.split('=')[-1] - if value == '"AUTO"' or value == '"{0}"'.format(hostname): + # wicked's source accepts values with double quotes, single quotes, and no quotes at all. + if value in ('"AUTO"', "'AUTO'", 'AUTO') or value == '"{0}"'.format(hostname): # Return if auto send host-name is configured or the current # hostname is already set up to be sent return From 1f84bac5d92bdaa5548a91af2a4a8719a4df744b Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Thu, 4 Aug 2022 10:58:04 -0700 Subject: [PATCH 40/58] refactor logcollector memory usage (#2637) * refactor logcollector memory usage * address PR comments * fix pylint error --- azurelinuxagent/agent.py | 31 ++-- azurelinuxagent/common/cgroup.py | 1 + azurelinuxagent/common/cgroupconfigurator.py | 6 +- azurelinuxagent/common/logcollector.py | 27 +++- azurelinuxagent/ga/collect_logs.py | 154 +++++++++++++++---- tests/common/test_logcollector.py | 61 ++++++-- tests/ga/test_collect_logs.py | 93 ++++++++++- 7 files changed, 302 insertions(+), 71 deletions(-) diff --git a/azurelinuxagent/agent.py b/azurelinuxagent/agent.py index b70429a129..8c303482e8 100644 --- a/azurelinuxagent/agent.py +++ b/azurelinuxagent/agent.py @@ -45,7 +45,7 @@ PY_VERSION_MAJOR, PY_VERSION_MINOR, \ PY_VERSION_MICRO, GOAL_STATE_AGENT_VERSION, \ get_daemon_version, set_daemon_version -from azurelinuxagent.ga.collect_logs import CollectLogsHandler +from azurelinuxagent.ga.collect_logs import CollectLogsHandler, get_log_collector_monitor_handler from azurelinuxagent.pa.provision.default import ProvisionHandler @@ -196,36 +196,45 @@ def show_configuration(self): print("{0} = {1}".format(k, configuration[k])) def collect_logs(self, is_full_mode): + logger.set_prefix("LogCollector") + if is_full_mode: - print("Running log collector mode full") + logger.info("Running log collector mode full") else: - print("Running log collector mode normal") + logger.info("Running log collector mode normal") # Check the cgroups unit + cpu_cgroup_path, memory_cgroup_path, log_collector_monitor = None, None, None if CollectLogsHandler.should_validate_cgroups(): - cpu_cgroup_path, memory_cgroup_path = SystemdCgroupsApi.get_process_cgroup_relative_paths("self") + cgroups_api = SystemdCgroupsApi() + cpu_cgroup_path, memory_cgroup_path = cgroups_api.get_process_cgroup_paths("self") cpu_slice_matches = (cgroupconfigurator.LOGCOLLECTOR_SLICE in cpu_cgroup_path) memory_slice_matches = (cgroupconfigurator.LOGCOLLECTOR_SLICE in memory_cgroup_path) if not cpu_slice_matches or not memory_slice_matches: - print("The Log Collector process is not in the proper cgroups:") + logger.info("The Log Collector process is not in the proper cgroups:") if not cpu_slice_matches: - print("\tunexpected cpu slice") + logger.info("\tunexpected cpu slice") if not memory_slice_matches: - print("\tunexpected memory slice") + logger.info("\tunexpected memory slice") sys.exit(logcollector.INVALID_CGROUPS_ERRCODE) try: - log_collector = LogCollector(is_full_mode) + log_collector = LogCollector(is_full_mode, cpu_cgroup_path, memory_cgroup_path) + log_collector_monitor = get_log_collector_monitor_handler(log_collector.cgroups) + log_collector_monitor.run() archive = log_collector.collect_logs_and_get_archive() - print("Log collection successfully completed. Archive can be found at {0} " + logger.info("Log collection successfully completed. Archive can be found at {0} " "and detailed log output can be found at {1}".format(archive, OUTPUT_RESULTS_FILE_PATH)) except Exception as e: - print("Log collection completed unsuccessfully. Error: {0}".format(ustr(e))) - print("Detailed log output can be found at {0}".format(OUTPUT_RESULTS_FILE_PATH)) + logger.error("Log collection completed unsuccessfully. Error: {0}".format(ustr(e))) + logger.info("Detailed log output can be found at {0}".format(OUTPUT_RESULTS_FILE_PATH)) sys.exit(1) + finally: + if log_collector_monitor is not None: + log_collector_monitor.stop() @staticmethod def setup_firewall(firewall_metadata): diff --git a/azurelinuxagent/common/cgroup.py b/azurelinuxagent/common/cgroup.py index 2af4c0a117..b22ea2994e 100644 --- a/azurelinuxagent/common/cgroup.py +++ b/azurelinuxagent/common/cgroup.py @@ -29,6 +29,7 @@ _DEFAULT_REPORT_PERIOD = timedelta(seconds=conf.get_cgroup_check_period()) AGENT_NAME_TELEMETRY = "walinuxagent.service" # Name used for telemetry; it needs to be consistent even if the name of the service changes +AGENT_LOG_COLLECTOR = "azure-walinuxagent-logcollector" class CounterNotFound(Exception): diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index d79af493d6..31b8457894 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -74,10 +74,9 @@ CPUAccounting=yes CPUQuota={cpu_quota} MemoryAccounting=yes -MemoryLimit={memory_limit} """ _LOGCOLLECTOR_CPU_QUOTA = "5%" -_LOGCOLLECTOR_MEMORY_LIMIT = "30M" # K for kb, M for mb +LOGCOLLECTOR_MEMORY_LIMIT = 30 * 1024 ** 2 # 30Mb _AGENT_DROP_IN_FILE_SLICE = "10-Slice.conf" _AGENT_DROP_IN_FILE_SLICE_CONTENTS = """ @@ -349,8 +348,7 @@ def __setup_azure_slice(): files_to_create.append((vmextensions_slice, _VMEXTENSIONS_SLICE_CONTENTS)) if not os.path.exists(logcollector_slice): - slice_contents = _LOGCOLLECTOR_SLICE_CONTENTS_FMT.format(cpu_quota=_LOGCOLLECTOR_CPU_QUOTA, - memory_limit=_LOGCOLLECTOR_MEMORY_LIMIT) + slice_contents = _LOGCOLLECTOR_SLICE_CONTENTS_FMT.format(cpu_quota=_LOGCOLLECTOR_CPU_QUOTA) files_to_create.append((logcollector_slice, slice_contents)) diff --git a/azurelinuxagent/common/logcollector.py b/azurelinuxagent/common/logcollector.py index 9b88681fa1..b0da848fc5 100644 --- a/azurelinuxagent/common/logcollector.py +++ b/azurelinuxagent/common/logcollector.py @@ -26,12 +26,15 @@ from datetime import datetime from heapq import heappush, heappop +from azurelinuxagent.common.cgroup import CpuCgroup, AGENT_LOG_COLLECTOR, MemoryCgroup from azurelinuxagent.common.conf import get_lib_dir, get_ext_log_dir, get_agent_log_file +from azurelinuxagent.common.event import initialize_event_logger_vminfo_common_parameters from azurelinuxagent.common.future import ustr from azurelinuxagent.common.logcollector_manifests import MANIFEST_NORMAL, MANIFEST_FULL # Please note: be careful when adding agent dependencies in this module. # This module uses its own logger and logs to its own file, not to the agent log. +from azurelinuxagent.common.protocol.util import get_protocol_util _EXTENSION_LOG_DIR = get_ext_log_dir() _AGENT_LIB_DIR = get_lib_dir() @@ -45,7 +48,7 @@ CGROUPS_UNIT = "collect-logs.scope" -FORCE_KILLED_ERRCODE = -9 +GRACEFUL_KILL_ERRCODE = 3 INVALID_CGROUPS_ERRCODE = 2 _MUST_COLLECT_FILES = [ @@ -67,12 +70,14 @@ class LogCollector(object): _TRUNCATED_FILE_PREFIX = "truncated_" - def __init__(self, is_full_mode=False): + def __init__(self, is_full_mode=False, cpu_cgroup_path=None, memory_cgroup_path=None): self._is_full_mode = is_full_mode self._manifest = MANIFEST_FULL if is_full_mode else MANIFEST_NORMAL self._must_collect_files = self._expand_must_collect_files() self._create_base_dirs() self._set_logger() + self._initialize_telemetry() + self.cgroups = self._set_resource_usage_cgroups(cpu_cgroup_path, memory_cgroup_path) @staticmethod def _mkdir(dirname): @@ -99,6 +104,24 @@ def _set_logger(): _LOGGER.addHandler(_f_handler) _LOGGER.setLevel(logging.INFO) + @staticmethod + def _set_resource_usage_cgroups(cpu_cgroup_path, memory_cgroup_path): + cpu_cgroup = CpuCgroup(AGENT_LOG_COLLECTOR, cpu_cgroup_path) + msg = "Started tracking cpu cgroup {0}".format(cpu_cgroup) + _LOGGER.info(msg) + cpu_cgroup.initialize_cpu_usage() + memory_cgroup = MemoryCgroup(AGENT_LOG_COLLECTOR, memory_cgroup_path) + msg = "Started tracking memory cgroup {0}".format(memory_cgroup) + _LOGGER.info(msg) + return [cpu_cgroup, memory_cgroup] + + @staticmethod + def _initialize_telemetry(): + protocol = get_protocol_util().get_protocol() + protocol.client.update_goal_state(force_update=True) + # Initialize the common parameters for telemetry events + initialize_event_logger_vminfo_common_parameters(protocol) + @staticmethod def _run_shell_command(command, stdout=subprocess.PIPE, log_output=False): """ diff --git a/azurelinuxagent/ga/collect_logs.py b/azurelinuxagent/ga/collect_logs.py index dc62fccf21..616d875a30 100644 --- a/azurelinuxagent/ga/collect_logs.py +++ b/azurelinuxagent/ga/collect_logs.py @@ -16,7 +16,6 @@ # # Requires Python 2.6+ and Openssl 1.0+ # - import datetime import os import sys @@ -26,17 +25,19 @@ import azurelinuxagent.common.conf as conf from azurelinuxagent.common import logger -from azurelinuxagent.common.event import elapsed_milliseconds, add_event, WALAEventOperation -from azurelinuxagent.common.future import subprocess_dev_null, ustr +from azurelinuxagent.common.cgroup import MetricsCounter +from azurelinuxagent.common.event import elapsed_milliseconds, add_event, WALAEventOperation, report_metric +from azurelinuxagent.common.future import ustr from azurelinuxagent.common.interfaces import ThreadHandlerInterface -from azurelinuxagent.common.logcollector import COMPRESSED_ARCHIVE_PATH -from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator +from azurelinuxagent.common.logcollector import COMPRESSED_ARCHIVE_PATH, GRACEFUL_KILL_ERRCODE +from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator, LOGCOLLECTOR_MEMORY_LIMIT from azurelinuxagent.common.protocol.util import get_protocol_util from azurelinuxagent.common.utils import shellutil from azurelinuxagent.common.utils.shellutil import CommandError from azurelinuxagent.common.version import PY_VERSION_MAJOR, PY_VERSION_MINOR, AGENT_NAME, CURRENT_VERSION -_INITIAL_LOG_COLLECTION_DELAY = 5 * 60 # Five minutes of delay +_INITIAL_LOG_COLLECTION_DELAY = 5 * 60 # Five minutes of delay + def get_collect_logs_handler(): return CollectLogsHandler() @@ -128,7 +129,10 @@ def stopped(self): def stop(self): self.should_run = False if self.is_alive(): - self.join() + try: + self.join() + except RuntimeError: + pass def init_protocols(self): # The initialization of ProtocolUtil for the log collection thread should be done within the thread itself @@ -167,10 +171,13 @@ def collect_and_send_logs(self): def _collect_logs(self): logger.info("Starting log collection...") - # Invoke the command line tool in the agent to collect logs, with resource limits on CPU and memory (RAM). + # Invoke the command line tool in the agent to collect logs, with resource limits on CPU. + # Some distros like ubuntu20 by default cpu and memory accounting enabled. Thus create nested cgroups under the logcollector slice + # So disabling CPU and Memory accounting prevents from creating nested cgroups, so that all the counters will be present in logcollector Cgroup systemd_cmd = [ - "systemd-run", "--unit={0}".format(logcollector.CGROUPS_UNIT), + "systemd-run", "--property=CPUAccounting=no", "--property=MemoryAccounting=no", + "--unit={0}".format(logcollector.CGROUPS_UNIT), "--slice={0}".format(cgroupconfigurator.LOGCOLLECTOR_SLICE), "--scope" ] @@ -178,17 +185,17 @@ def _collect_logs(self): collect_logs_cmd = [sys.executable, "-u", sys.argv[0], "-collect-logs"] final_command = systemd_cmd + collect_logs_cmd - def exec_command(output_file): + def exec_command(): start_time = datetime.datetime.utcnow() success = False msg = None try: - shellutil.run_command(final_command, log_error=False, stdout=output_file, stderr=output_file) + shellutil.run_command(final_command, log_error=False) duration = elapsed_milliseconds(start_time) archive_size = os.path.getsize(COMPRESSED_ARCHIVE_PATH) msg = "Successfully collected logs. Archive size: {0} b, elapsed time: {1} ms.".format(archive_size, - duration) + duration) logger.info(msg) success = True @@ -201,16 +208,17 @@ def exec_command(output_file): # pylint has limited (i.e. no) awareness of control flow w.r.t. typing. we disable=no-member # here because we know e must be a CommandError but pylint still considers the case where # e is a different type of exception. - err_msg = ustr("Log Collector exited with code {0}").format(e.returncode) # pylint: disable=no-member + err_msg = ustr("Log Collector exited with code {0}").format( + e.returncode) # pylint: disable=no-member - if e.returncode == logcollector.INVALID_CGROUPS_ERRCODE: # pylint: disable=no-member + if e.returncode == logcollector.INVALID_CGROUPS_ERRCODE: # pylint: disable=no-member logger.info("Disabling periodic log collection until service restart due to process error.") self.stop() - - # When the OOM killer is invoked on the log collector process, this error code is - # returned. Stop the periodic operation because it seems to be persistent. - elif e.returncode == logcollector.FORCE_KILLED_ERRCODE: # pylint: disable=no-member - logger.info("Disabling periodic log collection until service restart due to OOM error.") + + # When the log collector memory limit is exceeded, Agent gracefully exit the process with this error code. + # Stop the periodic operation because it seems to be persistent. + elif e.returncode == logcollector.GRACEFUL_KILL_ERRCODE: # pylint: disable=no-member + logger.info("Disabling periodic log collection until service restart due to exceeded process memory limit.") self.stop() else: logger.info(err_msg) @@ -227,17 +235,8 @@ def exec_command(output_file): is_success=success, message=msg, log_event=False) - - try: - logfile = open(conf.get_agent_log_file(), "a+") - except Exception: - with subprocess_dev_null() as DEVNULL: - return exec_command(DEVNULL) - else: - return exec_command(logfile) - finally: - if logfile is not None: - logfile.close() + + return exec_command() def _send_logs(self): msg = None @@ -261,3 +260,98 @@ def _send_logs(self): is_success=success, message=msg, log_event=False) + + +def get_log_collector_monitor_handler(cgroups): + return LogCollectorMonitorHandler(cgroups) + + +class LogCollectorMonitorHandler(ThreadHandlerInterface): + """ + Periodically monitor and checks the Log collector Cgroups and sends telemetry to Kusto. + """ + + _THREAD_NAME = "LogCollectorMonitorHandler" + + @staticmethod + def get_thread_name(): + return LogCollectorMonitorHandler._THREAD_NAME + + def __init__(self, cgroups): + self.event_thread = None + self.should_run = True + self.period = 2 # Log collector monitor runs every 2 secs. + self.cgroups = cgroups + self.__log_metrics = conf.get_cgroup_log_metrics() + + def run(self): + self.start() + + def stop(self): + self.should_run = False + if self.is_alive(): + self.join() + + def join(self): + self.event_thread.join() + + def stopped(self): + return not self.should_run + + def is_alive(self): + return self.event_thread is not None and self.event_thread.is_alive() + + def start(self): + self.event_thread = threading.Thread(target=self.daemon) + self.event_thread.setDaemon(True) + self.event_thread.setName(self.get_thread_name()) + self.event_thread.start() + + def daemon(self): + try: + while not self.stopped(): + try: + metrics = self._poll_resource_usage() + self._send_telemetry(metrics) + self._verify_memory_limit(metrics) + except Exception as e: + logger.error("An error occurred in the log collection monitor thread loop; " + "will skip the current iteration.\n{0}", ustr(e)) + finally: + time.sleep(self.period) + except Exception as e: + logger.error( + "An error occurred in the MonitorLogCollectorCgroupsHandler thread; will exit the thread.\n{0}", + ustr(e)) + + def _poll_resource_usage(self): + metrics = [] + for cgroup in self.cgroups: + metrics.extend(cgroup.get_tracked_metrics(track_throttled_time=True)) + return metrics + + def _send_telemetry(self, metrics): + for metric in metrics: + report_metric(metric.category, metric.counter, metric.instance, metric.value, log_event=self.__log_metrics) + + def _verify_memory_limit(self, metrics): + current_usage = 0 + max_usage = 0 + for metric in metrics: + if metric.counter == MetricsCounter.TOTAL_MEM_USAGE: + current_usage += metric.value + elif metric.counter == MetricsCounter.SWAP_MEM_USAGE: + current_usage += metric.value + elif metric.counter == MetricsCounter.MAX_MEM_USAGE: + max_usage = metric.value + + current_max = max(current_usage, max_usage) + if current_max > LOGCOLLECTOR_MEMORY_LIMIT: + msg = "Log collector memory limit {0} bytes exceeded. The max reported usage is {1} bytes.".format(LOGCOLLECTOR_MEMORY_LIMIT, current_max) + logger.info(msg) + add_event( + name=AGENT_NAME, + version=CURRENT_VERSION, + op=WALAEventOperation.LogCollection, + message=msg) + os._exit(GRACEFUL_KILL_ERRCODE) diff --git a/tests/common/test_logcollector.py b/tests/common/test_logcollector.py index d601d93cab..521e0f23ed 100644 --- a/tests/common/test_logcollector.py +++ b/tests/common/test_logcollector.py @@ -23,8 +23,9 @@ import zipfile from azurelinuxagent.common.logcollector import LogCollector +from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils.fileutil import rm_dirs, mkdir, rm_files -from tests.tools import AgentTestCase, is_python_version_26, patch, skip_if_predicate_true +from tests.tools import AgentTestCase, is_python_version_26, patch, skip_if_predicate_true, data_dir SMALL_FILE_SIZE = 1 * 1024 * 1024 # 1 MB LARGE_FILE_SIZE = 5 * 1024 * 1024 # 5 MB @@ -43,6 +44,7 @@ def setUpClass(cls): mkdir(cls.root_collect_dir) cls._mock_constants() + cls._mock_cgroup() @classmethod def _mock_constants(cls): @@ -69,6 +71,22 @@ def _mock_constants(cls): cls.compressed_archive_path) cls.mock_compressed_archive_path.start() + @classmethod + def _mock_cgroup(cls): + # CPU Cgroups compute usage based on /proc/stat and /sys/fs/cgroup/.../cpuacct.stat; use mock data for those + # files + original_read_file = fileutil.read_file + + def mock_read_file(filepath, **args): + if filepath == "/proc/stat": + filepath = os.path.join(data_dir, "cgroups", "proc_stat_t0") + elif filepath.endswith("/cpuacct.stat"): + filepath = os.path.join(data_dir, "cgroups", "cpuacct.stat_t0") + return original_read_file(filepath, **args) + + cls._mock_read_cpu_cgroup_file = patch("azurelinuxagent.common.utils.fileutil.read_file", side_effect=mock_read_file) + cls._mock_read_cpu_cgroup_file.start() + @classmethod def tearDownClass(cls): cls.mock_manifest.stop() @@ -76,6 +94,7 @@ def tearDownClass(cls): cls.mock_truncated_files_dir.stop() cls.mock_output_results_file_path.stop() cls.mock_compressed_archive_path.stop() + cls._mock_read_cpu_cgroup_file.stop() shutil.rmtree(cls.tmp_dir) @@ -192,8 +211,9 @@ def test_log_collector_parses_commands_in_manifest(self): diskinfo,""".format(folder_to_list, file_to_collect) with patch("azurelinuxagent.common.logcollector.MANIFEST_NORMAL", manifest): - log_collector = LogCollector() - archive = log_collector.collect_logs_and_get_archive() + with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): + log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") + archive = log_collector.collect_logs_and_get_archive() with open(self.output_results_file_path, "r") as fh: results = fh.readlines() @@ -220,8 +240,9 @@ def test_log_collector_uses_full_manifest_when_full_mode_enabled(self): """.format(file_to_collect) with patch("azurelinuxagent.common.logcollector.MANIFEST_FULL", manifest): - log_collector = LogCollector(is_full_mode=True) - archive = log_collector.collect_logs_and_get_archive() + with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): + log_collector = LogCollector(is_full_mode=True, cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") + archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(archive) self._assert_files_are_in_archive(expected_files=[file_to_collect]) @@ -233,8 +254,9 @@ def test_log_collector_should_collect_all_files(self): # All files in the manifest should be collected, since none of them are over the individual file size limit, # and combined they do not cross the archive size threshold. - log_collector = LogCollector() - archive = log_collector.collect_logs_and_get_archive() + with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): + log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") + archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(archive) @@ -254,8 +276,9 @@ def test_log_collector_should_collect_all_files(self): def test_log_collector_should_truncate_large_text_files_and_ignore_large_binary_files(self): # Set the size limit so that some files are too large to collect in full. with patch("azurelinuxagent.common.logcollector._FILE_SIZE_LIMIT", SMALL_FILE_SIZE): - log_collector = LogCollector() - archive = log_collector.collect_logs_and_get_archive() + with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): + log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") + archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(archive) @@ -287,8 +310,9 @@ def test_log_collector_should_prioritize_important_files_if_archive_too_big(self with patch("azurelinuxagent.common.logcollector._UNCOMPRESSED_ARCHIVE_SIZE_LIMIT", 10 * 1024 * 1024): with patch("azurelinuxagent.common.logcollector._MUST_COLLECT_FILES", must_collect_files): - log_collector = LogCollector() - archive = log_collector.collect_logs_and_get_archive() + with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): + log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") + archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(archive) @@ -337,8 +361,9 @@ def test_log_collector_should_prioritize_important_files_if_archive_too_big(self def test_log_collector_should_update_archive_when_files_are_new_or_modified_or_deleted(self): # Ensure the archive reflects the state of files on the disk at collection time. If a file was updated, it # needs to be updated in the archive, deleted if removed from disk, and added if not previously seen. - log_collector = LogCollector() - first_archive = log_collector.collect_logs_and_get_archive() + with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): + log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") + first_archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(first_archive) # Everything should be in the archive @@ -407,8 +432,9 @@ def test_log_collector_should_clean_up_uncollected_truncated_files(self): with patch("azurelinuxagent.common.logcollector._UNCOMPRESSED_ARCHIVE_SIZE_LIMIT", 2 * SMALL_FILE_SIZE): with patch("azurelinuxagent.common.logcollector._MUST_COLLECT_FILES", must_collect_files): with patch("azurelinuxagent.common.logcollector._FILE_SIZE_LIMIT", SMALL_FILE_SIZE): - log_collector = LogCollector() - archive = log_collector.collect_logs_and_get_archive() + with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): + log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") + archive = log_collector.collect_logs_and_get_archive() self._assert_archive_created(archive) @@ -428,8 +454,9 @@ def test_log_collector_should_clean_up_uncollected_truncated_files(self): with patch("azurelinuxagent.common.logcollector._UNCOMPRESSED_ARCHIVE_SIZE_LIMIT", 2 * SMALL_FILE_SIZE): with patch("azurelinuxagent.common.logcollector._MUST_COLLECT_FILES", must_collect_files): with patch("azurelinuxagent.common.logcollector._FILE_SIZE_LIMIT", SMALL_FILE_SIZE): - log_collector = LogCollector() - second_archive = log_collector.collect_logs_and_get_archive() + with patch('azurelinuxagent.common.logcollector.LogCollector._initialize_telemetry'): + log_collector = LogCollector(cpu_cgroup_path="dummy_cpu_path", memory_cgroup_path="dummy_memory_path") + second_archive = log_collector.collect_logs_and_get_archive() expected_files = [ os.path.join(self.root_collect_dir, "waagent.log"), diff --git a/tests/ga/test_collect_logs.py b/tests/ga/test_collect_logs.py index 27a03f5598..14593726d6 100644 --- a/tests/ga/test_collect_logs.py +++ b/tests/ga/test_collect_logs.py @@ -18,15 +18,18 @@ import os from azurelinuxagent.common import logger, conf +from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup, MetricValue from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator from azurelinuxagent.common.logger import Logger from azurelinuxagent.common.protocol.util import ProtocolUtil -from azurelinuxagent.ga.collect_logs import get_collect_logs_handler, is_log_collection_allowed +from azurelinuxagent.common.utils import fileutil +from azurelinuxagent.ga.collect_logs import get_collect_logs_handler, is_log_collection_allowed, \ + get_log_collector_monitor_handler from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.protocol.mockwiredata import DATA_FILE from tests.tools import Mock, MagicMock, patch, AgentTestCase, clear_singleton_instances, skip_if_predicate_true, \ - is_python_version_26 + is_python_version_26, data_dir @contextlib.contextmanager @@ -46,13 +49,14 @@ def _create_collect_logs_handler(iterations=1, cgroups_enabled=True, collect_log protocol_util = MagicMock() protocol_util.get_protocol = Mock(return_value=protocol) with patch("azurelinuxagent.ga.collect_logs.get_protocol_util", return_value=protocol_util): - with patch("azurelinuxagent.ga.collect_logs.CollectLogsHandler.stopped", side_effect=[False] * iterations + [True]): + with patch("azurelinuxagent.ga.collect_logs.CollectLogsHandler.stopped", + side_effect=[False] * iterations + [True]): with patch("time.sleep"): - # Grab the singleton to patch it cgroups_configurator_singleton = CGroupConfigurator.get_instance() with patch.object(cgroups_configurator_singleton, "enabled", return_value=cgroups_enabled): - with patch("azurelinuxagent.ga.collect_logs.conf.get_collect_logs", return_value=collect_logs_conf): + with patch("azurelinuxagent.ga.collect_logs.conf.get_collect_logs", + return_value=collect_logs_conf): def run_and_wait(): collect_logs_handler.run() collect_logs_handler.join() @@ -140,11 +144,14 @@ def http_put_handler(url, content, **__): collect_logs_handler.run_and_wait() self.assertEqual(http_put_handler.counter, 1, "The PUT API to upload logs should have been called once") self.assertTrue(os.path.exists(self.archive_path), "The archive file should exist on disk") - self.assertEqual(archive_size, len(http_put_handler.archive), "The archive file should have {0} bytes, not {1}".format(archive_size, len(http_put_handler.archive))) + self.assertEqual(archive_size, len(http_put_handler.archive), + "The archive file should have {0} bytes, not {1}".format(archive_size, + len(http_put_handler.archive))) def test_it_does_not_upload_logs_when_collection_is_unsuccessful(self): with _create_collect_logs_handler() as collect_logs_handler: - with patch("azurelinuxagent.ga.collect_logs.shellutil.run_command", side_effect=Exception("test exception")): + with patch("azurelinuxagent.ga.collect_logs.shellutil.run_command", + side_effect=Exception("test exception")): def http_put_handler(url, _, **__): if self.is_host_plugin_put_logs_request(url): http_put_handler.counter += 1 @@ -158,3 +165,75 @@ def http_put_handler(url, _, **__): collect_logs_handler.run_and_wait() self.assertFalse(os.path.exists(self.archive_path), "The archive file should not exist on disk") self.assertEqual(http_put_handler.counter, 0, "The PUT API to upload logs shouldn't have been called") + + +@contextlib.contextmanager +def _create_log_collector_monitor_handler(iterations=1): + """ + Creates an instance of LogCollectorMonitorHandler that + * Runs its main loop only the number of times given in the 'iterations' parameter, and + * Does not sleep at the end of each iteration + + The returned CollectLogsHandler is augmented with 2 methods: + * run_and_wait() - invokes run() and wait() on the CollectLogsHandler + + """ + with patch("azurelinuxagent.ga.collect_logs.LogCollectorMonitorHandler.stopped", + side_effect=[False] * iterations + [True]): + with patch("time.sleep"): + + original_read_file = fileutil.read_file + + def mock_read_file(filepath, **args): + if filepath == "/proc/stat": + filepath = os.path.join(data_dir, "cgroups", "proc_stat_t0") + elif filepath.endswith("/cpuacct.stat"): + filepath = os.path.join(data_dir, "cgroups", "cpuacct.stat_t0") + return original_read_file(filepath, **args) + + with patch("azurelinuxagent.common.utils.fileutil.read_file", side_effect=mock_read_file): + def run_and_wait(): + monitor_log_collector.run() + monitor_log_collector.join() + + cgroups = [ + CpuCgroup("test", "dummy_cpu_path"), + MemoryCgroup("test", "dummy_memory_path") + ] + monitor_log_collector = get_log_collector_monitor_handler(cgroups) + monitor_log_collector.run_and_wait = run_and_wait + yield monitor_log_collector + + +class TestLogCollectorMonitorHandler(AgentTestCase): + + @patch('azurelinuxagent.common.event.EventLogger.add_metric') + @patch("azurelinuxagent.ga.collect_logs.LogCollectorMonitorHandler._poll_resource_usage") + def test_send_extension_metrics_telemetry(self, patch_poll_resource_usage, patch_add_metric): + with _create_log_collector_monitor_handler() as log_collector_monitor_handler: + patch_poll_resource_usage.return_value = [MetricValue("Process", "% Processor Time", "service", 1), + MetricValue("Process", "Throttled Time", "service", 1), + MetricValue("Memory", "Total Memory Usage", "service", 1), + MetricValue("Memory", "Max Memory Usage", "service", 1), + MetricValue("Memory", "Swap Memory Usage", "service", 1) + ] + log_collector_monitor_handler.run_and_wait() + self.assertEqual(1, patch_poll_resource_usage.call_count) + self.assertEqual(5, patch_add_metric.call_count) # Five metrics being sent. + + @patch("os._exit", side_effect=Exception) + @patch("azurelinuxagent.ga.collect_logs.LogCollectorMonitorHandler._poll_resource_usage") + def test_verify_log_collector_memory_limit_exceeded(self, patch_poll_resource_usage, mock_exit): + with _create_log_collector_monitor_handler() as log_collector_monitor_handler: + with patch("azurelinuxagent.common.cgroupconfigurator.LOGCOLLECTOR_MEMORY_LIMIT", 8): + patch_poll_resource_usage.return_value = [MetricValue("Process", "% Processor Time", "service", 1), + MetricValue("Process", "Throttled Time", "service", 1), + MetricValue("Memory", "Total Memory Usage", "service", 9), + MetricValue("Memory", "Max Memory Usage", "service", 7), + MetricValue("Memory", "Swap Memory Usage", "service", 0) + + ] + try: + log_collector_monitor_handler.run_and_wait() + except Exception: + self.assertEqual(mock_exit.call_count, 1) From 9b2c33d10c440ea9021c0ca940ecea6572309581 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Fri, 5 Aug 2022 12:42:05 -0700 Subject: [PATCH 41/58] Fix UNKNOWN(Zombie) Process in unexpected processes check (#2644) --- azurelinuxagent/common/cgroupconfigurator.py | 22 ++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index 31b8457894..47f1da35ab 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -650,8 +650,9 @@ def _check_processes_in_agent_cgroup(self): current = process while current != 0 and current not in agent_commands: current = self._get_parent(current) - # Process started by agent will have a marker and check if that marker found in process environment. - if current == 0 and not self.__is_process_descendant_of_the_agent(process): + # Verify if Process started by agent based on the marker found in process environment or process is in Zombie state. + # If so, consider it as valid process in agent cgroup. + if current == 0 and not (self.__is_process_descendant_of_the_agent(process) or self.__is_zombie_process(process)): unexpected.append(self.__format_process(process)) if len(unexpected) >= 5: # collect just a small sample break @@ -704,6 +705,23 @@ def __is_process_descendant_of_the_agent(pid): pass return False + @staticmethod + def __is_zombie_process(pid): + """ + Returns True if process is in Zombie state otherwise False. + + Ex: cat /proc/18171/stat + 18171 (python3) S 18103 18103 18103 0 -1 4194624 57736 64902 0 3 + """ + try: + stat = '/proc/{0}/stat'.format(pid) + if os.path.exists(stat): + with open(stat, "r") as stat_file: + return stat_file.read().split()[2] == 'Z' + except Exception: + pass + return False + @staticmethod def _check_agent_throttled_time(cgroup_metrics): for metric in cgroup_metrics: From e2b61fe7680003ec947c36a6cf3aadf8219b4af5 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 16 Aug 2022 08:01:14 -0700 Subject: [PATCH 42/58] Save sharedconfig to disk (#2649) (#2652) * Save sharedconfig to disk * Update tests * pylint warnings Co-authored-by: narrieta (cherry picked from commit 6f9c516ac9ee20413ab3c90acdf125acfd3e9976) --- azurelinuxagent/common/protocol/goal_state.py | 12 +++++++++--- azurelinuxagent/common/utils/archive.py | 11 +++++++---- azurelinuxagent/ga/update.py | 3 +-- tests/protocol/test_goal_state.py | 11 ++++++++++- tests/utils/test_archive.py | 9 +++++++-- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index 4d354e5673..97ae270f87 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -33,7 +33,7 @@ from azurelinuxagent.common.protocol.hostplugin import VmSettingsNotSupported, VmSettingsSupportStopped from azurelinuxagent.common.protocol.restapi import Cert, CertList, RemoteAccessUser, RemoteAccessUsersList from azurelinuxagent.common.utils import fileutil -from azurelinuxagent.common.utils.archive import GoalStateHistory +from azurelinuxagent.common.utils.archive import GoalStateHistory, SHARED_CONF_FILE_NAME from azurelinuxagent.common.utils.cryptutil import CryptUtil from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext, getattrib @@ -345,8 +345,14 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): shared_conf_uri = findtext(xml_doc, "SharedConfig") xml_text = self._wire_client.fetch_config(shared_conf_uri, self._wire_client.get_header()) - shared_conf = SharedConfig(xml_text) + shared_config = SharedConfig(xml_text) self._history.save_shared_conf(xml_text) + # SharedConfig.xml is used by other components (Azsec and Singularity/HPC Infiniband), so save it to the agent's root directory as well + shared_config_file = os.path.join(conf.get_lib_dir(), SHARED_CONF_FILE_NAME) + try: + fileutil.write_file(shared_config_file, xml_text) + except Exception as e: + logger.warn("Failed to save {0}: {1}".format(shared_config, e)) certs = EmptyCertificates() certs_uri = findtext(xml_doc, "Certificates") @@ -372,7 +378,7 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): self._role_config_name = role_config_name self._container_id = container_id self._hosting_env = hosting_env - self._shared_conf = shared_conf + self._shared_conf = shared_config self._certs = certs self._remote_access = remote_access diff --git a/azurelinuxagent/common/utils/archive.py b/azurelinuxagent/common/utils/archive.py index b624d1742c..1f8fdd9311 100644 --- a/azurelinuxagent/common/utils/archive.py +++ b/azurelinuxagent/common/utils/archive.py @@ -46,11 +46,13 @@ _MAX_ARCHIVED_STATES = 50 _CACHE_PATTERNS = [ + # + # Note that SharedConfig.xml is not included here; this file is used by other components (Azsec and Singularity/HPC Infiniband) + # re.compile(r"^VmSettings\.\d+\.json$"), re.compile(r"^(.*)\.(\d+)\.(agentsManifest)$", re.IGNORECASE), re.compile(r"^(.*)\.(\d+)\.(manifest\.xml)$", re.IGNORECASE), re.compile(r"^(.*)\.(\d+)\.(xml)$", re.IGNORECASE), - re.compile(r"^SharedConfig\.xml$", re.IGNORECASE), re.compile(r"^HostingEnvironmentConfig\.xml$", re.IGNORECASE), re.compile(r"^RemoteAccess\.xml$", re.IGNORECASE), re.compile(r"^waagent_status\.\d+\.json$"), @@ -78,12 +80,12 @@ _VM_SETTINGS_FILE_NAME = "VmSettings.json" _CERTIFICATES_FILE_NAME = "Certificates.json" _HOSTING_ENV_FILE_NAME = "HostingEnvironmentConfig.xml" -_SHARED_CONF_FILE_NAME = "SharedConfig.xml" _REMOTE_ACCESS_FILE_NAME = "RemoteAccess.xml" _EXT_CONF_FILE_NAME = "ExtensionsConfig.xml" _MANIFEST_FILE_NAME = "{0}.manifest.xml" AGENT_STATUS_FILE = "waagent_status.json" +SHARED_CONF_FILE_NAME = "SharedConfig.xml" # TODO: use @total_ordering once RHEL/CentOS and SLES 11 are EOL. # @total_ordering first appeared in Python 2.7 and 3.2 @@ -166,9 +168,10 @@ def __init__(self, lib_dir): def purge_legacy_goal_state_history(): lib_dir = conf.get_lib_dir() for current_file in os.listdir(lib_dir): + # Don't remove the placeholder goal state file. # TODO: See comment in GoalStateHistory._save_placeholder and remove this code when no longer needed if current_file == _PLACEHOLDER_FILE_NAME: - return + continue # END TODO full_path = os.path.join(lib_dir, current_file) for pattern in _CACHE_PATTERNS: @@ -302,4 +305,4 @@ def save_hosting_env(self, text): self.save(text, _HOSTING_ENV_FILE_NAME) def save_shared_conf(self, text): - self.save(text, _SHARED_CONF_FILE_NAME) + self.save(text, SHARED_CONF_FILE_NAME) diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index c9887543dc..68c0fb4eaa 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -378,6 +378,7 @@ def run(self, debug=False): self._ensure_firewall_rules_persisted(dst_ip=protocol.get_endpoint()) self._add_accept_tcp_firewall_rule_if_not_enabled(dst_ip=protocol.get_endpoint()) self._reset_legacy_blacklisted_agents() + self._cleanup_legacy_goal_state_history() # Get all thread handlers telemetry_handler = get_send_telemetry_events_handler(self.protocol_util) @@ -396,8 +397,6 @@ def run(self, debug=False): logger.info("Goal State Period: {0} sec. This indicates how often the agent checks for new goal states and reports status.", self._goal_state_period) - self._cleanup_legacy_goal_state_history() - while self.is_running: self._check_daemon_running(debug) self._check_threads_running(all_thread_handlers) diff --git a/tests/protocol/test_goal_state.py b/tests/protocol/test_goal_state.py index c774171595..87a1db50e1 100644 --- a/tests/protocol/test_goal_state.py +++ b/tests/protocol/test_goal_state.py @@ -8,6 +8,7 @@ import re import time +from azurelinuxagent.common import conf from azurelinuxagent.common.future import httpclient from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateSource, GoalStateChannel from azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config import ExtensionsGoalStateFromExtensionsConfig @@ -96,7 +97,15 @@ def test_fetch_goal_state_should_raise_on_incomplete_goal_state(self): GoalState(protocol.client) self.assertEqual(_GET_GOAL_STATE_MAX_ATTEMPTS, mock_sleep.call_count, "Unexpected number of retries") - def test_instantiating_goal_state_should_save_the_goal_state_to_the_history_directory(self): + def test_fetching_the_goal_state_should_save_the_shared_config(self): + # SharedConfig.xml is used by other components (Azsec and Singularity/HPC Infiniband); verify that we do not delete it + with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: + _ = GoalState(protocol.client) + + shared_config = os.path.join(conf.get_lib_dir(), 'SharedConfig.xml') + self.assertTrue(os.path.exists(shared_config), "{0} should have been created".format(shared_config)) + + def test_fetching_the_goal_state_should_save_the_goal_state_to_the_history_directory(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: protocol.mock_wire_data.set_incarnation(999) protocol.mock_wire_data.set_etag(888) diff --git a/tests/utils/test_archive.py b/tests/utils/test_archive.py index ce97d65fde..54766862f8 100644 --- a/tests/utils/test_archive.py +++ b/tests/utils/test_archive.py @@ -133,27 +133,32 @@ def test_goal_state_history_init_should_purge_old_items(self): def test_purge_legacy_goal_state_history(self): with patch("azurelinuxagent.common.conf.get_lib_dir", return_value=self.tmp_dir): + # SharedConfig.xml is used by other components (Azsec and Singularity/HPC Infiniband); verify that we do not delete it + shared_config = os.path.join(self.tmp_dir, 'SharedConfig.xml') + legacy_files = [ 'GoalState.2.xml', 'VmSettings.2.json', 'Prod.2.manifest.xml', 'ExtensionsConfig.2.xml', 'Microsoft.Azure.Extensions.CustomScript.1.xml', - 'SharedConfig.xml', 'HostingEnvironmentConfig.xml', 'RemoteAccess.xml', 'waagent_status.1.json' ] legacy_files = [os.path.join(self.tmp_dir, f) for f in legacy_files] + + self._write_file(shared_config) for f in legacy_files: self._write_file(f) StateArchiver.purge_legacy_goal_state_history() + self.assertTrue(os.path.exists(shared_config), "{0} should not have been removed".format(shared_config)) + for f in legacy_files: self.assertFalse(os.path.exists(f), "Legacy file {0} was not removed".format(f)) - @staticmethod def parse_isoformat(timestamp_str): return datetime.strptime(timestamp_str, '%Y-%m-%dT%H:%M:%S.%f') From 3aebcdd781549b55356d0682d135eec6d53b7831 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 17 Aug 2022 11:14:01 -0700 Subject: [PATCH 43/58] Refactor download logic (#2651) * Refactor download logic * Cleanup Co-authored-by: narrieta --- ...sions_goal_state_from_extensions_config.py | 46 +-- .../extensions_goal_state_from_vm_settings.py | 2 +- azurelinuxagent/common/protocol/wire.py | 307 ++++++++---------- azurelinuxagent/ga/exthandlers.py | 52 +-- .../ext_conf-requested_version.xml | 4 +- tests/data/hostgaplugin/ext_conf.xml | 4 +- ...tings-difference_in_required_features.json | 4 +- .../hostgaplugin/vm_settings-out-of-sync.json | 2 +- .../vm_settings-requested_version.json | 4 +- tests/data/hostgaplugin/vm_settings.json | 6 +- tests/data/wire/certs.xml | 156 ++++----- tests/data/wire/certs_no_format_specified.xml | 152 ++++----- tests/data/wire/ext_conf-no_gs_metadata.xml | 2 +- tests/data/wire/ext_conf.xml | 2 +- .../wire/ext_conf_additional_locations.xml | 2 +- tests/data/wire/ext_conf_autoupgrade.xml | 2 +- .../ext_conf_autoupgrade_internalversion.xml | 2 +- ..._conf_dependencies_with_empty_settings.xml | 2 +- .../wire/ext_conf_in_vm_artifacts_profile.xml | 2 +- ...ext_conf_in_vm_empty_artifacts_profile.xml | 2 +- tests/data/wire/ext_conf_in_vm_metadata.xml | 2 +- tests/data/wire/ext_conf_internalversion.xml | 2 +- .../ext_conf_invalid_and_valid_handlers.xml | 6 +- .../wire/ext_conf_invalid_vm_metadata.xml | 2 +- .../ext_conf_missing_requested_version.xml | 2 +- .../wire/ext_conf_multiple_extensions.xml | 8 +- tests/data/wire/ext_conf_no_public.xml | 2 +- .../data/wire/ext_conf_requested_version.xml | 2 +- .../data/wire/ext_conf_required_features.xml | 2 +- tests/data/wire/ext_conf_sequencing.xml | 4 +- .../wire/ext_conf_settings_case_mismatch.xml | 10 +- tests/data/wire/ext_conf_upgradeguid.xml | 2 +- ...multiple_depends_on_for_single_handler.xml | 6 +- ..._multiple_runtime_settings_same_plugin.xml | 4 +- ...onf_multiple_settings_for_same_handler.xml | 4 +- ..._conf_plugin_settings_version_mismatch.xml | 4 +- ..._and_multi_config_settings_same_plugin.xml | 4 +- tests/data/wire/trans_cert | 34 +- tests/data/wire/trans_prv | 52 +-- tests/data/wire/trans_pub | 14 +- tests/ga/test_extension.py | 12 +- .../ga/test_exthandlers_download_extension.py | 104 +++--- tests/ga/test_report_status.py | 7 +- tests/protocol/test_wire.py | 83 ++--- 44 files changed, 511 insertions(+), 616 deletions(-) diff --git a/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py b/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py index 8dce261ce6..27af3b7949 100644 --- a/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py +++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py @@ -19,13 +19,12 @@ from collections import defaultdict -import azurelinuxagent.common.logger as logger +from azurelinuxagent.common import logger from azurelinuxagent.common.event import add_event, WALAEventOperation from azurelinuxagent.common.exception import ExtensionsConfigError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState, GoalStateChannel, GoalStateSource from azurelinuxagent.common.protocol.restapi import ExtensionSettings, Extension, VMAgentManifest, ExtensionState, InVMGoalStateMetaData -from azurelinuxagent.common.utils import restutil from azurelinuxagent.common.utils.textutil import parse_doc, parse_json, findall, find, findtext, getattrib, gettext, format_exception, \ is_str_none_or_whitespace, is_str_empty @@ -81,51 +80,42 @@ def _parse_extensions_config(self, xml_text, wire_client): self._status_upload_blob_type = getattrib(status_upload_node, "statusBlobType") logger.verbose("Extension config shows status blob type as [{0}]", self._status_upload_blob_type) - self._on_hold = self._fetch_extensions_on_hold(xml_doc, wire_client) + self._on_hold = ExtensionsGoalStateFromExtensionsConfig._fetch_extensions_on_hold(xml_doc, wire_client) in_vm_gs_metadata = InVMGoalStateMetaData(find(xml_doc, "InVMGoalStateMetaData")) self._activity_id = self._string_to_id(in_vm_gs_metadata.activity_id) self._correlation_id = self._string_to_id(in_vm_gs_metadata.correlation_id) self._created_on_timestamp = self._ticks_to_utc_timestamp(in_vm_gs_metadata.created_on_ticks) - def _fetch_extensions_on_hold(self, xml_doc, wire_client): + @staticmethod + def _fetch_extensions_on_hold(xml_doc, wire_client): + def log_info(message): + logger.info(message) + add_event(op=WALAEventOperation.ArtifactsProfileBlob, message=message, is_success=True, log_event=False) + + def log_warning(message): + logger.warn(message) + add_event(op=WALAEventOperation.ArtifactsProfileBlob, message=message, is_success=False, log_event=False) + artifacts_profile_blob = findtext(xml_doc, "InVMArtifactsProfileBlob") if is_str_none_or_whitespace(artifacts_profile_blob): + log_info("ExtensionsConfig does not include a InVMArtifactsProfileBlob; will assume the VM is not on hold") return False - def fetch_direct(): - content, _ = wire_client.fetch(artifacts_profile_blob) - return content - - def fetch_through_host(): - host = wire_client.get_host_plugin() - uri, headers = host.get_artifact_request(artifacts_profile_blob) - content, _ = wire_client.fetch(uri, headers, use_proxy=False, retry_codes=restutil.HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES) - return content - - logger.verbose("Retrieving the artifacts profile") - try: - profile = wire_client.send_request_using_appropriate_channel(fetch_direct, fetch_through_host) - if profile is None: - logger.warn("Failed to fetch artifacts profile from blob {0}", artifacts_profile_blob) - return False + profile = wire_client.fetch_artifacts_profile_blob(artifacts_profile_blob) except Exception as error: - logger.warn("Exception retrieving artifacts profile from blob {0}. Error: {1}".format(artifacts_profile_blob, ustr(error))) + log_warning("Can't download the artifacts profile blob; will assume the VM is not on hold. {0}".format(ustr(error))) return False if is_str_empty(profile): + log_info("The artifacts profile blob is empty; will assume the VM is not on hold.") return False - logger.verbose("Artifacts profile downloaded") - try: artifacts_profile = _InVMArtifactsProfile(profile) - except Exception: - logger.warn("Could not parse artifacts profile blob") - msg = "Content: [{0}]".format(profile) - logger.verbose(msg) - add_event(op=WALAEventOperation.ArtifactsProfileBlob, is_success=False, message=msg, log_event=False) + except Exception as exception: + log_warning("Can't parse the artifacts profile blob; will assume the VM is not on hold. Error: {0}".format(ustr(exception))) return False return artifacts_profile.get_on_hold() diff --git a/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py index 10e036c9c9..db7237b9ed 100644 --- a/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py +++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py @@ -291,7 +291,7 @@ def _parse_extensions(self, vm_settings): # "settingsSeqNo": 0, # "settings": [ # { - # "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + # "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", # "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", # "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" # } diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py index b8b05c98b8..6d48474dd2 100644 --- a/azurelinuxagent/common/protocol/wire.py +++ b/azurelinuxagent/common/protocol/wire.py @@ -20,19 +20,20 @@ import os import random import time -import xml.sax.saxutils as saxutils + from collections import defaultdict from datetime import datetime, timedelta +from xml.sax import saxutils -import azurelinuxagent.common.conf as conf -import azurelinuxagent.common.logger as logger -import azurelinuxagent.common.utils.textutil as textutil +from azurelinuxagent.common import conf +from azurelinuxagent.common import logger +from azurelinuxagent.common.utils import textutil from azurelinuxagent.common.agent_supported_feature import get_agent_supported_features_list_for_crp, SupportedFeatureNames from azurelinuxagent.common.datacontract import validate_param from azurelinuxagent.common.event import add_event, WALAEventOperation, report_event, \ CollectOrReportEventDebugInfo, add_periodic from azurelinuxagent.common.exception import ProtocolNotFoundError, \ - ResourceGoneError, ExtensionDownloadError, InvalidContainerError, ProtocolError, HttpError + ResourceGoneError, ExtensionDownloadError, InvalidContainerError, ProtocolError, HttpError, ExtensionErrorCodes from azurelinuxagent.common.future import httpclient, bytebuffer, ustr from azurelinuxagent.common.protocol.goal_state import GoalState, TRANSPORT_CERT_FILE_NAME, TRANSPORT_PRV_FILE_NAME from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol @@ -58,6 +59,8 @@ MAX_EVENT_BUFFER_SIZE = 2 ** 16 - 2 ** 10 +_DOWNLOAD_TIMEOUT = timedelta(minutes=5) + class UploadError(HttpError): pass @@ -127,25 +130,6 @@ def get_ext_handler_pkgs(self, ext_handler): def get_goal_state(self): return self.client.get_goal_state() - def _download_ext_handler_pkg_through_host(self, uri, destination): - host = self.client.get_host_plugin() - uri, headers = host.get_artifact_request(uri, host.manifest_uri) - success = self.client.stream(uri, destination, headers=headers, use_proxy=False, max_retry=1) # set max_retry to 1 because extension packages already have a retry loop (see ExtHandlerInstance.download()) - return success - - def download_ext_handler_pkg(self, uri, destination, headers=None, use_proxy=True): # pylint: disable=W0613 - direct_func = lambda: self.client.stream(uri, destination, headers=None, use_proxy=True, max_retry=1) - # NOTE: the host_func may be called after refreshing the goal state, be careful about any goal state data - # in the lambda. - host_func = lambda: self._download_ext_handler_pkg_through_host(uri, destination) - - try: - success = self.client.send_request_using_appropriate_channel(direct_func, host_func) is not None - except Exception: - success = False - - return success - def report_provision_status(self, provision_status): validate_param("provision_status", provision_status, ProvisionStatus) @@ -589,8 +573,7 @@ def call_wireserver(self, http_req, *args, **kwargs): raise except Exception as e: - raise ProtocolError("[Wireserver Exception] {0}".format( - ustr(e))) + raise ProtocolError("[Wireserver Exception] {0}".format(ustr(e))) return resp @@ -602,19 +585,9 @@ def decode_config(self, data): return xml_text def fetch_config(self, uri, headers): - resp = self.call_wireserver(restutil.http_get, - uri, - headers=headers) + resp = self.call_wireserver(restutil.http_get, uri, headers=headers) return self.decode_config(resp.read()) - def fetch_cache(self, local_file): - if not os.path.isfile(local_file): - raise ProtocolError("{0} is missing.".format(local_file)) - try: - return fileutil.read_file(local_file) - except IOError as e: - raise ProtocolError("Failed to read cache: {0}".format(e)) - @staticmethod def call_storage_service(http_req, *args, **kwargs): # Default to use the configured HTTP proxy @@ -623,95 +596,134 @@ def call_storage_service(http_req, *args, **kwargs): return http_req(*args, **kwargs) - def fetch_manifest_through_host(self, uri): - host = self.get_host_plugin() - uri, headers = host.get_artifact_request(uri) - response, _ = self.fetch(uri, headers, use_proxy=False, retry_codes=restutil.HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES) - return response + def fetch_artifacts_profile_blob(self, uri): + return self._fetch_content("artifacts profile blob", [uri])[1] # _fetch_content returns a (uri, content) tuple + + def fetch_manifest(self, uris): + uri, content = self._fetch_content("manifest", uris) + self.get_host_plugin().update_manifest_uri(uri) + return content + + def _fetch_content(self, download_type, uris): + """ + Walks the given list of 'uris' issuing HTTP GET requests; returns a tuple with the URI and the content of the first successful request. + + The 'download_type' is added to any log messages produced by this method; it should describe the type of content of the given URIs + (e.g. "manifest", "extension package", etc). + """ + host_ga_plugin = self.get_host_plugin() + + direct_download = lambda uri: self.fetch(uri)[0] + + def hgap_download(uri): + request_uri, request_headers = host_ga_plugin.get_artifact_request(uri) + response, _ = self.fetch(request_uri, request_headers, use_proxy=False, retry_codes=restutil.HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES) + return response - def fetch_manifest(self, version_uris, timeout_in_minutes=5, timeout_in_ms=0): - logger.verbose("Fetch manifest") - version_uris_shuffled = version_uris - random.shuffle(version_uris_shuffled) + return self._download_with_fallback_channel(download_type, uris, direct_download=direct_download, hgap_download=hgap_download) - uris_tried = 0 + def download_extension(self, uris, destination, on_downloaded=lambda: True): + """ + Walks the given list of 'uris' issuing HTTP GET requests and saves the content of the first successful request to 'destination'. + + When the download is successful, this method invokes the 'on_downloaded' callback function, which can be used to process the results of the download. + on_downloaded() should return True on success and False on failure (it should not raise any exceptions); ff the return value is False, the download + is considered a failure and the next URI is tried. + """ + host_ga_plugin = self.get_host_plugin() + + direct_download = lambda uri: self.stream(uri, destination, headers=None, use_proxy=True) + + def hgap_download(uri): + request_uri, request_headers = host_ga_plugin.get_artifact_request(uri, host_ga_plugin.manifest_uri) + return self.stream(request_uri, destination, headers=request_headers, use_proxy=False) + + self._download_with_fallback_channel("extension package", uris, direct_download=direct_download, hgap_download=hgap_download, on_downloaded=on_downloaded) + + def _download_with_fallback_channel(self, download_type, uris, direct_download, hgap_download, on_downloaded=lambda: True): + """ + Walks the given list of 'uris' issuing HTTP GET requests, attempting to download the content of each URI. The download is done using both the default and + the fallback channels, until one of them succeeds. The 'direct_download' and 'hgap_download' functions define the logic to do direct calls to the URI or + to use the HostGAPlugin as a proxy for the download. Initially the default channel is the direct download and the fallback channel is the HostGAPlugin, + but the default can be depending on the success/failure of each channel (see _download_using_appropriate_channel() for the logic to do this). + + The 'download_type' is added to any log messages produced by this method; it should describe the type of content of the given URIs + (e.g. "manifest", "extension package", etc). + + When the download is successful download_extension() invokes the 'on_downloaded' function, which can be used to process the results of the download. This + function should return True on success, and False on failure (it should not raise any exceptions). If the return value is False, the download is considered + a failure and the next URI is tried. + + When the download succeeds, this method returns a (uri, response) tuple where the first item is the URI of the successful download and the second item is + the response returned by the successful channel (i.e. one of direct_download and hgap_download). + + This method enforces a timeout (_DOWNLOAD_TIMEOUT) on the download and raises an exception if the limit is exceeded. + """ + logger.verbose("Downloading {0}", download_type) start_time = datetime.now() - for version_uri in version_uris_shuffled: - - if datetime.now() - start_time > timedelta(minutes=timeout_in_minutes, milliseconds=timeout_in_ms): - logger.warn("Agent timed-out after {0} minutes while fetching extension manifests. {1}/{2} uris tried.", - timeout_in_minutes, uris_tried, len(version_uris)) - break - - # GA expects a location and failoverLocation in ExtensionsConfig, but - # this is not always the case. See #1147. - if version_uri is None: - logger.verbose('The specified manifest URL is empty, ignored.') - continue - - # Disable W0640: OK to use version_uri in a lambda within the loop's body - direct_func = lambda: self.fetch(version_uri)[0] # pylint: disable=W0640 - # NOTE: the host_func may be called after refreshing the goal state, be careful about any goal state data - # in the lambda. - # Disable W0640: OK to use version_uri in a lambda within the loop's body - host_func = lambda: self.fetch_manifest_through_host(version_uri) # pylint: disable=W0640 + + uris_shuffled = uris + random.shuffle(uris_shuffled) + most_recent_error = "None" + + for index, uri in enumerate(uris_shuffled): + elapsed = datetime.now() - start_time + if elapsed > _DOWNLOAD_TIMEOUT: + message = "Timeout downloading {0}. Elapsed: {1} URIs tried: {2}/{3}. Last error: {4}".format(download_type, elapsed, index, len(uris), ustr(most_recent_error)) + raise ExtensionDownloadError(message, code=ExtensionErrorCodes.PluginManifestDownloadError) try: - manifest = self.send_request_using_appropriate_channel(direct_func, host_func) - if manifest is not None: - host = self.get_host_plugin() - host.update_manifest_uri(version_uri) - return manifest - except Exception as error: - logger.warn("Failed to fetch manifest from {0}. Error: {1}", version_uri, ustr(error)) + # Disable W0640: OK to use uri in a lambda within the loop's body + response = self._download_using_appropriate_channel(lambda: direct_download(uri), lambda: hgap_download(uri)) # pylint: disable=W0640 - uris_tried += 1 + if on_downloaded(): + return uri, response - raise ExtensionDownloadError("Failed to fetch manifest from all sources") + except Exception as exception: + most_recent_error = exception - def stream(self, uri, destination, headers=None, use_proxy=None, max_retry=None): + raise ExtensionDownloadError("Failed to download {0} from all URIs. Last error: {1}".format(download_type, ustr(most_recent_error)), code=ExtensionErrorCodes.PluginManifestDownloadError) + + def stream(self, uri, destination, headers=None, use_proxy=None): """ - max_retry indicates the maximum number of retries for the HTTP request; None indicates that the default value should be used + Downloads the content of the given 'uri' and saves it to the 'destination' file. """ - success = False - logger.verbose("Fetch [{0}] with headers [{1}] to file [{2}]", uri, headers, destination) + try: + logger.verbose("Fetch [{0}] with headers [{1}] to file [{2}]", uri, headers, destination) - response = self._fetch_response(uri, headers, use_proxy, max_retry=max_retry) - if response is not None and not restutil.request_failed(response): - chunk_size = 1024 * 1024 # 1MB buffer - try: + response = self._fetch_response(uri, headers, use_proxy) + if response is not None and not restutil.request_failed(response): + chunk_size = 1024 * 1024 # 1MB buffer with open(destination, 'wb', chunk_size) as destination_fh: complete = False while not complete: chunk = response.read(chunk_size) destination_fh.write(chunk) complete = len(chunk) < chunk_size - success = True - except Exception as error: - logger.error('Error streaming {0} to {1}: {2}'.format(uri, destination, ustr(error))) - - return success + return "" + except: + if os.path.exists(destination): # delete the destination file, in case we did a partial download + try: + os.remove(destination) + except Exception as exception: + logger.warn("Can't delete {0}: {1}", destination, ustr(exception)) + raise - def fetch(self, uri, headers=None, use_proxy=None, decode=True, max_retry=None, retry_codes=None, ok_codes=None): + def fetch(self, uri, headers=None, use_proxy=None, decode=True, retry_codes=None, ok_codes=None): """ - max_retry indicates the maximum number of retries for the HTTP request; None indicates that the default value should be used - Returns a tuple with the content and headers of the response. The headers are a list of (name, value) tuples. """ logger.verbose("Fetch [{0}] with headers [{1}]", uri, headers) content = None response_headers = None - response = self._fetch_response(uri, headers, use_proxy, max_retry=max_retry, retry_codes=retry_codes, ok_codes=ok_codes) + response = self._fetch_response(uri, headers, use_proxy, retry_codes=retry_codes, ok_codes=ok_codes) if response is not None and not restutil.request_failed(response, ok_codes=ok_codes): response_content = response.read() content = self.decode_config(response_content) if decode else response_content response_headers = response.getheaders() return content, response_headers - def _fetch_response(self, uri, headers=None, use_proxy=None, max_retry=None, retry_codes=None, ok_codes=None): - """ - max_retry indicates the maximum number of retries for the HTTP request; None indicates that the default value should be used - """ + def _fetch_response(self, uri, headers=None, use_proxy=None, retry_codes=None, ok_codes=None): resp = None try: resp = self.call_storage_service( @@ -719,7 +731,6 @@ def _fetch_response(self, uri, headers=None, use_proxy=None, max_retry=None, ret uri, headers=headers, use_proxy=use_proxy, - max_retry=max_retry, retry_codes=retry_codes) host_plugin = self.get_host_plugin() @@ -743,10 +754,7 @@ def _fetch_response(self, uri, headers=None, use_proxy=None, max_retry=None, ret msg = "Fetch failed: {0}".format(error) logger.warn(msg) report_event(op=WALAEventOperation.HttpGet, is_success=False, message=msg, log_event=False) - - if isinstance(error, (InvalidContainerError, ResourceGoneError)): - # These are retryable errors that should force a goal state refresh in the host plugin - raise + raise return resp @@ -844,14 +852,9 @@ def _call_hostplugin_with_container_check(self, host_func): """ Calls host_func on host channel and accounts for stale resource (ResourceGoneError or InvalidContainerError). If stale, it refreshes the goal state and retries host_func. - This method can throw, so the callers need to handle that. """ try: - ret = host_func() - if ret in (None, False): - raise Exception("Request failed using the host channel.") - - return ret + return host_func() except (ResourceGoneError, InvalidContainerError) as error: host_plugin = self.get_host_plugin() @@ -871,9 +874,6 @@ def _call_hostplugin_with_container_check(self, host_func): try: ret = host_func() - if ret in (None, False): - raise Exception("Request failed using the host channel after goal state refresh.") - msg = "[PERIODIC] Request succeeded using the host plugin channel after goal state refresh. " \ "ContainerId changed from {0} to {1}, " \ "role config file changed from {2} to {3}.".format(old_container_id, new_container_id, @@ -900,70 +900,36 @@ def _call_hostplugin_with_container_check(self, host_func): log_event=True) raise - def __send_request_using_host_channel(self, host_func): - """ - Calls the host_func on host channel with retries for stale goal state and handles any exceptions, consistent with the caller for direct channel. - At the time of writing, host_func internally calls either: - 1) WireClient.stream which returns a boolean, or - 2) WireClient.fetch which returns None or a HTTP response. - This method returns either None (failure case where host_func returned None or False), True or an HTTP response. - """ - ret = None - try: - ret = self._call_hostplugin_with_container_check(host_func) - except Exception as error: - logger.periodic_info(logger.EVERY_HOUR, "[PERIODIC] Request failed using the host channel. Error: {0}".format(ustr(error))) - - return ret - - @staticmethod - def __send_request_using_direct_channel(direct_func): - """ - Calls the direct_func on direct channel and handles any exceptions, consistent with the caller for host channel. - At the time of writing, direct_func internally calls either: - 1) WireClient.stream which returns a boolean, or - 2) WireClient.fetch which returns None or a HTTP response. - This method returns either None (failure case where direct_func returned None or False), True or an HTTP response. - """ - ret = None - try: - ret = direct_func() - - if ret in (None, False): - logger.periodic_info(logger.EVERY_HOUR, "[PERIODIC] Request failed using the direct channel.") - return None - except Exception as error: - logger.periodic_info(logger.EVERY_HOUR, "[PERIODIC] Request failed using the direct channel. Error: {0}".format(ustr(error))) - - return ret - - def send_request_using_appropriate_channel(self, direct_func, host_func): + def _download_using_appropriate_channel(self, direct_download, hgap_download): """ - Determines which communication channel to use. By default, the primary channel is direct, host channel is secondary. - We call the primary channel first and return on success. If primary fails, we try secondary. If secondary fails, - we return and *don't* switch the default channel. If secondary succeeds, we change the default channel. - This method doesn't raise since the calls to direct_func and host_func are already wrapped and handle any exceptions. - Possible return values are manifest, artifacts profile, True or None. + Does a download using both the default and fallback channels. By default, the primary channel is direct, host channel is the fallback. + We call the primary channel first and return on success. If primary fails, we try the fallback. If fallback fails, + we return and *don't* switch the default channel. If fallback succeeds, we change the default channel. """ - direct_channel = lambda: self.__send_request_using_direct_channel(direct_func) - host_channel = lambda: self.__send_request_using_host_channel(host_func) + hgap_download_function_with_retry = lambda: self._call_hostplugin_with_container_check(hgap_download) if HostPluginProtocol.is_default_channel: - primary_channel, secondary_channel = host_channel, direct_channel + primary_channel, secondary_channel = hgap_download_function_with_retry, direct_download else: - primary_channel, secondary_channel = direct_channel, host_channel + primary_channel, secondary_channel = direct_download, hgap_download_function_with_retry + + try: + return primary_channel() + except Exception as exception: + primary_channel_error = exception - ret = primary_channel() - if ret is not None: - return ret + try: + return_value = secondary_channel() - ret = secondary_channel() - if ret is not None: + # Since the secondary channel succeeded, flip the default channel HostPluginProtocol.is_default_channel = not HostPluginProtocol.is_default_channel - message = "Default channel changed to {0} channel.".format("HostGA" if HostPluginProtocol.is_default_channel else "direct") + message = "Default channel changed to {0} channel.".format("HostGAPlugin" if HostPluginProtocol.is_default_channel else "Direct") logger.info(message) add_event(AGENT_NAME, op=WALAEventOperation.DefaultChannelChange, version=CURRENT_VERSION, is_success=True, message=message, log_event=False) - return ret + + return return_value + except Exception as exception: + raise HttpError("Download failed both on the primary and fallback channels. Primary: [{0}] Fallback: [{1}]".format(ustr(primary_channel_error), ustr(exception))) def upload_status_blob(self): extensions_goal_state = self.get_goal_state().extensions_goal_state @@ -1160,9 +1126,12 @@ def get_header_for_xml_content(self): } def get_header_for_cert(self): - trans_cert_file = os.path.join(conf.get_lib_dir(), - TRANSPORT_CERT_FILE_NAME) - content = self.fetch_cache(trans_cert_file) + trans_cert_file = os.path.join(conf.get_lib_dir(), TRANSPORT_CERT_FILE_NAME) + try: + content = fileutil.read_file(trans_cert_file) + except IOError as e: + raise ProtocolError("Failed to read {0}: {1}".format(trans_cert_file, e)) + cert = get_bytes_from_pem(content) return { "x-ms-agent-name": "WALinuxAgent", @@ -1181,10 +1150,6 @@ def get_on_hold(self): return self.get_goal_state().extensions_goal_state.on_hold def upload_logs(self, content): - host_func = lambda: self._upload_logs_through_host(content) - return self._call_hostplugin_with_container_check(host_func) - - def _upload_logs_through_host(self, content): host = self.get_host_plugin() return host.put_vm_log(content) diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py index 21c3d7b2dd..d6b5ab7b97 100644 --- a/azurelinuxagent/ga/exthandlers.py +++ b/azurelinuxagent/ga/exthandlers.py @@ -21,7 +21,6 @@ import glob import json import os -import random import re import shutil import stat @@ -32,10 +31,10 @@ from collections import defaultdict from functools import partial -import azurelinuxagent.common.conf as conf -import azurelinuxagent.common.logger as logger -import azurelinuxagent.common.utils.fileutil as fileutil -import azurelinuxagent.common.version as version +from azurelinuxagent.common import conf +from azurelinuxagent.common import logger +from azurelinuxagent.common.utils import fileutil +from azurelinuxagent.common import version from azurelinuxagent.common.agent_supported_feature import get_agent_supported_features_list_for_extensions, \ SupportedFeatureNames, get_supported_feature_by_name, get_agent_supported_features_list_for_crp from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator @@ -67,8 +66,6 @@ HANDLER_COMPLETE_NAME_PATTERN = re.compile(_HANDLER_PATTERN + r'$', re.IGNORECASE) HANDLER_PKG_EXT = ".zip" -NUMBER_OF_DOWNLOAD_RETRIES = 2 - # This is the default value for the env variables, whenever we call a command which is not an update scenario, we # set the env variable value to NOT_RUN to reduce ambiguity for the extension publishers NOT_RUN = "NOT_RUN" @@ -1232,18 +1229,6 @@ def report_event(self, name=None, message="", is_success=True, duration=0, log_e add_event(name=name, version=ext_handler_version, message=message, op=self.operation, is_success=is_success, duration=duration, log_event=log_event) - def _download_extension_package(self, source_uri, target_file): - self.logger.info("Downloading extension package: {0}", source_uri) - try: - if not self.protocol.download_ext_handler_pkg(source_uri, target_file): - raise Exception("Failed to download extension package from {0}".format(source_uri)) - except Exception as exception: - self.logger.info("Error downloading extension package: {0}", ustr(exception)) - if os.path.exists(target_file): - os.remove(target_file) - return False - return True - def _unzip_extension_package(self, source_file, target_directory): self.logger.info("Unzipping extension package: {0}", source_file) try: @@ -1274,33 +1259,8 @@ def download(self): self.logger.info("The existing extension package is invalid, will ignore it.") if not package_exists: - downloaded = False - i = 0 - while i < NUMBER_OF_DOWNLOAD_RETRIES: - uris_shuffled = self.pkg.uris - random.shuffle(uris_shuffled) - - for uri in uris_shuffled: - if not self._download_extension_package(uri, destination): - continue - - if self._unzip_extension_package(destination, self.get_base_dir()): - downloaded = True - break - - if downloaded: - break - - self.logger.info("Failed to download the extension package from all uris, will retry after a minute") - time.sleep(60) - i += 1 - - if not downloaded: - raise ExtensionDownloadError("Failed to download extension", - code=ExtensionErrorCodes.PluginManifestDownloadError) - - duration = elapsed_milliseconds(begin_utc) - self.report_event(message="Download succeeded", duration=duration) + self.protocol.client.download_extension(self.pkg.uris, destination, on_downloaded=lambda: self._unzip_extension_package(destination, self.get_base_dir())) + self.report_event(message="Download succeeded", duration=elapsed_milliseconds(begin_utc)) self.pkg_file = destination diff --git a/tests/data/hostgaplugin/ext_conf-requested_version.xml b/tests/data/hostgaplugin/ext_conf-requested_version.xml index bbb8a20feb..48cc95cc9f 100644 --- a/tests/data/hostgaplugin/ext_conf-requested_version.xml +++ b/tests/data/hostgaplugin/ext_conf-requested_version.xml @@ -60,7 +60,7 @@ "runtimeSettings": [ { "handlerSettings": { - "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": {"GCS_AUTO_CONFIG":true} } @@ -73,7 +73,7 @@ "runtimeSettings": [ { "handlerSettings": { - "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": {"enableGenevaUpload":true} } diff --git a/tests/data/hostgaplugin/ext_conf.xml b/tests/data/hostgaplugin/ext_conf.xml index ebd90aa0b2..8ede27f8a0 100644 --- a/tests/data/hostgaplugin/ext_conf.xml +++ b/tests/data/hostgaplugin/ext_conf.xml @@ -58,7 +58,7 @@ "runtimeSettings": [ { "handlerSettings": { - "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent==", "publicSettings": {"GCS_AUTO_CONFIG":true} } @@ -71,7 +71,7 @@ "runtimeSettings": [ { "handlerSettings": { - "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent==", "publicSettings": {"enableGenevaUpload":true} } diff --git a/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json b/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json index 5601268706..a17776828e 100644 --- a/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json +++ b/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json @@ -56,7 +56,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } @@ -76,7 +76,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"enableGenevaUpload\":true}" } diff --git a/tests/data/hostgaplugin/vm_settings-out-of-sync.json b/tests/data/hostgaplugin/vm_settings-out-of-sync.json index 1f369ae5bc..c35fdb5a33 100644 --- a/tests/data/hostgaplugin/vm_settings-out-of-sync.json +++ b/tests/data/hostgaplugin/vm_settings-out-of-sync.json @@ -56,7 +56,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } diff --git a/tests/data/hostgaplugin/vm_settings-requested_version.json b/tests/data/hostgaplugin/vm_settings-requested_version.json index 98959dd4ec..e7e5135f90 100644 --- a/tests/data/hostgaplugin/vm_settings-requested_version.json +++ b/tests/data/hostgaplugin/vm_settings-requested_version.json @@ -56,7 +56,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } @@ -74,7 +74,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEFpB/HKM/7evRk+DBz754wUwDQYJKoZIhvcNAQEBBQAEggEADPJwniDeIUXzxNrZCloitFdscQ59Bz1dj9DLBREAiM8jmxM0LLicTJDUv272Qm/4ZQgdqpFYBFjGab/9MX+Ih2x47FkVY1woBkckMaC/QOFv84gbboeQCmJYZC/rZJdh8rCMS+CEPq3uH1PVrvtSdZ9uxnaJ+E4exTPPviIiLIPtqWafNlzdbBt8HZjYaVw+SSe+CGzD2pAQeNttq3Rt/6NjCzrjG8ufKwvRoqnrInMs4x6nnN5/xvobKIBSv4/726usfk8Ug+9Q6Benvfpmre2+1M5PnGTfq78cO3o6mI3cPoBUjp5M0iJjAMGeMt81tyHkimZrEZm6pLa4NQMOEjArBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECC5nVaiJaWt+gAhgeYvxUOYHXw==", "publicSettings": "{\"enableGenevaUpload\":true}" } diff --git a/tests/data/hostgaplugin/vm_settings.json b/tests/data/hostgaplugin/vm_settings.json index a4ef0f785f..3018616ab4 100644 --- a/tests/data/hostgaplugin/vm_settings.json +++ b/tests/data/hostgaplugin/vm_settings.json @@ -56,7 +56,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent==", "publicSettings": "{\"GCS_AUTO_CONFIG\":true}" } @@ -76,7 +76,7 @@ "settingsSeqNo": 0, "settings": [ { - "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent==", "publicSettings": "{\"enableGenevaUpload\":true}" } @@ -192,7 +192,7 @@ "isMultiConfig": false, "settings": [ { - "protectedSettingsCertThumbprint": "4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3", + "protectedSettingsCertThumbprint": "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F", "protectedSettings": "MIIBsAYJKoZIhvcNAQcDoIIBoTCCAZ0CAQAxggFpddesZQewdDBgegkxNzA1BgoJkgergres/Microsoft.OSTCExtensions.VMAccessForLinux==" } ] diff --git a/tests/data/wire/certs.xml b/tests/data/wire/certs.xml index 6717c30f1f..5908de7938 100644 --- a/tests/data/wire/certs.xml +++ b/tests/data/wire/certs.xml @@ -1,81 +1,85 @@ 2012-11-30 - 12 + 3 Pkcs7BlobWithPfxContents - MIINswYJKoZIhvcNAQcDoIINpDCCDaACAQIxggEwMIIBLAIBAoAUvyL+x6GkZXog -QNfsXRZAdD9lc7IwDQYJKoZIhvcNAQEBBQAEggEArhMPepD/RqwdPcHEVqvrdZid -72vXrOCuacRBhwlCGrNlg8oI+vbqmT6CSv6thDpet31ALUzsI4uQHq1EVfV1+pXy -NlYD1CKhBCoJxs2fSPU4rc8fv0qs5JAjnbtW7lhnrqFrXYcyBYjpURKfa9qMYBmj -NdijN+1T4E5qjxPr7zK5Dalp7Cgp9P2diH4Nax2nixotfek3MrEFBaiiegDd+7tE -ux685GWYPqB5Fn4OsDkkYOdb0OE2qzLRrnlCIiBCt8VubWH3kMEmSCxBwSJupmQ8 -sxCWk+sBPQ9gJSt2sIqfx/61F8Lpu6WzP+ZOnMLTUn2wLU/d1FN85HXmnQALzTCC -DGUGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIbEcBfddWPv+AggxAAOAt/kCXiffe -GeJG0P2K9Q18XZS6Rz7Xcz+Kp2PVgqHKRpPjjmB2ufsRO0pM4z/qkHTOdpfacB4h -gz912D9U04hC8mt0fqGNTvRNAFVFLsmo7KXc/a8vfZNrGWEnYn7y1WfP52pqA/Ei -SNFf0NVtMyqg5Gx+hZ/NpWAE5vcmRRdoYyWeg13lhlW96QUxf/W7vY/D5KpAGACI -ok79/XI4eJkbq3Dps0oO/difNcvdkE74EU/GPuL68yR0CdzzafbLxzV+B43TBRgP -jH1hCdRqaspjAaZL5LGfp1QUM8HZIKHuTze/+4dWzS1XR3/ix9q/2QFI7YCuXpuE -un3AFYXE4QX/6kcPklZwh9FqjSie3I5HtC1vczqYVjqT4oHrs8ktkZ7oAzeXaXTF -k6+JQNNa/IyJw24I1MR77q7HlHSSfhXX5cFjVCd/+SiA4HJQjJgeIuXZ+dXmSPdL -9xLbDbtppifFyNaXdlSzcsvepKy0WLF49RmbL7Bnd46ce/gdQ6Midwi2MTnUtapu -tHmu/iJtaUpwXXC0B93PHfAk7Y3SgeY4tl/gKzn9/x5SPAcHiNRtOsNBU8ZThzos -Wh41xMLZavmX8Yfm/XWtl4eU6xfhcRAbJQx7E1ymGEt7xGqyPV7hjqhoB9i3oR5N -itxHgf1+jw/cr7hob+Trd1hFqZO6ePMyWpqUg97G2ThJvWx6cv+KRtTlVA6/r/UH -gRGBArJKBlLpXO6dAHFztT3Y6DFThrus4RItcfA8rltfQcRm8d0nPb4lCa5kRbCx -iudq3djWtTIe64sfk8jsc6ahWYSovM+NmhbpxEUbZVWLVEcHAYOeMbKgXSu5sxNO -JZNeFdzZqDRRY9fGjYNS7DdNOmrMmWKH+KXuMCItpNZsZS/3W7QxAo3ugYLdUylU -Zg8H/BjUGZCGn1rEBAuQX78m0SZ1xHlgHSwJIOmxOJUDHLPHtThfbELY9ec14yi5 -so1aQwhhfhPvF+xuXBrVeTAfhFNYkf2uxcEp7+tgFAc5W0QfT9SBn5vSvIxv+dT4 -7B2Pg1l/zjdsM74g58lmRJeDoz4psAq+Uk7n3ImBhIku9qX632Q1hanjC8D4xM4W -sI/W0ADCuAbY7LmwMpAMdrGg//SJUnBftlom7C9VA3EVf8Eo+OZH9hze+gIgUq+E -iEUL5M4vOHK2ttsYrSkAt8MZzjQiTlDr1yzcg8fDIrqEAi5arjTPz0n2s0NFptNW -lRD+Xz6pCXrnRgR8YSWpxvq3EWSJbZkSEk/eOmah22sFnnBZpDqn9+UArAznXrRi -nYK9w38aMGPKM39ymG8kcbY7jmDZlRgGs2ab0Fdj1jl3CRo5IUatkOJwCEMd/tkB -eXLQ8hspJhpFnVNReX0oithVZir+j36epk9Yn8d1l+YlKmuynjunKl9fhmoq5Q6i -DFzdYpqBV+x9nVhnmPfGyrOkXvGL0X6vmXAEif/4JoOW4IZpyXjgn+VoCJUoae5J -Djl45Bcc2Phrn4HW4Gg/+pIwTFqqZZ2jFrznNdgeIxTGjBrVsyJUeO3BHI0mVLaq -jtjhTshYCI7mXOis9W3ic0RwE8rgdDXOYKHhLVw9c4094P/43utSVXE7UzbEhhLE -Ngb4H5UGrQmPTNbq40tMUMUCej3zIKuVOvamzeE0IwLhkjNrvKhCG1EUhX4uoJKu -DQ++3KVIVeYSv3+78Jfw9F3usAXxX1ICU74/La5DUNjU7DVodLDvCAy5y1jxP3Ic -If6m7aBYVjFSQAcD8PZPeIEl9W4ZnbwyBfSDd11P2a8JcZ7N99GiiH3yS1QgJnAO -g9XAgjT4Gcn7k4lHPHLULgijfiDSvt94Ga4/hse0F0akeZslVN/bygyib7x7Lzmq -JkepRianrvKHbatuxvcajt/d+dxCnr32Q1qCEc5fcgDsjvviRL2tKR0qhuYjn1zR -Vk/fRtYOmlaGBVzUXcjLRAg3gC9+Gy8KvXIDrnHxD+9Ob+DUP9fgbKqMeOzKcCK8 -NSfSQ+tQjBYD5Ku4zAPUQJoRGgx43vXzcl2Z2i3E2otpoH82Kx8S9WlVEUlTtBjQ -QIGM5aR0QUNt8z34t2KWRA8SpP54VzBmEPdwLnzna+PkrGKsKiHVn4K+HfjDp1uW -xyO8VjrolAOYosTPXMpNp2u/FoFxaAPTa/TvmKc0kQ3ED9/sGLS2twDnEccvHP+9 -zzrnzzN3T2CWuXveDpuyuAty3EoAid1nuC86WakSaAZoa8H2QoRgsrkkBCq+K/yl -4FO9wuP+ksZoVq3mEDQ9qv6H4JJEWurfkws3OqrA5gENcLmSUkZie4oqAxeOD4Hh -Zx4ckG5egQYr0PnOd2r7ZbIizv3MKT4RBrfOzrE6cvm9bJEzNWXdDyIxZ/kuoLA6 -zX7gGLdGhg7dqzKqnGtopLAsyM1b/utRtWxOTGO9K9lRxyX82oCVT9Yw0DwwA+cH -Gutg1w7JHrIAYEtY0ezHgxhqMGuuTyJMX9Vr0D+9DdMeBK7hVOeSnxkaQ0f9HvF6 -0XI/2OTIoBSCBpUXjpgsYt7m7n2rFJGJmtqgLAosCAkacHnHLwX0EnzBw3sdDU6Q -jFXUWIDd5xUsNkFDCbspLMFs22hjNI6f/GREwd23Q4ujF8pUIcxcfbs2myjbK45s -tsn/jrkxmKRgwCIeN/H7CM+4GXSkEGLWbiGCxWzWt9wW1F4M7NW9nho3D1Pi2LBL -1ByTmjfo/9u9haWrp53enDLJJbcaslfe+zvo3J70Nnzu3m3oJ3dmUxgJIstG10g3 -lhpUm1ynvx04IFkYJ3kr/QHG/xGS+yh/pMZlwcUSpjEgYFmjFHU4A1Ng4LGI4lnw -5wisay4J884xmDgGfK0sdVQyW5rExIg63yYXp2GskRdDdwvWlFUzPzGgCNXQU96A -ljZfjs2u4IiVCC3uVsNbGqCeSdAl9HC5xKuPNbw5yTxPkeRL1ouSdkBy7rvdFaFf -dMPw6sBRNW8ZFInlgOncR3+xT/rZxru87LCq+3hRN3kw3hvFldrW2QzZSksO759b -pJEP+4fxuG96Wq25fRmzHzE0bdJ+2qF3fp/hy4oRi+eVPa0vHdtkymE4OUFWftb6 -+P++JVOzZ4ZxYA8zyUoJb0YCaxL+Jp/QqiUiH8WZVmYZmswqR48sUUKr7TIvpNbY -6jEH6F7KiZCoWfKH12tUC69iRYx3UT/4Bmsgi3S4yUxfieYRMIwihtpP4i0O+OjB -/DPbb13qj8ZSfXJ+jmF2SRFfFG+2T7NJqm09JvT9UcslVd+vpUySNe9UAlpcvNGZ -2+j180ZU7YAgpwdVwdvqiJxkeVtAsIeqAvIXMFm1PDe7FJB0BiSVZdihB6cjnKBI -dv7Lc1tI2sQe7QSfk+gtionLrEnto+aXF5uVM5LMKi3gLElz7oXEIhn54OeEciB1 -cEmyX3Kb4HMRDMHyJxqJXwxm88RgC6RekoPvstu+AfX/NgSpRj5beaj9XkweJT3H -rKWhkjq4Ghsn1LoodxluMMHd61m47JyoqIP9PBKoW+Na0VUKIVHw9e9YeW0nY1Zi -5qFA/pHPAt9AbEilRay6NEm8P7TTlNo216amc8byPXanoNrqBYZQHhZ93A4yl6jy -RdpYskMivT+Sh1nhZAioKqqTZ3HiFR8hFGspAt5gJc4WLYevmxSicGa6AMyhrkvG -rvOSdjY6JY/NkxtcgeycBX5MLF7uDbhUeqittvmlcrVN6+V+2HIbCCrvtow9pcX9 -EkaaNttj5M0RzjQxogCG+S5TkhCy04YvKIkaGJFi8xO3icdlxgOrKD8lhtbf4UpR -cDuytl70JD95mSUWL53UYjeRf9OsLRJMHQOpS02japkMwCb/ngMCQuUXA8hGkBZL -Xw7RwwPuM1Lx8edMXn5C0E8UK5e0QmI/dVIl2aglXk2oBMBJbnyrbfUPm462SG6u -ke4gQKFmVy2rKICqSkh2DMr0NzeYEUjZ6KbmQcV7sKiFxQ0/ROk8eqkYYxGWUWJv -ylPF1OTLH0AIbGlFPLQO4lMPh05yznZTac4tmowADSHY9RCxad1BjBeine2pj48D -u36OnnuQIsedxt5YC+h1bs+mIvwMVsnMLidse38M/RayCDitEBvL0KeG3vWYzaAL -h0FCZGOW0ilVk8tTF5+XWtsQEp1PpclvkcBMkU3DtBUnlmPSKNfJT0iRr2T0sVW1 -h+249Wj0Bw== + MIIOgwYJKoZIhvcNAQcDoIIOdDCCDnACAQIxggEwMIIBLAIBAoAUZcG9X+5aK8VZ +FY8eJV9j+RImq58wDQYJKoZIhvcNAQEBBQAEggEAn/hOytP/StyRuXHcqFq6x+Za +7gHfO8prXWdZW4e28NLt/x5ZOBHDDZ6buwwdXEZME0+RoiJvLqP2RNhZkEO8bkna +pS76xLZE4NXyfxkeEs1vJYis0WJdt/56uCzBuud2SBLuMWoAWgF5alokN0uFpVgm +CKCos+xv6Pisolc6geM8xQTYe6sLf5Z23LWftWfJqzuo/29glCCre7R80OLeZe5w +pN6XztbYz06nhVByC35To8Lm0akWAAKU7sfqM1Nty4P0rwUJPKXo42uN1GKYbDbF +x8piCAd+rs+q4Alu3qK/YaTPpMb2ECRMH6CYB8Klf/CbuWykkfS8zrsnpXT1kzCC +DTUGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQInjJWFaJcZz2Agg0QX6NlJUH17o20 +90gfjWV01mPmzLKx71JT+hyzKr5vHywDSRI/mdb3RqA59ZrIKeyWr0HXEOuABlul +nxjc/Rfk1tiLQwh0iqlOjlMtRsxS6yDA0WNwK2Y9gaXcdgDm5Vioai18l4Pd0qzK +fsof5a/jEJyunW1CZK19QciwfQ2pS8QbRYgeLRZRft2I+kv6cWXlGS6YrMqKQC8t +QMxnXR4AuzVllPLbbIgtM3l9oS+6jl7jKyKogeroJ9FNLjoMBJLldRLGPRhkCmdJ +Z1m+s/BAVUH08qgj2kmHzucdULLjlRcmma9m/h91TcQCXHAavf7S+U9QwIyGRh83 +t4Y7EqbQ93mOgjajFzILSL7AT/irgJpDu6CJqMu3EMNDA0mjxn5Cdvj40sufL/g3 +UyBwqosmIwAPzNDmhPtTKvHaHfGY/k8WhoIYfAA5Lhq1z22/RODZOY0Ch2XyxQM4 +s35eppe6IhnwyMv6HfrCrqE/o/16OrvvbaFQTeTlMvU0P7MIR4pVW6tRq4NEa5Wx +JcvGutuMuzH1VMcqcKdc7wYyOqDOGU43kcV8PiALTIcqrhD8NDrKks1jSkyqQw2h +sJQckNaQIcCXkUQecQa2UGe0l4HDSJ5gAETSenjyLBKFHf3hxiWBpw446/bODgdk +0+oyreZqMpRz3vn9LC+Yt7SuVbTzRdx7nlKIiNvo8+btOuVm44evchLFHAq3Ni8D +c+tP/wss3K4Xdp+t5SvEY/nLIu11Lw44HDVMYTuNz3Ya9psL70ZLaLZM8NromnEl +CUMRNTPoOC7/KDRh2E9d6c1V4CC43wAsRhksGJnSYoiSVAhaVgLqVFQTsqNHxmcg +3Y9AEBVzm3fZg6+DxAYu+amb+r8lk0Pp+N1t6rVbKXhkbAAxg0UDO3pY8Xcz0Y3g +Qdd5rnHh1rJrehku7zTHvQaXEddUWmCoUGIXJ+bt4VOhErL6s5/j8GSG0xmfxgSE +jnGj4Jwd0Vv19uZjsBDQ54R88GcA9YX8r48gr9JAwplrQ50m9KX6GwQhDRYKN/Dh +zOt9DCUkqMqdi5T4v2qNTfkL7iXBMhsSkeYUQ/tFLyv4QQyli5uTUZ5FNXohOVAx +TNyV9+gcV5WiBR0Aje6rwPW3oTkrPnVfZCdBwt/mZjPNMO5Se7D/lWE33yYu7bJ+ +gaxRNynhEOB7RaOePzDjn7LExahFmTFV0sgQxwQ2BYsfI22cdkAf6qOxdK/kqiQm +lgzRpDjyPIFhaCCHnXyJdSqcHmDrCjcg2P6AVCDJGdFOBvupeJ7Kg7WV5EY7G6AU +ng16tyumJSMWSzSks9M0Ikop6xhq3cV+Q0OArJoreQ6eonezXjM9Y865xjF80nJL +V4lcRxdXfoKpXJwzc++pgkY9t55J0+cEyBvIXfKud1/HHOhewhoy5ATyi9LLM91n +iW1DaQXlvHZgE7GFMSCVLxy6ZopBbm9tF0NQDFi8zUtGulD3Gkoc/Bp+DWb2vsX4 +S8W9vByNvIz/SWOGNbEs2irTRXccMAL7JHJ+74bwZZi5DRrqyQWHCn/3Ls2YPI6z +lnfl15EE4G7g3+nrvP2lZFBXjsdG/U3HYi+tAyHkRN3oXvgnt9N76PoY8dlsNf6c +RuNqgk31uO1sX/8du3Jxz87MlzWiG3kbAHMvbcoCgy/dW4JQcM3Sqg5PmF8i9wD1 +ZuqZ7zHpWILIWd13TM3UDolQZzl+GXEX62dPPL1vBtxHhDgQicdaWFXa6DX3dVwt +DToWaAqrAPIrgxvNk5FHNCTEVTQkmCIL5JoinZSk7BAl8b085CPM6F7OjB5CR4Ts +V+6UaTUZqk+z+raL+HJNW2ds1r7+t8Po5CydMBS4M/pE7b/laUnbRu7rO8cqKucn +n+eYimib/0YuqZj9u2RXso4kzdOyIxGSGHkmSzYuoNRx80r+jHtcBBTqXk37t0FY +X5O7QItCE+uwV1Sa12yg2dgJ6vKRPCEVyMoYUBwNbKEcw1pjG9Em7HwjOZK0UrO1 +yKRz6kxffVKN9Naf7lOnXooVuedY/jcaZ2zCZtASlOe8iiQK5prM4sbMixMp9ovL +tTxy9E9kgvaI/mkzarloKPQGsk0WzuH+i39M3DOXrMf5HwfE+A55u1gnrHsxQlxp +z5acwN42+4ln6axs4aweMGAhyEtBW8TdsNomwuPk+tpqZXHI2pqS4/aVOk8R8VE7 +IqtBx2QBMINT79PDPOn3K6v9HEt9fUHJ2TWJvKRKfsu5lECJPJSJA8OQ7zzw6zQt +NXw8UhZRmNW0+eI5dykg+XsII7+njYa33EJ1Sy1Ni8ZT/izKfrKCwEm44KVAyUG5 +qUjghPPMNQY3D0qOl54DRfGVOxbHztUooblW+DnlLlpOy/+/B+H9Dscxosdx2/Mo +RftJOMlLqK7AYIYAlw1zvqZo0pf7rCcLSLt+6FrPtNZe6ULFUacZ3RqyTZovsZi5 +Ucda3bLdOHX6tKL21bRfN7L0/BjF6BJETpG3p+rBYOyCwO6HvdenpMm6cT02nrfP +QJtImjeW1ov6Pw02zNlIZAXFir78Z6AcMhV2iKEJxc1RMFBcXmylNXJmGlKYB3lJ +jWo6qumLewTz5vzRu0vZCmOf+bKmuyVxckPbrzP+4OHKhpm95Kp6sUn2pvh0S8H3 +w1pjfZ9+sIaVgMspfRPgoWTyZ0flFvAX6DHWYVejMebwfAqZaa+UAJJ6jWQbMNzo +ZtOhzCjV+2ZBYHvSiY7dtfaLwQJeMWEKIw32kEYv/Ts33n7dD/pAzZu0WCyfoqsQ +MEXhbZYSCQTJ8/gqvdlurWOJL091z6Uw810YVt+wMqsBo5lnMsS3GqkzgM2PVzuV +taddovr5CrWfAjQaFG8wcETiKEQFWS9JctKo0F+gwLwkVyc4fBSkjVmIliw1jXGu +Enf2mBei+n8EaRB2nNa/CBVGQM24WEeMNq+TqaMvnEonvMtCIEpuJAO/NzJ1pxw0 +9S+LKq3lFoIQoON5glsjV82WseAbFXmynBmSbyUY/mZQpjuNSnwLfpz4630x5vuV +VNglsZ8lW9XtSPh6GkMj+lLOCqJ5aZ4UEXDSYW7IaH4sPuQ4eAAUsKx/XlbmaOad +hgK+3gHYi98fiGGQjt9OqKzQRxVFnHtoSwbMp/gjAWqjDCFdo7RkCqFjfB1DsSj0 +TrjZU1lVMrmdEhtUNjqfRpWN82f55fxZdrHEPUQIrOywdbRiNbONwm4AfSE8ViPz ++SltYpQfF6g+tfZMwsoPSevLjdcmb1k3n8/lsEL99wpMT3NbibaXCjeJCZbAYK05 +rUw5bFTVAuv6i3Bax3rx5DqyQANS3S8TBVYrdXf9x7RpQ8oeb4oo+qn293bP4n5m +nW/D/yvsAJYcm3lD7oW7D369nV/mwKPpNC4B9q6N1FiUndvdFSbyzfNfSF9LV0RU +A/4Qm05HtE3PAUFYfwwP8MDg0HdltMn83VfqrEi/d76xlcxfoIh2RQQgqxCIS6KE +AExIY/hPYDVxApznI39xNOp7IqdPEX3i7Cv7aHeFAwbhXYMNnkfFJJTkHRdcRiJ/ +RE1QPlC7ijH+IF02PE/seYg4GWrkeW3jvi+IKQ9BPBoYIx0P+7wHXf4ZGtZMourd +N4fdwzFCDMFkS7wQC/GOqZltzF/gz1fWEGXRTH3Lqx0iKyiiLs2trQhFOzNw3B7E +WxCIUjRMAAJ6vvUdvoFlMw8WfBkzCVple4yrCqIw6fJEq8v0q8EQ7qKDTfyPnFBt +CtQZuTozfdPDnVHGmGPQKUODH/6Vwl+9/l7HDvV8/D/HKDnP581ix1a3bdokNtSK +7rBfovpzYltYGpVxsC6MZByYEpvIh5nHQouLR4L3Je2wB3F9nBGjNhBvGDQlxcne +AAgywpOpQfvfsnYRWt2vlQzwhHUgWhJmGMhGMmn4oKc5su87G7yzFEnq/yIUMOm/ +X0Zof/Qm92KCJS7YkLzP1GDO9XPMe+ZHeHVNXhVNCRxGNbHCHB9+g9v090sLLmal +jpgrDks19uHv0yYiMqBdpstzxClRWxgHwrZO6jtbr5jeJuLVUxV0uuX76oeomUj2 +mAwoD5cB1U8W9Ew+cMjp5v6gg0LTk90HftjhrZmMA0Ll6TqFWjxge+jsswOY1SZi +peuQGIHFcuQ7SEcyIbqju3bmeEGZwTz51yo8x2WqpCwB1a4UTngWJgDCySAI58fM +eRL6r478CAZjk+fu9ZA85B7tFczl3lj0B4QHxkX370ZeCHy39qw8vMYIcPk3ytI0 +vmj5UCSeQDHHDcwo54wi83IFEWUFh18gP4ty5Tfvs6qv7qd455UQZTAO7lwpdBlp +MJGlMqBHjDLGyY80p+O4vdlQBZ1uMH+48u91mokUP8p+tVVKh7bAw/HPG+SQsuNR +DXF+gTm/hRuY7IYe3C7Myzc8bDTtFw6Es9BLAqzFFAMjzDVz7wY1rnZQq4mmLcKg +AAMJaqItipKAroYIntXXJ3U8fsUt03M= - + \ No newline at end of file diff --git a/tests/data/wire/certs_no_format_specified.xml b/tests/data/wire/certs_no_format_specified.xml index c971d1de2c..4ab91a8597 100644 --- a/tests/data/wire/certs_no_format_specified.xml +++ b/tests/data/wire/certs_no_format_specified.xml @@ -3,79 +3,83 @@ 2012-11-30 12 - MIINswYJKoZIhvcNAQcDoIINpDCCDaACAQIxggEwMIIBLAIBAoAUvyL+x6GkZXog -QNfsXRZAdD9lc7IwDQYJKoZIhvcNAQEBBQAEggEArhMPepD/RqwdPcHEVqvrdZid -72vXrOCuacRBhwlCGrNlg8oI+vbqmT6CSv6thDpet31ALUzsI4uQHq1EVfV1+pXy -NlYD1CKhBCoJxs2fSPU4rc8fv0qs5JAjnbtW7lhnrqFrXYcyBYjpURKfa9qMYBmj -NdijN+1T4E5qjxPr7zK5Dalp7Cgp9P2diH4Nax2nixotfek3MrEFBaiiegDd+7tE -ux685GWYPqB5Fn4OsDkkYOdb0OE2qzLRrnlCIiBCt8VubWH3kMEmSCxBwSJupmQ8 -sxCWk+sBPQ9gJSt2sIqfx/61F8Lpu6WzP+ZOnMLTUn2wLU/d1FN85HXmnQALzTCC -DGUGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIbEcBfddWPv+AggxAAOAt/kCXiffe -GeJG0P2K9Q18XZS6Rz7Xcz+Kp2PVgqHKRpPjjmB2ufsRO0pM4z/qkHTOdpfacB4h -gz912D9U04hC8mt0fqGNTvRNAFVFLsmo7KXc/a8vfZNrGWEnYn7y1WfP52pqA/Ei -SNFf0NVtMyqg5Gx+hZ/NpWAE5vcmRRdoYyWeg13lhlW96QUxf/W7vY/D5KpAGACI -ok79/XI4eJkbq3Dps0oO/difNcvdkE74EU/GPuL68yR0CdzzafbLxzV+B43TBRgP -jH1hCdRqaspjAaZL5LGfp1QUM8HZIKHuTze/+4dWzS1XR3/ix9q/2QFI7YCuXpuE -un3AFYXE4QX/6kcPklZwh9FqjSie3I5HtC1vczqYVjqT4oHrs8ktkZ7oAzeXaXTF -k6+JQNNa/IyJw24I1MR77q7HlHSSfhXX5cFjVCd/+SiA4HJQjJgeIuXZ+dXmSPdL -9xLbDbtppifFyNaXdlSzcsvepKy0WLF49RmbL7Bnd46ce/gdQ6Midwi2MTnUtapu -tHmu/iJtaUpwXXC0B93PHfAk7Y3SgeY4tl/gKzn9/x5SPAcHiNRtOsNBU8ZThzos -Wh41xMLZavmX8Yfm/XWtl4eU6xfhcRAbJQx7E1ymGEt7xGqyPV7hjqhoB9i3oR5N -itxHgf1+jw/cr7hob+Trd1hFqZO6ePMyWpqUg97G2ThJvWx6cv+KRtTlVA6/r/UH -gRGBArJKBlLpXO6dAHFztT3Y6DFThrus4RItcfA8rltfQcRm8d0nPb4lCa5kRbCx -iudq3djWtTIe64sfk8jsc6ahWYSovM+NmhbpxEUbZVWLVEcHAYOeMbKgXSu5sxNO -JZNeFdzZqDRRY9fGjYNS7DdNOmrMmWKH+KXuMCItpNZsZS/3W7QxAo3ugYLdUylU -Zg8H/BjUGZCGn1rEBAuQX78m0SZ1xHlgHSwJIOmxOJUDHLPHtThfbELY9ec14yi5 -so1aQwhhfhPvF+xuXBrVeTAfhFNYkf2uxcEp7+tgFAc5W0QfT9SBn5vSvIxv+dT4 -7B2Pg1l/zjdsM74g58lmRJeDoz4psAq+Uk7n3ImBhIku9qX632Q1hanjC8D4xM4W -sI/W0ADCuAbY7LmwMpAMdrGg//SJUnBftlom7C9VA3EVf8Eo+OZH9hze+gIgUq+E -iEUL5M4vOHK2ttsYrSkAt8MZzjQiTlDr1yzcg8fDIrqEAi5arjTPz0n2s0NFptNW -lRD+Xz6pCXrnRgR8YSWpxvq3EWSJbZkSEk/eOmah22sFnnBZpDqn9+UArAznXrRi -nYK9w38aMGPKM39ymG8kcbY7jmDZlRgGs2ab0Fdj1jl3CRo5IUatkOJwCEMd/tkB -eXLQ8hspJhpFnVNReX0oithVZir+j36epk9Yn8d1l+YlKmuynjunKl9fhmoq5Q6i -DFzdYpqBV+x9nVhnmPfGyrOkXvGL0X6vmXAEif/4JoOW4IZpyXjgn+VoCJUoae5J -Djl45Bcc2Phrn4HW4Gg/+pIwTFqqZZ2jFrznNdgeIxTGjBrVsyJUeO3BHI0mVLaq -jtjhTshYCI7mXOis9W3ic0RwE8rgdDXOYKHhLVw9c4094P/43utSVXE7UzbEhhLE -Ngb4H5UGrQmPTNbq40tMUMUCej3zIKuVOvamzeE0IwLhkjNrvKhCG1EUhX4uoJKu -DQ++3KVIVeYSv3+78Jfw9F3usAXxX1ICU74/La5DUNjU7DVodLDvCAy5y1jxP3Ic -If6m7aBYVjFSQAcD8PZPeIEl9W4ZnbwyBfSDd11P2a8JcZ7N99GiiH3yS1QgJnAO -g9XAgjT4Gcn7k4lHPHLULgijfiDSvt94Ga4/hse0F0akeZslVN/bygyib7x7Lzmq -JkepRianrvKHbatuxvcajt/d+dxCnr32Q1qCEc5fcgDsjvviRL2tKR0qhuYjn1zR -Vk/fRtYOmlaGBVzUXcjLRAg3gC9+Gy8KvXIDrnHxD+9Ob+DUP9fgbKqMeOzKcCK8 -NSfSQ+tQjBYD5Ku4zAPUQJoRGgx43vXzcl2Z2i3E2otpoH82Kx8S9WlVEUlTtBjQ -QIGM5aR0QUNt8z34t2KWRA8SpP54VzBmEPdwLnzna+PkrGKsKiHVn4K+HfjDp1uW -xyO8VjrolAOYosTPXMpNp2u/FoFxaAPTa/TvmKc0kQ3ED9/sGLS2twDnEccvHP+9 -zzrnzzN3T2CWuXveDpuyuAty3EoAid1nuC86WakSaAZoa8H2QoRgsrkkBCq+K/yl -4FO9wuP+ksZoVq3mEDQ9qv6H4JJEWurfkws3OqrA5gENcLmSUkZie4oqAxeOD4Hh -Zx4ckG5egQYr0PnOd2r7ZbIizv3MKT4RBrfOzrE6cvm9bJEzNWXdDyIxZ/kuoLA6 -zX7gGLdGhg7dqzKqnGtopLAsyM1b/utRtWxOTGO9K9lRxyX82oCVT9Yw0DwwA+cH -Gutg1w7JHrIAYEtY0ezHgxhqMGuuTyJMX9Vr0D+9DdMeBK7hVOeSnxkaQ0f9HvF6 -0XI/2OTIoBSCBpUXjpgsYt7m7n2rFJGJmtqgLAosCAkacHnHLwX0EnzBw3sdDU6Q -jFXUWIDd5xUsNkFDCbspLMFs22hjNI6f/GREwd23Q4ujF8pUIcxcfbs2myjbK45s -tsn/jrkxmKRgwCIeN/H7CM+4GXSkEGLWbiGCxWzWt9wW1F4M7NW9nho3D1Pi2LBL -1ByTmjfo/9u9haWrp53enDLJJbcaslfe+zvo3J70Nnzu3m3oJ3dmUxgJIstG10g3 -lhpUm1ynvx04IFkYJ3kr/QHG/xGS+yh/pMZlwcUSpjEgYFmjFHU4A1Ng4LGI4lnw -5wisay4J884xmDgGfK0sdVQyW5rExIg63yYXp2GskRdDdwvWlFUzPzGgCNXQU96A -ljZfjs2u4IiVCC3uVsNbGqCeSdAl9HC5xKuPNbw5yTxPkeRL1ouSdkBy7rvdFaFf -dMPw6sBRNW8ZFInlgOncR3+xT/rZxru87LCq+3hRN3kw3hvFldrW2QzZSksO759b -pJEP+4fxuG96Wq25fRmzHzE0bdJ+2qF3fp/hy4oRi+eVPa0vHdtkymE4OUFWftb6 -+P++JVOzZ4ZxYA8zyUoJb0YCaxL+Jp/QqiUiH8WZVmYZmswqR48sUUKr7TIvpNbY -6jEH6F7KiZCoWfKH12tUC69iRYx3UT/4Bmsgi3S4yUxfieYRMIwihtpP4i0O+OjB -/DPbb13qj8ZSfXJ+jmF2SRFfFG+2T7NJqm09JvT9UcslVd+vpUySNe9UAlpcvNGZ -2+j180ZU7YAgpwdVwdvqiJxkeVtAsIeqAvIXMFm1PDe7FJB0BiSVZdihB6cjnKBI -dv7Lc1tI2sQe7QSfk+gtionLrEnto+aXF5uVM5LMKi3gLElz7oXEIhn54OeEciB1 -cEmyX3Kb4HMRDMHyJxqJXwxm88RgC6RekoPvstu+AfX/NgSpRj5beaj9XkweJT3H -rKWhkjq4Ghsn1LoodxluMMHd61m47JyoqIP9PBKoW+Na0VUKIVHw9e9YeW0nY1Zi -5qFA/pHPAt9AbEilRay6NEm8P7TTlNo216amc8byPXanoNrqBYZQHhZ93A4yl6jy -RdpYskMivT+Sh1nhZAioKqqTZ3HiFR8hFGspAt5gJc4WLYevmxSicGa6AMyhrkvG -rvOSdjY6JY/NkxtcgeycBX5MLF7uDbhUeqittvmlcrVN6+V+2HIbCCrvtow9pcX9 -EkaaNttj5M0RzjQxogCG+S5TkhCy04YvKIkaGJFi8xO3icdlxgOrKD8lhtbf4UpR -cDuytl70JD95mSUWL53UYjeRf9OsLRJMHQOpS02japkMwCb/ngMCQuUXA8hGkBZL -Xw7RwwPuM1Lx8edMXn5C0E8UK5e0QmI/dVIl2aglXk2oBMBJbnyrbfUPm462SG6u -ke4gQKFmVy2rKICqSkh2DMr0NzeYEUjZ6KbmQcV7sKiFxQ0/ROk8eqkYYxGWUWJv -ylPF1OTLH0AIbGlFPLQO4lMPh05yznZTac4tmowADSHY9RCxad1BjBeine2pj48D -u36OnnuQIsedxt5YC+h1bs+mIvwMVsnMLidse38M/RayCDitEBvL0KeG3vWYzaAL -h0FCZGOW0ilVk8tTF5+XWtsQEp1PpclvkcBMkU3DtBUnlmPSKNfJT0iRr2T0sVW1 -h+249Wj0Bw== + MIIOgwYJKoZIhvcNAQcDoIIOdDCCDnACAQIxggEwMIIBLAIBAoAUZcG9X+5aK8VZ +FY8eJV9j+RImq58wDQYJKoZIhvcNAQEBBQAEggEAn/hOytP/StyRuXHcqFq6x+Za +7gHfO8prXWdZW4e28NLt/x5ZOBHDDZ6buwwdXEZME0+RoiJvLqP2RNhZkEO8bkna +pS76xLZE4NXyfxkeEs1vJYis0WJdt/56uCzBuud2SBLuMWoAWgF5alokN0uFpVgm +CKCos+xv6Pisolc6geM8xQTYe6sLf5Z23LWftWfJqzuo/29glCCre7R80OLeZe5w +pN6XztbYz06nhVByC35To8Lm0akWAAKU7sfqM1Nty4P0rwUJPKXo42uN1GKYbDbF +x8piCAd+rs+q4Alu3qK/YaTPpMb2ECRMH6CYB8Klf/CbuWykkfS8zrsnpXT1kzCC +DTUGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQInjJWFaJcZz2Agg0QX6NlJUH17o20 +90gfjWV01mPmzLKx71JT+hyzKr5vHywDSRI/mdb3RqA59ZrIKeyWr0HXEOuABlul +nxjc/Rfk1tiLQwh0iqlOjlMtRsxS6yDA0WNwK2Y9gaXcdgDm5Vioai18l4Pd0qzK +fsof5a/jEJyunW1CZK19QciwfQ2pS8QbRYgeLRZRft2I+kv6cWXlGS6YrMqKQC8t +QMxnXR4AuzVllPLbbIgtM3l9oS+6jl7jKyKogeroJ9FNLjoMBJLldRLGPRhkCmdJ +Z1m+s/BAVUH08qgj2kmHzucdULLjlRcmma9m/h91TcQCXHAavf7S+U9QwIyGRh83 +t4Y7EqbQ93mOgjajFzILSL7AT/irgJpDu6CJqMu3EMNDA0mjxn5Cdvj40sufL/g3 +UyBwqosmIwAPzNDmhPtTKvHaHfGY/k8WhoIYfAA5Lhq1z22/RODZOY0Ch2XyxQM4 +s35eppe6IhnwyMv6HfrCrqE/o/16OrvvbaFQTeTlMvU0P7MIR4pVW6tRq4NEa5Wx +JcvGutuMuzH1VMcqcKdc7wYyOqDOGU43kcV8PiALTIcqrhD8NDrKks1jSkyqQw2h +sJQckNaQIcCXkUQecQa2UGe0l4HDSJ5gAETSenjyLBKFHf3hxiWBpw446/bODgdk +0+oyreZqMpRz3vn9LC+Yt7SuVbTzRdx7nlKIiNvo8+btOuVm44evchLFHAq3Ni8D +c+tP/wss3K4Xdp+t5SvEY/nLIu11Lw44HDVMYTuNz3Ya9psL70ZLaLZM8NromnEl +CUMRNTPoOC7/KDRh2E9d6c1V4CC43wAsRhksGJnSYoiSVAhaVgLqVFQTsqNHxmcg +3Y9AEBVzm3fZg6+DxAYu+amb+r8lk0Pp+N1t6rVbKXhkbAAxg0UDO3pY8Xcz0Y3g +Qdd5rnHh1rJrehku7zTHvQaXEddUWmCoUGIXJ+bt4VOhErL6s5/j8GSG0xmfxgSE +jnGj4Jwd0Vv19uZjsBDQ54R88GcA9YX8r48gr9JAwplrQ50m9KX6GwQhDRYKN/Dh +zOt9DCUkqMqdi5T4v2qNTfkL7iXBMhsSkeYUQ/tFLyv4QQyli5uTUZ5FNXohOVAx +TNyV9+gcV5WiBR0Aje6rwPW3oTkrPnVfZCdBwt/mZjPNMO5Se7D/lWE33yYu7bJ+ +gaxRNynhEOB7RaOePzDjn7LExahFmTFV0sgQxwQ2BYsfI22cdkAf6qOxdK/kqiQm +lgzRpDjyPIFhaCCHnXyJdSqcHmDrCjcg2P6AVCDJGdFOBvupeJ7Kg7WV5EY7G6AU +ng16tyumJSMWSzSks9M0Ikop6xhq3cV+Q0OArJoreQ6eonezXjM9Y865xjF80nJL +V4lcRxdXfoKpXJwzc++pgkY9t55J0+cEyBvIXfKud1/HHOhewhoy5ATyi9LLM91n +iW1DaQXlvHZgE7GFMSCVLxy6ZopBbm9tF0NQDFi8zUtGulD3Gkoc/Bp+DWb2vsX4 +S8W9vByNvIz/SWOGNbEs2irTRXccMAL7JHJ+74bwZZi5DRrqyQWHCn/3Ls2YPI6z +lnfl15EE4G7g3+nrvP2lZFBXjsdG/U3HYi+tAyHkRN3oXvgnt9N76PoY8dlsNf6c +RuNqgk31uO1sX/8du3Jxz87MlzWiG3kbAHMvbcoCgy/dW4JQcM3Sqg5PmF8i9wD1 +ZuqZ7zHpWILIWd13TM3UDolQZzl+GXEX62dPPL1vBtxHhDgQicdaWFXa6DX3dVwt +DToWaAqrAPIrgxvNk5FHNCTEVTQkmCIL5JoinZSk7BAl8b085CPM6F7OjB5CR4Ts +V+6UaTUZqk+z+raL+HJNW2ds1r7+t8Po5CydMBS4M/pE7b/laUnbRu7rO8cqKucn +n+eYimib/0YuqZj9u2RXso4kzdOyIxGSGHkmSzYuoNRx80r+jHtcBBTqXk37t0FY +X5O7QItCE+uwV1Sa12yg2dgJ6vKRPCEVyMoYUBwNbKEcw1pjG9Em7HwjOZK0UrO1 +yKRz6kxffVKN9Naf7lOnXooVuedY/jcaZ2zCZtASlOe8iiQK5prM4sbMixMp9ovL +tTxy9E9kgvaI/mkzarloKPQGsk0WzuH+i39M3DOXrMf5HwfE+A55u1gnrHsxQlxp +z5acwN42+4ln6axs4aweMGAhyEtBW8TdsNomwuPk+tpqZXHI2pqS4/aVOk8R8VE7 +IqtBx2QBMINT79PDPOn3K6v9HEt9fUHJ2TWJvKRKfsu5lECJPJSJA8OQ7zzw6zQt +NXw8UhZRmNW0+eI5dykg+XsII7+njYa33EJ1Sy1Ni8ZT/izKfrKCwEm44KVAyUG5 +qUjghPPMNQY3D0qOl54DRfGVOxbHztUooblW+DnlLlpOy/+/B+H9Dscxosdx2/Mo +RftJOMlLqK7AYIYAlw1zvqZo0pf7rCcLSLt+6FrPtNZe6ULFUacZ3RqyTZovsZi5 +Ucda3bLdOHX6tKL21bRfN7L0/BjF6BJETpG3p+rBYOyCwO6HvdenpMm6cT02nrfP +QJtImjeW1ov6Pw02zNlIZAXFir78Z6AcMhV2iKEJxc1RMFBcXmylNXJmGlKYB3lJ +jWo6qumLewTz5vzRu0vZCmOf+bKmuyVxckPbrzP+4OHKhpm95Kp6sUn2pvh0S8H3 +w1pjfZ9+sIaVgMspfRPgoWTyZ0flFvAX6DHWYVejMebwfAqZaa+UAJJ6jWQbMNzo +ZtOhzCjV+2ZBYHvSiY7dtfaLwQJeMWEKIw32kEYv/Ts33n7dD/pAzZu0WCyfoqsQ +MEXhbZYSCQTJ8/gqvdlurWOJL091z6Uw810YVt+wMqsBo5lnMsS3GqkzgM2PVzuV +taddovr5CrWfAjQaFG8wcETiKEQFWS9JctKo0F+gwLwkVyc4fBSkjVmIliw1jXGu +Enf2mBei+n8EaRB2nNa/CBVGQM24WEeMNq+TqaMvnEonvMtCIEpuJAO/NzJ1pxw0 +9S+LKq3lFoIQoON5glsjV82WseAbFXmynBmSbyUY/mZQpjuNSnwLfpz4630x5vuV +VNglsZ8lW9XtSPh6GkMj+lLOCqJ5aZ4UEXDSYW7IaH4sPuQ4eAAUsKx/XlbmaOad +hgK+3gHYi98fiGGQjt9OqKzQRxVFnHtoSwbMp/gjAWqjDCFdo7RkCqFjfB1DsSj0 +TrjZU1lVMrmdEhtUNjqfRpWN82f55fxZdrHEPUQIrOywdbRiNbONwm4AfSE8ViPz ++SltYpQfF6g+tfZMwsoPSevLjdcmb1k3n8/lsEL99wpMT3NbibaXCjeJCZbAYK05 +rUw5bFTVAuv6i3Bax3rx5DqyQANS3S8TBVYrdXf9x7RpQ8oeb4oo+qn293bP4n5m +nW/D/yvsAJYcm3lD7oW7D369nV/mwKPpNC4B9q6N1FiUndvdFSbyzfNfSF9LV0RU +A/4Qm05HtE3PAUFYfwwP8MDg0HdltMn83VfqrEi/d76xlcxfoIh2RQQgqxCIS6KE +AExIY/hPYDVxApznI39xNOp7IqdPEX3i7Cv7aHeFAwbhXYMNnkfFJJTkHRdcRiJ/ +RE1QPlC7ijH+IF02PE/seYg4GWrkeW3jvi+IKQ9BPBoYIx0P+7wHXf4ZGtZMourd +N4fdwzFCDMFkS7wQC/GOqZltzF/gz1fWEGXRTH3Lqx0iKyiiLs2trQhFOzNw3B7E +WxCIUjRMAAJ6vvUdvoFlMw8WfBkzCVple4yrCqIw6fJEq8v0q8EQ7qKDTfyPnFBt +CtQZuTozfdPDnVHGmGPQKUODH/6Vwl+9/l7HDvV8/D/HKDnP581ix1a3bdokNtSK +7rBfovpzYltYGpVxsC6MZByYEpvIh5nHQouLR4L3Je2wB3F9nBGjNhBvGDQlxcne +AAgywpOpQfvfsnYRWt2vlQzwhHUgWhJmGMhGMmn4oKc5su87G7yzFEnq/yIUMOm/ +X0Zof/Qm92KCJS7YkLzP1GDO9XPMe+ZHeHVNXhVNCRxGNbHCHB9+g9v090sLLmal +jpgrDks19uHv0yYiMqBdpstzxClRWxgHwrZO6jtbr5jeJuLVUxV0uuX76oeomUj2 +mAwoD5cB1U8W9Ew+cMjp5v6gg0LTk90HftjhrZmMA0Ll6TqFWjxge+jsswOY1SZi +peuQGIHFcuQ7SEcyIbqju3bmeEGZwTz51yo8x2WqpCwB1a4UTngWJgDCySAI58fM +eRL6r478CAZjk+fu9ZA85B7tFczl3lj0B4QHxkX370ZeCHy39qw8vMYIcPk3ytI0 +vmj5UCSeQDHHDcwo54wi83IFEWUFh18gP4ty5Tfvs6qv7qd455UQZTAO7lwpdBlp +MJGlMqBHjDLGyY80p+O4vdlQBZ1uMH+48u91mokUP8p+tVVKh7bAw/HPG+SQsuNR +DXF+gTm/hRuY7IYe3C7Myzc8bDTtFw6Es9BLAqzFFAMjzDVz7wY1rnZQq4mmLcKg +AAMJaqItipKAroYIntXXJ3U8fsUt03M= diff --git a/tests/data/wire/ext_conf-no_gs_metadata.xml b/tests/data/wire/ext_conf-no_gs_metadata.xml index a97384ef14..605e484254 100644 --- a/tests/data/wire/ext_conf-no_gs_metadata.xml +++ b/tests/data/wire/ext_conf-no_gs_metadata.xml @@ -19,7 +19,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf.xml b/tests/data/wire/ext_conf.xml index 8706278fa3..54d785159f 100644 --- a/tests/data/wire/ext_conf.xml +++ b/tests/data/wire/ext_conf.xml @@ -19,7 +19,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_additional_locations.xml b/tests/data/wire/ext_conf_additional_locations.xml index 9ba6381915..8f5e746b06 100644 --- a/tests/data/wire/ext_conf_additional_locations.xml +++ b/tests/data/wire/ext_conf_additional_locations.xml @@ -24,7 +24,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_autoupgrade.xml b/tests/data/wire/ext_conf_autoupgrade.xml index 29ea034ef8..77a201ad9c 100644 --- a/tests/data/wire/ext_conf_autoupgrade.xml +++ b/tests/data/wire/ext_conf_autoupgrade.xml @@ -21,7 +21,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_autoupgrade_internalversion.xml b/tests/data/wire/ext_conf_autoupgrade_internalversion.xml index f62563b658..44cad87819 100644 --- a/tests/data/wire/ext_conf_autoupgrade_internalversion.xml +++ b/tests/data/wire/ext_conf_autoupgrade_internalversion.xml @@ -21,7 +21,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_dependencies_with_empty_settings.xml b/tests/data/wire/ext_conf_dependencies_with_empty_settings.xml index fb9a50ccdc..b26395ec23 100644 --- a/tests/data/wire/ext_conf_dependencies_with_empty_settings.xml +++ b/tests/data/wire/ext_conf_dependencies_with_empty_settings.xml @@ -25,7 +25,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_in_vm_artifacts_profile.xml b/tests/data/wire/ext_conf_in_vm_artifacts_profile.xml index fea39e7164..a1af74f784 100644 --- a/tests/data/wire/ext_conf_in_vm_artifacts_profile.xml +++ b/tests/data/wire/ext_conf_in_vm_artifacts_profile.xml @@ -20,7 +20,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_in_vm_empty_artifacts_profile.xml b/tests/data/wire/ext_conf_in_vm_empty_artifacts_profile.xml index fc741fa4a3..cd5bb3d3e9 100644 --- a/tests/data/wire/ext_conf_in_vm_empty_artifacts_profile.xml +++ b/tests/data/wire/ext_conf_in_vm_empty_artifacts_profile.xml @@ -20,7 +20,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_in_vm_metadata.xml b/tests/data/wire/ext_conf_in_vm_metadata.xml index 9724927e49..9a4f89cb81 100644 --- a/tests/data/wire/ext_conf_in_vm_metadata.xml +++ b/tests/data/wire/ext_conf_in_vm_metadata.xml @@ -21,7 +21,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_internalversion.xml b/tests/data/wire/ext_conf_internalversion.xml index f62563b658..44cad87819 100644 --- a/tests/data/wire/ext_conf_internalversion.xml +++ b/tests/data/wire/ext_conf_internalversion.xml @@ -21,7 +21,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_invalid_and_valid_handlers.xml b/tests/data/wire/ext_conf_invalid_and_valid_handlers.xml index cd175e746e..f9c95d694a 100644 --- a/tests/data/wire/ext_conf_invalid_and_valid_handlers.xml +++ b/tests/data/wire/ext_conf_invalid_and_valid_handlers.xml @@ -22,11 +22,11 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_invalid_vm_metadata.xml b/tests/data/wire/ext_conf_invalid_vm_metadata.xml index 081e734e8b..7c766220e5 100644 --- a/tests/data/wire/ext_conf_invalid_vm_metadata.xml +++ b/tests/data/wire/ext_conf_invalid_vm_metadata.xml @@ -21,7 +21,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_missing_requested_version.xml b/tests/data/wire/ext_conf_missing_requested_version.xml index 3f2b59df25..84043e2d75 100644 --- a/tests/data/wire/ext_conf_missing_requested_version.xml +++ b/tests/data/wire/ext_conf_missing_requested_version.xml @@ -27,7 +27,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} diff --git a/tests/data/wire/ext_conf_multiple_extensions.xml b/tests/data/wire/ext_conf_multiple_extensions.xml index 5cd65c63e0..5845a179f5 100644 --- a/tests/data/wire/ext_conf_multiple_extensions.xml +++ b/tests/data/wire/ext_conf_multiple_extensions.xml @@ -25,22 +25,22 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIIB4AYJKoZIhvcNAQcDoIIB0TCCAc0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEH3vWjYIrceWQigVQwoS8z0wDQYJKoZIhvcNAQEBBQAEggEANYey5W0qDqC6RHZlVnpLp2dWrMr1Rt5TCFkOjq1jU4y2y1FPtsTTKq9Z5pdGb/IHQo9VcT+OFglO3bChMbqc1vgmk4wkTQkgJVD3C8Rq4nv3uvQIux+g8zsa1MPKT5fTwG/dcrBp9xqySJLexUiuJljmNJgorGc0KtLwjnad4HTSKudDSo5DGskSDLxxLZYx0VVtQvgekOOwT/0C0pN4+JS/766jdUAnHR3oOuD5Dx7/c6EhFSoiYXMA0bUzH7VZeF8j/rkP1xscLQRrCScCNV2Ox424Y4RBbcbP/p69lDxGURcIKLKrIUhQdC8CfUMkQUEmFDLcOtxutCTFBZYMJzBbBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECCuc0a4Gl8PAgDgcHekee/CivSTCXntJiCrltUDob8cX4YtIS6lq3H08Ar+2tKkpg5e3bOkdAo3q2GfIrGDm4MtVWw==","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIIB4AYJKoZIhvcNAQcDoIIB0TCCAc0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEH3vWjYIrceWQigVQwoS8z0wDQYJKoZIhvcNAQEBBQAEggEANYey5W0qDqC6RHZlVnpLp2dWrMr1Rt5TCFkOjq1jU4y2y1FPtsTTKq9Z5pdGb/IHQo9VcT+OFglO3bChMbqc1vgmk4wkTQkgJVD3C8Rq4nv3uvQIux+g8zsa1MPKT5fTwG/dcrBp9xqySJLexUiuJljmNJgorGc0KtLwjnad4HTSKudDSo5DGskSDLxxLZYx0VVtQvgekOOwT/0C0pN4+JS/766jdUAnHR3oOuD5Dx7/c6EhFSoiYXMA0bUzH7VZeF8j/rkP1xscLQRrCScCNV2Ox424Y4RBbcbP/p69lDxGURcIKLKrIUhQdC8CfUMkQUEmFDLcOtxutCTFBZYMJzBbBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECCuc0a4Gl8PAgDgcHekee/CivSTCXntJiCrltUDob8cX4YtIS6lq3H08Ar+2tKkpg5e3bOkdAo3q2GfIrGDm4MtVWw==","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIIBwAYJKoZIhvcNAQcDoIIBsTCCAa0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEH3vWjYIrceWQigVQwoS8z0wDQYJKoZIhvcNAQEBBQAEggEABILhQPoMx3NEbd/sS0xAAE4rJXwzJSE0bWr4OaKpcGS4ePtaNW8XWm+psYR9CBlXuGCuDVlFEdPmO2Ai8NX8TvT7RVYYc6yVQKpNQqO6Q9g9O52XXX4tBSFSCfoTzd1kbGC1c2wbXDyeROGCjraWuGHd4C9s9gytpgAlYicZjOqV3deo30F4vXZ+ZhCNpMkOvSXcsNpzTzQ/mskwNubN8MPkg/jEAzTHRpiJl3tjGtTqm00GHMqFF8/31jnoLQeQnWSmY+FBpiTUhPzyjufIcoZ+ueGXZiJ77xyH2Rghh5wvQM8oTVy2dwFQGeqjHOVgdgRNi/HgfZhcdltaQ8kjYDA7BgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECHPM0ZKBn+aWgBiVPT7zlkJA8eGuH7bNMTQCtGoJezToa24=","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIIBwAYJKoZIhvcNAQcDoIIBsTCCAa0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEH3vWjYIrceWQigVQwoS8z0wDQYJKoZIhvcNAQEBBQAEggEABILhQPoMx3NEbd/sS0xAAE4rJXwzJSE0bWr4OaKpcGS4ePtaNW8XWm+psYR9CBlXuGCuDVlFEdPmO2Ai8NX8TvT7RVYYc6yVQKpNQqO6Q9g9O52XXX4tBSFSCfoTzd1kbGC1c2wbXDyeROGCjraWuGHd4C9s9gytpgAlYicZjOqV3deo30F4vXZ+ZhCNpMkOvSXcsNpzTzQ/mskwNubN8MPkg/jEAzTHRpiJl3tjGtTqm00GHMqFF8/31jnoLQeQnWSmY+FBpiTUhPzyjufIcoZ+ueGXZiJ77xyH2Rghh5wvQM8oTVy2dwFQGeqjHOVgdgRNi/HgfZhcdltaQ8kjYDA7BgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECHPM0ZKBn+aWgBiVPT7zlkJA8eGuH7bNMTQCtGoJezToa24=","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIIB4AYJKoZIhvcNAQcDoIIB0TCCAc0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEH3vWjYIrceWQigVQwoS8z0wDQYJKoZIhvcNAQEBBQAEggEAGSKUDRN64DIB7FS7yKXa07OXaFPhmdNnNDOAOD3/WVFb9fQ2bztV46waq7iRO+lpz7LSerRzIe6Kod9zCfK7ryukRomVHIfTIBwPjQ+Otn8ZD2aVcrxR0EI95x/SGyiESJRQnOMbpoVSWSu2KJUCPfycQ4ODbaazDc61k0JCmmRy12rQ4ttyWKhYwpwI2OYFHGr39N/YYq6H8skHj5ve1605i4P9XpfEyIwF5BbX59tDOAFFQtX7jzQcz//LtaHHjwLmysmD9OG5XyvfbBICwSYJfMX9Jh1aahLwcjL8Bd0vYyGL1ItMQF5KfDwog4+HLcRGx+S02Yngm3/YKS9DmzBbBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECFGLNfK0bO5OgDgH90bRzqfgKK6EEh52XJfHz9G/ZL1mqP/ueWqo95PtEFo1gvI7z25V/pT0tBGibXgRhQXLFmwVTA==","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIIB4AYJKoZIhvcNAQcDoIIB0TCCAc0CAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEH3vWjYIrceWQigVQwoS8z0wDQYJKoZIhvcNAQEBBQAEggEAGSKUDRN64DIB7FS7yKXa07OXaFPhmdNnNDOAOD3/WVFb9fQ2bztV46waq7iRO+lpz7LSerRzIe6Kod9zCfK7ryukRomVHIfTIBwPjQ+Otn8ZD2aVcrxR0EI95x/SGyiESJRQnOMbpoVSWSu2KJUCPfycQ4ODbaazDc61k0JCmmRy12rQ4ttyWKhYwpwI2OYFHGr39N/YYq6H8skHj5ve1605i4P9XpfEyIwF5BbX59tDOAFFQtX7jzQcz//LtaHHjwLmysmD9OG5XyvfbBICwSYJfMX9Jh1aahLwcjL8Bd0vYyGL1ItMQF5KfDwog4+HLcRGx+S02Yngm3/YKS9DmzBbBgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcECFGLNfK0bO5OgDgH90bRzqfgKK6EEh52XJfHz9G/ZL1mqP/ueWqo95PtEFo1gvI7z25V/pT0tBGibXgRhQXLFmwVTA==","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIIEzAYJKoZIhvcNAQcDoIIEvTCCBLkCAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEH3vWjYIrceWQigVQwoS8z0wDQYJKoZIhvcNAQEBBQAEggEAFqLDBFGeuglluYmZb0Zw+ZlMiMIws9/LgmurVSRUTU/nSleIc9vOLcukfMeCpMativzHe23iDFy6p3XDkViNcuzqbhlPq5LQsXXg+xaUrrg8Xy+q7KUQdxzPdNBdpgkUh6yE2EFbqVLQ/7x+TkkSsw35uPT0nEqSj3yYFGH7X/NJ49fKU+ZvFDp/N+o54UbE6ZdxlHFtz6NJFxx5w4z5adQ8DgnUyS0bJ2denolknODfSW2D2alm00SXlI88CAjeHgEDkoLCduwkrDkSFAODcAiEHHX8oYCnfanatpjm7ZgSutS9y7+XUnGWxDYoujHDI9bbV0WpyDcx/DIrlZ+WcTCCA0UGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIrL18Lbp1qU6AggMgGklvozqr8HqYP+DwkvxdwHSpo+23QFxh70os+NJRtVgBv5NjPEziXo3FpXHMPvt0kp0IwXbwyy5vwnjCTA2sQOYgj77X6RmwF6+1gt2DIHDN1Q6jWzdcXZVHykSiF3gshbebRKO0hydfCaCyYL36HOZ8ugyCctOon5EflrnoOYDDHRbsr30DAxZCAwGOGZEeoU2+U+YdhuMvplnMryD1f6b8FQ7jXihe/zczAibX5/22NxhsVgALdsV5h6hwuTbspDt3V15/VU8ak7a4xxdBfXOX0HcQI86oqsFr7S7zIveoQHsW+wzlyMjwi6DRPFpz2wFkv5ivgFEvtCzDQP4aCqGI8VdqzR7aUDnuqiSCe/cbmv5mSmTYlDPTR03WS0IvgyeoNAzqCbYQe44AUBEZb/yT8Z3XxwW0GzcPMZQ0XjpcZiaKAueN9V8nJgNCEDPTJqpSjy+tEHmSgxn70+E57F0vzPvdQ3vOEeRj8zlBblHd4uVrhxdBMUuQ73JEQEha5rz0qcUy04Wmjld1rBuX6pdOqrArAYzTLJbIuLqDjlnYFsHLs9QBGvIEb9VFOlAm5JW8npBbIRHXqPfwZWs60+uNksTtsN3MxBxUWJPOByb4xRNx+nRpTOvfKKFlgq1ReK5bGSTCB7x0Ft3+T42LOQDrBPyxxtGzWs+aq05qFgI4n0h8X82wxJflK+kUdwvvG/ZY5MM+/le2zOrUeyzvxXsHoRetgg+DOk7v+v7VsuT1KuvTXvgzxoOFF3/T2pNPpE3h6bbP2BUqZ2yzPNziGFslywDLZ8W3OUZoQejGqobRePdgUoBi5q2um/sPnq81kOJ/qhIOVq581ZD4IQWLot8eK8vX0G/y7y71YelRR51cUfgR5WvZZf6LvYw+GpwOtSViugl9QxGCviSLgHTJSSEm0ijtbzKhwP4vEyydNDrz8+WYB8DNIV7K2Pc8JyxAM03FYX30CaaJ40pbEUuVQVEnkAD2E//29/ZzgNTf/LBMzMEP5j7wlL+QQpmPAtL/FlBrOJ4nDEqsOOhWzI1MN51xRZuv3e2RqzVPiSmrKtk=","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIIEzAYJKoZIhvcNAQcDoIIEvTCCBLkCAQAxggFpMIIBZQIBADBNMDkxNzA1BgoJkiaJk/IsZAEZFidXaW5kb3dzIEF6dXJlIENSUCBDZXJ0aWZpY2F0ZSBHZW5lcmF0b3ICEH3vWjYIrceWQigVQwoS8z0wDQYJKoZIhvcNAQEBBQAEggEAFqLDBFGeuglluYmZb0Zw+ZlMiMIws9/LgmurVSRUTU/nSleIc9vOLcukfMeCpMativzHe23iDFy6p3XDkViNcuzqbhlPq5LQsXXg+xaUrrg8Xy+q7KUQdxzPdNBdpgkUh6yE2EFbqVLQ/7x+TkkSsw35uPT0nEqSj3yYFGH7X/NJ49fKU+ZvFDp/N+o54UbE6ZdxlHFtz6NJFxx5w4z5adQ8DgnUyS0bJ2denolknODfSW2D2alm00SXlI88CAjeHgEDkoLCduwkrDkSFAODcAiEHHX8oYCnfanatpjm7ZgSutS9y7+XUnGWxDYoujHDI9bbV0WpyDcx/DIrlZ+WcTCCA0UGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQIrL18Lbp1qU6AggMgGklvozqr8HqYP+DwkvxdwHSpo+23QFxh70os+NJRtVgBv5NjPEziXo3FpXHMPvt0kp0IwXbwyy5vwnjCTA2sQOYgj77X6RmwF6+1gt2DIHDN1Q6jWzdcXZVHykSiF3gshbebRKO0hydfCaCyYL36HOZ8ugyCctOon5EflrnoOYDDHRbsr30DAxZCAwGOGZEeoU2+U+YdhuMvplnMryD1f6b8FQ7jXihe/zczAibX5/22NxhsVgALdsV5h6hwuTbspDt3V15/VU8ak7a4xxdBfXOX0HcQI86oqsFr7S7zIveoQHsW+wzlyMjwi6DRPFpz2wFkv5ivgFEvtCzDQP4aCqGI8VdqzR7aUDnuqiSCe/cbmv5mSmTYlDPTR03WS0IvgyeoNAzqCbYQe44AUBEZb/yT8Z3XxwW0GzcPMZQ0XjpcZiaKAueN9V8nJgNCEDPTJqpSjy+tEHmSgxn70+E57F0vzPvdQ3vOEeRj8zlBblHd4uVrhxdBMUuQ73JEQEha5rz0qcUy04Wmjld1rBuX6pdOqrArAYzTLJbIuLqDjlnYFsHLs9QBGvIEb9VFOlAm5JW8npBbIRHXqPfwZWs60+uNksTtsN3MxBxUWJPOByb4xRNx+nRpTOvfKKFlgq1ReK5bGSTCB7x0Ft3+T42LOQDrBPyxxtGzWs+aq05qFgI4n0h8X82wxJflK+kUdwvvG/ZY5MM+/le2zOrUeyzvxXsHoRetgg+DOk7v+v7VsuT1KuvTXvgzxoOFF3/T2pNPpE3h6bbP2BUqZ2yzPNziGFslywDLZ8W3OUZoQejGqobRePdgUoBi5q2um/sPnq81kOJ/qhIOVq581ZD4IQWLot8eK8vX0G/y7y71YelRR51cUfgR5WvZZf6LvYw+GpwOtSViugl9QxGCviSLgHTJSSEm0ijtbzKhwP4vEyydNDrz8+WYB8DNIV7K2Pc8JyxAM03FYX30CaaJ40pbEUuVQVEnkAD2E//29/ZzgNTf/LBMzMEP5j7wlL+QQpmPAtL/FlBrOJ4nDEqsOOhWzI1MN51xRZuv3e2RqzVPiSmrKtk=","publicSettings":{"foo":"bar"}}}]} diff --git a/tests/data/wire/ext_conf_no_public.xml b/tests/data/wire/ext_conf_no_public.xml index 95619ae3b5..63e7013cc0 100644 --- a/tests/data/wire/ext_conf_no_public.xml +++ b/tests/data/wire/ext_conf_no_public.xml @@ -39,7 +39,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK"}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK"}}]} diff --git a/tests/data/wire/ext_conf_requested_version.xml b/tests/data/wire/ext_conf_requested_version.xml index 4d25cd522f..d12352c297 100644 --- a/tests/data/wire/ext_conf_requested_version.xml +++ b/tests/data/wire/ext_conf_requested_version.xml @@ -21,7 +21,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} diff --git a/tests/data/wire/ext_conf_required_features.xml b/tests/data/wire/ext_conf_required_features.xml index 69ed73a4a2..798ba5c52d 100644 --- a/tests/data/wire/ext_conf_required_features.xml +++ b/tests/data/wire/ext_conf_required_features.xml @@ -32,7 +32,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_sequencing.xml b/tests/data/wire/ext_conf_sequencing.xml index 6fa8451bfd..3c9a2ddd79 100644 --- a/tests/data/wire/ext_conf_sequencing.xml +++ b/tests/data/wire/ext_conf_sequencing.xml @@ -23,12 +23,12 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/ext_conf_settings_case_mismatch.xml b/tests/data/wire/ext_conf_settings_case_mismatch.xml index 86ed75779d..71286c5bf5 100644 --- a/tests/data/wire/ext_conf_settings_case_mismatch.xml +++ b/tests/data/wire/ext_conf_settings_case_mismatch.xml @@ -25,27 +25,27 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} diff --git a/tests/data/wire/ext_conf_upgradeguid.xml b/tests/data/wire/ext_conf_upgradeguid.xml index 6c11d6e577..2ec7147bb8 100644 --- a/tests/data/wire/ext_conf_upgradeguid.xml +++ b/tests/data/wire/ext_conf_upgradeguid.xml @@ -19,7 +19,7 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/invalid_config/ext_conf_multiple_depends_on_for_single_handler.xml b/tests/data/wire/invalid_config/ext_conf_multiple_depends_on_for_single_handler.xml index 88a2a05b76..8d76b732c9 100644 --- a/tests/data/wire/invalid_config/ext_conf_multiple_depends_on_for_single_handler.xml +++ b/tests/data/wire/invalid_config/ext_conf_multiple_depends_on_for_single_handler.xml @@ -28,16 +28,16 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/invalid_config/ext_conf_multiple_runtime_settings_same_plugin.xml b/tests/data/wire/invalid_config/ext_conf_multiple_runtime_settings_same_plugin.xml index 8ce90371f6..43e1e02819 100644 --- a/tests/data/wire/invalid_config/ext_conf_multiple_runtime_settings_same_plugin.xml +++ b/tests/data/wire/invalid_config/ext_conf_multiple_runtime_settings_same_plugin.xml @@ -21,8 +21,8 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/invalid_config/ext_conf_multiple_settings_for_same_handler.xml b/tests/data/wire/invalid_config/ext_conf_multiple_settings_for_same_handler.xml index 81ef176df0..7351c8bf56 100644 --- a/tests/data/wire/invalid_config/ext_conf_multiple_settings_for_same_handler.xml +++ b/tests/data/wire/invalid_config/ext_conf_multiple_settings_for_same_handler.xml @@ -21,10 +21,10 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/invalid_config/ext_conf_plugin_settings_version_mismatch.xml b/tests/data/wire/invalid_config/ext_conf_plugin_settings_version_mismatch.xml index cba2fe5c3b..dcf1014641 100644 --- a/tests/data/wire/invalid_config/ext_conf_plugin_settings_version_mismatch.xml +++ b/tests/data/wire/invalid_config/ext_conf_plugin_settings_version_mismatch.xml @@ -19,10 +19,10 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/invalid_config/ext_conf_single_and_multi_config_settings_same_plugin.xml b/tests/data/wire/invalid_config/ext_conf_single_and_multi_config_settings_same_plugin.xml index 4362c37d24..8a30ddbaf2 100644 --- a/tests/data/wire/invalid_config/ext_conf_single_and_multi_config_settings_same_plugin.xml +++ b/tests/data/wire/invalid_config/ext_conf_single_and_multi_config_settings_same_plugin.xml @@ -21,8 +21,8 @@ - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} - {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} + {"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F","protectedSettings":"MIICWgYJK","publicSettings":{"foo":"bar"}}}]} https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo diff --git a/tests/data/wire/trans_cert b/tests/data/wire/trans_cert index d560ae253b..35793e019f 100644 --- a/tests/data/wire/trans_cert +++ b/tests/data/wire/trans_cert @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDBzCCAe+gAwIBAgIJANujJuVt5eC8MA0GCSqGSIb3DQEBCwUAMBkxFzAVBgNV -BAMMDkxpbnV4VHJhbnNwb3J0MCAXDTE0MTAyNDA3MjgwN1oYDzIxMDQwNzEyMDcy -ODA3WjAZMRcwFQYDVQQDDA5MaW51eFRyYW5zcG9ydDCCASIwDQYJKoZIhvcNAQEB -BQADggEPADCCAQoCggEBANPcJAkd6V5NeogSKjIeTXOWC5xzKTyuJPt4YZMVSosU -0lI6a0wHp+g2fP22zrVswW+QJz6AVWojIEqLQup3WyCXZTv8RUblHnIjkvX/+J/G -aLmz0G5JzZIpELL2C8IfQLH2IiPlK9LOQH00W74WFcK3QqcJ6Kw8GcVaeSXT1r7X -QcGMqEjcWJkpKLoMJv3LMufE+JMdbXDUGY+Ps7Zicu8KXvBPaKVsc6H2jrqBS8et -jXbzLyrezTUDz45rmyRJzCO5Sk2pohuYg73wUykAUPVxd7L8WnSyqz1v4zrObqnw -BAyor67JR/hjTBfjFOvd8qFGonfiv2Vnz9XsYFTZsXECAwEAAaNQME4wHQYDVR0O -BBYEFL8i/sehpGV6IEDX7F0WQHQ/ZXOyMB8GA1UdIwQYMBaAFL8i/sehpGV6IEDX -7F0WQHQ/ZXOyMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAMPLrimT -Gptu5pLRHPT8OFRN+skNSkepYaUaJuq6cSKxLumSYkD8++rohu+1+a7t1YNjjNSJ -8ohRAynRJ7aRqwBmyX2OPLRpOfyRZwR0rcFfAMORm/jOE6WBdqgYD2L2b+tZplGt -/QqgQzebaekXh/032FK4c74Zg5r3R3tfNSUMG6nLauWzYHbQ5SCdkuQwV0ehGqh5 -VF1AOdmz4CC2237BNznDFQhkeU0LrqqAoE/hv5ih7klJKZdS88rOYEnVJsFFJb0g -qaycXjOm5Khgl4hKrd+DBD/qj4IVVzsmdpFli72k6WLBHGOXusUGo/3isci2iAIt -DsfY6XGSEIhZnA4= +MIIDEzCCAfugAwIBAgIUDcHXiRT74wOkLZYnyoZibT9+2G8wDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOTGludXhUcmFuc3BvcnQwHhcNMjIwODEyMTgzMTM5WhcN +MjQwODExMTgzMTM5WjAZMRcwFQYDVQQDDA5MaW51eFRyYW5zcG9ydDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAK/XWh+Djc2WYoJ/8FkZd8OV3V47fID5 +WV8hSBz/i/hVUKHhCWTQfE4VcQBGYFyK8lMKIBV7t6Bq05TQGuB8148HSjIboDx3 +Ndd0C/+lYcBE1izMrHKZYhcy7lSlEUk+y5iye0cA5k/dlJhfwoxWolw0E2dMOjlY +qzkEGJdyS6+hFddo696HzD7OYhxh1r50aHPWqY8NnC51487loOtPs4LYA2bd3HSg +ECpOtKzyJW+GP0H2vBa7MrXrZOnD1K2j2xb8nTnYnpNtlmnZPj7VYFsLOlsq547X +nFiSptPWslbVogkUVkCZlAqkMcJ/OtH70ZVjLyjFd6j7J/Wy8MrA7pECAwEAAaNT +MFEwHQYDVR0OBBYEFGXBvV/uWivFWRWPHiVfY/kSJqufMB8GA1UdIwQYMBaAFGXB +vV/uWivFWRWPHiVfY/kSJqufMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBABjatix/q90u6X/Jar/UkKiL2zx36s4huPU9KG8F51g48cYRjrvpK4+H +K6avCGArl7h1gczaGS7LTOHFUU25eg/BBcKcXEO3aryQph2A167ip89UM55LxlnC +QVVV9HAnEw5qAoh0wlZ65fVN+SE8FdasYlbbbp7c4At/LZruSj+IIapZDwwJxcBk +YlSOa34v1Uay09+Hgu95dYQjI9txJW1ViRVlDpKbieGTzROI6s3uk+3rhxxlH2Zi +Z9UqNmPfH9UE1xgSk/wkMWW22h/x51qIRKAZ4EzmdHVXdT/BarIuHxtHH8hIPNSL +FjetCMVZNBej2HXL9cY5UVFYCG6JG0Q= -----END CERTIFICATE----- diff --git a/tests/data/wire/trans_prv b/tests/data/wire/trans_prv index 063cf1586a..17bdb07c65 100644 --- a/tests/data/wire/trans_prv +++ b/tests/data/wire/trans_prv @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDT3CQJHeleTXqI -EioyHk1zlguccyk8riT7eGGTFUqLFNJSOmtMB6foNnz9ts61bMFvkCc+gFVqIyBK -i0Lqd1sgl2U7/EVG5R5yI5L1//ifxmi5s9BuSc2SKRCy9gvCH0Cx9iIj5SvSzkB9 -NFu+FhXCt0KnCeisPBnFWnkl09a+10HBjKhI3FiZKSi6DCb9yzLnxPiTHW1w1BmP -j7O2YnLvCl7wT2ilbHOh9o66gUvHrY128y8q3s01A8+Oa5skScwjuUpNqaIbmIO9 -8FMpAFD1cXey/Fp0sqs9b+M6zm6p8AQMqK+uyUf4Y0wX4xTr3fKhRqJ34r9lZ8/V -7GBU2bFxAgMBAAECggEBAM4hsfog3VAAyIieS+npq+gbhH6bWfMNaTQ3g5CNNbMu -9hhFeOJHzKnWYjSlamgBQhAfTN+2E+Up+iAtcVUZ/lMumrQLlwgMo1vgmvu5Kxmh -/YE5oEG+k0JzrCjD1trwd4zvc3ZDYyk/vmVTzTOc311N248UyArUiyqHBbq1a4rP -tJhCLn2c4S7flXGF0MDVGZyV9V7J8N8leq/dRGMB027Li21T+B4mPHXa6b8tpRPL -4vc8sHoUJDa2/+mFDJ2XbZfmlgd3MmIPlRn1VWoW7mxgT/AObsPl7LuQx7+t80Wx -hIMjuKUHRACQSLwHxJ3SQRFWp4xbztnXSRXYuHTscLUCgYEA//Uu0qIm/FgC45yG -nXtoax4+7UXhxrsWDEkbtL6RQ0TSTiwaaI6RSQcjrKDVSo/xo4ZySTYcRgp5GKlI -CrWyNM+UnIzTNbZOtvSIAfjxYxMsq1vwpTlOB5/g+cMukeGg39yUlrjVNoFpv4i6 -9t4yYuEaF4Vww0FDd2nNKhhW648CgYEA0+UYH6TKu03zDXqFpwf4DP2VoSo8OgfQ -eN93lpFNyjrfzvxDZkGF+7M/ebyYuI6hFplVMu6BpgpFP7UVJpW0Hn/sXkTq7F1Q -rTJTtkTp2+uxQVP/PzSOqK0Twi5ifkfoEOkPkNNtTiXzwCW6Qmmcvln2u893pyR5 -gqo5BHR7Ev8CgYAb7bXpN9ZHLJdMHLU3k9Kl9YvqOfjTxXA3cPa79xtEmsrTys4q -4HuL22KSII6Fb0VvkWkBAg19uwDRpw78VC0YxBm0J02Yi8b1AaOhi3dTVzFFlWeh -r6oK/PAAcMKxGkyCgMAZ3hstsltGkfXMoBwhW+yL6nyOYZ2p9vpzAGrjkwKBgQDF -0huzbyXVt/AxpTEhv07U0enfjI6tnp4COp5q8zyskEph8yD5VjK/yZh5DpmFs6Kw -dnYUFpbzbKM51tToMNr3nnYNjEnGYVfwWgvNHok1x9S0KLcjSu3ki7DmmGdbfcYq -A2uEyd5CFyx5Nr+tQOwUyeiPbiFG6caHNmQExLoiAQKBgFPy9H8///xsadYmZ18k -r77R2CvU7ArxlLfp9dr19aGYKvHvnpsY6EuChkWfy8Xjqn3ogzgrHz/rn3mlGUpK -vbtwtsknAHtTbotXJwfaBZv2RGgGRr3DzNo6ll2Aez0lNblZFXq132h7+y5iLvar -4euORaD/fuM4UPlR5mN+bypU +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCv11ofg43NlmKC +f/BZGXfDld1eO3yA+VlfIUgc/4v4VVCh4Qlk0HxOFXEARmBcivJTCiAVe7egatOU +0BrgfNePB0oyG6A8dzXXdAv/pWHARNYszKxymWIXMu5UpRFJPsuYsntHAOZP3ZSY +X8KMVqJcNBNnTDo5WKs5BBiXckuvoRXXaOveh8w+zmIcYda+dGhz1qmPDZwudePO +5aDrT7OC2ANm3dx0oBAqTrSs8iVvhj9B9rwWuzK162Tpw9Sto9sW/J052J6TbZZp +2T4+1WBbCzpbKueO15xYkqbT1rJW1aIJFFZAmZQKpDHCfzrR+9GVYy8oxXeo+yf1 +svDKwO6RAgMBAAECggEAEwBogsNKjY7Usll09Yvk/0OwmkA/YgiP+dG04z1SONGv +Vu7kfvpwlFeI0IjKXPW+3e5YLTojS7h/iLM8VEnpWVFmWSfXFvGi5ddqfIO4nnhR +1KGBeRjOGsesLYVw6sNYaPXQkImuWa8OIbEnatbp0KDn/9+i4xOL3StuJN97Ak1u +Giq4gwFbag4/QctBZ+5P0t77W+uzWcvEyNgK6rndfPWxqwmJSBFchY6O3s1l6NY8 +vSmyYhYRgFXEgX0nDumGfEXsF1Cj9tzYT2DUZc2f6+UCtXCD49qnoKawLhCrl5Uh +QGs82TR5FSn7zLW4MbFody6p8UDw6pYiWlPPR7fmgQKBgQDO3j5RCXf0276K56BA +rFpOOmivq3fxElRVCSRRRVPKHDYKQiPKnNXoa/pSl8a6CfjJaJzkNj+wTEFdQRGm +Ia123kR/1S21/zgGZNmbUGby+A4fKxBY101/JQweucRN7aw3XLKPXhOL1NPyKdWh +dARvjZvEl1qR6s07Y6jZgpkGqQKBgQDZmqVWvUgACdxkCYEzDf3Fc2G/8oL4VxWJ +HHr5zib+DDhTfKrgQyA9CZ97stZfrR7KYnsLJH8jnj/w/CNOI0G+41KroICRsnjT +5bm7/sT5uwLwu+FAQzITiehj7Te1lwsqtS8yOnXBTQ3hzaw9yhAsuhefx+WT2UCd +Y8Od13nhqQKBgQCR2LR8s71D/81F52nfTuRYNOvrtmtYpkCYt1pIhiU94EflUZ4k +UhCpzb7tjh5IuZEShtPePbUHWavX0HFd/G5s2OXYbnbM0oQwVdfpnXUHpgVmyhi7 +WghENN1nqDcTbha17X/ifkQvmLxZBk+chcw+zcrdfowXRkCtt2Sq/V1gCQKBgH/w +UK3C9AYxxgZ7IB9oZoAk6p/0cdSZPuwydotRDdPoU2WissTQMrAwbDhKWYg/PQ84 +/6b5elbywB1r4UYbrJgTB5Qo9e6zxB6xvpYtoJpDveLUVAd4eoTKXHwECPEXMVWW +2XzqqjlQmIzeZBqgJwplD2a+HNjkrvzanzS6b8qhAoGBAIun0EEc/Zc0ZxzgDPen +A9/7jV++QCrNsevxGH8yrhPP4UqTVSHGR9H+RAif7zTBTn0OwzSBz6hFbPmxum3m +cKabsKVN3poz3TBvfyhgjYosMWvCHpNhif09lyd/s2FezPGyK1Nyf5cKNEWjFGKw ++fCPJ/Ihp4iwacNU1Pu9m050 -----END PRIVATE KEY----- diff --git a/tests/data/wire/trans_pub b/tests/data/wire/trans_pub index e4a8ea8aad..330ff42712 100644 --- a/tests/data/wire/trans_pub +++ b/tests/data/wire/trans_pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA09wkCR3pXk16iBIqMh5N -c5YLnHMpPK4k+3hhkxVKixTSUjprTAen6DZ8/bbOtWzBb5AnPoBVaiMgSotC6ndb -IJdlO/xFRuUeciOS9f/4n8ZoubPQbknNkikQsvYLwh9AsfYiI+Ur0s5AfTRbvhYV -wrdCpwnorDwZxVp5JdPWvtdBwYyoSNxYmSkougwm/csy58T4kx1tcNQZj4+ztmJy -7wpe8E9opWxzofaOuoFLx62NdvMvKt7NNQPPjmubJEnMI7lKTamiG5iDvfBTKQBQ -9XF3svxadLKrPW/jOs5uqfAEDKivrslH+GNMF+MU693yoUaid+K/ZWfP1exgVNmx -cQIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr9daH4ONzZZign/wWRl3 +w5XdXjt8gPlZXyFIHP+L+FVQoeEJZNB8ThVxAEZgXIryUwogFXu3oGrTlNAa4HzX +jwdKMhugPHc113QL/6VhwETWLMyscpliFzLuVKURST7LmLJ7RwDmT92UmF/CjFai +XDQTZ0w6OVirOQQYl3JLr6EV12jr3ofMPs5iHGHWvnRoc9apjw2cLnXjzuWg60+z +gtgDZt3cdKAQKk60rPIlb4Y/Qfa8Frsytetk6cPUraPbFvydOdiek22Wadk+PtVg +Wws6WyrnjtecWJKm09ayVtWiCRRWQJmUCqQxwn860fvRlWMvKMV3qPsn9bLwysDu +kQIDAQAB -----END PUBLIC KEY----- diff --git a/tests/ga/test_extension.py b/tests/ga/test_extension.py index e115748c36..ca502f3c22 100644 --- a/tests/ga/test_extension.py +++ b/tests/ga/test_extension.py @@ -15,6 +15,7 @@ # Requires Python 2.6+ and Openssl 1.0+ # import contextlib +import datetime import glob import json import os.path @@ -41,6 +42,7 @@ ExtensionErrorCodes, ExtensionError, GoalStateAggregateStatusCodes from azurelinuxagent.common.protocol.restapi import ExtensionSettings, Extension, ExtHandlerStatus, \ ExtensionStatus, ExtensionRequestedState +from azurelinuxagent.common.protocol import wire from azurelinuxagent.common.protocol.wire import WireProtocol, InVMArtifactsProfile from azurelinuxagent.common.utils.restutil import KNOWN_WIRESERVER_IP @@ -3302,9 +3304,13 @@ def manifest_location_handler(url, **kwargs): with mock_wire_protocol(self.test_data, http_get_handler=manifest_location_handler) as protocol: ext_handlers = protocol.get_goal_state().extensions_goal_state.extensions - with self.assertRaises(ExtensionDownloadError): - protocol.client.fetch_manifest(ext_handlers[0].manifest_uris, - timeout_in_minutes=0, timeout_in_ms=200) + download_timeout = wire._DOWNLOAD_TIMEOUT + wire._DOWNLOAD_TIMEOUT = datetime.timedelta(minutes=0) + try: + with self.assertRaises(ExtensionDownloadError): + protocol.client.fetch_manifest(ext_handlers[0].manifest_uris) + finally: + wire._DOWNLOAD_TIMEOUT = download_timeout # New test cases should be added here.This class uses mock_wire_protocol diff --git a/tests/ga/test_exthandlers_download_extension.py b/tests/ga/test_exthandlers_download_extension.py index 3f33e72b81..e3212ae14f 100644 --- a/tests/ga/test_exthandlers_download_extension.py +++ b/tests/ga/test_exthandlers_download_extension.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the Apache License. +import contextlib import os import time import zipfile @@ -8,8 +9,8 @@ from azurelinuxagent.common.exception import ExtensionDownloadError, ExtensionErrorCodes from azurelinuxagent.common.protocol.restapi import Extension, ExtHandlerPackage from azurelinuxagent.common.protocol.wire import WireProtocol -from azurelinuxagent.ga.exthandlers import ExtHandlerInstance, NUMBER_OF_DOWNLOAD_RETRIES, ExtHandlerState -from tests.tools import AgentTestCase, patch, mock_sleep +from azurelinuxagent.ga.exthandlers import ExtHandlerInstance, ExtHandlerState +from tests.tools import AgentTestCase, patch, Mock class DownloadExtensionTestCase(AgentTestCase): @@ -35,6 +36,8 @@ def setUp(self): ext_handler.version = "1.0.0" protocol = WireProtocol("http://Microsoft.CPlat.Core.RunCommandLinux/foo-bar") + protocol.client.get_host_plugin = Mock() + protocol.client.get_artifact_request = Mock(return_value=(None, None)) self.pkg = ExtHandlerPackage() self.pkg.uris = [ @@ -95,17 +98,25 @@ def _assert_download_and_expand_succeeded(self): self.assertTrue(os.path.exists(self._get_extension_package_file()), "The extension package was not downloaded to the expected location") self.assertTrue(os.path.exists(self._get_extension_command_file()), "The extension package was not expanded to the expected location") + @staticmethod + @contextlib.contextmanager + def create_mock_stream(stream_function): + with patch("azurelinuxagent.common.protocol.wire.WireClient.stream", side_effect=stream_function) as mock_stream: + mock_stream.download_failures = 0 + with patch('time.sleep'): # don't sleep in-between retries + yield mock_stream + def test_it_should_download_and_expand_extension_package(self): - def download_ext_handler_pkg(_uri, destination): + def stream(_, destination, **__): DownloadExtensionTestCase._create_zip_file(destination) return True - with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: + with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.report_event") as mock_report_event: self.ext_handler_instance.download() # first download attempt should succeed - mock_download_ext_handler_pkg.assert_called_once() + mock_stream.assert_called_once() mock_report_event.assert_called_once() self._assert_download_and_expand_succeeded() @@ -113,26 +124,26 @@ def download_ext_handler_pkg(_uri, destination): def test_it_should_use_existing_extension_package_when_already_downloaded(self): DownloadExtensionTestCase._create_zip_file(self._get_extension_package_file()) - with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg") as mock_download_ext_handler_pkg: + with DownloadExtensionTestCase.create_mock_stream(lambda: None) as mock_stream: with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.report_event") as mock_report_event: self.ext_handler_instance.download() - mock_download_ext_handler_pkg.assert_not_called() + mock_stream.assert_not_called() mock_report_event.assert_not_called() self.assertTrue(os.path.exists(self._get_extension_command_file()), "The extension package was not expanded to the expected location") def test_it_should_ignore_existing_extension_package_when_it_is_invalid(self): - def download_ext_handler_pkg(_uri, destination): + def stream(_, destination, **__): DownloadExtensionTestCase._create_zip_file(destination) return True DownloadExtensionTestCase._create_invalid_zip_file(self._get_extension_package_file()) - with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: + with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: self.ext_handler_instance.download() - mock_download_ext_handler_pkg.assert_called_once() + mock_stream.assert_called_once() self._assert_download_and_expand_succeeded() @@ -147,108 +158,95 @@ def test_it_should_maintain_extension_handler_state_when_good_zip_exists(self): "Ensure that the state is maintained for extension HandlerState") def test_it_should_maintain_extension_handler_state_when_bad_zip_exists_and_recovers_with_good_zip(self): - def download_ext_handler_pkg(_uri, destination): + def stream(_, destination, **__): DownloadExtensionTestCase._create_zip_file(destination) return True DownloadExtensionTestCase._create_invalid_zip_file(self._get_extension_package_file()) self.ext_handler_instance.set_handler_state(ExtHandlerState.NotInstalled) - with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", - side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: + with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: self.ext_handler_instance.download() - mock_download_ext_handler_pkg.assert_called_once() + mock_stream.assert_called_once() self._assert_download_and_expand_succeeded() self.assertEqual(self.ext_handler_instance.get_handler_state(), ExtHandlerState.NotInstalled, "Ensure that the state is maintained for extension HandlerState") - @patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)) - def test_it_should_maintain_extension_handler_state_when_it_downloads_bad_zips(self, _): - def download_ext_handler_pkg(_uri, destination): + def test_it_should_maintain_extension_handler_state_when_it_downloads_bad_zips(self): + def stream(_, destination, **__): DownloadExtensionTestCase._create_invalid_zip_file(destination) return True self.ext_handler_instance.set_handler_state(ExtHandlerState.NotInstalled) - with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", - side_effect=download_ext_handler_pkg): + with DownloadExtensionTestCase.create_mock_stream(stream): with self.assertRaises(ExtensionDownloadError): self.ext_handler_instance.download() - self.assertFalse(os.path.exists(self._get_extension_package_file()), - "The bad zip extension package should not be downloaded to the expected location") - self.assertFalse(os.path.exists(self._get_extension_command_file()), - "The extension package should not expanded be to the expected location due to bad zip") - self.assertEqual(self.ext_handler_instance.get_handler_state(), ExtHandlerState.NotInstalled, - "Ensure that the state is maintained for extension HandlerState") + self.assertFalse(os.path.exists(self._get_extension_package_file()), "The bad zip extension package should not be downloaded to the expected location") + self.assertFalse(os.path.exists(self._get_extension_command_file()), "The extension package should not expanded be to the expected location due to bad zip") + self.assertEqual(self.ext_handler_instance.get_handler_state(), ExtHandlerState.NotInstalled, "Ensure that the state is maintained for extension HandlerState") def test_it_should_use_alternate_uris_when_download_fails(self): - self.download_failures = 0 # pylint: disable=attribute-defined-outside-init - - def download_ext_handler_pkg(_uri, destination): + def stream(_, destination, **__): # fail a few times, then succeed - if self.download_failures < 3: - self.download_failures += 1 - return False + if mock_stream.download_failures < 3: + mock_stream.download_failures += 1 + return None DownloadExtensionTestCase._create_zip_file(destination) return True - with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: + with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: self.ext_handler_instance.download() - self.assertEqual(mock_download_ext_handler_pkg.call_count, self.download_failures + 1) + self.assertEqual(mock_stream.call_count, mock_stream.download_failures + 1) self._assert_download_and_expand_succeeded() def test_it_should_use_alternate_uris_when_download_raises_an_exception(self): - self.download_failures = 0 # pylint: disable=attribute-defined-outside-init - - def download_ext_handler_pkg(_uri, destination): + def stream(_, destination, **__): # fail a few times, then succeed - if self.download_failures < 3: - self.download_failures += 1 + if mock_stream.download_failures < 3: + mock_stream.download_failures += 1 raise Exception("Download failed") DownloadExtensionTestCase._create_zip_file(destination) return True - with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: + with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: self.ext_handler_instance.download() - self.assertEqual(mock_download_ext_handler_pkg.call_count, self.download_failures + 1) + self.assertEqual(mock_stream.call_count, mock_stream.download_failures + 1) self._assert_download_and_expand_succeeded() def test_it_should_use_alternate_uris_when_it_downloads_an_invalid_package(self): - self.download_failures = 0 # pylint: disable=attribute-defined-outside-init - - def download_ext_handler_pkg(_uri, destination): + def stream(_, destination, **__): # fail a few times, then succeed - if self.download_failures < 3: - self.download_failures += 1 + if mock_stream.download_failures < 3: + mock_stream.download_failures += 1 DownloadExtensionTestCase._create_invalid_zip_file(destination) else: DownloadExtensionTestCase._create_zip_file(destination) return True - with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: + with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: self.ext_handler_instance.download() - self.assertEqual(mock_download_ext_handler_pkg.call_count, self.download_failures + 1) + self.assertEqual(mock_stream.call_count, mock_stream.download_failures + 1) self._assert_download_and_expand_succeeded() def test_it_should_raise_an_exception_when_all_downloads_fail(self): - def download_ext_handler_pkg(_uri, _destination): + def stream(_, __, **___): DownloadExtensionTestCase._create_invalid_zip_file(self._get_extension_package_file()) return True - with patch("time.sleep", lambda *_: None): - with patch("azurelinuxagent.common.protocol.wire.WireProtocol.download_ext_handler_pkg", side_effect=download_ext_handler_pkg) as mock_download_ext_handler_pkg: - with self.assertRaises(ExtensionDownloadError) as context_manager: - self.ext_handler_instance.download() + with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: + with self.assertRaises(ExtensionDownloadError) as context_manager: + self.ext_handler_instance.download() - self.assertEqual(mock_download_ext_handler_pkg.call_count, NUMBER_OF_DOWNLOAD_RETRIES * len(self.pkg.uris)) + self.assertEqual(mock_stream.call_count, len(self.pkg.uris)) self.assertRegex(str(context_manager.exception), "Failed to download extension") self.assertEqual(context_manager.exception.code, ExtensionErrorCodes.PluginManifestDownloadError) diff --git a/tests/ga/test_report_status.py b/tests/ga/test_report_status.py index e4e1a96421..c5a20b5167 100644 --- a/tests/ga/test_report_status.py +++ b/tests/ga/test_report_status.py @@ -82,15 +82,18 @@ def test_report_status_should_log_errors_only_once_per_goal_state(self): self.assertEqual(0, logger_warn.call_count, "UpdateHandler._report_status() should not report WARNINGS when there are no errors") with patch("azurelinuxagent.ga.update.ExtensionsSummary.__init__", side_effect=Exception("TEST EXCEPTION")): # simulate an error during _report_status() + get_warnings = lambda: [args[0] for args, _ in logger_warn.call_args_list if "TEST EXCEPTION" in args[0]] + update_handler._report_status(exthandlers_handler) update_handler._report_status(exthandlers_handler) update_handler._report_status(exthandlers_handler) - self.assertEqual(1, logger_warn.call_count, "UpdateHandler._report_status() should report only 1 WARNING when there are multiple errors within the same goal state") + + self.assertEqual(1, len(get_warnings()), "UpdateHandler._report_status() should report only 1 WARNING when there are multiple errors within the same goal state") exthandlers_handler.protocol.mock_wire_data.set_incarnation(999) update_handler._try_update_goal_state(exthandlers_handler.protocol) update_handler._report_status(exthandlers_handler) - self.assertEqual(2, logger_warn.call_count, "UpdateHandler._report_status() should continue reporting errors after a new goal state") + self.assertEqual(2, len(get_warnings()), "UpdateHandler._report_status() should continue reporting errors after a new goal state") def test_update_handler_should_add_fast_track_to_supported_features_when_it_is_supported(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py index c564af7217..e96cf598a6 100644 --- a/tests/protocol/test_wire.py +++ b/tests/protocol/test_wire.py @@ -94,11 +94,11 @@ def _test_getters(self, test_data, certsMustBePresent, __, MockCryptUtil, _): protocol.get_ext_handler_pkgs(ext_handler) crt1 = os.path.join(self.tmp_dir, - '33B0ABCE4673538650971C10F7D7397E71561F35.crt') + '38B85D88F03D1A8E1C671EB169274C09BC4D4703.crt') crt2 = os.path.join(self.tmp_dir, - '4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3.crt') + 'BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F.crt') prv2 = os.path.join(self.tmp_dir, - '4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3.prv') + 'BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F.prv') if certsMustBePresent: self.assertTrue(os.path.isfile(crt1)) self.assertTrue(os.path.isfile(crt2)) @@ -295,7 +295,7 @@ def http_get_handler(url, *_, **kwargs): events = [kwargs for _, kwargs in add_event.call_args_list if kwargs['op'] == WALAEventOperation.ArtifactsProfileBlob] self.assertEqual(1, len(events), "Expected 1 event for operation ArtifactsProfileBlob. Got: {0}".format(events)) self.assertFalse(events[0]['is_success'], "Expected ArtifactsProfileBlob's success to be False") - self.assertTrue('invalid json' in events[0]['message'], "Expected 'invalid json' as the reason for the operation failure. Got: {0}".format(events[0]['message'])) + self.assertTrue("Can't parse the artifacts profile blob" in events[0]['message'], "Expected 'Can't parse the artifacts profile blob as the reason for the operation failure. Got: {0}".format(events[0]['message'])) @patch("socket.gethostname", return_value="hostname") @patch("time.gmtime", return_value=time.localtime(1485543256)) @@ -511,10 +511,9 @@ def http_get_handler(url, *_, **__): with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: HostPluginProtocol.is_default_channel = False - success = protocol.download_ext_handler_pkg(extension_url, target_file) + protocol.client.download_extension([extension_url], target_file) urls = protocol.get_tracked_urls() - self.assertTrue(success, "The download should have succeeded") self.assertEqual(len(urls), 1, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], extension_url, "The extension should have been downloaded over the direct channel") self.assertTrue(os.path.exists(target_file), "The extension package was not downloaded") @@ -534,10 +533,9 @@ def http_get_handler(url, *_, **kwargs): with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: HostPluginProtocol.is_default_channel = False - success = protocol.download_ext_handler_pkg(extension_url, target_file) + protocol.client.download_extension([extension_url], target_file) urls = protocol.get_tracked_urls() - self.assertTrue(success, "The download should have succeeded") self.assertEqual(len(urls), 2, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], extension_url, "The first attempt should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The retry attempt should have been over the host channel") @@ -571,10 +569,9 @@ def http_get_handler(url, *_, **kwargs): protocol.set_http_handlers(http_get_handler=http_get_handler) - success = protocol.download_ext_handler_pkg(extension_url, target_file) + protocol.client.download_extension([extension_url], target_file) urls = protocol.get_tracked_urls() - self.assertTrue(success, "The download should have succeeded") self.assertEqual(len(urls), 4, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], extension_url, "The first attempt should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The second attempt should have been over the host channel") @@ -604,10 +601,10 @@ def http_get_handler(url, *_, **kwargs): protocol.set_http_handlers(http_get_handler=http_get_handler) - success = protocol.download_ext_handler_pkg(extension_url, target_file) + with self.assertRaises(ExtensionDownloadError): + protocol.client.download_extension([extension_url], target_file) urls = protocol.get_tracked_urls() - self.assertFalse(success, "The download should have failed") self.assertEqual(len(urls), 2, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], extension_url, "The first attempt should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The second attempt should have been over the host channel") @@ -838,50 +835,18 @@ def http_get_handler(url, *_, **kwargs): self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[3]), "The retry request should have been over the host channel") self.assertFalse(HostPluginProtocol.is_default_channel, "The default channel should not have changed") - def test_upload_logs_should_not_refresh_plugin_when_first_attempt_succeeds(self): - def http_put_handler(url, *_, **__): # pylint: disable=inconsistent-return-statements - if self.is_host_plugin_put_logs_request(url): - return MockHttpResponse(200) - - with mock_wire_protocol(mockwiredata.DATA_FILE, http_put_handler=http_put_handler) as protocol: - content = b"test" - protocol.client.upload_logs(content) - - urls = protocol.get_tracked_urls() - self.assertEqual(len(urls), 1, 'Expected one post request to the host: [{0}]'.format(urls)) - - def test_upload_logs_should_retry_the_host_channel_after_refreshing_the_host_plugin(self): - def http_put_handler(url, *_, **__): - if self.is_host_plugin_put_logs_request(url): - if http_put_handler.host_plugin_calls == 0: - http_put_handler.host_plugin_calls += 1 - return ResourceGoneError("Exception to fake a stale goal state") - protocol.track_url(url) - return None - http_put_handler.host_plugin_calls = 0 - - with mock_wire_protocol(mockwiredata.DATA_FILE_IN_VM_ARTIFACTS_PROFILE, http_put_handler=http_put_handler) \ - as protocol: - content = b"test" - protocol.client.upload_logs(content) - - urls = protocol.get_tracked_urls() - self.assertEqual(len(urls), 2, "Invalid number of requests: [{0}]".format(urls)) - self.assertTrue(self.is_host_plugin_put_logs_request(urls[0]), "The first request should have been over the host channel") - self.assertTrue(self.is_host_plugin_put_logs_request(urls[1]), "The second request should have been over the host channel") - @staticmethod def _set_and_fail_helper_channel_functions(fail_direct=False, fail_host=False): def direct_func(*_): direct_func.counter += 1 if direct_func.fail: - return None + raise Exception("Direct channel failed") return "direct" def host_func(*_): host_func.counter += 1 if host_func.fail: - return None + raise Exception("Host channel failed") return "host" direct_func.counter = 0 @@ -892,7 +857,7 @@ def host_func(*_): return direct_func, host_func - def test_send_request_using_appropriate_channel_should_not_invoke_secondary_when_primary_channel_succeeds(self): + def test_download_using_appropriate_channel_should_not_invoke_secondary_when_primary_channel_succeeds(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: # Scenario #1: Direct channel default HostPluginProtocol.is_default_channel = False @@ -900,7 +865,7 @@ def test_send_request_using_appropriate_channel_should_not_invoke_secondary_when direct_func, host_func = self._set_and_fail_helper_channel_functions() # Assert we're only calling the primary channel (direct) and that it succeeds. for iteration in range(5): - ret = protocol.client.send_request_using_appropriate_channel(direct_func, host_func) + ret = protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual("direct", ret) self.assertEqual(iteration + 1, direct_func.counter) self.assertEqual(0, host_func.counter) @@ -912,13 +877,13 @@ def test_send_request_using_appropriate_channel_should_not_invoke_secondary_when # Assert we're only calling the primary channel (host) and that it succeeds. for iteration in range(5): - ret = protocol.client.send_request_using_appropriate_channel(direct_func, host_func) + ret = protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual("host", ret) self.assertEqual(0, direct_func.counter) self.assertEqual(iteration + 1, host_func.counter) self.assertTrue(HostPluginProtocol.is_default_channel) - def test_send_request_using_appropriate_channel_should_not_change_default_channel_if_none_succeeds(self): + def test_download_using_appropriate_channel_should_not_change_default_channel_if_none_succeeds(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: # Scenario #1: Direct channel is default HostPluginProtocol.is_default_channel = False @@ -926,8 +891,8 @@ def test_send_request_using_appropriate_channel_should_not_change_default_channe # Assert we keep trying both channels, but the default channel doesn't change for iteration in range(5): - ret = protocol.client.send_request_using_appropriate_channel(direct_func, host_func) - self.assertEqual(None, ret) + with self.assertRaises(HttpError): + protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual(iteration + 1, direct_func.counter) self.assertEqual(iteration + 1, host_func.counter) self.assertFalse(HostPluginProtocol.is_default_channel) @@ -938,20 +903,20 @@ def test_send_request_using_appropriate_channel_should_not_change_default_channe # Assert we keep trying both channels, but the default channel doesn't change for iteration in range(5): - ret = protocol.client.send_request_using_appropriate_channel(direct_func, host_func) - self.assertEqual(None, ret) + with self.assertRaises(HttpError): + protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual(iteration + 1, direct_func.counter) self.assertEqual(iteration + 1, host_func.counter) self.assertTrue(HostPluginProtocol.is_default_channel) - def test_send_request_using_appropriate_channel_should_change_default_channel_when_secondary_succeeds(self): + def test_download_using_appropriate_channel_should_change_default_channel_when_secondary_succeeds(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: # Scenario #1: Direct channel is default HostPluginProtocol.is_default_channel = False direct_func, host_func = self._set_and_fail_helper_channel_functions(fail_direct=True, fail_host=False) # Assert we've called both channels and the default channel changed - ret = protocol.client.send_request_using_appropriate_channel(direct_func, host_func) + ret = protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual("host", ret) self.assertEqual(1, direct_func.counter) self.assertEqual(1, host_func.counter) @@ -959,7 +924,7 @@ def test_send_request_using_appropriate_channel_should_change_default_channel_wh # If host keeps succeeding, assert we keep calling only that channel and not changing the default. for iteration in range(5): - ret = protocol.client.send_request_using_appropriate_channel(direct_func, host_func) + ret = protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual("host", ret) self.assertEqual(1, direct_func.counter) self.assertEqual(1 + iteration + 1, host_func.counter) @@ -970,7 +935,7 @@ def test_send_request_using_appropriate_channel_should_change_default_channel_wh direct_func, host_func = self._set_and_fail_helper_channel_functions(fail_direct=False, fail_host=True) # Assert we've called both channels and the default channel changed - ret = protocol.client.send_request_using_appropriate_channel(direct_func, host_func) + ret = protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual("direct", ret) self.assertEqual(1, direct_func.counter) self.assertEqual(1, host_func.counter) @@ -978,7 +943,7 @@ def test_send_request_using_appropriate_channel_should_change_default_channel_wh # If direct keeps succeeding, assert we keep calling only that channel and not changing the default. for iteration in range(5): - ret = protocol.client.send_request_using_appropriate_channel(direct_func, host_func) + ret = protocol.client._download_using_appropriate_channel(direct_func, host_func) self.assertEqual("direct", ret) self.assertEqual(1 + iteration + 1, direct_func.counter) self.assertEqual(1, host_func.counter) From 51b27a10ef841d0566a1eca1d965732380b123f9 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Tue, 30 Aug 2022 12:27:26 -0700 Subject: [PATCH 44/58] update max memory usage check (#2658) --- azurelinuxagent/ga/collect_logs.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/azurelinuxagent/ga/collect_logs.py b/azurelinuxagent/ga/collect_logs.py index 616d875a30..95c42f3a76 100644 --- a/azurelinuxagent/ga/collect_logs.py +++ b/azurelinuxagent/ga/collect_logs.py @@ -336,18 +336,14 @@ def _send_telemetry(self, metrics): def _verify_memory_limit(self, metrics): current_usage = 0 - max_usage = 0 for metric in metrics: if metric.counter == MetricsCounter.TOTAL_MEM_USAGE: current_usage += metric.value elif metric.counter == MetricsCounter.SWAP_MEM_USAGE: current_usage += metric.value - elif metric.counter == MetricsCounter.MAX_MEM_USAGE: - max_usage = metric.value - current_max = max(current_usage, max_usage) - if current_max > LOGCOLLECTOR_MEMORY_LIMIT: - msg = "Log collector memory limit {0} bytes exceeded. The max reported usage is {1} bytes.".format(LOGCOLLECTOR_MEMORY_LIMIT, current_max) + if current_usage > LOGCOLLECTOR_MEMORY_LIMIT: + msg = "Log collector memory limit {0} bytes exceeded. The max reported usage is {1} bytes.".format(LOGCOLLECTOR_MEMORY_LIMIT, current_usage) logger.info(msg) add_event( name=AGENT_NAME, From ef996e2adca539160e843f1e9d8fac172653d564 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Fri, 2 Sep 2022 17:01:08 -0700 Subject: [PATCH 45/58] Add x-ms-verify-from-artifacts-blob to extensionsArtifact calls (#2655) * Add x-ms-verify-from-artifacts-blob to extensionsArtifact calls * Check for null goal state * Improve API interface Co-authored-by: narrieta --- .../common/protocol/extensions_goal_state.py | 6 +- ...sions_goal_state_from_extensions_config.py | 16 +- .../extensions_goal_state_from_vm_settings.py | 16 +- azurelinuxagent/common/protocol/goal_state.py | 81 +++++++++- azurelinuxagent/common/protocol/hostplugin.py | 17 +- azurelinuxagent/common/protocol/restapi.py | 8 +- azurelinuxagent/common/protocol/wire.py | 95 +---------- azurelinuxagent/common/utils/archive.py | 7 +- azurelinuxagent/ga/exthandlers.py | 9 +- azurelinuxagent/ga/update.py | 64 ++++---- ci/3.6.pylintrc | 13 +- tests/ga/test_extension.py | 6 +- .../ga/test_exthandlers_download_extension.py | 8 + tests/ga/test_update.py | 151 ++++++++---------- ..._extensions_goal_state_from_vm_settings.py | 29 ++-- tests/protocol/test_hostplugin.py | 6 +- tests/protocol/test_wire.py | 30 ++-- 17 files changed, 286 insertions(+), 276 deletions(-) diff --git a/azurelinuxagent/common/protocol/extensions_goal_state.py b/azurelinuxagent/common/protocol/extensions_goal_state.py index 74c761caee..337cff112a 100644 --- a/azurelinuxagent/common/protocol/extensions_goal_state.py +++ b/azurelinuxagent/common/protocol/extensions_goal_state.py @@ -17,7 +17,7 @@ # Requires Python 2.6+ and Openssl 1.0+ import datetime -import azurelinuxagent.common.logger as logger +from azurelinuxagent.common import logger from azurelinuxagent.common.AgentGlobals import AgentGlobals from azurelinuxagent.common.exception import AgentError from azurelinuxagent.common.utils import textutil @@ -126,7 +126,7 @@ def on_hold(self): raise NotImplementedError() @property - def agent_manifests(self): + def agent_families(self): raise NotImplementedError() @property @@ -233,7 +233,7 @@ def on_hold(self): return False @property - def agent_manifests(self): + def agent_families(self): return [] @property diff --git a/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py b/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py index 27af3b7949..a8bfa25054 100644 --- a/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py +++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_extensions_config.py @@ -24,7 +24,7 @@ from azurelinuxagent.common.exception import ExtensionsConfigError from azurelinuxagent.common.future import ustr from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState, GoalStateChannel, GoalStateSource -from azurelinuxagent.common.protocol.restapi import ExtensionSettings, Extension, VMAgentManifest, ExtensionState, InVMGoalStateMetaData +from azurelinuxagent.common.protocol.restapi import ExtensionSettings, Extension, VMAgentFamily, ExtensionState, InVMGoalStateMetaData from azurelinuxagent.common.utils.textutil import parse_doc, parse_json, findall, find, findtext, getattrib, gettext, format_exception, \ is_str_none_or_whitespace, is_str_empty @@ -43,7 +43,7 @@ def __init__(self, incarnation, xml_text, wire_client): self._activity_id = None self._correlation_id = None self._created_on_timestamp = None - self._agent_manifests = [] + self._agent_families = [] self._extensions = [] try: @@ -59,14 +59,14 @@ def _parse_extensions_config(self, xml_text, wire_client): ga_families = findall(ga_families_list, "GAFamily") for ga_family in ga_families: - family = findtext(ga_family, "Name") + name = findtext(ga_family, "Name") version = findtext(ga_family, "Version") uris_list = find(ga_family, "Uris") uris = findall(uris_list, "Uri") - manifest = VMAgentManifest(family, version) + family = VMAgentFamily(name, version) for uri in uris: - manifest.uris.append(gettext(uri)) - self._agent_manifests.append(manifest) + family.uris.append(gettext(uri)) + self._agent_families.append(family) self.__parse_plugins_and_settings_and_populate_ext_handlers(xml_doc) @@ -172,8 +172,8 @@ def on_hold(self): return self._on_hold @property - def agent_manifests(self): - return self._agent_manifests + def agent_families(self): + return self._agent_families @property def extensions(self): diff --git a/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py index db7237b9ed..f6496bfd38 100644 --- a/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py +++ b/azurelinuxagent/common/protocol/extensions_goal_state_from_vm_settings.py @@ -20,11 +20,11 @@ import re import sys +from azurelinuxagent.common import logger from azurelinuxagent.common.AgentGlobals import AgentGlobals from azurelinuxagent.common.future import ustr -import azurelinuxagent.common.logger as logger from azurelinuxagent.common.protocol.extensions_goal_state import ExtensionsGoalState, GoalStateChannel, VmSettingsParseError -from azurelinuxagent.common.protocol.restapi import VMAgentManifest, Extension, ExtensionRequestedState, ExtensionSettings +from azurelinuxagent.common.protocol.restapi import VMAgentFamily, Extension, ExtensionRequestedState, ExtensionSettings from azurelinuxagent.common.utils.flexible_version import FlexibleVersion @@ -48,7 +48,7 @@ def __init__(self, etag, json_text, correlation_id): self._status_upload_blob_type = None self._required_features = [] self._on_hold = False - self._agent_manifests = [] + self._agent_families = [] self._extensions = [] try: @@ -134,8 +134,8 @@ def on_hold(self): return self._on_hold @property - def agent_manifests(self): - return self._agent_manifests + def agent_families(self): + return self._agent_families @property def extensions(self): @@ -269,10 +269,10 @@ def _parse_agent_manifests(self, vm_settings): uris = family.get("uris") if uris is None: uris = [] - manifest = VMAgentManifest(name, version) + agent_family = VMAgentFamily(name, version) for u in uris: - manifest.uris.append(u) - self._agent_manifests.append(manifest) + agent_family.uris.append(u) + self._agent_families.append(agent_family) def _parse_extensions(self, vm_settings): # Sample (NOTE: The first sample is single-config, the second multi-config): diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index 97ae270f87..c2b95cfb25 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -21,8 +21,8 @@ import time import json -import azurelinuxagent.common.conf as conf -import azurelinuxagent.common.logger as logger +from azurelinuxagent.common import conf +from azurelinuxagent.common import logger from azurelinuxagent.common.AgentGlobals import AgentGlobals from azurelinuxagent.common.datacontract import set_properties from azurelinuxagent.common.event import add_event, WALAEventOperation @@ -31,11 +31,11 @@ from azurelinuxagent.common.protocol.extensions_goal_state_factory import ExtensionsGoalStateFactory from azurelinuxagent.common.protocol.extensions_goal_state import VmSettingsParseError, GoalStateSource from azurelinuxagent.common.protocol.hostplugin import VmSettingsNotSupported, VmSettingsSupportStopped -from azurelinuxagent.common.protocol.restapi import Cert, CertList, RemoteAccessUser, RemoteAccessUsersList +from azurelinuxagent.common.protocol.restapi import Cert, CertList, RemoteAccessUser, RemoteAccessUsersList, ExtHandlerPackage, ExtHandlerPackageList from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils.archive import GoalStateHistory, SHARED_CONF_FILE_NAME from azurelinuxagent.common.utils.cryptutil import CryptUtil -from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext, getattrib +from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext, getattrib, gettext GOAL_STATE_URI = "http://{0}/machine/?comp=goalstate" @@ -129,6 +129,36 @@ def shared_conf(self): def remote_access(self): return self._remote_access + def fetch_agent_manifest(self, family_name, uris): + """ + This is a convenience method that wraps WireClient.fetch_manifest(), but adds the required 'use_verify_header' parameter and saves + the manifest to the history folder. + """ + return self._fetch_manifest("agent", "waagent.{0}".format(family_name), uris) + + def fetch_extension_manifest(self, extension_name, uris): + """ + This is a convenience method that wraps WireClient.fetch_manifest(), but adds the required 'use_verify_header' parameter and saves + the manifest to the history folder. + """ + return self._fetch_manifest("extension", extension_name, uris) + + def _fetch_manifest(self, manifest_type, name, uris): + try: + is_fast_track = self.extensions_goal_state.source == GoalStateSource.FastTrack + xml_text = self._wire_client.fetch_manifest(uris, use_verify_header=is_fast_track) + self._history.save_manifest(name, xml_text) + return ExtensionManifest(xml_text) + except Exception as e: + raise ProtocolError("Failed to retrieve {0} manifest. Error: {1}".format(manifest_type, ustr(e))) + + def download_extension(self, uris, destination, on_downloaded=lambda: True): + """ + This is a convenience method that wraps WireClient.download_extension(), but adds the required 'use_verify_header' parameter. + """ + is_fast_track = self.extensions_goal_state.source == GoalStateSource.FastTrack + self._wire_client.download_extension(uris, destination, use_verify_header=is_fast_track, on_downloaded=on_downloaded) + @staticmethod def update_host_plugin_headers(wire_client): """ @@ -562,3 +592,46 @@ def _parse_user(user): remote_access_user = RemoteAccessUser(name, encrypted_password, expiration) return remote_access_user + +class ExtensionManifest(object): + def __init__(self, xml_text): + if xml_text is None: + raise ValueError("ExtensionManifest is None") + logger.verbose("Load ExtensionManifest.xml") + self.pkg_list = ExtHandlerPackageList() + self._parse(xml_text) + + def _parse(self, xml_text): + xml_doc = parse_doc(xml_text) + self._handle_packages(findall(find(xml_doc, + "Plugins"), + "Plugin"), + False) + self._handle_packages(findall(find(xml_doc, + "InternalPlugins"), + "Plugin"), + True) + + def _handle_packages(self, packages, isinternal): + for package in packages: + version = findtext(package, "Version") + + disallow_major_upgrade = findtext(package, + "DisallowMajorVersionUpgrade") + if disallow_major_upgrade is None: + disallow_major_upgrade = '' + disallow_major_upgrade = disallow_major_upgrade.lower() == "true" + + uris = find(package, "Uris") + uri_list = findall(uris, "Uri") + uri_list = [gettext(x) for x in uri_list] + pkg = ExtHandlerPackage() + pkg.version = version + pkg.disallow_major_upgrade = disallow_major_upgrade + for uri in uri_list: + pkg.uris.append(uri) + + pkg.isinternal = isinternal + self.pkg_list.versions.append(pkg) + + diff --git a/azurelinuxagent/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py index 583b95ecf7..56cbafc11e 100644 --- a/azurelinuxagent/common/protocol/hostplugin.py +++ b/azurelinuxagent/common/protocol/hostplugin.py @@ -55,6 +55,7 @@ _HEADER_HOST_CONFIG_NAME = "x-ms-host-config-name" _HEADER_ARTIFACT_LOCATION = "x-ms-artifact-location" _HEADER_ARTIFACT_MANIFEST_LOCATION = "x-ms-artifact-manifest-location" +_HEADER_VERIFY_FROM_ARTIFACTS_BLOB = "x-ms-verify-from-artifacts-blob" MAXIMUM_PAGEBLOB_PAGE_SIZE = 4 * 1024 * 1024 # Max page size: 4MB @@ -183,19 +184,21 @@ def get_vm_settings_request(self, correlation_id): return url, headers - def get_artifact_request(self, artifact_url, artifact_manifest_url=None): + def get_artifact_request(self, artifact_url, use_verify_header, artifact_manifest_url=None): if not self.ensure_initialized(): raise ProtocolError("HostGAPlugin: Host plugin channel is not available") if textutil.is_str_none_or_whitespace(artifact_url): raise ProtocolError("HostGAPlugin: No extension artifact url was provided") - url = URI_FORMAT_GET_EXTENSION_ARTIFACT.format(self.endpoint, - HOST_PLUGIN_PORT) - headers = {_HEADER_VERSION: API_VERSION, - _HEADER_CONTAINER_ID: self.container_id, - _HEADER_HOST_CONFIG_NAME: self.role_config_name, - _HEADER_ARTIFACT_LOCATION: artifact_url} + url = URI_FORMAT_GET_EXTENSION_ARTIFACT.format(self.endpoint, HOST_PLUGIN_PORT) + headers = { + _HEADER_VERSION: API_VERSION, + _HEADER_CONTAINER_ID: self.container_id, + _HEADER_HOST_CONFIG_NAME: self.role_config_name, + _HEADER_ARTIFACT_LOCATION: artifact_url} + if use_verify_header: + headers[_HEADER_VERIFY_FROM_ARTIFACTS_BLOB] = "true" if artifact_manifest_url is not None: headers[_HEADER_ARTIFACT_MANIFEST_LOCATION] = artifact_manifest_url diff --git a/azurelinuxagent/common/protocol/restapi.py b/azurelinuxagent/common/protocol/restapi.py index 0edd4623ab..725e2d7bb4 100644 --- a/azurelinuxagent/common/protocol/restapi.py +++ b/azurelinuxagent/common/protocol/restapi.py @@ -68,9 +68,9 @@ def __init__(self): self.certificates = DataContractList(Cert) -class VMAgentManifest(object): - def __init__(self, family, version=None): - self.family = family +class VMAgentFamily(object): + def __init__(self, name, version=None): + self.name = name # This is the Requested version as specified by the Goal State, it defaults to 0.0.0.0 if not specified in GS self.requested_version_string = VERSION_0 if version is None else version self.uris = [] @@ -91,7 +91,7 @@ def __repr__(self): return self.__str__() def __str__(self): - return "[family: '{0}' uris: {1}]".format(self.family, self.uris) + return "[name: '{0}' uris: {1}]".format(self.name, self.uris) class ExtensionState(object): diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py index 6d48474dd2..167d4820a5 100644 --- a/azurelinuxagent/common/protocol/wire.py +++ b/azurelinuxagent/common/protocol/wire.py @@ -37,11 +37,9 @@ from azurelinuxagent.common.future import httpclient, bytebuffer, ustr from azurelinuxagent.common.protocol.goal_state import GoalState, TRANSPORT_CERT_FILE_NAME, TRANSPORT_PRV_FILE_NAME from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol -from azurelinuxagent.common.protocol.restapi import DataContract, ExtHandlerPackage, \ - ExtHandlerPackageList, ProvisionStatus, VMInfo, VMStatus +from azurelinuxagent.common.protocol.restapi import DataContract, ProvisionStatus, VMInfo, VMStatus from azurelinuxagent.common.telemetryevent import GuestAgentExtensionEventsSchema from azurelinuxagent.common.utils import fileutil, restutil -from azurelinuxagent.common.utils.archive import _MANIFEST_FILE_NAME from azurelinuxagent.common.utils.cryptutil import CryptUtil from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, \ findtext, gettext, remove_bom, get_bytes_from_pem, parse_json @@ -111,22 +109,6 @@ def get_certs(self): certificates = self.client.get_certs() return certificates.cert_list - def get_vmagent_manifests(self): - goal_state = self.client.get_goal_state() - ext_conf = goal_state.extensions_goal_state - return ext_conf.agent_manifests, goal_state.incarnation - - def get_vmagent_pkgs(self, vmagent_manifest): - goal_state = self.client.get_goal_state() - ga_manifest = self.client.fetch_gafamily_manifest(vmagent_manifest, goal_state) - valid_pkg_list = ga_manifest.pkg_list - return valid_pkg_list - - def get_ext_handler_pkgs(self, ext_handler): - logger.verbose("Get extension handler package") - man = self.client.get_ext_manifest(ext_handler) - return man.pkg_list - def get_goal_state(self): return self.client.get_goal_state() @@ -597,14 +579,14 @@ def call_storage_service(http_req, *args, **kwargs): return http_req(*args, **kwargs) def fetch_artifacts_profile_blob(self, uri): - return self._fetch_content("artifacts profile blob", [uri])[1] # _fetch_content returns a (uri, content) tuple + return self._fetch_content("artifacts profile blob", [uri], use_verify_header=False)[1] # _fetch_content returns a (uri, content) tuple - def fetch_manifest(self, uris): - uri, content = self._fetch_content("manifest", uris) + def fetch_manifest(self, uris, use_verify_header): + uri, content = self._fetch_content("manifest", uris, use_verify_header=use_verify_header) self.get_host_plugin().update_manifest_uri(uri) return content - def _fetch_content(self, download_type, uris): + def _fetch_content(self, download_type, uris, use_verify_header): """ Walks the given list of 'uris' issuing HTTP GET requests; returns a tuple with the URI and the content of the first successful request. @@ -616,13 +598,13 @@ def _fetch_content(self, download_type, uris): direct_download = lambda uri: self.fetch(uri)[0] def hgap_download(uri): - request_uri, request_headers = host_ga_plugin.get_artifact_request(uri) + request_uri, request_headers = host_ga_plugin.get_artifact_request(uri, use_verify_header=use_verify_header) response, _ = self.fetch(request_uri, request_headers, use_proxy=False, retry_codes=restutil.HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES) return response return self._download_with_fallback_channel(download_type, uris, direct_download=direct_download, hgap_download=hgap_download) - def download_extension(self, uris, destination, on_downloaded=lambda: True): + def download_extension(self, uris, destination, use_verify_header, on_downloaded=lambda: True): """ Walks the given list of 'uris' issuing HTTP GET requests and saves the content of the first successful request to 'destination'. @@ -635,7 +617,7 @@ def download_extension(self, uris, destination, on_downloaded=lambda: True): direct_download = lambda uri: self.stream(uri, destination, headers=None, use_proxy=True) def hgap_download(uri): - request_uri, request_headers = host_ga_plugin.get_artifact_request(uri, host_ga_plugin.manifest_uri) + request_uri, request_headers = host_ga_plugin.get_artifact_request(uri, use_verify_header=use_verify_header, artifact_manifest_url=host_ga_plugin.manifest_uri) return self.stream(request_uri, destination, headers=request_headers, use_proxy=False) self._download_with_fallback_channel("extension package", uris, direct_download=direct_download, hgap_download=hgap_download, on_downloaded=on_downloaded) @@ -808,30 +790,11 @@ def get_certs(self): raise ProtocolError("Trying to fetch Certificates before initialization!") return self._goal_state.certs - def get_ext_manifest(self, ext_handler): - if self._goal_state is None: - raise ProtocolError("Trying to fetch Extension Manifest before initialization!") - - try: - xml_text = self.fetch_manifest(ext_handler.manifest_uris) - self._goal_state.save_to_history(xml_text, _MANIFEST_FILE_NAME.format(ext_handler.name)) - return ExtensionManifest(xml_text) - except Exception as e: - raise ExtensionDownloadError("Failed to retrieve extension manifest. Error: {0}".format(ustr(e))) - def get_remote_access(self): if self._goal_state is None: raise ProtocolError("Trying to fetch Remote Access before initialization!") return self._goal_state.remote_access - def fetch_gafamily_manifest(self, vmagent_manifest, goal_state): - try: - xml_text = self.fetch_manifest(vmagent_manifest.uris) - goal_state.save_to_history(xml_text, _MANIFEST_FILE_NAME.format(vmagent_manifest.family)) - return ExtensionManifest(xml_text) - except Exception as e: - raise ProtocolError("Failed to retrieve GAFamily manifest. Error: {0}".format(ustr(e))) - def check_wire_protocol_version(self): uri = VERSION_INFO_URI.format(self.get_endpoint()) version_info_xml = self.fetch_config(uri, None) @@ -1186,48 +1149,6 @@ def get_supported(self): return self.supported -class ExtensionManifest(object): - def __init__(self, xml_text): - if xml_text is None: - raise ValueError("ExtensionManifest is None") - logger.verbose("Load ExtensionManifest.xml") - self.pkg_list = ExtHandlerPackageList() - self._parse(xml_text) - - def _parse(self, xml_text): - xml_doc = parse_doc(xml_text) - self._handle_packages(findall(find(xml_doc, - "Plugins"), - "Plugin"), - False) - self._handle_packages(findall(find(xml_doc, - "InternalPlugins"), - "Plugin"), - True) - - def _handle_packages(self, packages, isinternal): - for package in packages: - version = findtext(package, "Version") - - disallow_major_upgrade = findtext(package, - "DisallowMajorVersionUpgrade") - if disallow_major_upgrade is None: - disallow_major_upgrade = '' - disallow_major_upgrade = disallow_major_upgrade.lower() == "true" - - uris = find(package, "Uris") - uri_list = findall(uris, "Uri") - uri_list = [gettext(x) for x in uri_list] - pkg = ExtHandlerPackage() - pkg.version = version - pkg.disallow_major_upgrade = disallow_major_upgrade - for uri in uri_list: - pkg.uris.append(uri) - - pkg.isinternal = isinternal - self.pkg_list.versions.append(pkg) - - # Do not extend this class class InVMArtifactsProfile(object): """ diff --git a/azurelinuxagent/common/utils/archive.py b/azurelinuxagent/common/utils/archive.py index 1f8fdd9311..0385b6db9e 100644 --- a/azurelinuxagent/common/utils/archive.py +++ b/azurelinuxagent/common/utils/archive.py @@ -7,8 +7,8 @@ import shutil import zipfile -import azurelinuxagent.common.logger as logger -import azurelinuxagent.common.conf as conf +from azurelinuxagent.common import conf +from azurelinuxagent.common import logger from azurelinuxagent.common.utils import fileutil, timeutil # pylint: disable=W0105 @@ -306,3 +306,6 @@ def save_hosting_env(self, text): def save_shared_conf(self, text): self.save(text, SHARED_CONF_FILE_NAME) + + def save_manifest(self, name, text): + self.save(text, _MANIFEST_FILE_NAME.format(name)) diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py index d6b5ab7b97..99f3809446 100644 --- a/azurelinuxagent/ga/exthandlers.py +++ b/azurelinuxagent/ga/exthandlers.py @@ -949,9 +949,9 @@ def get_ext_handlers_status_debug_info(self, vm_status): if status_blob_text is None: status_blob_text = "" - support_multi_config = dict() + support_multi_config = {} vm_status_data = get_properties(vm_status) - vm_handler_statuses = vm_status_data.get('vmAgent', dict()).get('extensionHandlers') + vm_handler_statuses = vm_status_data.get('vmAgent', {}).get('extensionHandlers') for handler_status in vm_handler_statuses: if handler_status.get('name') is not None: support_multi_config[handler_status.get('name')] = handler_status.get('supports_multi_config') @@ -1105,7 +1105,8 @@ def __truncate_file_head(filename, max_size, extension_name): def decide_version(self, target_state=None, extension=None): self.logger.verbose("Decide which version to use") try: - pkg_list = self.protocol.get_ext_handler_pkgs(self.ext_handler) + manifest = self.protocol.get_goal_state().fetch_extension_manifest(self.ext_handler.name, self.ext_handler.manifest_uris) + pkg_list = manifest.pkg_list except ProtocolError as e: raise ExtensionError("Failed to get ext handler pkgs", e) except ExtensionDownloadError: @@ -1259,7 +1260,7 @@ def download(self): self.logger.info("The existing extension package is invalid, will ignore it.") if not package_exists: - self.protocol.client.download_extension(self.pkg.uris, destination, on_downloaded=lambda: self._unzip_extension_package(destination, self.get_base_dir())) + self.protocol.get_goal_state().download_extension(self.pkg.uris, destination, on_downloaded=lambda: self._unzip_extension_package(destination, self.get_base_dir())) self.report_event(message="Download succeeded", duration=elapsed_milliseconds(begin_utc)) self.pkg_file = destination diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index 68c0fb4eaa..0fac66f259 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -31,12 +31,10 @@ import zipfile from datetime import datetime, timedelta -import azurelinuxagent.common.conf as conf -import azurelinuxagent.common.logger as logger +from azurelinuxagent.common import conf +from azurelinuxagent.common import logger from azurelinuxagent.common.protocol.imds import get_imds_client -import azurelinuxagent.common.utils.fileutil as fileutil -import azurelinuxagent.common.utils.restutil as restutil -import azurelinuxagent.common.utils.textutil as textutil +from azurelinuxagent.common.utils import fileutil, restutil, textutil from azurelinuxagent.common.agent_supported_feature import get_supported_feature_by_name, SupportedFeatureNames from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator from azurelinuxagent.common.event import add_event, initialize_event_logger_vminfo_common_parameters, \ @@ -45,6 +43,7 @@ from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil, systemd from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler +from azurelinuxagent.common.protocol.goal_state import GoalStateSource from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol, VmSettingsNotSupported from azurelinuxagent.common.protocol.restapi import VMAgentUpdateStatus, VMAgentUpdateStatuses, ExtHandlerPackageList, \ VERSION_0 @@ -576,7 +575,7 @@ def handle_updates_for_requested_version(): # The call to get_latest_agent_greater_than_daemon() also finds all agents in directory and sets the self.agents property. # This state is used to find the GuestAgent object with the current version later if requested version is available in last GS. available_agent = self.get_latest_agent_greater_than_daemon() - requested_version, _ = self.__get_requested_version_and_manifest_from_last_gs(protocol) + requested_version, _ = self.__get_requested_version_and_agent_family_from_last_gs() if requested_version is not None: # If requested version specified, upgrade/downgrade to the specified version instantly as this is # driven by the goal state (as compared to the agent periodically checking for new upgrades every hour) @@ -662,7 +661,7 @@ def _cleanup_legacy_goal_state_history(): except Exception as exception: logger.warn("Error removing legacy history files: {0}", ustr(exception)) - def __get_vmagent_update_status(self, protocol, goal_state_changed): + def __get_vmagent_update_status(self, goal_state_changed): """ This function gets the VMAgent update status as per the last GoalState. Returns: None if the last GS does not ask for requested version else VMAgentUpdateStatus @@ -673,7 +672,7 @@ def __get_vmagent_update_status(self, protocol, goal_state_changed): update_status = None try: - requested_version, manifest = self.__get_requested_version_and_manifest_from_last_gs(protocol) + requested_version, manifest = self.__get_requested_version_and_agent_family_from_last_gs() if manifest is None and goal_state_changed: logger.info("Unable to report update status as no matching manifest found for family: {0}".format( conf.get_autoupdate_gafamily())) @@ -699,7 +698,7 @@ def __get_vmagent_update_status(self, protocol, goal_state_changed): return update_status def _report_status(self, exthandlers_handler): - vm_agent_update_status = self.__get_vmagent_update_status(exthandlers_handler.protocol, self._processing_new_extensions_goal_state()) + vm_agent_update_status = self.__get_vmagent_update_status(self._processing_new_extensions_goal_state()) # report_ext_handlers_status does its own error handling and returns None if an error occurred vm_status = exthandlers_handler.report_ext_handlers_status( goal_state_changed=self._processing_new_extensions_goal_state(), @@ -1068,22 +1067,21 @@ def _shutdown(self): str(e)) return - @staticmethod - def __get_requested_version_and_manifest_from_last_gs(protocol): + def __get_requested_version_and_agent_family_from_last_gs(self): """ Get the requested version and corresponding manifests from last GS if supported Returns: (Requested Version, Manifest) if supported and available (None, None) if no manifests found in the last GS (None, manifest) if not supported or not specified in GS """ - family = conf.get_autoupdate_gafamily() - manifest_list, _ = protocol.get_vmagent_manifests() - manifests = [m for m in manifest_list if m.family == family and len(m.uris) > 0] - if len(manifests) == 0: + family_name = conf.get_autoupdate_gafamily() + agent_families = self._goal_state.extensions_goal_state.agent_families + agent_families = [m for m in agent_families if m.name == family_name and len(m.uris) > 0] + if len(agent_families) == 0: return None, None - if conf.get_enable_ga_versioning() and manifests[0].is_requested_version_specified: - return manifests[0].requested_version, manifests[0] - return None, manifests[0] + if conf.get_enable_ga_versioning() and agent_families[0].is_requested_version_specified: + return agent_families[0].requested_version, agent_families[0] + return None, agent_families[0] def _download_agent_if_upgrade_available(self, protocol, base_version=CURRENT_VERSION): """ @@ -1130,18 +1128,18 @@ def agent_upgrade_time_elapsed(now_): return False return True - family = conf.get_autoupdate_gafamily() + agent_family_name = conf.get_autoupdate_gafamily() gs_updated = False daemon_version = self.__get_daemon_version_for_update() try: # Fetch the agent manifests from the latest Goal State goal_state_id = self._goal_state.extensions_goal_state.id gs_updated = self._processing_new_extensions_goal_state() - requested_version, manifest = self.__get_requested_version_and_manifest_from_last_gs(protocol) - if manifest is None: + requested_version, agent_family = self.__get_requested_version_and_agent_family_from_last_gs() + if agent_family is None: logger.verbose( u"No manifest links found for agent family: {0} for goal state {1}, skipping update check".format( - family, goal_state_id)) + agent_family_name, goal_state_id)) return False except Exception as err: # If there's some issues in fetching the agent manifests, report it only on goal state change @@ -1166,7 +1164,7 @@ def agent_upgrade_time_elapsed(now_): return False logger.info("No requested version specified, checking for all versions for agent update (family: {0})", - family) + agent_family_name) self.last_attempt_time = now try: @@ -1183,7 +1181,8 @@ def agent_upgrade_time_elapsed(now_): logger.info(msg) add_event(AGENT_NAME, op=WALAEventOperation.AgentUpgrade, is_success=True, message=msg) else: - pkg_list = protocol.get_vmagent_pkgs(manifest) + agent_manifest = self._goal_state.fetch_agent_manifest(agent_family.name, agent_family.uris) + pkg_list = agent_manifest.pkg_list packages_to_download = pkg_list.versions # Verify the requested version is in GA family manifest (if specified) @@ -1202,7 +1201,7 @@ def agent_upgrade_time_elapsed(now_): # Set the agents to those available for download at least as current as the existing agent # or to the requested version (if specified) host = self._get_host_plugin(protocol=protocol) - agents_to_download = [GuestAgent(pkg=pkg, host=host) for pkg in packages_to_download] + agents_to_download = [GuestAgent(is_fast_track_goal_state=self._goal_state.extensions_goal_state.source == GoalStateSource.FastTrack, pkg=pkg, host=host) for pkg in packages_to_download] # Filter out the agents that were downloaded/extracted successfully. If the agent was not installed properly, # we delete the directory and the zip package from the filesystem @@ -1468,7 +1467,16 @@ def _reset_legacy_blacklisted_agents(self): class GuestAgent(object): - def __init__(self, path=None, pkg=None, host=None): + def __init__(self, path=None, pkg=None, is_fast_track_goal_state=False, host=None): + """ + If 'path' is given, the object is initialized to the version installed under that path. + + If 'pkg' is given, the version specified in the package information is downloaded and the object is + initialized to that version. + + 'is_fast_track_goal_state' and 'host' are using only when a package is downloaded. + """ + self._is_fast_track_goal_state = is_fast_track_goal_state self.pkg = pkg self.host = host version = None @@ -1614,7 +1622,7 @@ def _download(self): else: logger.verbose("Using host plugin as default channel") - uri, headers = self.host.get_artifact_request(uri, self.host.manifest_uri) + uri, headers = self.host.get_artifact_request(uri, use_verify_header=self._is_fast_track_goal_state, artifact_manifest_url=self.host.manifest_uri) try: if self._fetch(uri, headers=headers, use_proxy=False, retry_codes=restutil.HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES): if not HostPluginProtocol.is_default_channel: @@ -1692,7 +1700,7 @@ def _load_manifest(self): try: manifests = json.load(manifest_file) except Exception as e: - msg = u"Agent {0} has a malformed {1}".format(self.name, AGENT_MANIFEST_FILE) + msg = u"Agent {0} has a malformed {1} ({2})".format(self.name, AGENT_MANIFEST_FILE, ustr(e)) raise UpdateError(msg) if type(manifests) is list: if len(manifests) <= 0: diff --git a/ci/3.6.pylintrc b/ci/3.6.pylintrc index 7b0712d93e..fcbae93831 100644 --- a/ci/3.6.pylintrc +++ b/ci/3.6.pylintrc @@ -3,18 +3,22 @@ [MESSAGES CONTROL] disable=C, # (C) convention, for programming standard violation + broad-except, # (W0703): *Catching too general exception %s* consider-using-dict-comprehension, # (R1717): *Consider using a dictionary comprehension* consider-using-in, # (R1714): *Consider merging these comparisons with "in" to %r* consider-using-set-comprehension, # (R1718): *Consider using a set comprehension* consider-using-with, # (R1732): *Emitted if a resource-allocating assignment or call may be replaced by a 'with' block* duplicate-code, # (R0801): *Similar lines in %s files* - no-init, # (W0232): Class has no __init__ method + fixme, # Used when a warning note as FIXME or TODO is detected no-else-break, # (R1723): *Unnecessary "%s" after "break"* no-else-continue, # (R1724): *Unnecessary "%s" after "continue"* no-else-raise, # (R1720): *Unnecessary "%s" after "raise"* no-else-return, # (R1705): *Unnecessary "%s" after "return"* + no-init, # (W0232): Class has no __init__ method no-self-use, # (R0201): *Method could be a function* protected-access, # (W0212): Access to a protected member of a client class + raise-missing-from, # (W0707): *Consider explicitly re-raising using the 'from' keyword* + redundant-u-string-prefix, # The u prefix for strings is no longer necessary in Python >=3.0 simplifiable-if-expression, # (R1719): *The if expression can be replaced with %s* simplifiable-if-statement, # (R1703): *The if statement can be replaced with %s* super-with-arguments, # (R1725): *Consider using Python 3 style super() without arguments* @@ -29,9 +33,8 @@ disable=C, # (C) convention, for programming standard violation too-many-public-methods, # (R0904): *Too many public methods (%s/%s)* too-many-return-statements, # (R0911): *Too many return statements (%s/%s)* too-many-statements, # (R0915): *Too many statements (%s/%s)* + unspecified-encoding, # (W1514): Using open without explicitly specifying an encoding + use-a-generator, # (R1729): *Use a generator instead '%s(%s)'* useless-object-inheritance, # (R0205): *Class %r inherits from object, can be safely removed from bases in python3* useless-return, # (R1711): *Useless return at end of function or method* - use-a-generator, # (R1729): *Use a generator instead '%s(%s)'* - broad-except, # (W0703): *Catching too general exception %s* - raise-missing-from, # (W0707): *Consider explicitly re-raising using the 'from' keyword* - fixme, # Used when a warning note as FIXME or TODO is detected + diff --git a/tests/ga/test_extension.py b/tests/ga/test_extension.py index ca502f3c22..1f261ee26f 100644 --- a/tests/ga/test_extension.py +++ b/tests/ga/test_extension.py @@ -1361,7 +1361,7 @@ def test_ext_handler_report_status_resource_gone(self, mock_add_event, *args): def test_ext_handler_download_failure_permanent_ProtocolError(self, mock_add_event, mock_error_state, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter - protocol.get_ext_handler_pkgs = Mock(side_effect=ProtocolError) + protocol.get_goal_state().fetch_extension_manifest = Mock(side_effect=ProtocolError) mock_error_state.return_value = True @@ -2496,7 +2496,7 @@ def _set_dependency_levels(self, dependency_levels, exthandlers_handler): """ Creates extensions with the given dependencyLevel """ - handler_map = dict() + handler_map = {} all_handlers = [] for handler_name, level in dependency_levels: if handler_map.get(handler_name) is None: @@ -3308,7 +3308,7 @@ def manifest_location_handler(url, **kwargs): wire._DOWNLOAD_TIMEOUT = datetime.timedelta(minutes=0) try: with self.assertRaises(ExtensionDownloadError): - protocol.client.fetch_manifest(ext_handlers[0].manifest_uris) + protocol.client.fetch_manifest(ext_handlers[0].manifest_uris, use_verify_header=False) finally: wire._DOWNLOAD_TIMEOUT = download_timeout diff --git a/tests/ga/test_exthandlers_download_extension.py b/tests/ga/test_exthandlers_download_extension.py index e3212ae14f..3a9683889f 100644 --- a/tests/ga/test_exthandlers_download_extension.py +++ b/tests/ga/test_exthandlers_download_extension.py @@ -10,6 +10,8 @@ from azurelinuxagent.common.protocol.restapi import Extension, ExtHandlerPackage from azurelinuxagent.common.protocol.wire import WireProtocol from azurelinuxagent.ga.exthandlers import ExtHandlerInstance, ExtHandlerState +from tests.protocol import mockwiredata +from tests.protocol.mocks import mock_wire_protocol from tests.tools import AgentTestCase, patch, Mock @@ -39,6 +41,12 @@ def setUp(self): protocol.client.get_host_plugin = Mock() protocol.client.get_artifact_request = Mock(return_value=(None, None)) + # create a dummy goal state, since downloads are done via the GoalState class + with mock_wire_protocol(mockwiredata.DATA_FILE) as p: + goal_state = p.get_goal_state() + goal_state._wire_client = protocol.client + protocol.client._goal_state = goal_state + self.pkg = ExtHandlerPackage() self.pkg.uris = [ 'https://zrdfepirv2cy4prdstr00a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712-foobar/Microsoft.CPlat.Core__RunCommandLinux__1.0.0', diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index edcf669d62..695b6d578d 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -33,7 +33,7 @@ from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler from azurelinuxagent.common.protocol.hostplugin import URI_FORMAT_GET_API_VERSIONS, HOST_PLUGIN_PORT, \ URI_FORMAT_GET_EXTENSION_ARTIFACT, HostPluginProtocol -from azurelinuxagent.common.protocol.restapi import VMAgentManifest, \ +from azurelinuxagent.common.protocol.restapi import VMAgentFamily, \ ExtHandlerPackage, ExtHandlerPackageList, Extension, VMStatus, ExtHandlerStatus, ExtensionStatus, \ VMAgentUpdateStatuses from azurelinuxagent.common.protocol.util import ProtocolUtil @@ -194,7 +194,7 @@ def rename_agent_bin(self, path, dst_v): shutil.move(src_bin, dst_bin) def agents(self): - return [GuestAgent(path=path) for path in self.agent_dirs()] + return [GuestAgent(is_fast_track_goal_state=False, path=path) for path in self.agent_dirs()] def agent_count(self): return len(self.agent_dirs()) @@ -313,7 +313,7 @@ def replicate_agents(self, shutil.copytree(from_path, to_path) self.rename_agent_bin(to_path, dst_v) if not is_available: - GuestAgent(to_path).mark_failure(is_fatal=True) + GuestAgent(is_fast_track_goal_state=False, path=to_path).mark_failure(is_fatal=True) return dst_v @@ -411,7 +411,7 @@ def test_creation(self): self.expand_agents() - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) self.assertNotEqual(None, agent) self.assertEqual(self._get_agent_name(), agent.name) self.assertEqual(self._get_agent_version(), agent.version) @@ -436,7 +436,7 @@ def test_creation(self): def test_clear_error(self, mock_downloaded): # pylint: disable=unused-argument self.expand_agents() - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) agent.mark_failure(is_fatal=True) self.assertTrue(agent.error.last_failure > 0.0) @@ -453,7 +453,7 @@ def test_clear_error(self, mock_downloaded): # pylint: disable=unused-argument @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") def test_is_available(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) self.assertFalse(agent.is_available) agent._unpack() @@ -465,7 +465,7 @@ def test_is_available(self, mock_loaded, mock_downloaded): # pylint: disable=un @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") def test_is_blacklisted(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) self.assertFalse(agent.is_blacklisted) agent._unpack() @@ -481,7 +481,7 @@ def test_is_blacklisted(self, mock_loaded, mock_downloaded): # pylint: disable= def test_resource_gone_error_not_blacklisted(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument try: mock_downloaded.side_effect = ResourceGoneError() - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) self.assertFalse(agent.is_blacklisted) except ResourceGoneError: pass @@ -493,7 +493,7 @@ def test_resource_gone_error_not_blacklisted(self, mock_loaded, mock_downloaded) def test_ioerror_not_blacklisted(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument try: mock_downloaded.side_effect = IOError() - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) self.assertFalse(agent.is_blacklisted) except IOError: pass @@ -503,7 +503,7 @@ def test_ioerror_not_blacklisted(self, mock_loaded, mock_downloaded): # pylint: @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") def test_is_downloaded(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) self.assertFalse(agent.is_downloaded) agent._unpack() self.assertTrue(agent.is_downloaded) @@ -511,7 +511,7 @@ def test_is_downloaded(self, mock_loaded, mock_downloaded): # pylint: disable=u @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") def test_mark_failure(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) agent.mark_failure() self.assertEqual(1, agent.error.failure_count) @@ -523,7 +523,7 @@ def test_mark_failure(self, mock_loaded, mock_downloaded): # pylint: disable=un @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") def test_unpack(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) self.assertFalse(os.path.isdir(agent.get_agent_dir())) agent._unpack() self.assertTrue(os.path.isdir(agent.get_agent_dir())) @@ -532,7 +532,7 @@ def test_unpack(self, mock_loaded, mock_downloaded): # pylint: disable=unused-a @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") def test_unpack_fail(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) self.assertFalse(os.path.isdir(agent.get_agent_dir())) os.remove(agent.get_agent_pkg_path()) self.assertRaises(UpdateError, agent._unpack) @@ -540,7 +540,7 @@ def test_unpack_fail(self, mock_loaded, mock_downloaded): # pylint: disable=unu @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") def test_load_manifest(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) agent._unpack() agent._load_manifest() self.assertEqual(agent.manifest.get_enable_command(), @@ -549,7 +549,7 @@ def test_load_manifest(self, mock_loaded, mock_downloaded): # pylint: disable=u @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") def test_load_manifest_missing(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) self.assertFalse(os.path.isdir(agent.get_agent_dir())) agent._unpack() os.remove(agent.get_agent_manifest_path()) @@ -558,7 +558,7 @@ def test_load_manifest_missing(self, mock_loaded, mock_downloaded): # pylint: d @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") def test_load_manifest_is_empty(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) self.assertFalse(os.path.isdir(agent.get_agent_dir())) agent._unpack() self.assertTrue(os.path.isfile(agent.get_agent_manifest_path())) @@ -570,7 +570,7 @@ def test_load_manifest_is_empty(self, mock_loaded, mock_downloaded): # pylint: @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") def test_load_manifest_is_malformed(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) self.assertFalse(os.path.isdir(agent.get_agent_dir())) agent._unpack() self.assertTrue(os.path.isfile(agent.get_agent_manifest_path())) @@ -580,7 +580,7 @@ def test_load_manifest_is_malformed(self, mock_loaded, mock_downloaded): # pyli self.assertRaises(UpdateError, agent._load_manifest) def test_load_error(self): - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) agent.error = None agent._load_error() @@ -598,7 +598,7 @@ def test_download(self, mock_http_get, mock_loaded, mock_downloaded): # pylint: pkg = ExtHandlerPackage(version=str(self._get_agent_version())) pkg.uris.append(None) - agent = GuestAgent(pkg=pkg) + agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) agent._download() self.assertTrue(os.path.isfile(agent.get_agent_pkg_path())) @@ -614,7 +614,7 @@ def test_download_fail(self, mock_http_get, mock_loaded, mock_downloaded): # py pkg = ExtHandlerPackage(version=str(self._get_agent_version())) pkg.uris.append(None) - agent = GuestAgent(pkg=pkg) + agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) self.assertRaises(UpdateError, agent._download) self.assertFalse(os.path.isfile(agent.get_agent_pkg_path())) @@ -640,7 +640,7 @@ def test_download_fallback(self, mock_http_post, mock_http_get, mock_loaded, moc pkg = ExtHandlerPackage(version=str(self._get_agent_version())) pkg.uris.append(ext_uri) - agent = GuestAgent(pkg=pkg) + agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) agent.host = mock_host # ensure fallback fails gracefully, no http @@ -689,7 +689,7 @@ def test_ensure_downloaded(self, mock_http_get): pkg = ExtHandlerPackage(version=str(self._get_agent_version())) pkg.uris.append(None) - agent = GuestAgent(pkg=pkg) + agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) self.assertTrue(os.path.isfile(agent.get_agent_manifest_path())) self.assertTrue(agent.is_downloaded) @@ -701,7 +701,7 @@ def test_ensure_failure_in_download_cleans_up_filesystem(self, _): pkg = ExtHandlerPackage(version=str(self._get_agent_version())) pkg.uris.append(None) - agent = GuestAgent(pkg=pkg) + agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) self.assertFalse(agent.is_blacklisted, "The agent should not be blacklisted if unable to unpack/download") self.assertFalse(os.path.exists(agent.get_agent_dir()), "Agent directory should be cleaned up") @@ -714,7 +714,7 @@ def test_ensure_downloaded_unpack_failure_cleans_file_system(self, *_): pkg = ExtHandlerPackage(version=str(self._get_agent_version())) pkg.uris.append(None) - agent = GuestAgent(pkg=pkg) + agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) self.assertFalse(agent.is_blacklisted, "The agent should not be blacklisted if unable to unpack/download") self.assertFalse(os.path.exists(agent.get_agent_dir()), "Agent directory should be cleaned up") @@ -728,7 +728,7 @@ def test_ensure_downloaded_load_manifest_cleans_up_agent_directories(self, *_): pkg = ExtHandlerPackage(version=str(self._get_agent_version())) pkg.uris.append(None) - agent = GuestAgent(pkg=pkg) + agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) self.assertFalse(agent.is_blacklisted, "The agent should not be blacklisted if unable to unpack/download") self.assertFalse(os.path.exists(agent.get_agent_dir()), "Agent directory should be cleaned up") @@ -738,7 +738,7 @@ def test_ensure_downloaded_load_manifest_cleans_up_agent_directories(self, *_): @patch("azurelinuxagent.ga.update.GuestAgent._unpack") @patch("azurelinuxagent.ga.update.GuestAgent._load_manifest") def test_ensure_download_skips_blacklisted(self, mock_manifest, mock_unpack, mock_download): # pylint: disable=unused-argument - agent = GuestAgent(path=self.agent_path) + agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) self.assertEqual(0, mock_download.call_count) agent.clear_error() @@ -747,7 +747,7 @@ def test_ensure_download_skips_blacklisted(self, mock_manifest, mock_unpack, moc pkg = ExtHandlerPackage(version=str(self._get_agent_version())) pkg.uris.append(None) - agent = GuestAgent(pkg=pkg) + agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) self.assertEqual(1, agent.error.failure_count) self.assertTrue(agent.error.was_fatal) @@ -764,6 +764,9 @@ def setUp(self): protocol = Mock() self.update_handler.protocol_util = Mock() self.update_handler.protocol_util.get_protocol = Mock(return_value=protocol) + self.update_handler._goal_state = Mock() + self.update_handler._goal_state.extensions_goal_state = Mock() + self.update_handler._goal_state.extensions_goal_state.source = "Fabric" # Since ProtocolUtil is a singleton per thread, we need to clear it to ensure that the test cases do not reuse # a previous state @@ -951,7 +954,7 @@ def test_evaluate_agent_health_resets_with_new_agent(self): def test_filter_blacklisted_agents(self): self.prepare_agents() - self.update_handler._set_and_sort_agents([GuestAgent(path=path) for path in self.agent_dirs()]) + self.update_handler._set_and_sort_agents([GuestAgent(is_fast_track_goal_state=False, path=path) for path in self.agent_dirs()]) self.assertEqual(len(self.agent_dirs()), len(self.update_handler.agents)) kept_agents = self.update_handler.agents[::2] @@ -1022,7 +1025,7 @@ def test_get_latest_agent_skips_unavailable(self): latest_version = self.prepare_agents(count=self.agent_count() + 1, is_available=False) latest_path = os.path.join(self.tmp_dir, "{0}-{1}".format(AGENT_NAME, latest_version)) - self.assertFalse(GuestAgent(latest_path).is_available) + self.assertFalse(GuestAgent(is_fast_track_goal_state=False, path=latest_path).is_available) latest_agent = self.update_handler.get_latest_agent_greater_than_daemon() self.assertTrue(latest_agent.version < latest_version) @@ -1297,14 +1300,14 @@ def test_get_latest_agent_should_return_latest_agent_even_on_bad_error_json(self def test_set_agents_sets_agents(self): self.prepare_agents() - self.update_handler._set_and_sort_agents([GuestAgent(path=path) for path in self.agent_dirs()]) + self.update_handler._set_and_sort_agents([GuestAgent(is_fast_track_goal_state=False, path=path) for path in self.agent_dirs()]) self.assertTrue(len(self.update_handler.agents) > 0) self.assertEqual(len(self.agent_dirs()), len(self.update_handler.agents)) def test_set_agents_sorts_agents(self): self.prepare_agents() - self.update_handler._set_and_sort_agents([GuestAgent(path=path) for path in self.agent_dirs()]) + self.update_handler._set_and_sort_agents([GuestAgent(is_fast_track_goal_state=False, path=path) for path in self.agent_dirs()]) v = FlexibleVersion("100000") for a in self.update_handler.agents: @@ -1370,7 +1373,7 @@ def test_upgrade_available_handles_missing_family(self): with mock_wire_protocol(data_file) as protocol: self.update_handler.protocol_util = protocol with patch('azurelinuxagent.common.logger.warn') as mock_logger: - with patch('tests.ga.test_update.ProtocolMock.get_vmagent_pkgs', side_effect=ProtocolError): + with patch('azurelinuxagent.common.protocol.goal_state.GoalState.fetch_agent_manifest', side_effect=ProtocolError): self.assertFalse(self.update_handler._download_agent_if_upgrade_available(protocol, base_version=CURRENT_VERSION)) self.assertEqual(0, mock_logger.call_count) @@ -2522,15 +2525,42 @@ def __init__(self, return_value=0, side_effect=None): self.wait = Mock(return_value=return_value, side_effect=side_effect) -class ExtensionsGoalStateMock(object): - def __init__(self, identifier): - self.id = identifier - - class GoalStateMock(object): - def __init__(self, incarnation): + def __init__(self, incarnation, family, versions): + if versions is None: + versions = [] + self.incarnation = incarnation - self.extensions_goal_state = ExtensionsGoalStateMock(incarnation) + self.extensions_goal_state = Mock() + self.extensions_goal_state.id = incarnation + self.extensions_goal_state.agent_families = GoalStateMock._create_agent_families(family, versions) + + agent_manifest = Mock() + agent_manifest.pkg_list = GoalStateMock._create_packages(versions) + self.fetch_agent_manifest = Mock(return_value=agent_manifest) + + @staticmethod + def _create_agent_families(family, versions): + families = [] + + if len(versions) > 0 and family is not None: + manifest = VMAgentFamily(name=family) + for i in range(0, 10): + manifest.uris.append("https://nowhere.msft/agent/{0}".format(i)) + families.append(manifest) + + return families + + @staticmethod + def _create_packages(versions): + packages = ExtHandlerPackageList() + for version in versions: + package = ExtHandlerPackage(str(version)) + for i in range(0, 5): + package_uri = "https://nowhere.msft/agent_pkg/{0}".format(i) + package.uris.append(package_uri) + packages.versions.append(package) + return packages class ProtocolMock(object): @@ -2538,63 +2568,22 @@ def __init__(self, family="TestAgent", etag=42, versions=None, client=None): self.family = family self.client = client self.call_counts = { - "get_vmagent_manifests": 0, - "get_vmagent_pkgs": 0, "update_goal_state": 0 } - self._goal_state = GoalStateMock(etag) + self._goal_state = GoalStateMock(etag, family, versions) self.goal_state_is_stale = False self.etag = etag self.versions = versions if versions is not None else [] - self.create_manifests() - self.create_packages() def emulate_stale_goal_state(self): self.goal_state_is_stale = True - def create_manifests(self): - self.agent_manifests = [] - if len(self.versions) <= 0: - return - - if self.family is not None: - manifest = VMAgentManifest(family=self.family) - for i in range(0, 10): - manifest.uris.append("https://nowhere.msft/agent/{0}".format(i)) - self.agent_manifests.append(manifest) - - def create_packages(self): - self.agent_packages = ExtHandlerPackageList() - if len(self.versions) <= 0: - return - - for version in self.versions: - package = ExtHandlerPackage(str(version)) - for i in range(0, 5): - package_uri = "https://nowhere.msft/agent_pkg/{0}".format(i) - package.uris.append(package_uri) - self.agent_packages.versions.append(package) - def get_protocol(self): return self def get_goal_state(self): return self._goal_state - def get_vmagent_manifests(self): - self.call_counts["get_vmagent_manifests"] += 1 - if self.goal_state_is_stale: - self.goal_state_is_stale = False - raise ResourceGoneError() - return self.agent_manifests, self.etag - - def get_vmagent_pkgs(self, manifest): # pylint: disable=unused-argument - self.call_counts["get_vmagent_pkgs"] += 1 - if self.goal_state_is_stale: - self.goal_state_is_stale = False - raise ResourceGoneError() - return self.agent_packages - def update_goal_state(self): self.call_counts["update_goal_state"] += 1 diff --git a/tests/protocol/test_extensions_goal_state_from_vm_settings.py b/tests/protocol/test_extensions_goal_state_from_vm_settings.py index 8cdfa81bf9..fb97a075f6 100644 --- a/tests/protocol/test_extensions_goal_state_from_vm_settings.py +++ b/tests/protocol/test_extensions_goal_state_from_vm_settings.py @@ -2,6 +2,7 @@ # Licensed under the Apache License. import json +from azurelinuxagent.common.protocol.goal_state import GoalState from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateChannel from azurelinuxagent.common.protocol.extensions_goal_state_from_vm_settings import _CaseFoldedDict from tests.protocol.mocks import mockwiredata, mock_wire_protocol @@ -28,12 +29,12 @@ def assert_property(name, value): # for the rest of the attributes, we check only 1 item in each container (but check the length of the container) # - # agent manifests - self.assertEqual(2, len(extensions_goal_state.agent_manifests), "Incorrect number of agent manifests. Got: {0}".format(extensions_goal_state.agent_manifests)) - self.assertEqual("Prod", extensions_goal_state.agent_manifests[0].family, "Incorrect agent family.") - self.assertEqual(2, len(extensions_goal_state.agent_manifests[0].uris), "Incorrect number of uris. Got: {0}".format(extensions_goal_state.agent_manifests[0].uris)) + # agent families + self.assertEqual(2, len(extensions_goal_state.agent_families), "Incorrect number of agent families. Got: {0}".format(extensions_goal_state.agent_families)) + self.assertEqual("Prod", extensions_goal_state.agent_families[0].name, "Incorrect agent family.") + self.assertEqual(2, len(extensions_goal_state.agent_families[0].uris), "Incorrect number of uris. Got: {0}".format(extensions_goal_state.agent_families[0].uris)) expected = "https://zrdfepirv2cdm03prdstr01a.blob.core.windows.net/7d89d439b79f4452950452399add2c90/Microsoft.OSTCLinuxAgent_Prod_uscentraleuap_manifest.xml" - self.assertEqual(expected, extensions_goal_state.agent_manifests[0].uris[0], "Unexpected URI for the agent manifest.") + self.assertEqual(expected, extensions_goal_state.agent_families[0].uris[0], "Unexpected URI for the agent manifest.") # extensions self.assertEqual(5, len(extensions_goal_state.extensions), "Incorrect number of extensions. Got: {0}".format(extensions_goal_state.extensions)) @@ -49,16 +50,18 @@ def assert_property(name, value): def test_it_should_parse_requested_version_properly(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: - manifests, _ = protocol.get_vmagent_manifests() - for manifest in manifests: - self.assertEqual(manifest.requested_version_string, "0.0.0.0", "Version should be None") + goal_state = GoalState(protocol.client) + families = goal_state.extensions_goal_state.agent_families + for family in families: + self.assertEqual(family.requested_version_string, "0.0.0.0", "Version should be None") data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() data_file["vm_settings"] = "hostgaplugin/vm_settings-requested_version.json" with mock_wire_protocol(data_file) as protocol: - manifests, _ = protocol.get_vmagent_manifests() - for manifest in manifests: - self.assertEqual(manifest.requested_version_string, "9.9.9.9", "Version should be 9.9.9.9") + goal_state = GoalState(protocol.client) + families = goal_state.extensions_goal_state.agent_families + for family in families: + self.assertEqual(family.requested_version_string, "9.9.9.9", "Version should be 9.9.9.9") def test_it_should_parse_missing_status_upload_blob_as_none(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() @@ -74,8 +77,8 @@ def test_it_should_parse_missing_agent_manifests_as_empty(self): data_file["vm_settings"] = "hostgaplugin/vm_settings-no_manifests.json" with mock_wire_protocol(data_file) as protocol: extensions_goal_state = protocol.get_goal_state().extensions_goal_state - self.assertEqual(1, len(extensions_goal_state.agent_manifests), "Expected exactly one agent manifest. Got: {0}".format(extensions_goal_state.agent_manifests)) - self.assertListEqual([], extensions_goal_state.agent_manifests[0].uris, "Expected an empty list of agent manifests") + self.assertEqual(1, len(extensions_goal_state.agent_families), "Expected exactly one agent manifest. Got: {0}".format(extensions_goal_state.agent_families)) + self.assertListEqual([], extensions_goal_state.agent_families[0].uris, "Expected an empty list of agent manifests") def test_it_should_parse_missing_extension_manifests_as_empty(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() diff --git a/tests/protocol/test_hostplugin.py b/tests/protocol/test_hostplugin.py index 9f96f7d554..42d8579d52 100644 --- a/tests/protocol/test_hostplugin.py +++ b/tests/protocol/test_hostplugin.py @@ -23,9 +23,7 @@ import sys import unittest -import azurelinuxagent.common.protocol.hostplugin as hostplugin -import azurelinuxagent.common.protocol.restapi as restapi -import azurelinuxagent.common.protocol.wire as wire +from azurelinuxagent.common.protocol import hostplugin, restapi, wire from azurelinuxagent.common import conf from azurelinuxagent.common.errorstate import ErrorState from azurelinuxagent.common.exception import HttpError, ResourceGoneError, ProtocolError @@ -609,7 +607,7 @@ def test_validate_get_extension_artifacts(self): self.assertTrue(host_client.health_service is not None) with patch.object(wire.HostPluginProtocol, "get_api_versions", return_value=api_versions) as patch_get: # pylint: disable=unused-variable - actual_url, actual_headers = host_client.get_artifact_request(sas_url) + actual_url, actual_headers = host_client.get_artifact_request(sas_url, use_verify_header=False) self.assertTrue(host_client.is_initialized) self.assertFalse(host_client.api_versions is None) self.assertEqual(expected_url, actual_url) diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py index e96cf598a6..0cfaa95fbf 100644 --- a/tests/protocol/test_wire.py +++ b/tests/protocol/test_wire.py @@ -91,7 +91,7 @@ def _test_getters(self, test_data, certsMustBePresent, __, MockCryptUtil, _): protocol.get_certs() ext_handlers = protocol.get_goal_state().extensions_goal_state.extensions for ext_handler in ext_handlers: - protocol.get_ext_handler_pkgs(ext_handler) + protocol.get_goal_state().fetch_extension_manifest(ext_handler.name, ext_handler.manifest_uris) crt1 = os.path.join(self.tmp_dir, '38B85D88F03D1A8E1C671EB169274C09BC4D4703.crt') @@ -471,9 +471,9 @@ def test_get_ext_conf_without_extensions_should_retrieve_vmagent_manifests_info( ext_handlers_names = [ext_handler.name for ext_handler in extensions_goal_state.extensions] self.assertEqual(0, len(extensions_goal_state.extensions), "Unexpected number of extension handlers in the extension config: [{0}]".format(ext_handlers_names)) - vmagent_manifests = [manifest.family for manifest in extensions_goal_state.agent_manifests] - self.assertEqual(0, len(extensions_goal_state.agent_manifests), - "Unexpected number of vmagent manifests in the extension config: [{0}]".format(vmagent_manifests)) + vmagent_families = [manifest.name for manifest in extensions_goal_state.agent_families] + self.assertEqual(0, len(extensions_goal_state.agent_families), + "Unexpected number of vmagent manifests in the extension config: [{0}]".format(vmagent_families)) self.assertFalse(extensions_goal_state.on_hold, "Extensions On Hold is expected to be False") @@ -486,9 +486,9 @@ def test_get_ext_conf_with_extensions_should_retrieve_ext_handlers_and_vmagent_m ext_handlers_names = [ext_handler.name for ext_handler in extensions_goal_state.extensions] self.assertEqual(1, len(extensions_goal_state.extensions), "Unexpected number of extension handlers in the extension config: [{0}]".format(ext_handlers_names)) - vmagent_manifests = [manifest.family for manifest in extensions_goal_state.agent_manifests] - self.assertEqual(2, len(extensions_goal_state.agent_manifests), - "Unexpected number of vmagent manifests in the extension config: [{0}]".format(vmagent_manifests)) + vmagent_families = [manifest.name for manifest in extensions_goal_state.agent_families] + self.assertEqual(2, len(extensions_goal_state.agent_families), + "Unexpected number of vmagent manifests in the extension config: [{0}]".format(vmagent_families)) self.assertEqual("https://test.blob.core.windows.net/vhds/test-cs12.test-cs12.test-cs12.status?sr=b&sp=rw" "&se=9999-01-01&sk=key1&sv=2014-02-14&sig=hfRh7gzUE7sUtYwke78IOlZOrTRCYvkec4hGZ9zZzXo", extensions_goal_state.status_upload_blob, "Unexpected value for status upload blob URI") @@ -511,7 +511,7 @@ def http_get_handler(url, *_, **__): with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: HostPluginProtocol.is_default_channel = False - protocol.client.download_extension([extension_url], target_file) + protocol.client.download_extension([extension_url], target_file, use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 1, "Unexpected number of HTTP requests: [{0}]".format(urls)) @@ -533,7 +533,7 @@ def http_get_handler(url, *_, **kwargs): with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: HostPluginProtocol.is_default_channel = False - protocol.client.download_extension([extension_url], target_file) + protocol.client.download_extension([extension_url], target_file, use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 2, "Unexpected number of HTTP requests: [{0}]".format(urls)) @@ -569,7 +569,7 @@ def http_get_handler(url, *_, **kwargs): protocol.set_http_handlers(http_get_handler=http_get_handler) - protocol.client.download_extension([extension_url], target_file) + protocol.client.download_extension([extension_url], target_file, use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 4, "Unexpected number of HTTP requests: [{0}]".format(urls)) @@ -602,7 +602,7 @@ def http_get_handler(url, *_, **kwargs): protocol.set_http_handlers(http_get_handler=http_get_handler) with self.assertRaises(ExtensionDownloadError): - protocol.client.download_extension([extension_url], target_file) + protocol.client.download_extension([extension_url], target_file, use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 2, "Unexpected number of HTTP requests: [{0}]".format(urls)) @@ -625,7 +625,7 @@ def http_get_handler(url, *_, **__): with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: HostPluginProtocol.is_default_channel = False - manifest = protocol.client.fetch_manifest([manifest_url]) + manifest = protocol.client.fetch_manifest([manifest_url], use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(manifest, manifest_xml, 'The expected manifest was not downloaded') @@ -648,7 +648,7 @@ def http_get_handler(url, *_, **kwargs): HostPluginProtocol.is_default_channel = False try: - manifest = protocol.client.fetch_manifest([manifest_url]) + manifest = protocol.client.fetch_manifest([manifest_url], use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(manifest, manifest_xml, 'The expected manifest was not downloaded') @@ -685,7 +685,7 @@ def http_get_handler(url, *_, **kwargs): protocol.client.get_host_plugin() protocol.set_http_handlers(http_get_handler=http_get_handler) - manifest = protocol.client.fetch_manifest([manifest_url]) + manifest = protocol.client.fetch_manifest([manifest_url], use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(manifest, manifest_xml) @@ -719,7 +719,7 @@ def http_get_handler(url, *_, **kwargs): protocol.set_http_handlers(http_get_handler=http_get_handler) with self.assertRaises(ExtensionDownloadError): - protocol.client.fetch_manifest([manifest_url]) + protocol.client.fetch_manifest([manifest_url], use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 4, "Unexpected number of HTTP requests: [{0}]".format(urls)) From 12f444b22ddc3defe041d6944260cf9e6a33aae1 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Fri, 9 Sep 2022 15:19:17 -0700 Subject: [PATCH 46/58] use 30 secs timeout for put vmagent log api (#2662) * use 30 secs timeout for put vmagent log api * address CR comments * fix unit tests --- azurelinuxagent/common/protocol/hostplugin.py | 3 +- azurelinuxagent/common/utils/restutil.py | 40 ++++++++++++------- tests/protocol/mocks.py | 6 +-- tests/protocol/test_goal_state.py | 2 +- tests/protocol/test_wire.py | 2 +- tests/utils/test_rest_util.py | 8 ++-- 6 files changed, 37 insertions(+), 24 deletions(-) diff --git a/azurelinuxagent/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py index 56cbafc11e..55997d6490 100644 --- a/azurelinuxagent/common/protocol/hostplugin.py +++ b/azurelinuxagent/common/protocol/hostplugin.py @@ -269,7 +269,8 @@ def put_vm_log(self, content): response = restutil.http_put(url, data=content, headers=self._build_log_headers(), - redact_data=True) + redact_data=True, + timeout=30) if restutil.request_failed(response): error_response = restutil.read_response_error(response) diff --git a/azurelinuxagent/common/utils/restutil.py b/azurelinuxagent/common/utils/restutil.py index 8c2fc4e4e6..2180a376ae 100644 --- a/azurelinuxagent/common/utils/restutil.py +++ b/azurelinuxagent/common/utils/restutil.py @@ -309,7 +309,7 @@ def redact_sas_tokens_in_urls(url): return SAS_TOKEN_RETRIEVAL_REGEX.sub(r"\1" + REDACTED_TEXT + r"\3", url) -def _http_request(method, host, rel_uri, port=None, data=None, secure=False, +def _http_request(method, host, rel_uri, timeout, port=None, data=None, secure=False, headers=None, proxy_host=None, proxy_port=None, redact_data=False): headers = {} if headers is None else headers @@ -334,13 +334,13 @@ def _http_request(method, host, rel_uri, port=None, data=None, secure=False, if secure: conn = httpclient.HTTPSConnection(conn_host, conn_port, - timeout=10) + timeout=timeout) if use_proxy: conn.set_tunnel(host, port) else: conn = httpclient.HTTPConnection(conn_host, conn_port, - timeout=10) + timeout=timeout) payload = data if redact_data: @@ -358,7 +358,8 @@ def _http_request(method, host, rel_uri, port=None, data=None, secure=False, def http_request(method, - url, data, headers=None, + url, data, timeout, + headers=None, use_proxy=False, max_retry=None, retry_codes=None, @@ -446,6 +447,7 @@ def http_request(method, resp = _http_request(method, host, rel_uri, + timeout, port=port, data=data, secure=secure, @@ -510,7 +512,8 @@ def http_get(url, max_retry=None, retry_codes=None, retry_delay=DELAY_IN_SECONDS, - return_raw_response=False): + return_raw_response=False, + timeout=10): """ NOTE: This method provides some logic to handle errors in the HTTP request, including checking the HTTP status of the response and handling some exceptions. If return_raw_response is set to True all the error handling will be skipped and the @@ -523,7 +526,8 @@ def http_get(url, if retry_codes is None: retry_codes = RETRY_CODES return http_request("GET", - url, None, headers=headers, + url, None, timeout, + headers=headers, use_proxy=use_proxy, max_retry=max_retry, retry_codes=retry_codes, @@ -536,14 +540,16 @@ def http_head(url, use_proxy=False, max_retry=None, retry_codes=None, - retry_delay=DELAY_IN_SECONDS): + retry_delay=DELAY_IN_SECONDS, + timeout=10): if max_retry is None: max_retry = DEFAULT_RETRIES if retry_codes is None: retry_codes = RETRY_CODES return http_request("HEAD", - url, None, headers=headers, + url, None, timeout, + headers=headers, use_proxy=use_proxy, max_retry=max_retry, retry_codes=retry_codes, @@ -556,14 +562,16 @@ def http_post(url, use_proxy=False, max_retry=None, retry_codes=None, - retry_delay=DELAY_IN_SECONDS): + retry_delay=DELAY_IN_SECONDS, + timeout=10): if max_retry is None: max_retry = DEFAULT_RETRIES if retry_codes is None: retry_codes = RETRY_CODES return http_request("POST", - url, data, headers=headers, + url, data, timeout, + headers=headers, use_proxy=use_proxy, max_retry=max_retry, retry_codes=retry_codes, @@ -577,14 +585,16 @@ def http_put(url, max_retry=None, retry_codes=None, retry_delay=DELAY_IN_SECONDS, - redact_data=False): + redact_data=False, + timeout=10): if max_retry is None: max_retry = DEFAULT_RETRIES if retry_codes is None: retry_codes = RETRY_CODES return http_request("PUT", - url, data, headers=headers, + url, data, timeout, + headers=headers, use_proxy=use_proxy, max_retry=max_retry, retry_codes=retry_codes, @@ -597,14 +607,16 @@ def http_delete(url, use_proxy=False, max_retry=None, retry_codes=None, - retry_delay=DELAY_IN_SECONDS): + retry_delay=DELAY_IN_SECONDS, + timeout=10): if max_retry is None: max_retry = DEFAULT_RETRIES if retry_codes is None: retry_codes = RETRY_CODES return http_request("DELETE", - url, None, headers=headers, + url, None, timeout, + headers=headers, use_proxy=use_proxy, max_retry=max_retry, retry_codes=retry_codes, diff --git a/tests/protocol/mocks.py b/tests/protocol/mocks.py index 8b69580637..b74138888b 100644 --- a/tests/protocol/mocks.py +++ b/tests/protocol/mocks.py @@ -70,10 +70,10 @@ def http_handlers(get, post, put): # original_http_request = restutil.http_request - def http_request(method, url, data, **kwargs): + def http_request(method, url, data, timeout, **kwargs): # call the original resutil.http_request if the request should be mocked if protocol.do_not_mock(method, url): - return original_http_request(method, url, data, **kwargs) + return original_http_request(method, url, data, timeout, **kwargs) # if there is a handler for the request, use it handler = None @@ -109,7 +109,7 @@ def http_request(method, url, data, **kwargs): # if there was not a response for the request then fail it or call the original resutil.http_request if fail_on_unknown_request: raise ValueError('Unknown HTTP request: {0} [{1}]'.format(url, method)) - return original_http_request(method, url, data, **kwargs) + return original_http_request(method, url, data, timeout, **kwargs) # # functions to start/stop the mocks diff --git a/tests/protocol/test_goal_state.py b/tests/protocol/test_goal_state.py index 87a1db50e1..d853363c73 100644 --- a/tests/protocol/test_goal_state.py +++ b/tests/protocol/test_goal_state.py @@ -68,7 +68,7 @@ def test_it_should_retry_get_vm_settings_on_resource_gone_error(self): request_headers = [] # we expect a retry with new headers and use this array to persist the headers of each request - def http_get_vm_settings(_method, _host, _relative_url, **kwargs): + def http_get_vm_settings(_method, _host, _relative_url, _timeout, **kwargs): request_headers.append(kwargs["headers"]) if len(request_headers) == 1: # Fail the first request with status GONE and update the mock data to return the new Container ID and RoleConfigName that should be diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py index 0cfaa95fbf..b9fe23e413 100644 --- a/tests/protocol/test_wire.py +++ b/tests/protocol/test_wire.py @@ -405,7 +405,7 @@ def test_send_encoded_event(self, mock_http_request, *args): first_call = mock_http_request.call_args_list[0] args, kwargs = first_call - method, url, body_received = args # pylint: disable=unused-variable + method, url, body_received, timeout = args # pylint: disable=unused-variable headers = kwargs['headers'] # the headers should include utf-8 encoding... diff --git a/tests/utils/test_rest_util.py b/tests/utils/test_rest_util.py index 02431b6232..a0b00f6cd9 100644 --- a/tests/utils/test_rest_util.py +++ b/tests/utils/test_rest_util.py @@ -352,7 +352,7 @@ def test_http_request_direct(self, HTTPConnection, HTTPSConnection): HTTPConnection.return_value = mock_conn - resp = restutil._http_request("GET", "foo", "/bar") + resp = restutil._http_request("GET", "foo", "/bar", 10) HTTPConnection.assert_has_calls([ call("foo", 80, timeout=10) @@ -375,7 +375,7 @@ def test_http_request_direct_secure(self, HTTPConnection, HTTPSConnection): HTTPSConnection.return_value = mock_conn - resp = restutil._http_request("GET", "foo", "/bar", secure=True) + resp = restutil._http_request("GET", "foo", "/bar", 10, secure=True) HTTPConnection.assert_not_called() HTTPSConnection.assert_has_calls([ @@ -398,7 +398,7 @@ def test_http_request_proxy(self, HTTPConnection, HTTPSConnection): HTTPConnection.return_value = mock_conn - resp = restutil._http_request("GET", "foo", "/bar", + resp = restutil._http_request("GET", "foo", "/bar", 10, proxy_host="foo.bar", proxy_port=23333) HTTPConnection.assert_has_calls([ @@ -530,7 +530,7 @@ def test_http_request_proxy_secure(self, HTTPConnection, HTTPSConnection): HTTPSConnection.return_value = mock_conn - resp = restutil._http_request("GET", "foo", "/bar", + resp = restutil._http_request("GET", "foo", "/bar", 10, proxy_host="foo.bar", proxy_port=23333, secure=True) From 45e960da74f6c1eda090841720c42bb1bf08f860 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Sat, 10 Sep 2022 09:08:17 -0700 Subject: [PATCH 47/58] update traceback.format_exception etype argument (#2663) * update format_exception etype argument * unit test --- azurelinuxagent/common/utils/textutil.py | 2 +- tests/utils/test_text_util.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/azurelinuxagent/common/utils/textutil.py b/azurelinuxagent/common/utils/textutil.py index 153eb80643..1ff7a7e912 100644 --- a/azurelinuxagent/common/utils/textutil.py +++ b/azurelinuxagent/common/utils/textutil.py @@ -445,7 +445,7 @@ def format_exception(exception): if tb is None or (sys.version_info[0] == 2 and e != exception): msg += "[Traceback not available]" else: - msg += ''.join(traceback.format_exception(etype=type(exception), value=exception, tb=tb)) + msg += ''.join(traceback.format_exception(type(exception), value=exception, tb=tb)) return msg diff --git a/tests/utils/test_text_util.py b/tests/utils/test_text_util.py index 3b29f93694..ff129c40be 100644 --- a/tests/utils/test_text_util.py +++ b/tests/utils/test_text_util.py @@ -206,6 +206,26 @@ def test_format_memory_value(self): self.assertRaises(ValueError, textutil.format_memory_value, 'KiloBytes', 1) self.assertRaises(TypeError, textutil.format_memory_value, 'bytes', None) + def test_format_exception(self): + """ + Test formatting of exception into human-readable format + """ + + def raise_exception(count=3): + if count <= 1: + raise Exception("Test Exception") + raise_exception(count - 1) + + msg = "" + try: + raise_exception() + except Exception as e: + msg = textutil.format_exception(e) + + self.assertIn("Test Exception", msg) + # Raise exception at count 1 after two nested calls since count starts at 3 + self.assertEqual(2, msg.count("raise_exception(count - 1)")) + if __name__ == '__main__': unittest.main() From 0e42320ec393e7af6fe7d55c9fd24db56343d86c Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Tue, 13 Sep 2022 15:44:25 -0700 Subject: [PATCH 48/58] set agent version to 2.9.0.0 (#2664) --- azurelinuxagent/common/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurelinuxagent/common/version.py b/azurelinuxagent/common/version.py index ff9c903b93..20d2c605d1 100644 --- a/azurelinuxagent/common/version.py +++ b/azurelinuxagent/common/version.py @@ -209,7 +209,7 @@ def has_logrotate(): # # When doing a release, be sure to use the actual agent version. Current agent version: 2.4.0.0 # -AGENT_VERSION = '9.9.9.9' +AGENT_VERSION = '2.9.0.0' AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION) AGENT_DESCRIPTION = """ The Azure Linux Agent supports the provisioning and running of Linux From 8a79ea77371859bde3ae1e0884c59f7530ea0d0d Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 20 Sep 2022 07:06:53 -0700 Subject: [PATCH 49/58] Add logging statements for mrseq migration during update (#2667) Co-authored-by: narrieta --- azurelinuxagent/ga/exthandlers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py index 99f3809446..c01fc15bca 100644 --- a/azurelinuxagent/ga/exthandlers.py +++ b/azurelinuxagent/ga/exthandlers.py @@ -1210,7 +1210,10 @@ def copy_status_files(self, old_ext_handler_i): old_ext_mrseq_file = os.path.join(old_ext_dir, "mrseq") if os.path.isfile(old_ext_mrseq_file): + logger.info("Migrating {0} to {1}.", old_ext_mrseq_file, new_ext_dir) shutil.copy2(old_ext_mrseq_file, new_ext_dir) + else: + logger.info("{0} does not exist, no migration is needed.", old_ext_mrseq_file) old_ext_status_dir = old_ext_handler_i.get_status_dir() new_ext_status_dir = self.get_status_dir() From a65135f7577faa605cb31ccae7d1f50766b46dd8 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Fri, 30 Sep 2022 11:26:46 -0700 Subject: [PATCH 50/58] Require HostGAPlugin >= 133 for Fast Track (#2673) Co-authored-by: narrieta --- azurelinuxagent/common/protocol/hostplugin.py | 6 +++--- .../vm_settings-difference_in_required_features.json | 2 +- tests/data/hostgaplugin/vm_settings-empty_depends_on.json | 2 +- .../hostgaplugin/vm_settings-fabric-no_thumbprints.json | 2 +- tests/data/hostgaplugin/vm_settings-invalid_blob_type.json | 2 +- tests/data/hostgaplugin/vm_settings-missing_cert.json | 2 +- tests/data/hostgaplugin/vm_settings-no_manifests.json | 2 +- .../hostgaplugin/vm_settings-no_status_upload_blob.json | 2 +- tests/data/hostgaplugin/vm_settings-out-of-sync.json | 2 +- tests/data/hostgaplugin/vm_settings-parse_error.json | 2 +- tests/data/hostgaplugin/vm_settings-requested_version.json | 2 +- tests/data/hostgaplugin/vm_settings.json | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/azurelinuxagent/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py index 55997d6490..0aaff2184d 100644 --- a/azurelinuxagent/common/protocol/hostplugin.py +++ b/azurelinuxagent/common/protocol/hostplugin.py @@ -491,7 +491,7 @@ def format_message(msg): try: # Raise if VmSettings are not supported, but check again periodically since the HostGAPlugin could have been updated since the last check # Note that self._host_plugin_supports_vm_settings can be None, so we need to compare against False - if self._supports_vm_settings == False and self._supports_vm_settings_next_check > datetime.datetime.now(): + if not self._supports_vm_settings and self._supports_vm_settings_next_check > datetime.datetime.now(): # Raise VmSettingsNotSupported directly instead of using raise_not_supported() to avoid resetting the timestamp for the next check raise VmSettingsNotSupported() @@ -551,8 +551,8 @@ def format_message(msg): logger.info(message) add_event(op=WALAEventOperation.HostPlugin, message=message, is_success=True) - # Don't support HostGAPlugin versions older than 124 - if vm_settings.host_ga_plugin_version < FlexibleVersion("1.0.8.124"): + # Don't support HostGAPlugin versions older than 133 + if vm_settings.host_ga_plugin_version < FlexibleVersion("1.0.8.133"): raise_not_supported() self._supports_vm_settings = True diff --git a/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json b/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json index a17776828e..71cdbf5c55 100644 --- a/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json +++ b/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-empty_depends_on.json b/tests/data/hostgaplugin/vm_settings-empty_depends_on.json index 94d9f0eb1f..2295ae85c0 100644 --- a/tests/data/hostgaplugin/vm_settings-empty_depends_on.json +++ b/tests/data/hostgaplugin/vm_settings-empty_depends_on.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9", "correlationId": "1bef4c48-044e-4225-8f42-1d1eac1eb158", diff --git a/tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json b/tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json index bbd9459336..a98f0affe5 100644 --- a/tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json +++ b/tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json b/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json index e7945845ac..ef63166dfb 100644 --- a/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json +++ b/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9", "correlationId": "1bef4c48-044e-4225-8f42-1d1eac1eb158", diff --git a/tests/data/hostgaplugin/vm_settings-missing_cert.json b/tests/data/hostgaplugin/vm_settings-missing_cert.json index a7192e942d..4ce8a20cf8 100644 --- a/tests/data/hostgaplugin/vm_settings-missing_cert.json +++ b/tests/data/hostgaplugin/vm_settings-missing_cert.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-no_manifests.json b/tests/data/hostgaplugin/vm_settings-no_manifests.json index 7ec3a5c3d1..c8d2bf138f 100644 --- a/tests/data/hostgaplugin/vm_settings-no_manifests.json +++ b/tests/data/hostgaplugin/vm_settings-no_manifests.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "89d50bf1-fa55-4257-8af3-3db0c9f81ab4", "correlationId": "c143f8f0-a66b-4881-8c06-1efd278b0b02", diff --git a/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json b/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json index 2f70b55762..502d9a99c2 100644 --- a/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json +++ b/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-out-of-sync.json b/tests/data/hostgaplugin/vm_settings-out-of-sync.json index c35fdb5a33..0d4806af9d 100644 --- a/tests/data/hostgaplugin/vm_settings-out-of-sync.json +++ b/tests/data/hostgaplugin/vm_settings-out-of-sync.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "AAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE", "correlationId": "EEEEEEEE-DDDD-CCCC-BBBB-AAAAAAAAAAAA", diff --git a/tests/data/hostgaplugin/vm_settings-parse_error.json b/tests/data/hostgaplugin/vm_settings-parse_error.json index bae5de4cb8..da82fda78e 100644 --- a/tests/data/hostgaplugin/vm_settings-parse_error.json +++ b/tests/data/hostgaplugin/vm_settings-parse_error.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": THIS_IS_A_SYNTAX_ERROR, "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-requested_version.json b/tests/data/hostgaplugin/vm_settings-requested_version.json index e7e5135f90..0f73cb255e 100644 --- a/tests/data/hostgaplugin/vm_settings-requested_version.json +++ b/tests/data/hostgaplugin/vm_settings-requested_version.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings.json b/tests/data/hostgaplugin/vm_settings.json index 3018616ab4..1f6d44debc 100644 --- a/tests/data/hostgaplugin/vm_settings.json +++ b/tests/data/hostgaplugin/vm_settings.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", From e7641bd3321ed15a41114238fd348f85d6e7ced2 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Fri, 30 Sep 2022 13:00:45 -0700 Subject: [PATCH 51/58] Additional telemetry for goal state (#2675) * Additional telemetry for goal state * add success message Co-authored-by: narrieta --- azurelinuxagent/common/protocol/goal_state.py | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index c2b95cfb25..ef47305037 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -192,7 +192,9 @@ def _update(self, force_update): incarnation, xml_text, xml_doc = GoalState._fetch_goal_state(self._wire_client) goal_state_updated = force_update or incarnation != self._incarnation if goal_state_updated: - self.logger.info('Fetched a new incarnation for the WireServer goal state [incarnation {0}]', incarnation) + message = 'Fetched a new incarnation for the WireServer goal state [incarnation {0}]'.format(incarnation) + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) vm_settings, vm_settings_updated = None, False try: @@ -203,11 +205,15 @@ def _update(self, force_update): if vm_settings_updated: self.logger.info('') - self.logger.info("Fetched new vmSettings [HostGAPlugin correlation ID: {0} eTag: {1} source: {2}]", vm_settings.hostga_plugin_correlation_id, vm_settings.etag, vm_settings.source) + message = "Fetched new vmSettings [HostGAPlugin correlation ID: {0} eTag: {1} source: {2}]".format(vm_settings.hostga_plugin_correlation_id, vm_settings.etag, vm_settings.source) + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) # Ignore the vmSettings if their source is Fabric (processing a Fabric goal state may require the tenant certificate and the vmSettings don't include it.) if vm_settings is not None and vm_settings.source == GoalStateSource.Fabric: if vm_settings_updated: - self.logger.info("The vmSettings originated via Fabric; will ignore them.") + message = "The vmSettings originated via Fabric; will ignore them." + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) vm_settings, vm_settings_updated = None, False # If neither goal state has changed we are done with the update @@ -265,7 +271,9 @@ def _check_certificates(self): raise GoalStateInconsistentError(message) def _restore_wire_server_goal_state(self, incarnation, xml_text, xml_doc, vm_settings_support_stopped_error): - self.logger.info('The HGAP stopped supporting vmSettings; will fetched the goal state from the WireServer.') + msg = 'The HGAP stopped supporting vmSettings; will fetched the goal state from the WireServer.' + self.logger.info(msg) + add_event(op=WALAEventOperation.VmSettings, message=msg) self._history = GoalStateHistory(datetime.datetime.utcnow(), incarnation) self._history.save_goal_state(xml_text) self._extensions_goal_state = self._fetch_full_wire_server_goal_state(incarnation, xml_doc) @@ -274,7 +282,7 @@ def _restore_wire_server_goal_state(self, incarnation, xml_text, xml_doc, vm_set msg = "Fetched a Fabric goal state older than the most recent FastTrack goal state; will skip it.\nFabric: {0}\nFastTrack: {1}".format( self._extensions_goal_state.created_on_timestamp, vm_settings_support_stopped_error.timestamp) self.logger.info(msg) - add_event(op=WALAEventOperation.VmSettings, message=msg, is_success=True) + add_event(op=WALAEventOperation.VmSettings, message=msg) def save_to_history(self, data, file_name): self._history.save(data, file_name) @@ -351,7 +359,9 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): """ try: self.logger.info('') - self.logger.info('Fetching full goal state from the WireServer [incarnation {0}]', incarnation) + message = 'Fetching full goal state from the WireServer [incarnation {0}]'.format(incarnation) + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) role_instance = find(xml_doc, "RoleInstance") role_instance_id = findtext(role_instance, "InstanceId") @@ -391,9 +401,12 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): certs = Certificates(xml_text, self.logger) # Log and save the certificates summary (i.e. the thumbprint but not the certificate itself) to the goal state history for c in certs.summary: - self.logger.info("Downloaded certificate {0}".format(c)) + message = "Downloaded certificate {0}".format(c) + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) if len(certs.warnings) > 0: self.logger.warn(certs.warnings) + add_event(op=WALAEventOperation.GoalState, message=certs.warnings) self._history.save_certificates(json.dumps(certs.summary)) remote_access = None @@ -418,7 +431,9 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): self.logger.warn("Fetching the goal state failed: {0}", ustr(exception)) raise ProtocolError(msg="Error fetching goal state", inner=exception) finally: - self.logger.info('Fetch goal state completed') + message = 'Fetch goal state completed' + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) class HostingEnv(object): @@ -455,9 +470,11 @@ def __init__(self, xml_text, my_logger): return # if the certificates format is not Pkcs7BlobWithPfxContents do not parse it - certificateFormat = findtext(xml_doc, "Format") - if certificateFormat and certificateFormat != "Pkcs7BlobWithPfxContents": - my_logger.warn("The Format is not Pkcs7BlobWithPfxContents. Format is " + certificateFormat) + certificate_format = findtext(xml_doc, "Format") + if certificate_format and certificate_format != "Pkcs7BlobWithPfxContents": + message = "The Format is not Pkcs7BlobWithPfxContents. Format is {0}".format(certificate_format) + my_logger.warn(message) + add_event(op=WALAEventOperation.GoalState, message=message) return cryptutil = CryptUtil(conf.get_openssl_cmd()) From d72201dd416c27a2842c945a9aba405a4c73f0b5 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Fri, 30 Sep 2022 14:37:57 -0700 Subject: [PATCH 52/58] version update to 2.9.0.1 (#2678) --- azurelinuxagent/common/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurelinuxagent/common/version.py b/azurelinuxagent/common/version.py index 20d2c605d1..79d32f1e3a 100644 --- a/azurelinuxagent/common/version.py +++ b/azurelinuxagent/common/version.py @@ -209,7 +209,7 @@ def has_logrotate(): # # When doing a release, be sure to use the actual agent version. Current agent version: 2.4.0.0 # -AGENT_VERSION = '2.9.0.0' +AGENT_VERSION = '2.9.0.1' AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION) AGENT_DESCRIPTION = """ The Azure Linux Agent supports the provisioning and running of Linux From 60f5b4dd465b3921ca50ec2e5c9fc4f1987d90b8 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:01:09 -0700 Subject: [PATCH 53/58] drop cgroup support for rhel (#2685) --- azurelinuxagent/common/cgroupapi.py | 2 +- tests/common/test_cgroupapi.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/azurelinuxagent/common/cgroupapi.py b/azurelinuxagent/common/cgroupapi.py index 2935e2516a..d7040747a0 100644 --- a/azurelinuxagent/common/cgroupapi.py +++ b/azurelinuxagent/common/cgroupapi.py @@ -60,7 +60,7 @@ def cgroups_supported(): except ValueError: return False return ((distro_name.lower() == 'ubuntu' and distro_version.major >= 16) or - (distro_name.lower() in ("centos", "redhat") and + (distro_name.lower() == "centos" and ((distro_version.major == 7 and distro_version.minor >= 4) or distro_version.major >= 8))) @staticmethod diff --git a/tests/common/test_cgroupapi.py b/tests/common/test_cgroupapi.py index 3b70214d39..ca380ed2cb 100644 --- a/tests/common/test_cgroupapi.py +++ b/tests/common/test_cgroupapi.py @@ -57,13 +57,13 @@ def test_cgroups_should_be_supported_only_on_ubuntu16_centos7dot4_redhat7dot4_an (['ubuntu', '20.04', 'focal'], True), (['ubuntu', '20.10', 'groovy'], True), (['centos', '7.8', 'Source'], True), - (['redhat', '7.8', 'Maipo'], True), - (['redhat', '7.9.1908', 'Core'], True), + (['redhat', '7.8', 'Maipo'], False), + (['redhat', '7.9.1908', 'Core'], False), (['centos', '8.1', 'Source'], True), - (['redhat', '8.2', 'Maipo'], True), - (['redhat', '8.2.2111', 'Core'], True), + (['redhat', '8.2', 'Maipo'], False), + (['redhat', '8.2.2111', 'Core'], False), (['centos', '7.4', 'Source'], True), - (['redhat', '7.4', 'Maipo'], True), + (['redhat', '7.4', 'Maipo'], False), (['centos', '7.5', 'Source'], True), (['centos', '7.3', 'Maipo'], False), (['redhat', '7.2', 'Maipo'], False), From 70bb383c8f873d859a6eee6670b014c2599f1787 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:20:07 -0700 Subject: [PATCH 54/58] version update to 2.9.0.2 (#2686) --- azurelinuxagent/common/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurelinuxagent/common/version.py b/azurelinuxagent/common/version.py index 79d32f1e3a..0e6270b0c7 100644 --- a/azurelinuxagent/common/version.py +++ b/azurelinuxagent/common/version.py @@ -209,7 +209,7 @@ def has_logrotate(): # # When doing a release, be sure to use the actual agent version. Current agent version: 2.4.0.0 # -AGENT_VERSION = '2.9.0.1' +AGENT_VERSION = '2.9.0.2' AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION) AGENT_DESCRIPTION = """ The Azure Linux Agent supports the provisioning and running of Linux From 2fa01e53c5de85c831e2cf01ae39113ac643f164 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Mon, 24 Oct 2022 12:01:19 -0700 Subject: [PATCH 55/58] drop cgroup support for centos (#2689) --- azurelinuxagent/common/cgroupapi.py | 4 +--- tests/common/test_cgroupapi.py | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/azurelinuxagent/common/cgroupapi.py b/azurelinuxagent/common/cgroupapi.py index d7040747a0..66e893ef6b 100644 --- a/azurelinuxagent/common/cgroupapi.py +++ b/azurelinuxagent/common/cgroupapi.py @@ -59,9 +59,7 @@ def cgroups_supported(): distro_version = FlexibleVersion(distro_info[1]) except ValueError: return False - return ((distro_name.lower() == 'ubuntu' and distro_version.major >= 16) or - (distro_name.lower() == "centos" and - ((distro_version.major == 7 and distro_version.minor >= 4) or distro_version.major >= 8))) + return distro_name.lower() == 'ubuntu' and distro_version.major >= 16 @staticmethod def track_cgroups(extension_cgroups): diff --git a/tests/common/test_cgroupapi.py b/tests/common/test_cgroupapi.py index ca380ed2cb..a31d57d722 100644 --- a/tests/common/test_cgroupapi.py +++ b/tests/common/test_cgroupapi.py @@ -56,15 +56,15 @@ def test_cgroups_should_be_supported_only_on_ubuntu16_centos7dot4_redhat7dot4_an (['ubuntu', '18.10', 'cosmic'], True), (['ubuntu', '20.04', 'focal'], True), (['ubuntu', '20.10', 'groovy'], True), - (['centos', '7.8', 'Source'], True), + (['centos', '7.8', 'Source'], False), (['redhat', '7.8', 'Maipo'], False), (['redhat', '7.9.1908', 'Core'], False), - (['centos', '8.1', 'Source'], True), + (['centos', '8.1', 'Source'], False), (['redhat', '8.2', 'Maipo'], False), (['redhat', '8.2.2111', 'Core'], False), - (['centos', '7.4', 'Source'], True), + (['centos', '7.4', 'Source'], False), (['redhat', '7.4', 'Maipo'], False), - (['centos', '7.5', 'Source'], True), + (['centos', '7.5', 'Source'], False), (['centos', '7.3', 'Maipo'], False), (['redhat', '7.2', 'Maipo'], False), (['bigip', '15.0.1', 'Final'], False), From 43cf6a2ac3a40e977e92cca8d8b0ce4c32b6c01f Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Mon, 24 Oct 2022 15:55:49 -0700 Subject: [PATCH 56/58] update version to 2.9.0.3 (#2690) --- azurelinuxagent/common/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurelinuxagent/common/version.py b/azurelinuxagent/common/version.py index 0e6270b0c7..6f08cd9b3f 100644 --- a/azurelinuxagent/common/version.py +++ b/azurelinuxagent/common/version.py @@ -209,7 +209,7 @@ def has_logrotate(): # # When doing a release, be sure to use the actual agent version. Current agent version: 2.4.0.0 # -AGENT_VERSION = '2.9.0.2' +AGENT_VERSION = '2.9.0.3' AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION) AGENT_DESCRIPTION = """ The Azure Linux Agent supports the provisioning and running of Linux From 49d4f7cbb38c4ef9e1e6b7450c62cf2b7235b157 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Mon, 31 Oct 2022 11:31:16 -0700 Subject: [PATCH 57/58] reset the quotas when agent drop the cgroup support (#2693) * reset the quotas when agent drop the cgroup support * address comments * log removed file names * remove only agent created files * fix pylint error * fix agent drop in path str --- azurelinuxagent/common/cgroupconfigurator.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index 47f1da35ab..b22a26bcd9 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -149,6 +149,25 @@ def initialize(self): try: if self._initialized: return + # This check is to reset the quotas if agent goes from cgroup supported to unsupported distros later in time. + if not CGroupsApi.cgroups_supported(): + agent_drop_in_path = systemd.get_agent_drop_in_path() + try: + if os.path.exists(agent_drop_in_path) and os.path.isdir(agent_drop_in_path): + files_to_cleanup = [] + agent_drop_in_file_slice = os.path.join(agent_drop_in_path, _AGENT_DROP_IN_FILE_SLICE) + agent_drop_in_file_cpu_accounting = os.path.join(agent_drop_in_path, + _DROP_IN_FILE_CPU_ACCOUNTING) + agent_drop_in_file_memory_accounting = os.path.join(agent_drop_in_path, + _DROP_IN_FILE_MEMORY_ACCOUNTING) + agent_drop_in_file_cpu_quota = os.path.join(agent_drop_in_path, _DROP_IN_FILE_CPU_QUOTA) + files_to_cleanup.extend([agent_drop_in_file_slice, agent_drop_in_file_cpu_accounting, + agent_drop_in_file_memory_accounting, agent_drop_in_file_cpu_quota]) + self.__cleanup_all_files(files_to_cleanup) + self.__reload_systemd_config() + logger.info("Agent reset the quotas if distro: {0} goes from supported to unsupported list", get_distro()) + except Exception as err: + logger.warn("Unable to delete Agent drop-in files while resetting the quotas: {0}".format(err)) # check whether cgroup monitoring is supported on the current distro self._cgroups_supported = CGroupsApi.cgroups_supported() From 045dbd3b9bf2ed74af69654ccecdb41d6f89676c Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Mon, 31 Oct 2022 13:02:42 -0700 Subject: [PATCH 58/58] version update to 2.9.0.4 (#2694) --- azurelinuxagent/common/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurelinuxagent/common/version.py b/azurelinuxagent/common/version.py index 6f08cd9b3f..20e11cb3a5 100644 --- a/azurelinuxagent/common/version.py +++ b/azurelinuxagent/common/version.py @@ -209,7 +209,7 @@ def has_logrotate(): # # When doing a release, be sure to use the actual agent version. Current agent version: 2.4.0.0 # -AGENT_VERSION = '2.9.0.3' +AGENT_VERSION = '2.9.0.4' AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION) AGENT_DESCRIPTION = """ The Azure Linux Agent supports the provisioning and running of Linux