diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py index c16660297e..deb911f51b 100644 --- a/azurelinuxagent/ga/exthandlers.py +++ b/azurelinuxagent/ga/exthandlers.py @@ -74,10 +74,13 @@ NUMBER_OF_DOWNLOAD_RETRIES = 5 -DISABLE_FAILED = "AZURE_GUEST_AGENT_DISABLE_FAILED" -UNINSTALL_FAILED = "AZURE_GUEST_AGENT_UNINSTALL_FAILED" -EXTENSION_PATH = "AZURE_GUEST_AGENT_EXTENSION_PATH" -EXTENSION_VERSION = "AZURE_GUEST_AGENT_EXTENSION_VERSION" + +class ExtCommandEnvVariable(object): + DisableFailed = "AZURE_GUEST_AGENT_DISABLE_FAILED" + UninstallFailed = "AZURE_GUEST_AGENT_UNINSTALL_FAILED" + ExtensionPath = "AZURE_GUEST_AGENT_EXTENSION_PATH" + ExtensionVersion = "AZURE_GUEST_AGENT_EXTENSION_VERSION" + ExtensionSeqNumber = "ConfigSequenceNumber" # At par with Windows Guest Agent def get_traceback(e): if sys.version_info[0] == 3: @@ -943,7 +946,7 @@ def initialize(self): def enable(self, uninstall_failed=False): env = {} if uninstall_failed: - env.update({UNINSTALL_FAILED: '1'}) + env.update({ExtCommandEnvVariable.UninstallFailed: '1'}) self.set_operation(WALAEventOperation.Enable) man = self.load_manifest() @@ -967,7 +970,7 @@ def disable(self): def install(self, uninstall_failed=False): env = {} if uninstall_failed: - env.update({UNINSTALL_FAILED: '1'}) + env.update({ExtCommandEnvVariable.UninstallFailed: '1'}) man = self.load_manifest() install_cmd = man.get_install_command() @@ -1018,7 +1021,7 @@ def update(self, version=None, disable_failed=False): env = {'VERSION': version} if disable_failed: - env.update({DISABLE_FAILED: "1"}) + env.update({ExtCommandEnvVariable.DisableFailed: "1"}) try: self.set_operation(WALAEventOperation.Update) @@ -1207,8 +1210,9 @@ def launch_command(self, cmd, timeout=300, extension_error_code=ExtensionErrorCo env = {} env.update(os.environ) # Always add Extension Path and version to the current launch_command (Ask from publishers) - env.update({EXTENSION_PATH: self.get_base_dir(), - EXTENSION_VERSION: self.ext_handler.properties.version}) + env.update({ExtCommandEnvVariable.ExtensionPath: base_dir, + ExtCommandEnvVariable.ExtensionVersion: self.ext_handler.properties.version, + ExtCommandEnvVariable.ExtensionSeqNumber: str(self.get_seq_no())}) try: # Some extensions erroneously begin cmd with a slash; don't interpret those @@ -1387,6 +1391,17 @@ def get_env_file(self): def get_log_dir(self): return os.path.join(conf.get_ext_log_dir(), self.ext_handler.name) + def get_seq_no(self): + runtime_settings = self.ext_handler.properties.extensions + # If no runtime_settings available for this ext_handler, then return 0 (this is the behavior we follow + # for update_settings) + if not runtime_settings or len(runtime_settings) == 0: + return "0" + # Currently for every runtime settings we use the same sequence number + # (Check : def parse_plugin_settings(self, ext_handler, plugin_settings) in wire.py) + # Will have to revisit once the feature to enable multiple runtime settings is rolled out by CRP + return self.ext_handler.properties.extensions[0].sequenceNumber + class HandlerEnvironment(object): def __init__(self, data): diff --git a/tests/ga/test_extension.py b/tests/ga/test_extension.py index 63e6276841..fee9470b7d 100644 --- a/tests/ga/test_extension.py +++ b/tests/ga/test_extension.py @@ -1588,13 +1588,13 @@ def test_both_env_var_should_clear_before_every_call_to_exthandler_run( # Ensure that the env variables were present in the first run when failures were thrown self.assertEqual(2, patch_continue_on_update.call_count) - self.assertTrue('-update' in update_kwargs['command'] and DISABLE_FAILED in update_kwargs['env'], + self.assertTrue('-update' in update_kwargs['command'] and ExtCommandEnvVariable.DisableFailed in update_kwargs['env'], "The update command call should have Disable Failed in env variable") - self.assertTrue('-install' in install_kwargs['command'] and DISABLE_FAILED not in install_kwargs['env'], + self.assertTrue('-install' in install_kwargs['command'] and ExtCommandEnvVariable.DisableFailed not in install_kwargs['env'], "The Disable Failed env variable should be removed from install command") - self.assertTrue('-install' in install_kwargs['command'] and UNINSTALL_FAILED in install_kwargs['env'], + self.assertTrue('-install' in install_kwargs['command'] and ExtCommandEnvVariable.UninstallFailed in install_kwargs['env'], "The install command call should have Uninstall Failed in env variable") - self.assertTrue('-enable' in enable_kwargs['command'] and UNINSTALL_FAILED in enable_kwargs['env'], + self.assertTrue('-enable' in enable_kwargs['command'] and ExtCommandEnvVariable.UninstallFailed in enable_kwargs['env'], "The enable command call should have Uninstall Failed in env variable") # Initiating another run which shouldn't have any failed env variables in it if no failures @@ -1604,8 +1604,8 @@ def test_both_env_var_should_clear_before_every_call_to_exthandler_run( _, new_enable_kwargs = patch_start_cmd.call_args # Ensure the new run didn't have any failed env variables - self.assertNotIn(DISABLE_FAILED, new_enable_kwargs['env']) - self.assertNotIn(UNINSTALL_FAILED, new_enable_kwargs['env']) + self.assertNotIn(ExtCommandEnvVariable.DisableFailed, new_enable_kwargs['env']) + self.assertNotIn(ExtCommandEnvVariable.UninstallFailed, new_enable_kwargs['env']) # Ensure the handler status and ext_status is successful self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.1") @@ -1620,13 +1620,106 @@ def test_ext_path_and_version_env_variables_set_for_ever_operation(self, *args): # Extension Path and Version should be set for all launch_command calls for args, kwargs in patch_start_cmd.call_args_list: - self.assertIn(EXTENSION_PATH, kwargs['env']) - self.assertIn('OSTCExtensions.ExampleHandlerLinux-1.0.0', kwargs['env'][EXTENSION_PATH]) - self.assertIn(EXTENSION_VERSION, kwargs['env']) - self.assertEqual("1.0.0", kwargs['env'][EXTENSION_VERSION]) + self.assertIn(ExtCommandEnvVariable.ExtensionPath, kwargs['env']) + self.assertIn('OSTCExtensions.ExampleHandlerLinux-1.0.0', kwargs['env'][ExtCommandEnvVariable.ExtensionPath]) + self.assertIn(ExtCommandEnvVariable.ExtensionVersion, kwargs['env']) + self.assertEqual("1.0.0", kwargs['env'][ExtCommandEnvVariable.ExtensionVersion]) self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") + @patch("azurelinuxagent.common.cgroupconfigurator.handle_process_completion", side_effect="Process Successful") + def test_ext_sequence_no_should_be_set_for_every_command_call(self, _, *args): + test_data = WireProtocolData(DATA_FILE_MULTIPLE_EXT) + exthandlers_handler, protocol = self._create_mock(test_data, *args) + + with patch("subprocess.Popen") as patch_popen: + exthandlers_handler.run() + + for _, kwargs in patch_popen.call_args_list: + self.assertIn(ExtCommandEnvVariable.ExtensionSeqNumber, kwargs['env']) + self.assertEqual(kwargs['env'][ExtCommandEnvVariable.ExtensionSeqNumber], "0") + + self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.0") + + # Next incarnation and seq for extensions, update version + test_data.goal_state = test_data.goal_state.replace("1<", "2<") + test_data.ext_conf = test_data.ext_conf.replace('version="1.0.0"', 'version="1.0.1"') + test_data.ext_conf = test_data.ext_conf.replace('seqNo="0"', 'seqNo="1"') + test_data.manifest = test_data.manifest.replace('1.0.0', '1.0.1') + exthandlers_handler, protocol = self._create_mock(test_data, *args) + + with patch("subprocess.Popen") as patch_popen: + exthandlers_handler.run() + + for _, kwargs in patch_popen.call_args_list: + self.assertIn(ExtCommandEnvVariable.ExtensionSeqNumber, kwargs['env']) + self.assertEqual(kwargs['env'][ExtCommandEnvVariable.ExtensionSeqNumber], "1") + + self._assert_handler_status(protocol.report_vm_status, "Ready", expected_ext_count=1, version="1.0.1") + + def test_ext_sequence_no_should_be_set_from_within_extension(self, *args): + + def create_test_dir_and_script(base_dir, test_file_name, test_file): + if not os.path.exists(base_dir): + os.mkdir(base_dir) + self.create_script(file_name=test_file_name, contents=test_file, + file_path=os.path.join(base_dir, test_file_name)) + + test_file_name = "testfile.sh" + handler_json = { + "installCommand": test_file_name, + "uninstallCommand": test_file_name, + "updateCommand": test_file_name, + "enableCommand": test_file_name, + "disableCommand": test_file_name, + "rebootAfterInstall": False, + "reportHeartbeat": False, + "continueOnUpdateFailure": False + } + manifest = HandlerManifest({'handlerManifest': handler_json}) + + # Script prints env variables passed to this process and prints all starting with ConfigSequenceNumber + test_file = """ + printenv | grep ConfigSequenceNumber + """ + + base_dir = os.path.join(conf.get_lib_dir(), 'OSTCExtensions.ExampleHandlerLinux-1.0.0') + create_test_dir_and_script(base_dir, test_file_name, test_file) + + test_data = WireProtocolData(DATA_FILE_EXT_SINGLE) + exthandlers_handler, protocol = self._create_mock(test_data, *args) + expected_seq_no = 0 + + with patch.object(ExtHandlerInstance, "load_manifest", return_value=manifest): + with patch.object(ExtHandlerInstance, 'report_event') as mock_report_event: + exthandlers_handler.run() + + for _, kwargs in mock_report_event.call_args_list: + # The output is of the format - 'testfile.sh\n[stdout]ConfigSequenceNumber=N\n[stderr]' + if test_file_name not in kwargs['message']: + continue + self.assertIn("{0}={1}".format(ExtCommandEnvVariable.ExtensionSeqNumber, expected_seq_no), + kwargs['message']) + + # Update goal state, extension version and seq no + test_data.goal_state = test_data.goal_state.replace("1<", "2<") + test_data.ext_conf = test_data.ext_conf.replace('version="1.0.0"', 'version="1.0.1"') + test_data.ext_conf = test_data.ext_conf.replace('seqNo="0"', 'seqNo="1"') + test_data.manifest = test_data.manifest.replace('1.0.0', '1.0.1') + expected_seq_no = 1 + base_dir = os.path.join(conf.get_lib_dir(), 'OSTCExtensions.ExampleHandlerLinux-1.0.1') + create_test_dir_and_script(base_dir, test_file_name, test_file) + + with patch.object(ExtHandlerInstance, 'report_event') as mock_report_event: + exthandlers_handler.run() + + for _, kwargs in mock_report_event.call_args_list: + # The output is of the format - 'testfile.sh\n[stdout]ConfigSequenceNumber=N\n[stderr]' + if test_file_name not in kwargs['message']: + continue + self.assertIn("{0}={1}".format(ExtCommandEnvVariable.ExtensionSeqNumber, expected_seq_no), + kwargs['message']) + @patch("azurelinuxagent.common.protocol.wire.CryptUtil") @patch("azurelinuxagent.common.utils.restutil.http_get") @@ -2095,7 +2188,7 @@ def test_disable_failed_env_variable_should_be_set_for_update_cmd_when_continue_ args, kwargs = patch_start_cmd.call_args - self.assertTrue('-update' in kwargs['command'] and DISABLE_FAILED in kwargs['env'], + self.assertTrue('-update' in kwargs['command'] and ExtCommandEnvVariable.DisableFailed in kwargs['env'], "The update command should have Disable Failed in env variable") def test_uninstall_failed_env_variable_should_set_for_install_when_continue_on_update_failure_is_true( @@ -2110,7 +2203,7 @@ def test_uninstall_failed_env_variable_should_set_for_install_when_continue_on_u args, kwargs = patch_start_cmd.call_args - self.assertTrue('-install' in kwargs['command'] and UNINSTALL_FAILED in kwargs['env'], + self.assertTrue('-install' in kwargs['command'] and ExtCommandEnvVariable.UninstallFailed in kwargs['env'], "The install command should have Uninstall Failed in env variable") def test_extension_error_should_be_raised_when_continue_on_update_failure_is_false_on_disable_failure(self, *args): @@ -2188,7 +2281,7 @@ def test_env_variable_should_not_set_when_continue_on_update_failure_is_false(se for args, kwargs in patch_launch_command.call_args_list: # Disable wont have any env variables, and Update would have only 'Version' in env param self.assertTrue(('env' not in kwargs and '-disable' in args[0]) or - ('-update' in args[0] and DISABLE_FAILED not in kwargs['env'])) + ('-update' in args[0] and ExtCommandEnvVariable.DisableFailed not in kwargs['env'])) @patch('time.sleep', side_effect=lambda _: mock_sleep(0.001)) def test_failed_env_variables_should_be_set_from_within_extension_commands(self, *args): @@ -2217,8 +2310,8 @@ def test_failed_env_variables_should_be_set_from_within_extension_commands(self, with patch.object(new_handler_i.logger, 'verbose', autospec=True) as mock_verbose: # Since we're not mocking the azurelinuxagent.common.cgroupconfigurator..handle_process_completion, - # both disable.cmd and uninstall.cmd would raise ExtensionError exceptions and set the DISABLE_FAILED and - # UNINSTALL_FAILED env variables. + # both disable.cmd and uninstall.cmd would raise ExtensionError exceptions and set the + # ExtCommandEnvVariable.DisableFailed and ExtCommandEnvVariable.UninstallFailed env variables. # For update and install we're running the script above to print all the env variables starting with AZURE_ # and verify accordingly if the corresponding env variables are set properly or not ExtHandlersHandler._update_extension_handler_and_return_if_failed(old_handler_i, new_handler_i) @@ -2232,15 +2325,17 @@ def test_failed_env_variables_should_be_set_from_within_extension_commands(self, # Ensure we're checking variables for update scenario self.assertEqual(update_file_name, update_command_args[1]) - self.assertIn(DISABLE_FAILED, update_args[0]) - self.assertTrue(EXTENSION_PATH in update_args[0] and EXTENSION_VERSION in update_args[0]) - self.assertNotIn(UNINSTALL_FAILED, update_args[0]) + self.assertIn(ExtCommandEnvVariable.DisableFailed, update_args[0]) + self.assertTrue(ExtCommandEnvVariable.ExtensionPath in update_args[0] and + ExtCommandEnvVariable.ExtensionVersion in update_args[0]) + self.assertNotIn(ExtCommandEnvVariable.UninstallFailed, update_args[0]) # Ensure we're checking variables for install scenario self.assertEqual(install_file_name, install_command_args[1]) - self.assertIn(UNINSTALL_FAILED, install_args[0]) - self.assertTrue(EXTENSION_PATH in install_args[0] and EXTENSION_VERSION in install_args[0]) - self.assertNotIn(DISABLE_FAILED, install_args[0]) + self.assertIn(ExtCommandEnvVariable.UninstallFailed, install_args[0]) + self.assertTrue(ExtCommandEnvVariable.ExtensionPath in install_args[0] and + ExtCommandEnvVariable.ExtensionVersion in install_args[0]) + self.assertNotIn(ExtCommandEnvVariable.DisableFailed, install_args[0]) if __name__ == '__main__': diff --git a/tests/ga/test_exthandlers.py b/tests/ga/test_exthandlers.py index 92bb9e4be2..7b0feb649e 100644 --- a/tests/ga/test_exthandlers.py +++ b/tests/ga/test_exthandlers.py @@ -231,6 +231,10 @@ def setUp(self): self.mock_get_log_dir = patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_log_dir", lambda *_: self.log_dir) self.mock_get_log_dir.start() + mock_sleep = time.sleep + self.mock_sleep = patch("time.sleep", lambda *_: mock_sleep(0.01)) + self.mock_sleep.start() + self.cgroups_enabled = CGroupConfigurator.get_instance().enabled() CGroupConfigurator.get_instance().disable() @@ -242,6 +246,7 @@ def tearDown(self): self.mock_get_log_dir.stop() self.mock_get_base_dir.stop() + self.mock_sleep.stop() AgentTestCase.tearDown(self)