Repository: Azure/WALinuxAgent
Pull request #3189 (2024-08-21 19:16:47)

send updatemode in heartbeat and don't send RSM supported feature flag if versioning disabled in agent
<!-- DO NOT DELETE THIS TEMPLATE -->

## Description

PR includes minor changes related to RSM

- Added updatedmode: RSM/SelfUpdate flag in heartbeat (Telemetry helps to determine which/how may vms enrolled into RSM)
- Fixing supported feature flag for rsm, when agent does not support GA versioning, we still send as agent supports RSM to CRP. In that case, CRP may send rsm requests and those requests will not be honored by agent. So now don't send RSM supported feature flag if versioning disabled in agent.
- For instance, agent is on version N and rsm update attempt failed few times for version N+1 and we send the status to CRP with failed status. Later if we get new request rsm version is same as current version(N), we still sending cached last error msg as rsm status. Fixing that too.


Issue # <!-- if any -->
<!--
Please add an informative description that covers that changes made by the pull request. 
This checklist is used to make sure that common issues in a pull request are addressed.
This will expedite the process of getting your pull request merged and avoid extra work on your part to fix issues discovered during the review process.
-->

---

### PR information
- [ ] The title of the PR is clear and informative.
- [ ] There are a small number of commits, each of which has an informative message. This means that previously merged commits do not appear in the history of the PR. For information on cleaning up the commits in your pull request, [see this page](https://github.com/Azure/azure-powershell/blob/master/documentation/development-docs/cleaning-up-commits.md).
- [ ] If applicable, the PR references the bug/issue that it fixes in the description.
- [ ] New Unit tests were added for the changes made

### Quality of Code and Contribution Guidelines
- [ ] I have read the [contribution guidelines](https://github.com/Azure/WALinuxAgent/blob/master/.github/CONTRIBUTING.md).


Patch:
diff --git a/azurelinuxagent/common/agent_supported_feature.py b/azurelinuxagent/common/agent_supported_feature.py
index 694c6363..f22a72ea 100644
--- a/azurelinuxagent/common/agent_supported_feature.py
+++ b/azurelinuxagent/common/agent_supported_feature.py
@@ -77,14 +77,15 @@ class _ETPFeature(AgentSupportedFeature):
 class _GAVersioningGovernanceFeature(AgentSupportedFeature):
     """
     CRP would drive the RSM update if agent reports that it does support RSM upgrades with this flag otherwise CRP fallback to largest version.
-    Agent doesn't report supported feature flag if auto update is disabled or old version of agent running that doesn't understand GA versioning.
+    Agent doesn't report supported feature flag if auto update is disabled or old version of agent running that doesn't understand GA versioning
+    or if explicitly support for versioning is disabled in agent
 
     Note: Especially Windows need this flag to report to CRP that GA doesn't support the updates. So linux adopted same flag to have a common solution.
     """
 
     __NAME = SupportedFeatureNames.GAVersioningGovernance
     __VERSION = "1.0"
-    __SUPPORTED = conf.get_auto_update_to_latest_version()
+    __SUPPORTED = conf.get_auto_update_to_latest_version() and conf.get_enable_ga_versioning()
 
     def __init__(self):
         super(_GAVersioningGovernanceFeature, self).__init__(name=self.__NAME,
diff --git a/azurelinuxagent/ga/agent_update_handler.py b/azurelinuxagent/ga/agent_update_handler.py
index 7eb77c8d..ee6a44f9 100644
--- a/azurelinuxagent/ga/agent_update_handler.py
+++ b/azurelinuxagent/ga/agent_update_handler.py
@@ -29,6 +29,14 @@ from azurelinuxagent.ga.rsm_version_updater import RSMVersionUpdater
 from azurelinuxagent.ga.self_update_version_updater import SelfUpdateVersionUpdater
 
 
+class UpdateMode(object):
+    """
+    Enum for Update modes
+    """
+    RSM = "RSM"
+    SelfUpdate = "SelfUpdate"
+
+
 def get_agent_update_handler(protocol):
     return AgentUpdateHandler(protocol)
 
@@ -179,6 +187,15 @@ class AgentUpdateHandler(object):
                     family, self._gs_id))
         return agent_family_manifests[0]
 
+    def get_current_update_mode(self):
+        """
+        Returns current update mode whether RSM or Self-Update
+        """
+        if isinstance(self._updater, RSMVersionUpdater):
+            return UpdateMode.RSM
+        else:
+            return UpdateMode.SelfUpdate
+
     def run(self, goal_state, ext_gs_updated):
 
         try:
@@ -188,6 +205,8 @@ class AgentUpdateHandler(object):
 
             # Update the state only on new goal state
             if ext_gs_updated:
+                # Reset the last reported update state on new goal state before we attempt update otherwise we keep reporting the last update error if any
+                self._last_attempted_update_error_msg = ""
                 self._gs_id = goal_state.extensions_goal_state.id
                 self._updater.sync_new_gs_id(self._gs_id)
 
diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py
index 2c2b3c26..9579fd14 100644
--- a/azurelinuxagent/ga/update.py
+++ b/azurelinuxagent/ga/update.py
@@ -394,7 +394,7 @@ class UpdateHandler(object):
                 self._check_daemon_running(debug)
                 self._check_threads_running(all_thread_handlers)
                 self._process_goal_state(exthandlers_handler, remote_access_handler, agent_update_handler)
-                self._send_heartbeat_telemetry(protocol)
+                self._send_heartbeat_telemetry(protocol, agent_update_handler)
                 self._check_agent_memory_usage()
                 time.sleep(self._goal_state_period)
 
@@ -1016,27 +1016,27 @@ class UpdateHandler(object):
 
         return pid_files, pid_file
 
-    def _send_heartbeat_telemetry(self, protocol):
+    def _send_heartbeat_telemetry(self, protocol, agent_update_handler):
         if self._last_telemetry_heartbeat is None:
             self._last_telemetry_heartbeat = datetime.utcnow() - UpdateHandler.TELEMETRY_HEARTBEAT_PERIOD
 
         if datetime.utcnow() >= (self._last_telemetry_heartbeat + UpdateHandler.TELEMETRY_HEARTBEAT_PERIOD):
             dropped_packets = self.osutil.get_firewall_dropped_packets(protocol.get_endpoint())
-            auto_update_enabled = 1 if conf.get_autoupdate_enabled() else 0
+            auto_update_enabled = 1 if conf.get_auto_update_to_latest_version() else 0
+            update_mode = agent_update_handler.get_current_update_mode()
 
-            telemetry_msg = "{0};{1};{2};{3};{4}".format(self._heartbeat_counter, self._heartbeat_id, dropped_packets,
-                                                         self._heartbeat_update_goal_state_error_count,
-                                                         auto_update_enabled)
-            debug_log_msg = "[DEBUG HeartbeatCounter: {0};HeartbeatId: {1};DroppedPackets: {2};" \
-                            "UpdateGSErrors: {3};AutoUpdate: {4}]".format(self._heartbeat_counter,
+            # Note: When we add new values to the heartbeat message, please add a semicolon at the end of the value.
+            # This helps to parse the message easily in kusto queries with regex
+            heartbeat_msg = "HeartbeatCounter: {0};HeartbeatId: {1};DroppedPackets: {2};" \
+                            "UpdateGSErrors: {3};AutoUpdate: {4};UpdateMode: {5};".format(self._heartbeat_counter,
                                                                           self._heartbeat_id, dropped_packets,
                                                                           self._heartbeat_update_goal_state_error_count,
-                                                                          auto_update_enabled)
+                                                                          auto_update_enabled, update_mode)
 
             # Write Heartbeat events/logs
             add_event(name=AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.HeartBeat, is_success=True,
-                      message=telemetry_msg, log_event=False)
-            logger.info(u"[HEARTBEAT] Agent {0} is running as the goal state agent {1}", CURRENT_AGENT, debug_log_msg)
+                      message=heartbeat_msg, log_event=False)
+            logger.info(u"[HEARTBEAT] Agent {0} is running as the goal state agent [DEBUG {1}]", CURRENT_AGENT, heartbeat_msg)
 
             # Update/Reset the counters
             self._heartbeat_counter += 1


Test patch:
diff --git a/tests/ga/test_agent_update_handler.py b/tests/ga/test_agent_update_handler.py
index ed372d50..c2d01a42 100644
--- a/tests/ga/test_agent_update_handler.py
+++ b/tests/ga/test_agent_update_handler.py
@@ -411,6 +411,29 @@ class TestAgentUpdate(UpdateTestCase):
             self.assertEqual("9.9.9.10", vm_agent_update_status.expected_version)
             self.assertIn("Failed to download agent package from all URIs", vm_agent_update_status.message)
 
+    def test_it_should_not_report_error_status_if_new_rsm_version_is_same_as_current_after_last_update_attempt_failed(self):
+        data_file = DATA_FILE.copy()
+        data_file["ext_conf"] = "wire/ext_conf_rsm_version.xml"
+
+        with self._get_agent_update_handler(test_data=data_file, protocol_get_error=True) as (agent_update_handler, _):
+            agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True)
+            vm_agent_update_status = agent_update_handler.get_vmagent_update_status()
+            self.assertEqual(VMAgentUpdateStatuses.Error, vm_agent_update_status.status)
+            self.assertEqual(1, vm_agent_update_status.code)
+            self.assertEqual("9.9.9.10", vm_agent_update_status.expected_version)
+            self.assertIn("Failed to download agent package from all URIs", vm_agent_update_status.message)
+
+            # Send same version GS after last update attempt failed
+            agent_update_handler._protocol.mock_wire_data.set_version_in_agent_family(
+                str(CURRENT_VERSION))
+            agent_update_handler._protocol.mock_wire_data.set_incarnation(2)
+            agent_update_handler._protocol.client.update_goal_state()
+            agent_update_handler.run(agent_update_handler._protocol.get_goal_state(), True)
+            vm_agent_update_status = agent_update_handler.get_vmagent_update_status()
+            self.assertEqual(VMAgentUpdateStatuses.Success, vm_agent_update_status.status)
+            self.assertEqual(0, vm_agent_update_status.code)
+            self.assertEqual(str(CURRENT_VERSION), vm_agent_update_status.expected_version)
+
     def test_it_should_report_update_status_with_missing_rsm_version_error(self):
         data_file = DATA_FILE.copy()
         data_file['ext_conf'] = "wire/ext_conf_version_missing_in_agent_family.xml"
diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py
index ef1a6905..cf690855 100644
--- a/tests/ga/test_update.py
+++ b/tests/ga/test_update.py
@@ -2449,11 +2449,11 @@ class HeartbeatTestCase(AgentTestCase):
 
         with mock_wire_protocol(wire_protocol_data.DATA_FILE) as mock_protocol:
             update_handler = get_update_handler()
-
+            agent_update_handler = Mock()
             update_handler.last_telemetry_heartbeat = datetime.utcnow() - timedelta(hours=1)
-            update_handler._send_heartbeat_telemetry(mock_protocol)
+            update_handler._send_heartbeat_telemetry(mock_protocol, agent_update_handler)
             self.assertEqual(1, patch_add_event.call_count)
-            self.assertTrue(any(call_args[0] == "[HEARTBEAT] Agent {0} is running as the goal state agent {1}"
+            self.assertTrue(any(call_args[0] == "[HEARTBEAT] Agent {0} is running as the goal state agent [DEBUG {1}]"
                             for call_args in patch_info.call_args), "The heartbeat was not written to the agent's log")