Repository: Azure/WALinuxAgent
Pull request #3349 (2025-04-18 21:18:19)

Add cgroupv2 distros to supported list
<!-- DO NOT DELETE THIS TEMPLATE -->

## Description

Adding rhel9 and azurelinux3 cgroupv2 distros to supported list

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
- [ ] Ensure development PR is based on the `develop` branch.
- [ ] 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).

nagworld9: > We should add v2 distros to log-collector-endorsed image set in images.yml

updated

Patch:
diff --git a/azurelinuxagent/ga/cgroupapi.py b/azurelinuxagent/ga/cgroupapi.py
index dd365599..99c12d57 100644
--- a/azurelinuxagent/ga/cgroupapi.py
+++ b/azurelinuxagent/ga/cgroupapi.py
@@ -67,7 +67,9 @@ class CGroupUtil(object):
         except ValueError:
             return False
         return (distro_name.lower() == 'ubuntu' and distro_version.major >= 16) or \
-               (distro_name.lower() in ('centos', 'redhat') and 8 <= distro_version.major < 9)
+            (distro_name.lower() in ('centos', 'redhat') and distro_version.major == 8) or \
+            (distro_name.lower() == 'rhel' and distro_version.major == 9) or \
+            (distro_name.lower() == 'azurelinux' and distro_version.major == 3)
 
     @staticmethod
     def get_extension_slice_name(extension_name, old_slice=False):
diff --git a/azurelinuxagent/ga/cgroupconfigurator.py b/azurelinuxagent/ga/cgroupconfigurator.py
index 14b3a919..ba3abe93 100644
--- a/azurelinuxagent/ga/cgroupconfigurator.py
+++ b/azurelinuxagent/ga/cgroupconfigurator.py
@@ -465,32 +465,32 @@ class CGroupConfigurator(object):
             except Exception as exception:
                 log_cgroup_warning("Error disabling cgroups: {0}".format(ustr(exception)))
 
-        @staticmethod
-        def _set_cpu_quota(unit_name, quota):
+        def _set_cpu_quota(self, unit_name, quota):
             """
             Sets CPU quota to the given percentage (100% == 1 CPU)
 
             NOTE: This is done using a systemtcl set-property --runtime; any local overrides in /etc folder on the VM will take precedence
             over this setting.
             """
-            quota_percentage = "{0}%".format(quota)
-            log_cgroup_info("Setting {0}'s CPUQuota to {1}".format(unit_name, quota_percentage))
-            CGroupConfigurator._Impl._try_set_cpu_quota(unit_name, quota_percentage)
+            if self._cgroups_api.can_enforce_cpu():
+                quota_percentage = "{0}%".format(quota)
+                log_cgroup_info("Setting {0}'s CPUQuota to {1}".format(unit_name, quota_percentage))
+                CGroupConfigurator._Impl._try_set_cpu_quota(unit_name, quota_percentage)
 
-        @staticmethod
-        def _reset_cpu_quota(unit_name):
+        def _reset_cpu_quota(self, unit_name):
             """
             Removes any CPUQuota on the agent
 
             NOTE: This resets the quota on the agent's default dropin file; any local overrides on the VM will take precedence
             over this setting.
             """
-            log_cgroup_info("Resetting {0}'s CPUQuota".format(unit_name), send_event=False)
-            if CGroupConfigurator._Impl._try_set_cpu_quota(unit_name, "infinity"): # systemd expresses no-quota as infinity, following the same convention
-                try:
-                    log_cgroup_info('Current CPUQuota: {0}'.format(systemd.get_unit_property(unit_name, "CPUQuotaPerSecUSec")))
-                except Exception as e:
-                    log_cgroup_warning('Failed to get current CPUQuotaPerSecUSec after reset: {0}'.format(ustr(e)))
+            if self._cgroups_api.can_enforce_cpu():
+                log_cgroup_info("Resetting {0}'s CPUQuota".format(unit_name), send_event=False)
+                if CGroupConfigurator._Impl._try_set_cpu_quota(unit_name, "infinity"): # systemd expresses no-quota as infinity, following the same convention
+                    try:
+                        log_cgroup_info('Current CPUQuota: {0}'.format(systemd.get_unit_property(unit_name, "CPUQuotaPerSecUSec")))
+                    except Exception as e:
+                        log_cgroup_warning('Failed to get current CPUQuotaPerSecUSec after reset: {0}'.format(ustr(e)))
 
         # W0238: Unused private member `_Impl.__try_set_cpu_quota(quota)` (unused-private-member)
         @staticmethod
@@ -938,7 +938,7 @@ class CGroupConfigurator(object):
                         self._cleanup_all_files(files_to_remove)
 
                         cpu_quota = service.get('cpuQuotaPercentage')
-                        cpu_quota = "{0}%".format(cpu_quota) if cpu_quota is not None else "infinity"  # following systemd convention for no-quota (infinity)
+                        cpu_quota = "{0}%".format(cpu_quota) if cpu_quota is not None and self._cgroups_api.can_enforce_cpu() else "infinity"  # following systemd convention for no-quota (infinity)
                         try:
                             properties_to_update, properties_values = self._get_unit_properties_requiring_update(service_name, cpu_quota)
                         except Exception as exception:


Test patch:
diff --git a/tests/ga/test_cgroupapi.py b/tests/ga/test_cgroupapi.py
index 61ec6a43..a287b171 100644
--- a/tests/ga/test_cgroupapi.py
+++ b/tests/ga/test_cgroupapi.py
@@ -54,7 +54,7 @@ class _MockedFileSystemTestCase(AgentTestCase):
 
 
 class CGroupUtilTestCase(AgentTestCase):
-    def test_cgroups_should_be_supported_only_on_ubuntu16_centos7dot4_redhat7dot4_and_later_versions(self):
+    def test_cgroups_should_be_supported_only_on_ubuntu16plus_centos8_redhat8_rhel9_azurelinux3(self):
         test_cases = [
             (['ubuntu', '16.04', 'xenial'], True),
             (['ubuntu', '16.10', 'yakkety'], True),
@@ -78,6 +78,13 @@ class CGroupUtilTestCase(AgentTestCase):
             (['bigip', '15.0.1', 'Final'], False),
             (['gaia', '273.562', 'R80.30'], False),
             (['debian', '9.1', ''], False),
+            (['rhel', '9.5', "source"], True),
+            (['rhel', '9.0', "core"], True),
+            (['rhel', '10.9', "core"], False),
+            (['mariner', '1.0', ''], False),
+            (['mariner', '2.2', ''], False),
+            (['azurelinux', '3.0', ''], True),
+            (['azurelinux', '3.10', ''], True)
         ]
 
         for (distro, supported) in test_cases:
diff --git a/tests/ga/test_cgroupconfigurator.py b/tests/ga/test_cgroupconfigurator.py
index 8cd6e8a1..ba6f1ce5 100644
--- a/tests/ga/test_cgroupconfigurator.py
+++ b/tests/ga/test_cgroupconfigurator.py
@@ -204,11 +204,34 @@ class CGroupConfiguratorSystemdTestCase(AgentTestCase):
                     any(cg for cg in tracked if tracked[cg].name == AGENT_NAME_TELEMETRY and 'cpu' in cg),
                     "The Agent's cpu is being tracked. Tracked: {0}".format(tracked))
 
-    def test_enforcement_not_enabled_in_v2(self):
+    def test_agent_enforcement_not_enabled_in_v2(self):
         with self._get_cgroup_configurator_v2() as configurator:
             cmd = 'systemctl set-property walinuxagent.service CPUQuota'
             self.assertNotIn(cmd, configurator.mocks.commands_call_list, "The command to set CPU quota was called")
 
+    def test_extension_enforcement_not_enabled_in_v2(self):
+        service_list = [
+            {
+                "name": "extension.service",
+                "cpuQuotaPercentage": 5
+            }
+        ]
+        with self._get_cgroup_configurator_v2() as configurator:
+            configurator.setup_extension_slice(extension_name="Microsoft.CPlat.Extension", cpu_quota=5)
+            cmd = 'systemctl set-property azure-vmextensions-Microsoft.CPlat.Extension.slice CPUAccounting=yes MemoryAccounting=yes CPUQuota'
+            self.assertNotIn(cmd, configurator.mocks.commands_call_list,
+                            "The command to set the CPU quota was not called")
+            cmd = 'systemctl set-property azure-vmextensions-Microsoft.CPlat.Extension.slice CPUQuota'
+            self.assertNotIn(cmd, configurator.mocks.commands_call_list,
+                            "The command to set the CPU quota was not called")
+            configurator.set_extension_services_cpu_memory_quota(service_list)
+            cmd = 'systemctl set-property extension.service CPUAccounting=yes MemoryAccounting=yes CPUQuota'
+            self.assertNotIn(cmd, configurator.mocks.commands_call_list,
+                          "The command to set the reset CPU quota was not called")
+            cmd = 'systemctl set-property extension.service CPUQuota'
+            self.assertNotIn(cmd, configurator.mocks.commands_call_list,
+                            "The command to set the CPU quota was not called")
+
     def test_initialize_should_not_create_unit_files(self):
         with self._get_cgroup_configurator() as configurator:
             # get the paths to the mocked files
diff --git a/tests_e2e/test_suites/cgroup_v2_enabled.yml b/tests_e2e/test_suites/cgroup_v2_enabled.yml
index 9408f40b..3bbb9248 100644
--- a/tests_e2e/test_suites/cgroup_v2_enabled.yml
+++ b/tests_e2e/test_suites/cgroup_v2_enabled.yml
@@ -6,9 +6,12 @@ name: "Cgroupv2Enabled"
 tests:
   - "agent_cgroups/agent_cgroups.py"
   - "ext_cgroups/ext_cgroups.py"
+  - "cgroup_v2_enabled/check_enforcement.py"
 images:
   - "ubuntu_2204"
   - "ubuntu_2404"
+  - "rhel_90"
+  - "azure-linux_3"
 # The DCR test extension installs sample service, so this test suite uses it to test services cgroups but this is only published in southcentralus region in public cloud.
 locations: "AzureCloud:southcentralus"
 skip_on_clouds:
diff --git a/tests_e2e/test_suites/images.yml b/tests_e2e/test_suites/images.yml
index 552dc083..dc872e9d 100644
--- a/tests_e2e/test_suites/images.yml
+++ b/tests_e2e/test_suites/images.yml
@@ -60,8 +60,10 @@ image-sets:
 
    # These are the distros which have periodic log collector support.
    log-collector-endorsed:
+      - "azure-linux_3"
       - "centos_82"
       - "rhel_82"
+      - "rhel_90"
       - "ubuntu_1604"
       - "ubuntu_1804"
       - "ubuntu_2004"
diff --git a/tests_e2e/tests/cgroup_v2_enabled/check_enforcement.py b/tests_e2e/tests/cgroup_v2_enabled/check_enforcement.py
new file mode 100644
index 00000000..3c83ab8b
--- /dev/null
+++ b/tests_e2e/tests/cgroup_v2_enabled/check_enforcement.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+
+# Microsoft Azure Linux Agent
+#
+# 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.
+#
+from assertpy import assert_that
+
+from tests_e2e.tests.lib.agent_test import AgentVmTest
+from tests_e2e.tests.lib.logging import log
+from tests_e2e.tests.lib.ssh_client import SshClient
+
+
+class CheckEnforcement(AgentVmTest):
+    """
+    This test verifies that the agent does not enforce limits in cgroupv2 distros.
+    """
+
+    def run(self):
+        ssh_client: SshClient = self._context.create_ssh_client()
+        #
+        # 2025-04-18T21:15:37.336480Z INFO ExtHandler ExtHandler [CGI] Setting azuremonitoragent's CPUQuota to 25%
+        log.info("Parsing agent log(Settings CPUQuota) on the test VM")
+        output = [line for line in ssh_client.run_command("grep -E 'INFO ExtHandler.*(Setting|Resetting).*CPUQuota' /var/log/waagent.log | head").strip().split('\n') if line]
+
+        assert_that(len(output) == 0).described_as("The agent log should not contain matching records of cpu quota being set. Output from the agent log:\n\t\t{0}".format('\n\t\t'.join(output))).is_true()
+        log.info("The agent log indicates that agent does not enforce limits in cgroupv2 distros")