Repository: Azure/WALinuxAgent
Pull request #1896 (2020-05-28 03:00:52)

Improvements in setup of cgroups
Previously the cgroups code would make several assumptions as to how cgroups were setup.

These changes discover the current setup instead of making those assumptions. Also, it sends telemetry for important parts of that setup.

codecov[bot]: # [Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896?src=pr&el=h1) Report
> Merging [#1896](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896?src=pr&el=desc) into [develop](https://codecov.io/gh/Azure/WALinuxAgent/commit/1268fdce6bf02b464fc87395c4c712a2fd7d5955&el=desc) will **decrease** coverage by `0.15%`.
> The diff coverage is `77.17%`.

[![Impacted file tree graph](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896/graphs/tree.svg?width=650&height=150&src=pr&token=ae90E2KYCw)](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896?src=pr&el=tree)

```diff
@@             Coverage Diff             @@
##           develop    #1896      +/-   ##
===========================================
- Coverage    69.20%   69.04%   -0.16%     
===========================================
  Files           83       83              
  Lines        11445    11548     +103     
  Branches      1589     1619      +30     
===========================================
+ Hits          7920     7973      +53     
- Misses        3202     3218      +16     
- Partials       323      357      +34     
```


| [Impacted Files](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [azurelinuxagent/common/osutil/default.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9vc3V0aWwvZGVmYXVsdC5weQ==) | `57.89% <ø> (-1.24%)` | :arrow_down: |
| [azurelinuxagent/common/osutil/ubuntu.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9vc3V0aWwvdWJ1bnR1LnB5) | `68.49% <ø> (-0.34%)` | :arrow_down: |
| [azurelinuxagent/daemon/main.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2RhZW1vbi9tYWluLnB5) | `72.16% <ø> (-0.57%)` | :arrow_down: |
| [azurelinuxagent/ga/exthandlers.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2dhL2V4dGhhbmRsZXJzLnB5) | `86.13% <ø> (-2.08%)` | :arrow_down: |
| [azurelinuxagent/ga/update.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2dhL3VwZGF0ZS5weQ==) | `87.67% <ø> (-0.99%)` | :arrow_down: |
| [azurelinuxagent/common/cgroupconfigurator.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBjb25maWd1cmF0b3IucHk=) | `74.60% <64.38%> (-15.62%)` | :arrow_down: |
| [azurelinuxagent/common/cgroupapi.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBhcGkucHk=) | `81.42% <84.90%> (-3.74%)` | :arrow_down: |
| [azurelinuxagent/common/cgroupstelemetry.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBzdGVsZW1ldHJ5LnB5) | `86.81% <100.00%> (ø)` | |
| [azurelinuxagent/common/event.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9ldmVudC5weQ==) | `85.90% <100.00%> (-0.42%)` | :arrow_down: |
| ... and [10 more](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896?src=pr&el=footer). Last update [1268fdc...d521d6c](https://codecov.io/gh/Azure/WALinuxAgent/pull/1896?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/azurelinuxagent/common/cgroupapi.py b/azurelinuxagent/common/cgroupapi.py
index c671a2e9..50790e1b 100644
--- a/azurelinuxagent/common/cgroupapi.py
+++ b/azurelinuxagent/common/cgroupapi.py
@@ -16,6 +16,7 @@
 
 import errno
 import os
+import re
 import shutil
 import subprocess
 import uuid
@@ -25,12 +26,13 @@ from azurelinuxagent.common.cgroup import CGroup
 from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry
 from azurelinuxagent.common.conf import get_agent_pid_file_path
 from azurelinuxagent.common.event import add_event, WALAEventOperation
-from azurelinuxagent.common.exception import CGroupsException, ExtensionErrorCodes, ExtensionError, \
-    ExtensionOperationError
+from azurelinuxagent.common.exception import CGroupsException, ExtensionErrorCodes, ExtensionError, ExtensionOperationError
 from azurelinuxagent.common.future import ustr
+from azurelinuxagent.common.osutil import get_osutil
 from azurelinuxagent.common.utils import fileutil, shellutil
 from azurelinuxagent.common.utils.extensionprocessutil import handle_process_completion, read_output
-from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION
+from azurelinuxagent.common.utils.flexible_version import FlexibleVersion
+from azurelinuxagent.common.version import get_distro
 
 CGROUPS_FILE_SYSTEM_ROOT = '/sys/fs/cgroup'
 CGROUP_CONTROLLERS = ["cpu", "memory"]
@@ -43,9 +45,6 @@ class CGroupsApi(object):
     """
     Interface for the cgroups API
     """
-    def create_agent_cgroups(self):
-        raise NotImplementedError()
-
     def create_extension_cgroups_root(self):
         raise NotImplementedError()
 
@@ -64,6 +63,16 @@ class CGroupsApi(object):
     def cleanup_legacy_cgroups(self):
         raise NotImplementedError()
 
+    @staticmethod
+    def cgroups_supported():
+        distro_info = get_distro()
+        distro_name = distro_info[0]
+        try:
+            distro_version = FlexibleVersion(distro_info[1])
+        except ValueError:
+            return False
+        return distro_name.lower() == 'ubuntu' and distro_version.major >= 16
+
     @staticmethod
     def track_cgroups(extension_cgroups):
         try:
@@ -146,6 +155,7 @@ class CGroupsApi(object):
             for _, cgroup in legacy_cgroups:
                 logger.info('Removing {0}', cgroup)
                 shutil.rmtree(cgroup, ignore_errors=True)
+        return len(legacy_cgroups)
 
 
 class FileSystemCgroupsApi(CGroupsApi):
@@ -202,6 +212,58 @@ class FileSystemCgroupsApi(CGroupsApi):
         fileutil.append_file(tasks_file, "{0}\n".format(pid))
         logger.info("Added PID {0} to cgroup {1}".format(pid, cgroup_path))
 
+    @staticmethod
+    def mount_cgroups():
+        def cgroup_path(tail=""):
+            return os.path.join(CGROUPS_FILE_SYSTEM_ROOT, tail).rstrip(os.path.sep)
+
+        try:
+            osutil = get_osutil()
+            path = cgroup_path()
+            if not os.path.exists(path):
+                fileutil.mkdir(path)
+                osutil.mount(device='cgroup_root',
+                           mount_point=path,
+                           option="-t tmpfs",
+                           chk_err=False)
+            elif not os.path.isdir(cgroup_path()):
+                logger.error("Could not mount cgroups: ordinary file at {0}", path)
+                return
+
+            controllers_to_mount = ['cpu,cpuacct', 'memory']
+            errors = 0
+            cpu_mounted = False
+            for controller in controllers_to_mount:
+                try:
+                    target_path = cgroup_path(controller)
+                    if not os.path.exists(target_path):
+                        fileutil.mkdir(target_path)
+                        osutil.mount(device=controller,
+                                   mount_point=target_path,
+                                   option="-t cgroup -o {0}".format(controller),
+                                   chk_err=False)
+                        if controller == 'cpu,cpuacct':
+                            cpu_mounted = True
+                except Exception as exception:
+                    errors += 1
+                    if errors == len(controllers_to_mount):
+                        raise
+                    logger.warn("Could not mount cgroup controller {0}: {1}", controller, ustr(exception))
+
+            if cpu_mounted:
+                for controller in ['cpu', 'cpuacct']:
+                    target_path = cgroup_path(controller)
+                    if not os.path.exists(target_path):
+                        os.symlink(cgroup_path('cpu,cpuacct'), target_path)
+
+        except OSError as oe:
+            # log a warning for read-only file systems
+            logger.warn("Could not mount cgroups: {0}", ustr(oe))
+            raise
+        except Exception as e:
+            logger.error("Could not mount cgroups: {0}", ustr(e))
+            raise
+
     def cleanup_legacy_cgroups(self):
         """
         Previous versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent;
@@ -213,9 +275,9 @@ class FileSystemCgroupsApi(CGroupsApi):
             logger.info("Writing daemon's PID ({0}) to {1}", daemon_pid, new_path)
             fileutil.append_file(os.path.join(new_path, "cgroup.procs"), daemon_pid)
             msg = "Moved daemon's PID from legacy cgroup to {0}".format(new_path)
-            add_event(AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.CGroupsCleanUp, is_success=True, message=msg)
+            add_event(op=WALAEventOperation.CGroupsCleanUp, is_success=True, message=msg)
 
-        CGroupsApi._foreach_legacy_cgroup(move_daemon_pid)
+        return CGroupsApi._foreach_legacy_cgroup(move_daemon_pid)
 
     def create_agent_cgroups(self):
         """
@@ -369,6 +431,96 @@ class SystemdCgroupsApi(CGroupsApi):
     Cgroups interface via systemd
     """
 
+    @staticmethod
+    def get_systemd_version():
+        # the output is similar to
+        #    $ systemctl --version
+        #    systemd 245 (245.4-4ubuntu3)
+        #    +PAM +AUDIT +SELINUX +IMA +APPARMOR +SMACK +SYSVINIT +UTMP etc
+        #
+        return shellutil.run_command(['systemctl', '--version'])
+
+    @staticmethod
+    def get_cpu_and_memory_mount_points():
+        """
+        Returns a tuple with the mount points for the cpu and memory controllers; the values can be None
+        if the corresponding controller is not mounted
+        """
+        # the output of mount is similar to
+        #     $ mount -t cgroup
+        #     cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
+        #     cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
+        #     cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
+        #     etc
+        #
+        cpu = None
+        memory = None
+        for line in shellutil.run_command(['mount', '-t', 'cgroup']).splitlines():
+            match = re.search(r'on\s+(?P<path>/\S+(memory|cpuacct))\s', line)
+            if match is not None:
+                path = match.group('path')
+                if 'cpuacct' in path:
+                    cpu = path
+                else:
+                    memory = path
+        return cpu, memory
+
+    @staticmethod
+    def get_cpu_and_memory_cgroup_relative_paths_for_process(process_id):
+        """
+        Returns a tuple with the path of the cpu and memory cgroups for the given process (relative to the mount point of the corresponding
+        controller).
+        The 'process_id' can be a numeric PID or the string "self" for the current process.
+        The values returned can be None if the process is not in a cgroup for that controller (e.g. the controller is not mounted).
+        """
+        # The contents of the file are similar to
+        #    # cat /proc/1218/cgroup
+        #    10:memory:/system.slice/walinuxagent.service
+        #    3:cpu,cpuacct:/system.slice/walinuxagent.service
+        #    etc
+        cpu_path = None
+        memory_path = None
+        for line in fileutil.read_file("/proc/{0}/cgroup".format(process_id)).splitlines():
+            match = re.match(r'\d+:(?P<controller>(memory|.*cpuacct.*)):(?P<path>.+)', line)
+            if match is not None:
+                controller = match.group('controller')
+                path = match.group('path').lstrip('/') if match.group('path') != '/' else None
+                if controller == 'memory':
+                    memory_path = path
+                else:
+                    cpu_path = path
+
+        return cpu_path, memory_path
+
+    @staticmethod
+    def get_cgroup2_controllers():
+        """
+        Returns a tuple with the mount point for the cgroups v2 controllers, and the currently mounted controllers;
+        either value can be None if cgroups v2 or its controllers are not mounted
+        """
+        # the output of mount is similar to
+        #     $ mount -t cgroup2
+        #     cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
+        #
+        for line in shellutil.run_command(['mount', '-t', 'cgroup2']).splitlines():
+            match = re.search(r'on\s+(?P<path>/\S+)\s', line)
+            if match is not None:
+                mount_point = match.group('path')
+                controllers = None
+                controllers_file = os.path.join(mount_point, 'cgroup.controllers')
+                if os.path.exists(controllers_file):
+                    controllers = fileutil.read_file(controllers_file)
+                return mount_point, controllers
+        return None, None
+
+    @staticmethod
+    def get_unit_property(unit_name, property_name):
+        output = shellutil.run_command(["systemctl", "show", unit_name, "--property", property_name])
+        match = re.match("[^=]+=(?P<value>.+)", output)
+        if match is None:
+            raise ValueError("Can't find property {0} of {1}", property_name, unit_name)
+        return match.group('value')
+
     @staticmethod
     def create_and_start_unit(unit_filename, unit_contents):
         try:
@@ -386,23 +538,6 @@ class SystemdCgroupsApi(CGroupsApi):
     def _get_extension_slice_name(self, extension_name):
         return "system-{0}-{1}.slice".format(EXTENSIONS_ROOT_CGROUP_NAME, self._get_extension_cgroup_name(extension_name))
 
-    def create_agent_cgroups(self):
-        try:
-            cgroup_unit = None
-            cgroup_paths = fileutil.read_file("/proc/self/cgroup")
-            for entry in cgroup_paths.splitlines():
-                fields = entry.split(':')
-                if fields[1] == "name=systemd":
-                    cgroup_unit = fields[2].lstrip(os.path.sep)
-
-            cpu_cgroup_path = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, 'cpu', cgroup_unit)
-            memory_cgroup_path = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, 'memory', cgroup_unit)
-
-            return [CGroup.create(cpu_cgroup_path, 'cpu', VM_AGENT_CGROUP_NAME),
-                    CGroup.create(memory_cgroup_path, 'memory', VM_AGENT_CGROUP_NAME)]
-        except Exception as e:
-            raise CGroupsException("Failed to get paths of agent's cgroups. Error: {0}".format(ustr(e)))
-
     def create_extension_cgroups_root(self):
         unit_contents = """
 [Unit]
@@ -517,12 +652,7 @@ After=system-{1}.slice""".format(extension_name, EXTENSIONS_ROOT_CGROUP_NAME)
                 event_msg = 'Failed to run systemd-run for unit {0}.scope. ' \
                             'Will retry invoking the extension without systemd. ' \
                             'Systemd-run error: {1}'.format(scope_name, err_msg)
-                add_event(AGENT_NAME,
-                          version=CURRENT_VERSION,
-                          op=WALAEventOperation.InvokeCommandUsingSystemd,
-                          is_success=False,
-                          log_event=False,
-                          message=event_msg)
+                add_event(op=WALAEventOperation.InvokeCommandUsingSystemd, is_success=False, log_event=False, message=event_msg)
                 logger.warn(event_msg)
 
                 # Reset the stdout and stderr
@@ -556,11 +686,7 @@ After=system-{1}.slice""".format(extension_name, EXTENSIONS_ROOT_CGROUP_NAME)
         """
         Previous versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent;
         starting from version 2.2.41 we track the agent service in walinuxagent.service instead of WALinuxAgent/WALinuxAgent. If
-        we find that any of the legacy groups include the PID of the daemon then we disable data collection for this instance
-        (under systemd, moving PIDs across the cgroup file system can produce unpredictable results)
+        we find that any of the legacy groups include the PID of the daemon then we need to disable data collection for this
+        instance (under systemd, moving PIDs across the cgroup file system can produce unpredictable results)
         """
-        def report_error(_, daemon_pid):
-            raise CGroupsException(
-                "The daemon's PID ({0}) was already added to the legacy cgroup; this invalidates resource usage data.".format(daemon_pid))
-
-        CGroupsApi._foreach_legacy_cgroup(report_error)
+        return CGroupsApi._foreach_legacy_cgroup(lambda *_: None)
diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py
index ea6983f5..8cad14af 100644
--- a/azurelinuxagent/common/cgroupconfigurator.py
+++ b/azurelinuxagent/common/cgroupconfigurator.py
@@ -18,13 +18,14 @@ import os
 import subprocess
 
 from azurelinuxagent.common import logger
-from azurelinuxagent.common.cgroupapi import CGroupsApi
+from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup
+from azurelinuxagent.common.cgroupapi import CGroupsApi, SystemdCgroupsApi
 from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry
-from azurelinuxagent.common.exception import CGroupsException, ExtensionErrorCodes
+from azurelinuxagent.common.exception import ExtensionErrorCodes, CGroupsException
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.osutil import get_osutil
+from azurelinuxagent.common.version import get_distro
 from azurelinuxagent.common.utils.extensionprocessutil import handle_process_completion
-from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION
 from azurelinuxagent.common.event import add_event, WALAEventOperation
 
 
@@ -36,48 +37,131 @@ class CGroupConfigurator(object):
     """
     class __impl(object):
         def __init__(self):
-            """
-            Ensures the cgroups file system is mounted and selects the correct API to interact with it
-            """
-            osutil = get_osutil()
-
-            self._cgroups_supported = osutil.is_cgroups_supported()
-
-            if self._cgroups_supported:
-                self._enabled = True
-                try:
-                    osutil.mount_cgroups()
-                    self._cgroups_api = CGroupsApi.create()
-                    status = "The cgroup filesystem is ready to use"
-                except Exception as e:
-                    status = ustr(e)
-                    self._enabled = False
-            else:
-                self._enabled = False
-                self._cgroups_api = None
-                status = "Cgroups are not supported by the platform"
+            self._initialized = False
+            self._cgroups_supported = False
+            self._cgroups_enabled = False
+            self._cgroups_api = None
 
-            logger.info("CGroups Status: {0}".format(status))
+        def initialize(self):
+            try:
+                if self._initialized:
+                    return
+
+                #
+                # check whether cgroup monitoring is supported on the current distro
+                #
+                self._cgroups_supported = CGroupsApi.cgroups_supported()
+                if not self._cgroups_supported:
+                    logger.info("Cgroup monitoring is not supported on {0}", get_distro())
+                    return
+
+                #
+                # check systemd
+                #
+                self._cgroups_api = CGroupsApi.create()
+
+                if not isinstance(self._cgroups_api, SystemdCgroupsApi):
+                    message = "systemd was not detected on {0}".format(get_distro())
+                    logger.warn(message)
+                    add_event(op=WALAEventOperation.CGroupsInitialize, is_success=False, message=message, log_event=False)
+                    return
+
+                def log_cgroup_info(format_string, *args):
+                    message = format_string.format(*args)
+                    logger.info(message)
+                    add_event(op=WALAEventOperation.CGroupsInfo, message=message)
+
+                def log_cgroup_warn(format_string, *args):
+                    message = format_string.format(*args)
+                    logger.warn(message)
+                    add_event(op=WALAEventOperation.CGroupsInfo, message=message, is_success=False, log_event=False)
+
+                log_cgroup_info("systemd version: {0}", self._cgroups_api.get_systemd_version())
+
+                #
+                # Older versions of the daemon (2.2.31-2.2.40) wrote their PID to /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent.  When running
+                # under systemd this could produce invalid resource usage data. Do not enable cgroups under this condition.
+                #
+                legacy_cgroups = self._cgroups_api.cleanup_legacy_cgroups()
+
+                if legacy_cgroups > 0:
+                    log_cgroup_warn("The daemon's PID was added to a legacy cgroup; will not monitor resource usage.")
+                    return
+
+                #
+                # check v1 controllers
+                #
+                cpu_controller_root, memory_controller_root = self._cgroups_api.get_cpu_and_memory_mount_points()
+
+                if cpu_controller_root is not None:
+                    logger.info("The CPU cgroup controller is mounted at {0}", cpu_controller_root)
+                else:
+                    log_cgroup_warn("The CPU cgroup controller is not mounted")
+
+                if memory_controller_root is not None:
+                    logger.info("The memory cgroup controller is mounted at {0}", memory_controller_root)
+                else:
+                    log_cgroup_warn("The memory cgroup controller is not mounted")
+
+                #
+                # check v2 controllers
+                #
+                cgroup2_mountpoint, cgroup2_controllers = self._cgroups_api.get_cgroup2_controllers()
+                if cgroup2_mountpoint is not None:
+                    log_cgroup_warn("cgroups v2 mounted at {0}.  Controllers: [{1}]", cgroup2_mountpoint, cgroup2_controllers)
+
+                #
+                # check the cgroups for the agent
+                #
+                agent_unit_name = get_osutil().get_service_name() + ".service"
+                cpu_cgroup_relative_path, memory_cgroup_relative_path = self._cgroups_api.get_cpu_and_memory_cgroup_relative_paths_for_process("self")
+                if cpu_cgroup_relative_path is None:
+                    log_cgroup_warn("The agent's process is not within a CPU cgroup")
+                else:
+                    cpu_accounting = self._cgroups_api.get_unit_property(agent_unit_name, "CPUAccounting")
+                    log_cgroup_info('CPUAccounting: {0}', cpu_accounting)
+
+                if memory_cgroup_relative_path is None:
+                    log_cgroup_warn("The agent's process is not within a memory cgroup")
+                else:
+                    memory_accounting = self._cgroups_api.get_unit_property(agent_unit_name, "MemoryAccounting")
+                    log_cgroup_info('MemoryAccounting: {0}', memory_accounting)
+
+                #
+                # All good, enable cgroups and start monitoring the agent
+                #
+                self._cgroups_enabled = True
+
+                if cpu_controller_root is None or cpu_cgroup_relative_path is None:
+                    logger.info("Will not track CPU for the agent's cgroup")
+                else:
+                    cpu_cgroup_path = os.path.join(cpu_controller_root, cpu_cgroup_relative_path)
+                    CGroupsTelemetry.track_cgroup(CpuCgroup(agent_unit_name, cpu_cgroup_path))
+
+                if memory_controller_root is None or memory_cgroup_relative_path is None:
+                    logger.info("Will not track memory for the agent's cgroup")
+                else:
+                    memory_cgroup_path = os.path.join(memory_controller_root, memory_cgroup_relative_path)
+                    CGroupsTelemetry.track_cgroup(MemoryCgroup(agent_unit_name, memory_cgroup_path))
 
-            add_event(
-                AGENT_NAME,
-                version=CURRENT_VERSION,
-                op=WALAEventOperation.InitializeCGroups,
-                is_success=self._enabled,
-                message=status,
-                log_event=False)
+            except Exception as e:
+                message = "Error initializing cgroups: {0}".format(ustr(e))
+                logger.warn(message)
+                add_event(op=WALAEventOperation.CGroupsInitialize, is_success=False, message=message, log_event=False)
+            finally:
+                self._initialized = True
 
         def enabled(self):
-            return self._enabled
+            return self._cgroups_enabled
 
         def enable(self):
             if not self._cgroups_supported:
-                raise CGroupsException("cgroups are not supported on the current platform")
+                raise CGroupsException("Attempted to enable cgroups, but they are not supported on the current platform")
 
-            self._enabled = True
+            self._cgroups_enabled = True
 
         def disable(self):
-            self._enabled = False
+            self._cgroups_enabled = False
             CGroupsTelemetry.reset()
 
         def _invoke_cgroup_operation(self, operation, error_message, on_error=None):
@@ -97,39 +181,6 @@ class CGroupConfigurator(object):
                     except Exception as ex:
                         logger.warn("CGroupConfigurator._invoke_cgroup_operation: {0}".format(ustr(e)))
 
-        def create_agent_cgroups(self, track_cgroups):
-            """
-            Creates and returns the cgroups needed to track the VM Agent
-            """
-            def __impl():
-                cgroups = self._cgroups_api.create_agent_cgroups()
-
-                if track_cgroups:
-                    for cgroup in cgroups:
-                        CGroupsTelemetry.track_cgroup(cgroup)
-
-                return cgroups
-
-            self._invoke_cgroup_operation(__impl, "Failed to create a cgroup for the VM Agent; resource usage for the Agent will not be tracked.")
-
-        def cleanup_legacy_cgroups(self):
-            def __impl():
-                self._cgroups_api.cleanup_legacy_cgroups()
-
-            message = 'Failed to process legacy cgroups. Collection of resource usage data will be disabled.'
-
-            def disable_cgroups(exception):
-                self.disable()
-                add_event(
-                    AGENT_NAME,
-                    version=CURRENT_VERSION,
-                    op=WALAEventOperation.CGroupsCleanUp,
-                    is_success=False,
-                    log_event=False,
-                    message='{0} {1}'.format(message, ustr(exception)))
-
-            self._invoke_cgroup_operation(__impl, message, on_error=disable_cgroups)
-
         def create_extension_cgroups_root(self):
             """
             Creates the container (directory/cgroup) that includes the cgroups for all extensions (/sys/fs/cgroup/*/walinuxagent.extensions)
@@ -200,7 +251,7 @@ class CGroupConfigurator(object):
 
             return process_output
 
-    # unique instance for the singleton (TODO: find a better pattern for a singleton)
+    # unique instance for the singleton
     _instance = None
 
     @staticmethod
diff --git a/azurelinuxagent/common/cgroupstelemetry.py b/azurelinuxagent/common/cgroupstelemetry.py
index 90961e4e..3010449c 100644
--- a/azurelinuxagent/common/cgroupstelemetry.py
+++ b/azurelinuxagent/common/cgroupstelemetry.py
@@ -81,7 +81,7 @@ class CGroupsTelemetry(object):
         with CGroupsTelemetry._rlock:
             if not CGroupsTelemetry.is_tracked(cgroup.path):
                 CGroupsTelemetry._tracked.append(cgroup)
-                logger.info("Started tracking new cgroup: {0}, path: {1}".format(cgroup.name, cgroup.path))
+                logger.info("Started tracking cgroup: {0}, path: {1}".format(cgroup.name, cgroup.path))
 
     @staticmethod
     def is_tracked(path):
diff --git a/azurelinuxagent/common/event.py b/azurelinuxagent/common/event.py
index 9b4fc01d..e1eaa815 100644
--- a/azurelinuxagent/common/event.py
+++ b/azurelinuxagent/common/event.py
@@ -36,7 +36,7 @@ from azurelinuxagent.common.osutil import get_osutil
 from azurelinuxagent.common.telemetryevent import TelemetryEventParam, TelemetryEvent
 from azurelinuxagent.common.utils import fileutil, textutil
 from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, getattrib
-from azurelinuxagent.common.version import CURRENT_VERSION, CURRENT_AGENT, DISTRO_NAME, DISTRO_VERSION, DISTRO_CODE_NAME, AGENT_EXECUTION_MODE
+from azurelinuxagent.common.version import CURRENT_VERSION, CURRENT_AGENT, AGENT_NAME, DISTRO_NAME, DISTRO_VERSION, DISTRO_CODE_NAME, AGENT_EXECUTION_MODE
 from azurelinuxagent.common.telemetryevent import TelemetryEventList
 from azurelinuxagent.common.protocol.imds import get_imds_client
 
@@ -72,6 +72,8 @@ class WALAEventOperation:
     ArtifactsProfileBlob = "ArtifactsProfileBlob"
     AutoUpdate = "AutoUpdate"
     CGroupsCleanUp = "CGroupsCleanUp"
+    CGroupsInfo = "CGroupsInfo"
+    CGroupsInitialize = "CGroupsInitialize"
     CGroupsLimitsCrossed = "CGroupsLimitsCrossed"
     ConfigurationChange = "ConfigurationChange"
     CustomData = "CustomData"
@@ -92,7 +94,6 @@ class WALAEventOperation:
     HttpErrors = "HttpErrors"
     ImdsHeartbeat = "ImdsHeartbeat"
     Install = "Install"
-    InitializeCGroups = "InitializeCGroups"
     InitializeHostPlugin = "InitializeHostPlugin"
     InvokeCommandUsingSystemd = "InvokeCommandUsingSystemd"
     Log = "Log"
@@ -689,7 +690,7 @@ def initialize_event_logger_vminfo_common_parameters(protocol, reporter=__event_
     reporter.initialize_vminfo_common_parameters(protocol)
 
 
-def add_event(name, op=WALAEventOperation.Unknown, is_success=True, duration=0, version=str(CURRENT_VERSION),
+def add_event(name=AGENT_NAME, op=WALAEventOperation.Unknown, is_success=True, duration=0, version=str(CURRENT_VERSION),
               message="", log_event=True, reporter=__event_logger__):
     if reporter.event_dir is None:
         logger.warn("Cannot add event -- Event reporter is not initialized.")
diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py
index 7bd1bbb1..f84f503d 100644
--- a/azurelinuxagent/common/osutil/default.py
+++ b/azurelinuxagent/common/osutil/default.py
@@ -95,8 +95,6 @@ IFNAMSIZ = 16
 
 IP_COMMAND_OUTPUT = re.compile('^\d+:\s+(\w+):\s+(.*)$')
 
-BASE_CGROUPS = '/sys/fs/cgroup'
-
 STORAGE_DEVICE_PATH = '/sys/bus/vmbus/devices/'
 GEN2_DEVICE_ID = 'f8b3781a-1e82-4818-a1c3-63d806ec15bb'
 
@@ -303,64 +301,6 @@ class DefaultOSUtil(object):
         return id_this.lower() == id_that.lower() or \
             id_this.lower() == self._correct_instance_id(id_that).lower()
 
-    @staticmethod
-    def is_cgroups_supported():
-        """
-        Enabled by default; disabled if the base path of cgroups doesn't exist.
-        """
-        return os.path.exists(BASE_CGROUPS)
-
-    @staticmethod
-    def _cgroup_path(tail=""):
-        return os.path.join(BASE_CGROUPS, tail).rstrip(os.path.sep)
-
-    def mount_cgroups(self):
-        try:
-            path = self._cgroup_path()
-            if not os.path.exists(path):
-                fileutil.mkdir(path)
-                self.mount(device='cgroup_root',
-                           mount_point=path,
-                           option="-t tmpfs",
-                           chk_err=False)
-            elif not os.path.isdir(self._cgroup_path()):
-                logger.error("Could not mount cgroups: ordinary file at {0}", path)
-                return
-
-            controllers_to_mount = ['cpu,cpuacct', 'memory']
-            errors = 0
-            cpu_mounted = False
-            for controller in controllers_to_mount:
-                try:
-                    target_path = self._cgroup_path(controller)
-                    if not os.path.exists(target_path):
-                        fileutil.mkdir(target_path)
-                        self.mount(device=controller,
-                                   mount_point=target_path,
-                                   option="-t cgroup -o {0}".format(controller),
-                                   chk_err=False)
-                        if controller == 'cpu,cpuacct':
-                            cpu_mounted = True
-                except Exception as exception:
-                    errors += 1
-                    if errors == len(controllers_to_mount):
-                        raise
-                    logger.warn("Could not mount cgroup controller {0}: {1}", controller, ustr(exception))
-
-            if cpu_mounted:
-                for controller in ['cpu', 'cpuacct']:
-                    target_path = self._cgroup_path(controller)
-                    if not os.path.exists(target_path):
-                        os.symlink(self._cgroup_path('cpu,cpuacct'), target_path)
-
-        except OSError as oe:
-            # log a warning for read-only file systems
-            logger.warn("Could not mount cgroups: {0}", ustr(oe))
-            raise
-        except Exception as e:
-            logger.error("Could not mount cgroups: {0}", ustr(e))
-            raise
-
     def get_agent_conf_file_path(self):
         return self.agent_conf_file_path
 
diff --git a/azurelinuxagent/common/osutil/ubuntu.py b/azurelinuxagent/common/osutil/ubuntu.py
index 99a8f89d..02953da9 100644
--- a/azurelinuxagent/common/osutil/ubuntu.py
+++ b/azurelinuxagent/common/osutil/ubuntu.py
@@ -62,9 +62,6 @@ class Ubuntu12OSUtil(Ubuntu14OSUtil):
     def get_dhcp_pid(self):
         return self._get_dhcp_pid(["pidof", "dhclient3"])
 
-    def mount_cgroups(self):
-        pass
-
 
 class Ubuntu16OSUtil(Ubuntu14OSUtil):
     """
@@ -80,12 +77,6 @@ class Ubuntu16OSUtil(Ubuntu14OSUtil):
     def unregister_agent_service(self):
         return shellutil.run("systemctl mask {0}".format(self.service_name), chk_err=False)
 
-    def mount_cgroups(self):
-        """
-        Mounted by default in Ubuntu 16.04
-        """
-        pass
-
 
 class Ubuntu18OSUtil(Ubuntu16OSUtil):
     """
diff --git a/azurelinuxagent/daemon/main.py b/azurelinuxagent/daemon/main.py
index 264c3268..82a86ee2 100644
--- a/azurelinuxagent/daemon/main.py
+++ b/azurelinuxagent/daemon/main.py
@@ -26,7 +26,6 @@ import azurelinuxagent.common.conf as conf
 import azurelinuxagent.common.logger as logger
 import azurelinuxagent.common.utils.fileutil as fileutil
 
-from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator
 from azurelinuxagent.common.event import add_event, WALAEventOperation, initialize_event_logger_vminfo_common_parameters
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.osutil import get_osutil
@@ -149,9 +148,6 @@ class DaemonHandler(object):
         # that require the goal state and IMDS
         self._initialize_telemetry()
 
-        # Initialize the agent cgroup
-        CGroupConfigurator.get_instance().create_agent_cgroups(track_cgroups=False)
-
         # Enable RDMA, continue in errors
         if conf.enable_rdma():
             nd_version = self.rdma_handler.get_rdma_version()
diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py
index ebe4eff9..77b305b0 100644
--- a/azurelinuxagent/ga/exthandlers.py
+++ b/azurelinuxagent/ga/exthandlers.py
@@ -921,9 +921,6 @@ class ExtHandlerInstance(object):
             fileutil.clean_ioerror(e, paths=[self.get_base_dir(), self.pkg_file])
             raise ExtensionDownloadError(u"Failed to initialize extension '{0}'".format(self.get_full_name()), e)
 
-        # Create cgroups for the extension
-        CGroupConfigurator.get_instance().create_extension_cgroups(self.get_full_name())
-
         # Save HandlerEnvironment.json
         self.create_handler_env()
 
@@ -992,9 +989,6 @@ class ExtHandlerInstance(object):
             self.report_event(message=message, is_success=False)
             self.logger.warn(message)
 
-        # Also remove the cgroups for the extension
-        CGroupConfigurator.get_instance().remove_extension_cgroups(self.get_full_name())
-
     def update(self, version=None, disable_exit_code=None, updating_from_version=None):
         if version is None:
             version = self.ext_handler.properties.version
diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py
index f4bcd2b2..ac17402d 100644
--- a/azurelinuxagent/ga/update.py
+++ b/azurelinuxagent/ga/update.py
@@ -502,9 +502,7 @@ class UpdateHandler(object):
 
     def _ensure_cgroups_initialized(self):
         configurator = CGroupConfigurator.get_instance()
-        configurator.create_agent_cgroups(track_cgroups=True)
-        configurator.cleanup_legacy_cgroups()
-        configurator.create_extension_cgroups_root()
+        configurator.initialize()
 
     def _evaluate_agent_health(self, latest_agent):
         """


Test patch:
diff --git a/tests/common/mock_cgroup_commands.py b/tests/common/mock_cgroup_commands.py
new file mode 100644
index 00000000..419181e9
--- /dev/null
+++ b/tests/common/mock_cgroup_commands.py
@@ -0,0 +1,104 @@
+# Copyright 2020 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 contextlib
+import os
+import re
+import subprocess
+
+from azurelinuxagent.common.utils import fileutil
+from tests.tools import patch, data_dir
+
+#
+# Default values for the mocked commands.
+#
+# The output comes from an Ubuntu 18 system
+#
+_default_commands = [
+    (r"systemctl --version",
+'''systemd 237
++PAM +AUDIT +SELINUX +IMA +APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 +SECCOMP +BLKID +ELFUTILS +KMOD -IDN2 +IDN -PCRE2 default-hierarchy=hybrid
+'''),
+
+    (r"mount -t cgroup",
+'''cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
+cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
+cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
+cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
+cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
+cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
+cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
+cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
+cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
+cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
+cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
+cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
+'''),
+
+    (r"mount -t cgroup2",
+'''cgroup on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime)
+'''),
+
+    (r"systemctl show walinuxagent\.service --property CPUAccounting",
+'''CPUAccounting=no
+'''),
+
+    (r"systemctl show walinuxagent\.service --property MemoryAccounting",
+'''MemoryAccounting=no
+'''),
+
+    (r"systemd-run --unit=([^\s]+) --scope ([^\s]+)",
+'''
+Running scope as unit: TEST_UNIT.scope
+Thu 28 May 2020 07:25:55 AM PDT
+''')
+]
+
+_default_files = {
+    "/proc/self/cgroup": os.path.join(data_dir, 'cgroups', 'proc_self_cgroup'),
+    "/sys/fs/cgroup/unified/cgroup.controllers": os.path.join(data_dir, 'cgroups', 'sys_fs_cgroup_unified_cgroup.controllers'),
+}
+
+@contextlib.contextmanager
+def mock_cgroup_commands():
+    original_popen = subprocess.Popen
+    original_read_file = fileutil.read_file
+    original_path_exists = os.path.exists
+
+    def mock_popen(command, *args, **kwargs):
+        if isinstance(command, list):
+            command_string = " ".join(command)
+            for c in _default_commands:
+                match = re.match(c[0], command_string)
+                if match is not None:
+                    command = ["echo", c[1]]
+        return original_popen(command, *args, **kwargs)
+    
+    def mock_read_file(filepath, **kwargs):
+        if filepath in _default_files:
+            filepath = _default_files[filepath]
+        return original_read_file(filepath, **kwargs)
+
+    def mock_path_exists(path):
+        if path in _default_files:
+            return True
+        return original_path_exists(path)
+
+    with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen) as patcher:
+        with patch("azurelinuxagent.common.cgroupapi.os.path.exists", side_effect=mock_path_exists):
+            with patch("azurelinuxagent.common.cgroupapi.fileutil.read_file", side_effect=mock_read_file):
+                yield patcher
+
diff --git a/tests/common/osutil/test_default_osutil.py b/tests/common/osutil/test_default_osutil.py
index b0299ade..6e40203d 100644
--- a/tests/common/osutil/test_default_osutil.py
+++ b/tests/common/osutil/test_default_osutil.py
@@ -17,167 +17,8 @@
 
 from azurelinuxagent.common.osutil.default import DefaultOSUtil, shellutil
 from tests.tools import AgentTestCase, patch
-import os
 
 
 class DefaultOsUtilTestCase(AgentTestCase):
-
-    def setUp(self):
-        AgentTestCase.setUp(self)
-        self.cgroups_file_system_root = os.path.join(self.tmp_dir, "cgroups")
-        self.mock_base_cgroups = patch("azurelinuxagent.common.osutil.default.BASE_CGROUPS", self.cgroups_file_system_root)
-        self.mock_base_cgroups.start()
-
-    def tearDown(self):
-        self.mock_base_cgroups.stop()
-
-    @staticmethod
-    def _get_mount_commands(mock):
-        mount_commands = ''
-        for call_args in mock.call_args_list:
-            args, kwargs = call_args
-            mount_commands += ';' + args[0]
-        return mount_commands
-
-    def test_mount_cgroups_should_mount_the_cpu_and_memory_controllers(self):
-        # the mount command requires root privileges; make it a no op and check only for file existence
-        original_run_get_output = shellutil.run_get_output
-
-        def mock_run_get_output(cmd, *args, **kwargs):
-            if cmd.startswith('mount '):
-                return 0, None
-            return original_run_get_output(cmd, *args, **kwargs)
-
-        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output:
-            DefaultOSUtil().mount_cgroups()
-
-            # the directories for the controllers should have been created
-            for controller in ['cpu', 'memory', 'cpuacct', 'cpu,cpuacct']:
-                directory = os.path.join(self.cgroups_file_system_root, controller)
-                self.assertTrue(os.path.exists(directory), "A directory for controller {0} was not created".format(controller))
-
-            # the cgroup filesystem and the cpu and memory controllers should have been mounted
-            mount_commands = DefaultOsUtilTestCase._get_mount_commands(patch_run_get_output)
-
-            self.assertRegex(mount_commands, ';mount.* cgroup_root ', 'The cgroups file system was not mounted')
-            self.assertRegex(mount_commands, ';mount.* cpu,cpuacct ', 'The cpu controller was not mounted')
-            self.assertRegex(mount_commands, ';mount.* memory ', 'The memory controller was not mounted')
-
-    def test_mount_cgroups_should_not_mount_the_cgroups_file_system_when_it_already_exists(self):
-        os.mkdir(self.cgroups_file_system_root)
-
-        original_run_get_output = shellutil.run_get_output
-
-        def mock_run_get_output(cmd, *args, **kwargs):
-            if cmd.startswith('mount '):
-                return 0, None
-            return original_run_get_output(cmd, *args, **kwargs)
-
-        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output:
-            DefaultOSUtil().mount_cgroups()
-
-            mount_commands = DefaultOsUtilTestCase._get_mount_commands(patch_run_get_output)
-
-            self.assertNotIn('cgroup_root', mount_commands, 'The cgroups file system should not have been mounted')
-            self.assertRegex(mount_commands, ';mount.* cpu,cpuacct ', 'The cpu controller was not mounted')
-            self.assertRegex(mount_commands, ';mount.* memory ', 'The memory controller was not mounted')
-
-    def test_mount_cgroups_should_not_mount_cgroup_controllers_when_they_already_exist(self):
-        os.mkdir(self.cgroups_file_system_root)
-        os.mkdir(os.path.join(self.cgroups_file_system_root, 'cpu,cpuacct'))
-        os.mkdir(os.path.join(self.cgroups_file_system_root, 'memory'))
-
-        original_run_get_output = shellutil.run_get_output
-
-        def mock_run_get_output(cmd, *args, **kwargs):
-            if cmd.startswith('mount '):
-                return 0, None
-            return original_run_get_output(cmd, *args, **kwargs)
-
-        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output:
-            DefaultOSUtil().mount_cgroups()
-
-            mount_commands = DefaultOsUtilTestCase._get_mount_commands(patch_run_get_output)
-
-            self.assertNotIn('cgroup_root', mount_commands, 'The cgroups file system should not have been mounted')
-            self.assertNotIn('cpu,cpuacct', mount_commands, 'The cpu controller should not have been mounted')
-            self.assertNotIn('memory', mount_commands, 'The memory controller should not have been mounted')
-
-    def test_mount_cgroups_should_handle_errors_when_mounting_an_individual_controller(self):
-        original_run_get_output = shellutil.run_get_output
-
-        def mock_run_get_output(cmd, *args, **kwargs):
-            if cmd.startswith('mount '):
-                if 'memory' in cmd:
-                    raise Exception('A test exception mounting the memory controller')
-                return 0, None
-            return original_run_get_output(cmd, *args, **kwargs)
-
-        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output:
-            with patch("azurelinuxagent.common.cgroupconfigurator.logger.warn") as mock_logger_warn:
-                DefaultOSUtil().mount_cgroups()
-
-                # the cgroup filesystem and the cpu controller should still have been mounted
-                mount_commands = DefaultOsUtilTestCase._get_mount_commands(patch_run_get_output)
-
-                self.assertRegex(mount_commands, ';mount.* cgroup_root ', 'The cgroups file system was not mounted')
-                self.assertRegex(mount_commands, ';mount.* cpu,cpuacct ', 'The cpu controller was not mounted')
-
-                # A warning should have been logged for the memory controller
-                args, kwargs = mock_logger_warn.call_args
-                self.assertIn('A test exception mounting the memory controller', args)
-
-    def test_mount_cgroups_should_raise_when_the_cgroups_filesystem_fails_to_mount(self):
-        original_run_get_output = shellutil.run_get_output
-
-        def mock_run_get_output(cmd, *args, **kwargs):
-            if cmd.startswith('mount '):
-                if 'cgroup_root' in cmd:
-                    raise Exception('A test exception mounting the cgroups file system')
-                return 0, None
-            return original_run_get_output(cmd, *args, **kwargs)
-
-        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output:
-            with self.assertRaises(Exception) as context_manager:
-                DefaultOSUtil().mount_cgroups()
-
-            self.assertRegex(str(context_manager.exception), 'A test exception mounting the cgroups file system')
-
-            mount_commands = DefaultOsUtilTestCase._get_mount_commands(patch_run_get_output)
-            self.assertNotIn('memory', mount_commands, 'The memory controller should not have been mounted')
-            self.assertNotIn('cpu', mount_commands, 'The cpu controller should not have been mounted')
-
-    def test_mount_cgroups_should_raise_when_all_controllers_fail_to_mount(self):
-        original_run_get_output = shellutil.run_get_output
-
-        def mock_run_get_output(cmd, *args, **kwargs):
-            if cmd.startswith('mount '):
-                if 'memory' in cmd or 'cpu,cpuacct' in cmd:
-                    raise Exception('A test exception mounting a cgroup controller')
-                return 0, None
-            return original_run_get_output(cmd, *args, **kwargs)
-
-        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output):
-            with self.assertRaises(Exception) as context_manager:
-                DefaultOSUtil().mount_cgroups()
-
-            self.assertRegex(str(context_manager.exception), 'A test exception mounting a cgroup controller')
-
-    def test_mount_cgroups_should_not_create_symbolic_links_when_the_cpu_controller_fails_to_mount(self):
-        original_run_get_output = shellutil.run_get_output
-
-        def mock_run_get_output(cmd, *args, **kwargs):
-            if cmd.startswith('mount '):
-                if 'cpu,cpuacct' in cmd:
-                    raise Exception('A test exception mounting the cpu controller')
-                return 0, None
-            return original_run_get_output(cmd, *args, **kwargs)
-
-        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output):
-            with patch("azurelinuxagent.common.osutil.default.os.symlink") as patch_symlink:
-                DefaultOSUtil().mount_cgroups()
-
-                self.assertEquals(patch_symlink.call_count, 0, 'A symbolic link should not have been created')
-
     def test_default_service_name(self):
         self.assertEquals(DefaultOSUtil().get_service_name(), "waagent")
diff --git a/tests/common/test_cgroupapi.py b/tests/common/test_cgroupapi.py
index bc3c0be9..8b998ebd 100644
--- a/tests/common/test_cgroupapi.py
+++ b/tests/common/test_cgroupapi.py
@@ -26,6 +26,7 @@ from azurelinuxagent.common.exception import CGroupsException, ExtensionError, E
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.utils import shellutil, fileutil
 from nose.plugins.attrib import attr
+from tests.common.mock_cgroup_commands import mock_cgroup_commands
 from tests.utils.cgroups_tools import CGroupsTools
 from tests.tools import AgentTestCase, patch, skip_if_predicate_false, is_systemd_present, i_am_root, mock_sleep
 
@@ -48,6 +49,26 @@ class _MockedFileSystemTestCase(AgentTestCase):
 
 
 class CGroupsApiTestCase(_MockedFileSystemTestCase):
+    def test_cgroups_should_be_supported_only_on_ubuntu_16_and_later(self):
+        test_cases = [
+            (['ubuntu', '16.04', 'xenial'], True),
+            (['ubuntu', '16.10', 'yakkety'], True),
+            (['ubuntu', '18.04', 'bionic'], True),
+            (['ubuntu', '18.10', 'cosmic'], True),
+            (['ubuntu', '20.04', 'focal'], True),
+            (['ubuntu', '20.10', 'groovy'], True),
+            (['centos', '7.5', 'Source'], False),
+            (['redhat', '7.7', 'Maipo'], False),
+            (['redhat', '7.7.1908', 'Core'], False),
+            (['bigip',  '15.0.1', 'Final'], False),
+            (['gaia', '273.562', 'R80.30'], False),
+            (['debian' '9.1', ''], False),
+        ]
+
+        for (distro, supported) in test_cases:
+            with patch("azurelinuxagent.common.cgroupapi.get_distro", return_value=distro):
+                self.assertEqual(CGroupsApi.cgroups_supported(), supported, "cgroups_supported() failed on {0}".format(distro))
+
     def test_create_should_return_a_SystemdCgroupsApi_on_systemd_platforms(self):
         with patch("azurelinuxagent.common.cgroupapi.CGroupsApi._is_systemd", return_value=True):
             api = CGroupsApi.create()
@@ -132,6 +153,166 @@ class CGroupsApiTestCase(_MockedFileSystemTestCase):
             self.assertEquals(message, 'A dummy message')
 
 
+class MountCgroupsTestCase(AgentTestCase):
+
+    def setUp(self):
+        AgentTestCase.setUp(self)
+        self.cgroups_file_system_root = os.path.join(self.tmp_dir, "cgroups")
+        self.mock_base_cgroups = patch("azurelinuxagent.common.cgroupapi.CGROUPS_FILE_SYSTEM_ROOT", self.cgroups_file_system_root)
+        self.mock_base_cgroups.start()
+
+    def tearDown(self):
+        self.mock_base_cgroups.stop()
+
+    @staticmethod
+    def _get_mount_commands(mock):
+        mount_commands = ''
+        for call_args in mock.call_args_list:
+            args, kwargs = call_args
+            mount_commands += ';' + args[0]
+        return mount_commands
+
+    def test_mount_cgroups_should_mount_the_cpu_and_memory_controllers(self):
+        # the mount command requires root privileges; make it a no op and check only for file existence
+        original_run_get_output = shellutil.run_get_output
+
+        def mock_run_get_output(cmd, *args, **kwargs):
+            if cmd.startswith('mount '):
+                return 0, None
+            return original_run_get_output(cmd, *args, **kwargs)
+
+        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output:
+            FileSystemCgroupsApi.mount_cgroups()
+
+            # the directories for the controllers should have been created
+            for controller in ['cpu', 'memory', 'cpuacct', 'cpu,cpuacct']:
+                directory = os.path.join(self.cgroups_file_system_root, controller)
+                self.assertTrue(os.path.exists(directory), "A directory for controller {0} was not created".format(controller))
+
+            # the cgroup filesystem and the cpu and memory controllers should have been mounted
+            mount_commands = MountCgroupsTestCase._get_mount_commands(patch_run_get_output)
+
+            self.assertRegex(mount_commands, ';mount.* cgroup_root ', 'The cgroups file system was not mounted')
+            self.assertRegex(mount_commands, ';mount.* cpu,cpuacct ', 'The cpu controller was not mounted')
+            self.assertRegex(mount_commands, ';mount.* memory ', 'The memory controller was not mounted')
+
+    def test_mount_cgroups_should_not_mount_the_cgroups_file_system_when_it_already_exists(self):
+        os.mkdir(self.cgroups_file_system_root)
+
+        original_run_get_output = shellutil.run_get_output
+
+        def mock_run_get_output(cmd, *args, **kwargs):
+            if cmd.startswith('mount '):
+                return 0, None
+            return original_run_get_output(cmd, *args, **kwargs)
+
+        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output:
+            FileSystemCgroupsApi.mount_cgroups()
+
+            mount_commands = MountCgroupsTestCase._get_mount_commands(patch_run_get_output)
+
+            self.assertNotIn('cgroup_root', mount_commands, 'The cgroups file system should not have been mounted')
+            self.assertRegex(mount_commands, ';mount.* cpu,cpuacct ', 'The cpu controller was not mounted')
+            self.assertRegex(mount_commands, ';mount.* memory ', 'The memory controller was not mounted')
+
+    def test_mount_cgroups_should_not_mount_cgroup_controllers_when_they_already_exist(self):
+        os.mkdir(self.cgroups_file_system_root)
+        os.mkdir(os.path.join(self.cgroups_file_system_root, 'cpu,cpuacct'))
+        os.mkdir(os.path.join(self.cgroups_file_system_root, 'memory'))
+
+        original_run_get_output = shellutil.run_get_output
+
+        def mock_run_get_output(cmd, *args, **kwargs):
+            if cmd.startswith('mount '):
+                return 0, None
+            return original_run_get_output(cmd, *args, **kwargs)
+
+        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output:
+            FileSystemCgroupsApi.mount_cgroups()
+
+            mount_commands = MountCgroupsTestCase._get_mount_commands(patch_run_get_output)
+
+            self.assertNotIn('cgroup_root', mount_commands, 'The cgroups file system should not have been mounted')
+            self.assertNotIn('cpu,cpuacct', mount_commands, 'The cpu controller should not have been mounted')
+            self.assertNotIn('memory', mount_commands, 'The memory controller should not have been mounted')
+
+    def test_mount_cgroups_should_handle_errors_when_mounting_an_individual_controller(self):
+        original_run_get_output = shellutil.run_get_output
+
+        def mock_run_get_output(cmd, *args, **kwargs):
+            if cmd.startswith('mount '):
+                if 'memory' in cmd:
+                    raise Exception('A test exception mounting the memory controller')
+                return 0, None
+            return original_run_get_output(cmd, *args, **kwargs)
+
+        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output:
+            with patch("azurelinuxagent.common.cgroupconfigurator.logger.warn") as mock_logger_warn:
+                FileSystemCgroupsApi.mount_cgroups()
+
+                # the cgroup filesystem and the cpu controller should still have been mounted
+                mount_commands = MountCgroupsTestCase._get_mount_commands(patch_run_get_output)
+
+                self.assertRegex(mount_commands, ';mount.* cgroup_root ', 'The cgroups file system was not mounted')
+                self.assertRegex(mount_commands, ';mount.* cpu,cpuacct ', 'The cpu controller was not mounted')
+
+                # A warning should have been logged for the memory controller
+                args, kwargs = mock_logger_warn.call_args
+                self.assertIn('A test exception mounting the memory controller', args)
+
+    def test_mount_cgroups_should_raise_when_the_cgroups_filesystem_fails_to_mount(self):
+        original_run_get_output = shellutil.run_get_output
+
+        def mock_run_get_output(cmd, *args, **kwargs):
+            if cmd.startswith('mount '):
+                if 'cgroup_root' in cmd:
+                    raise Exception('A test exception mounting the cgroups file system')
+                return 0, None
+            return original_run_get_output(cmd, *args, **kwargs)
+
+        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output) as patch_run_get_output:
+            with self.assertRaises(Exception) as context_manager:
+                FileSystemCgroupsApi.mount_cgroups()
+
+            self.assertRegex(str(context_manager.exception), 'A test exception mounting the cgroups file system')
+
+            mount_commands = MountCgroupsTestCase._get_mount_commands(patch_run_get_output)
+            self.assertNotIn('memory', mount_commands, 'The memory controller should not have been mounted')
+            self.assertNotIn('cpu', mount_commands, 'The cpu controller should not have been mounted')
+
+    def test_mount_cgroups_should_raise_when_all_controllers_fail_to_mount(self):
+        original_run_get_output = shellutil.run_get_output
+
+        def mock_run_get_output(cmd, *args, **kwargs):
+            if cmd.startswith('mount '):
+                if 'memory' in cmd or 'cpu,cpuacct' in cmd:
+                    raise Exception('A test exception mounting a cgroup controller')
+                return 0, None
+            return original_run_get_output(cmd, *args, **kwargs)
+
+        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output):
+            with self.assertRaises(Exception) as context_manager:
+                FileSystemCgroupsApi.mount_cgroups()
+
+            self.assertRegex(str(context_manager.exception), 'A test exception mounting a cgroup controller')
+
+    def test_mount_cgroups_should_not_create_symbolic_links_when_the_cpu_controller_fails_to_mount(self):
+        original_run_get_output = shellutil.run_get_output
+
+        def mock_run_get_output(cmd, *args, **kwargs):
+            if cmd.startswith('mount '):
+                if 'cpu,cpuacct' in cmd:
+                    raise Exception('A test exception mounting the cpu controller')
+                return 0, None
+            return original_run_get_output(cmd, *args, **kwargs)
+
+        with patch("azurelinuxagent.common.osutil.default.shellutil.run_get_output", side_effect=mock_run_get_output):
+            with patch("azurelinuxagent.common.osutil.default.os.symlink") as patch_symlink:
+                FileSystemCgroupsApi.mount_cgroups()
+
+                self.assertEquals(patch_symlink.call_count, 0, 'A symbolic link should not have been created')
+
+
 class FileSystemCgroupsApiTestCase(_MockedFileSystemTestCase):
     def test_cleanup_legacy_cgroups_should_move_daemon_pid_to_new_cgroup_and_remove_legacy_cgroups(self):
         # Set up a mock /var/run/waagent.pid file
@@ -285,6 +466,38 @@ class FileSystemCgroupsApiTestCase(_MockedFileSystemTestCase):
 
 @skip_if_predicate_false(is_systemd_present, "Systemd cgroups API doesn't manage cgroups on systems not using systemd.")
 class SystemdCgroupsApiTestCase(AgentTestCase):
+    def test_get_systemd_version_should_return_a_version_number(self):
+        with mock_cgroup_commands():
+            version_info = SystemdCgroupsApi.get_systemd_version()
+            found = re.search("systemd \d+", version_info) is not None
+            self.assertTrue(found, "Could not determine the systemd version: {0}".format(version_info))
+
+    def test_get_cpu_and_memory_mount_points_should_return_the_cgroup_mount_points(self):
+        with mock_cgroup_commands():
+            cpu, memory = SystemdCgroupsApi.get_cpu_and_memory_mount_points()
+            self.assertEquals(cpu, '/sys/fs/cgroup/cpu,cpuacct', "The mount point for the CPU controller is incorrect")
+            self.assertEquals(memory, '/sys/fs/cgroup/memory', "The mount point for the memory controller is incorrect")
+
+    def test_get_cpu_and_memory_cgroup_relative_paths_for_process_should_return_the_cgroup_relative_paths(self):
+        with mock_cgroup_commands():
+            cpu, memory = SystemdCgroupsApi.get_cpu_and_memory_cgroup_relative_paths_for_process('self')
+            self.assertEquals(cpu, "system.slice/walinuxagent.service", "The relative path for the CPU cgroup is incorrect")
+            self.assertEquals(memory, "system.slice/walinuxagent.service", "The relative memory for the CPU cgroup is incorrect")
+
+    def test_get_cgroup2_controllers_should_return_the_v2_cgroup_controllers(self):
+        with mock_cgroup_commands():
+            mount_point, controllers = SystemdCgroupsApi.get_cgroup2_controllers()
+
+            self.assertEquals(mount_point, "/sys/fs/cgroup/unified", "Invalid mount point for V2 cgroups")
+            self.assertIn("cpu", controllers, "The CPU controller is not in the list of V2 controllers")
+            self.assertIn("memory", controllers, "The memory controller is not in the list of V2 controllers")
+
+    def test_get_unit_property_should_return_the_value_of_the_given_property(self):
+        with mock_cgroup_commands():
+            cpu_accounting = SystemdCgroupsApi.get_unit_property("walinuxagent.service", "CPUAccounting")
+
+            self.assertEquals(cpu_accounting, "no", "Property {0} of {1} is incorrect".format("CPUAccounting", "walinuxagent.service"))
+
     def test_get_extensions_slice_root_name_should_return_the_root_slice_for_extensions(self):
         root_slice_name = SystemdCgroupsApi()._get_extensions_slice_root_name()
         self.assertEqual(root_slice_name, "system-walinuxagent.extensions.slice")
@@ -576,34 +789,6 @@ class SystemdCgroupsApiTestCase(AgentTestCase):
                         self.assertEquals(expected_output.format("very specific test message"), process_output)
                         self.assertEquals(extension_cgroups, [])
 
-    @patch("azurelinuxagent.common.utils.fileutil.read_file")
-    def test_create_agent_cgroups_should_create_cgroups_on_all_controllers(self, patch_read_file):
-        mock_proc_self_cgroup = '''12:blkio:/system.slice/walinuxagent.service
-11:memory:/system.slice/walinuxagent.service
-10:perf_event:/
-9:hugetlb:/
-8:freezer:/
-7:net_cls,net_prio:/
-6:devices:/system.slice/walinuxagent.service
-5:cpuset:/
-4:cpu,cpuacct:/system.slice/walinuxagent.service
-3:pids:/system.slice/walinuxagent.service
-2:rdma:/
-1:name=systemd:/system.slice/walinuxagent.service
-0::/system.slice/walinuxagent.service
-'''
-        patch_read_file.return_value = mock_proc_self_cgroup
-        agent_cgroups = SystemdCgroupsApi().create_agent_cgroups()
-
-        def assert_cgroup_created(controller):
-            expected_cgroup_path = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, controller, "system.slice", VM_AGENT_CGROUP_NAME)
-
-            self.assertTrue(any(cgroups.path == expected_cgroup_path for cgroups in agent_cgroups))
-            self.assertTrue(any(cgroups.name == VM_AGENT_CGROUP_NAME for cgroups in agent_cgroups))
-
-        assert_cgroup_created("cpu")
-        assert_cgroup_created("memory")
-
 
 class SystemdCgroupsApiMockedFileSystemTestCase(_MockedFileSystemTestCase):
     def test_cleanup_legacy_cgroups_should_remove_legacy_cgroups(self):
@@ -617,29 +802,8 @@ class SystemdCgroupsApiMockedFileSystemTestCase(_MockedFileSystemTestCase):
 
         with patch("azurelinuxagent.common.cgroupapi.add_event") as mock_add_event:
             with patch("azurelinuxagent.common.cgroupapi.get_agent_pid_file_path", return_value=daemon_pid_file):
-                    SystemdCgroupsApi().cleanup_legacy_cgroups()
-
-        self.assertFalse(os.path.exists(legacy_cpu_cgroup))
-        self.assertFalse(os.path.exists(legacy_memory_cgroup))
-
-    def test_cleanup_legacy_cgroups_should_report_an_error_when_the_daemon_pid_was_added_to_the_legacy_cgroups(self):
-        # Set up a mock /var/run/waagent.pid file
-        daemon_pid = "42"
-        daemon_pid_file = os.path.join(self.tmp_dir, "waagent.pid")
-        fileutil.write_file(daemon_pid_file, daemon_pid + "\n")
-
-        # Set up old controller cgroups and add the daemon's PID to them
-        legacy_cpu_cgroup = CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "cpu", daemon_pid)
-        legacy_memory_cgroup = CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "memory", daemon_pid)
-
-        with patch("azurelinuxagent.common.cgroupapi.add_event") as mock_add_event:
-            with patch("azurelinuxagent.common.cgroupapi.get_agent_pid_file_path", return_value=daemon_pid_file):
-                with self.assertRaises(CGroupsException) as context_manager:
-                    SystemdCgroupsApi().cleanup_legacy_cgroups()
-
-        self.assertEquals(str(context_manager.exception), "[CGroupsException] The daemon's PID ({0}) was already added to the legacy cgroup; this invalidates resource usage data.".format(daemon_pid))
-
-        # The method should have deleted the legacy cgroups
-        self.assertFalse(os.path.exists(legacy_cpu_cgroup))
-        self.assertFalse(os.path.exists(legacy_memory_cgroup))
+                legacy_cgroups = SystemdCgroupsApi().cleanup_legacy_cgroups()
 
+        self.assertEquals(legacy_cgroups, 2, "cleanup_legacy_cgroups() did not find all the expected cgroups")
+        self.assertFalse(os.path.exists(legacy_cpu_cgroup), "cleanup_legacy_cgroups() did not remove the CPU legacy cgroup")
+        self.assertFalse(os.path.exists(legacy_memory_cgroup), "cleanup_legacy_cgroups() did not remove the memory legacy cgroup")
diff --git a/tests/common/test_cgroupconfigurator.py b/tests/common/test_cgroupconfigurator.py
index 74567792..ba087e9f 100644
--- a/tests/common/test_cgroupconfigurator.py
+++ b/tests/common/test_cgroupconfigurator.py
@@ -17,117 +17,85 @@
 
 from __future__ import print_function
 
+import contextlib
+import os
 import subprocess
 
-import errno
-import os
-import re
 from azurelinuxagent.common.cgroup import CGroup
 from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator
 from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry
 from azurelinuxagent.common.exception import CGroupsException
-from azurelinuxagent.common.osutil.default import DefaultOSUtil
-from azurelinuxagent.common.utils import fileutil
-from tests.utils.cgroups_tools import CGroupsTools
+from tests.common.mock_cgroup_commands import mock_cgroup_commands
 from tests.tools import AgentTestCase, patch
 
 
-class CGroupConfiguratorTestCase(AgentTestCase):
-    @classmethod
-    def setUpClass(cls):
-        AgentTestCase.setUpClass()
-
-        # Use the file system implementation of CGroupsApi (FileSystemCgroupsApi)
-        cls.mock_is_systemd = patch("azurelinuxagent.common.cgroupapi.CGroupsApi._is_systemd", return_value=False)
-        cls.mock_is_systemd.start()
-
-        # Use the default implementation of osutil
-        cls.mock_get_osutil = patch("azurelinuxagent.common.cgroupconfigurator.get_osutil", return_value=DefaultOSUtil())
-        cls.mock_get_osutil.start()
-
-        # Currently osutil.is_cgroups_supported() returns False on Travis runs. We need to revisit this design; in the
-        # meanwhile mock the method to return True
-        cls.mock_is_cgroups_supported = patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.is_cgroups_supported", return_value=True)
-        cls.mock_is_cgroups_supported.start()
-
-        # Mounting the cgroup filesystem requires root privileges. Since these tests do not perform any actual operation on cgroups, make it a noop.
-        cls.mock_mount_cgroups = patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.mount_cgroups")
-        cls.mock_mount_cgroups.start()
-
+class CGroupConfiguratorSystemdTestCase(AgentTestCase):
     @classmethod
     def tearDownClass(cls):
-        cls.mock_mount_cgroups.stop()
-        cls.mock_is_cgroups_supported.stop()
-        cls.mock_get_osutil.stop()
-        cls.mock_is_systemd.stop()
-
+        CGroupConfigurator._instance = None
         AgentTestCase.tearDownClass()
 
-    def setUp(self):
-        AgentTestCase.setUp(self)
-        CGroupConfigurator._instance = None  # force get_instance() to create a new instance for each test
-
-        self.cgroups_file_system_root = os.path.join(self.tmp_dir, "cgroup")
-        os.mkdir(self.cgroups_file_system_root)
-        os.mkdir(os.path.join(self.cgroups_file_system_root, "cpu"))
-        os.mkdir(os.path.join(self.cgroups_file_system_root, "memory"))
-
-        self.mock_cgroups_file_system_root = patch("azurelinuxagent.common.cgroupapi.CGROUPS_FILE_SYSTEM_ROOT", self.cgroups_file_system_root)
-        self.mock_cgroups_file_system_root.start()
-
-    def tearDown(self):
-        self.mock_cgroups_file_system_root.stop()
+    @staticmethod
+    def _get_new_cgroup_configurator_instance(initialize=True):
+        CGroupConfigurator._instance = None
+        configurator = CGroupConfigurator.get_instance()
+        if initialize:
+            with patch('azurelinuxagent.common.cgroupapi.CGroupsApi.cgroups_supported', return_value=True):
+                with patch('azurelinuxagent.common.cgroupapi.CGroupsApi._is_systemd', return_value=True):
+                    with mock_cgroup_commands():
+                        configurator.initialize()
+        return configurator
 
-    def test_init_should_mount_the_cgroups_file_system(self):
-        with patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.mount_cgroups") as mock_mount_cgroups:
-            CGroupConfigurator.get_instance()
+    def test_initialize_should_start_tracking_the_agent_cgroups(self):
+        CGroupConfiguratorSystemdTestCase._get_new_cgroup_configurator_instance()
 
-        self.assertEqual(mock_mount_cgroups.call_count, 1)
+        tracked = CGroupsTelemetry._tracked
 
-    def test_init_should_disable_cgroups_when_they_are_not_supported(self):
-        with patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.is_cgroups_supported", return_value=False):
-            self.assertFalse(CGroupConfigurator.get_instance().enabled())
+        self.assertTrue(
+            any(cg for cg in tracked if cg.name == 'walinuxagent.service' and 'cpu' in cg.path),
+            "The Agent's CPU is not being tracked")
+        self.assertTrue(
+            any(cg for cg in tracked if cg.name == 'walinuxagent.service' and 'memory' in cg.path),
+            "The Agent's memory is not being tracked")
 
     def test_enable_and_disable_should_change_the_enabled_state_of_cgroups(self):
-        configurator = CGroupConfigurator.get_instance()
+        configurator = CGroupConfiguratorSystemdTestCase._get_new_cgroup_configurator_instance()
 
-        self.assertTrue(configurator.enabled())
+        self.assertTrue(configurator.enabled(), "CGroupConfigurator should be enabled by default")
 
         configurator.disable()
-        self.assertFalse(configurator.enabled())
+        self.assertFalse(configurator.enabled(), "disable() should disable the CGroupConfigurator")
 
         configurator.enable()
-        self.assertTrue(configurator.enabled())
+        self.assertTrue(configurator.enabled(), "enable() should enable the CGroupConfigurator")
 
     def test_enable_should_raise_CGroupsException_when_cgroups_are_not_supported(self):
-        with patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.is_cgroups_supported", return_value=False):
+        with patch("azurelinuxagent.common.cgroupapi.CGroupsApi.cgroups_supported", return_value=False):
+            configurator = CGroupConfiguratorSystemdTestCase._get_new_cgroup_configurator_instance(initialize=False)
+            configurator.initialize()
+
             with self.assertRaises(CGroupsException) as context_manager:
-                CGroupConfigurator.get_instance().enable()
-            self.assertIn("cgroups are not supported", str(context_manager.exception))
+                configurator.enable()
+            self.assertIn("Attempted to enable cgroups, but they are not supported on the current platform", str(context_manager.exception))
 
     def test_disable_should_reset_tracked_cgroups(self):
-        configurator = CGroupConfigurator.get_instance()
-
         # Start tracking a couple of dummy cgroups
         CGroupsTelemetry.track_cgroup(CGroup("dummy", "/sys/fs/cgroup/memory/system.slice/dummy.service", "cpu"))
         CGroupsTelemetry.track_cgroup(CGroup("dummy", "/sys/fs/cgroup/memory/system.slice/dummy.service", "memory"))
 
-        configurator.disable()
+        CGroupConfiguratorSystemdTestCase._get_new_cgroup_configurator_instance().disable()
 
-        self.assertFalse(configurator.enabled())
         self.assertEquals(len(CGroupsTelemetry._tracked), 0)
 
     def test_cgroup_operations_should_not_invoke_the_cgroup_api_when_cgroups_are_not_enabled(self):
-        configurator = CGroupConfigurator.get_instance()
+        configurator = CGroupConfiguratorSystemdTestCase._get_new_cgroup_configurator_instance()
         configurator.disable()
 
         # List of operations to test, and the functions to mock used in order to do verifications
         operations = [
-            [lambda: configurator.create_agent_cgroups(track_cgroups=False), "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.create_agent_cgroups"],
-            [lambda: configurator.cleanup_legacy_cgroups(),                  "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.cleanup_legacy_cgroups"],
-            [lambda: configurator.create_extension_cgroups_root(),           "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.create_extension_cgroups_root"],
-            [lambda: configurator.create_extension_cgroups("A.B.C-1.0.0"),   "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.create_extension_cgroups"],
-            [lambda: configurator.remove_extension_cgroups("A.B.C-1.0.0"),   "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.remove_extension_cgroups"]
+            [lambda: configurator.create_extension_cgroups_root(),           "azurelinuxagent.common.cgroupapi.SystemdCgroupsApi.create_extension_cgroups_root"],
+            [lambda: configurator.create_extension_cgroups("A.B.C-1.0.0"),   "azurelinuxagent.common.cgroupapi.SystemdCgroupsApi.create_extension_cgroups"],
+            [lambda: configurator.remove_extension_cgroups("A.B.C-1.0.0"),   "azurelinuxagent.common.cgroupapi.SystemdCgroupsApi.remove_extension_cgroups"]
         ]
 
         for op in operations:
@@ -137,17 +105,15 @@ class CGroupConfiguratorTestCase(AgentTestCase):
             self.assertEqual(mock_cgroup_api_operation.call_count, 0)
 
     def test_cgroup_operations_should_log_a_warning_when_the_cgroup_api_raises_an_exception(self):
-        configurator = CGroupConfigurator.get_instance()
+        configurator = CGroupConfiguratorSystemdTestCase._get_new_cgroup_configurator_instance()
 
         # cleanup_legacy_cgroups disables cgroups on error, so make disable() a no-op
         with patch.object(configurator, "disable"):
             # List of operations to test, and the functions to mock in order to raise exceptions
             operations = [
-                [lambda: configurator.create_agent_cgroups(track_cgroups=False), "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.create_agent_cgroups"],
-                [lambda: configurator.cleanup_legacy_cgroups(),                  "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.cleanup_legacy_cgroups"],
-                [lambda: configurator.create_extension_cgroups_root(),           "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.create_extension_cgroups_root"],
-                [lambda: configurator.create_extension_cgroups("A.B.C-1.0.0"),   "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.create_extension_cgroups"],
-                [lambda: configurator.remove_extension_cgroups("A.B.C-1.0.0"),   "azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.remove_extension_cgroups"]
+                [lambda: configurator.create_extension_cgroups_root(),           "azurelinuxagent.common.cgroupapi.SystemdCgroupsApi.create_extension_cgroups_root"],
+                [lambda: configurator.create_extension_cgroups("A.B.C-1.0.0"),   "azurelinuxagent.common.cgroupapi.SystemdCgroupsApi.create_extension_cgroups"],
+                [lambda: configurator.remove_extension_cgroups("A.B.C-1.0.0"),   "azurelinuxagent.common.cgroupapi.SystemdCgroupsApi.remove_extension_cgroups"]
             ]
 
             def raise_exception(*_):
@@ -164,32 +130,11 @@ class CGroupConfiguratorTestCase(AgentTestCase):
                     message = args[0]
                     self.assertIn("A TEST EXCEPTION", message)
 
-    def test_start_extension_command_should_forward_to_subprocess_popen_when_groups_are_not_enabled(self):
-        configurator = CGroupConfigurator.get_instance()
+    def test_start_extension_command_should_not_use_systemd_when_groups_are_not_enabled(self):
+        configurator = CGroupConfiguratorSystemdTestCase._get_new_cgroup_configurator_instance()
         configurator.disable()
 
-        with patch("azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.start_extension_command") as mock_fs:
-            with patch("azurelinuxagent.common.cgroupapi.SystemdCgroupsApi.start_extension_command") as mock_systemd:
-                with patch("azurelinuxagent.common.cgroupconfigurator.handle_process_completion") as mock_popen:
-                    configurator.start_extension_command(
-                        extension_name="Microsoft.Compute.TestExtension-1.2.3",
-                        command="date",
-                        timeout=300,
-                        shell=False,
-                        cwd=self.tmp_dir,
-                        env={},
-                        stdout=subprocess.PIPE,
-                        stderr=subprocess.PIPE)
-
-                    self.assertEqual(mock_popen.call_count, 1)
-                    self.assertEqual(mock_fs.call_count, 0)
-                    self.assertEqual(mock_systemd.call_count, 0)
-
-    def test_start_extension_command_should_forward_to_cgroups_api_when_groups_are_enabled(self):
-        configurator = CGroupConfigurator.get_instance()
-
-        with patch("azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.start_extension_command",
-                   return_value=[[], None]) as mock_start_extension_command:
+        with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", wraps=subprocess.Popen) as patcher:
             configurator.start_extension_command(
                 extension_name="Microsoft.Compute.TestExtension-1.2.3",
                 command="date",
@@ -200,15 +145,53 @@ class CGroupConfiguratorTestCase(AgentTestCase):
                 stdout=subprocess.PIPE,
                 stderr=subprocess.PIPE)
 
-            self.assertEqual(mock_start_extension_command.call_count, 1)
+            command_calls = [args[0] for args, _ in patcher.call_args_list if len(args) > 0 and "date" in args[0]]
+            self.assertEqual(len(command_calls), 1, "The test command should have been called exactly once [{0}]".format(command_calls))
+            self.assertNotIn("systemd-run", command_calls[0], "The command should not have been invoked using systemd")
+            self.assertEqual(command_calls[0], "date", "The command line should not have been modified")
+
+    @staticmethod
+    @contextlib.contextmanager
+    def _create_mock_popen(command):
+        """
+         Creates a mock for subprocess.Popen that replaces the given command with a dummy command (date); this allows
+        the tests below to run  on environments where systemd-run is not available
+        """
+        original_popen = subprocess.Popen
+
+        def mock_popen(command_arg, *args, **kwargs):
+            if command in command_arg:
+                return original_popen("date", *args, **kwargs)
+            else:
+                return original_popen(command_arg, *args, **kwargs)
+
+        with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen) as patcher:
+            patcher.get_command_calls = lambda: [args[0] for args, _ in patcher.call_args_list if len(args) > 0 and command in args[0]]
+            yield patcher
+
+    def test_start_extension_command_should_use_systemd_run_when_groups_are_enabled(self):
+        with CGroupConfiguratorSystemdTestCase._create_mock_popen("test command") as patch_popen:
+            CGroupConfiguratorSystemdTestCase._get_new_cgroup_configurator_instance().start_extension_command(
+                extension_name="Microsoft.Compute.TestExtension-1.2.3",
+                command="test command",
+                timeout=300,
+                shell=False,
+                cwd=self.tmp_dir,
+                env={},
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE)
+
+            command_calls = patch_popen.get_command_calls()
+            self.assertEqual(len(command_calls), 1, "The test command should have been called exactly once [{0}]".format(command_calls))
+            self.assertTrue(command_calls[0].startswith("systemd-run"), "The command should have been invoked using systemd [{0}]".format(command_calls))
 
     def test_start_extension_command_should_start_tracking_the_extension_cgroups(self):
         # CPU usage is initialized when we begin tracking a CPU cgroup; since this test does not retrieve the
         # CPU usage, there is no need for initialization
-        with patch("azurelinuxagent.common.cgroup.CpuCgroup.initialize_cpu_usage"):
-            CGroupConfigurator.get_instance().start_extension_command(
+        with CGroupConfiguratorSystemdTestCase._create_mock_popen("test command") as patch_popen:
+            CGroupConfiguratorSystemdTestCase._get_new_cgroup_configurator_instance().start_extension_command(
                 extension_name="Microsoft.Compute.TestExtension-1.2.3",
-                command="date",
+                command="test command",
                 timeout=300,
                 shell=False,
                 cwd=self.tmp_dir,
@@ -216,97 +199,36 @@ class CGroupConfiguratorTestCase(AgentTestCase):
                 stdout=subprocess.PIPE,
                 stderr=subprocess.PIPE)
 
-        self.assertTrue(CGroupsTelemetry.is_tracked(os.path.join(
-            self.cgroups_file_system_root, "cpu", "walinuxagent.extensions/Microsoft.Compute.TestExtension_1.2.3")))
-        self.assertTrue(CGroupsTelemetry.is_tracked(os.path.join(
-            self.cgroups_file_system_root, "memory", "walinuxagent.extensions/Microsoft.Compute.TestExtension_1.2.3")))
+        tracked = CGroupsTelemetry._tracked
+
+        self.assertTrue(
+            any(cg for cg in tracked 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 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):
-        configurator = CGroupConfigurator.get_instance()
+        configurator = CGroupConfiguratorSystemdTestCase._get_new_cgroup_configurator_instance()
+
+        original_popen = subprocess.Popen
 
-        def raise_exception(*_, **__):
-            raise Exception("A TEST EXCEPTION")
+        def mock_popen(command_arg, *args, **kwargs):
+            if "test command" in command_arg:
+                raise Exception("A TEST EXCEPTION")
+            return original_popen(command_arg, *args, **kwargs)
 
-        with patch("azurelinuxagent.common.cgroupapi.FileSystemCgroupsApi.start_extension_command", raise_exception):
+        with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen) as patcher:
             with self.assertRaises(Exception) as context_manager:
                 configurator.start_extension_command(
                     extension_name="Microsoft.Compute.TestExtension-1.2.3",
-                    command="date",
+                    command="test command",
                     timeout=300,
                     shell=False,
                     cwd=self.tmp_dir,
                     env={},
                     stdout=subprocess.PIPE,
                     stderr=subprocess.PIPE)
-            self.assertIn("A TEST EXCEPTION", str(context_manager.exception))
-
-    def test_cleanup_legacy_cgroups_should_disable_cgroups_when_it_fails_to_process_legacy_cgroups(self):
-        # Set up a mock /var/run/waagent.pid file
-        daemon_pid = "42"
-        daemon_pid_file = os.path.join(self.tmp_dir, "waagent.pid")
-        fileutil.write_file(daemon_pid_file, daemon_pid + "\n")
-
-        # Set up old controller cgroups and add the daemon PID to them
-        CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "cpu", daemon_pid)
-        CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "memory", daemon_pid)
-
-        # Set up new controller cgroups and add extension handler's PID to them
-        CGroupsTools.create_agent_cgroup(self.cgroups_file_system_root, "cpu", "999")
-        CGroupsTools.create_agent_cgroup(self.cgroups_file_system_root, "memory", "999")
-
-        def mock_append_file(filepath, contents, **kwargs):
-            if re.match(r'/.*/cpu/.*/cgroup.procs', filepath):
-                raise OSError(errno.ENOSPC, os.strerror(errno.ENOSPC))
-            fileutil.append_file(filepath, contents, **kwargs)
-
-        # Start tracking a couple of dummy cgroups
-        CGroupsTelemetry.track_cgroup(CGroup("dummy", "/sys/fs/cgroup/memory/system.slice/dummy.service", "cpu"))
-        CGroupsTelemetry.track_cgroup(CGroup("dummy", "/sys/fs/cgroup/memory/system.slice/dummy.service", "memory"))
 
-        cgroup_configurator = CGroupConfigurator.get_instance()
+                self.assertIn("A TEST EXCEPTION", str(context_manager.exception))
 
-        with patch("azurelinuxagent.common.cgroupconfigurator.add_event") as mock_add_event:
-            with patch("azurelinuxagent.common.cgroupapi.get_agent_pid_file_path", return_value=daemon_pid_file):
-                with patch("azurelinuxagent.common.cgroupapi.fileutil.append_file", side_effect=mock_append_file):
-                    cgroup_configurator.cleanup_legacy_cgroups()
-
-        self.assertEquals(len(mock_add_event.call_args_list), 1)
-        _, kwargs = mock_add_event.call_args_list[0]
-        self.assertEquals(kwargs['op'], 'CGroupsCleanUp')
-        self.assertFalse(kwargs['is_success'])
-        self.assertEquals(kwargs['message'], 'Failed to process legacy cgroups. Collection of resource usage data will be disabled. [Errno 28] No space left on device')
-
-        self.assertFalse(cgroup_configurator.enabled())
-        self.assertEquals(len(CGroupsTelemetry._tracked), 0)
-
-    @patch("azurelinuxagent.common.cgroupapi.CGroupsApi._is_systemd", return_value=True)
-    def test_cleanup_legacy_cgroups_should_disable_cgroups_when_the_daemon_was_added_to_the_legacy_cgroup_on_systemd(self, _):
-        # Set up a mock /var/run/waagent.pid file
-        daemon_pid = "42"
-        daemon_pid_file = os.path.join(self.tmp_dir, "waagent.pid")
-        fileutil.write_file(daemon_pid_file, daemon_pid + "\n")
-
-        # Set up old controller cgroups and add the daemon PID to them
-        CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "cpu", daemon_pid)
-        CGroupsTools.create_legacy_agent_cgroup(self.cgroups_file_system_root, "memory", daemon_pid)
-
-        # Start tracking a couple of dummy cgroups
-        CGroupsTelemetry.track_cgroup(CGroup("dummy", "/sys/fs/cgroup/memory/system.slice/dummy.service", "cpu"))
-        CGroupsTelemetry.track_cgroup(CGroup("dummy", "/sys/fs/cgroup/memory/system.slice/dummy.service", "memory"))
-
-        cgroup_configurator = CGroupConfigurator.get_instance()
-
-        with patch("azurelinuxagent.common.cgroupconfigurator.add_event") as mock_add_event:
-            with patch("azurelinuxagent.common.cgroupapi.get_agent_pid_file_path", return_value=daemon_pid_file):
-                    cgroup_configurator.cleanup_legacy_cgroups()
-
-        self.assertEquals(len(mock_add_event.call_args_list), 1)
-        _, kwargs = mock_add_event.call_args_list[0]
-        self.assertEquals(kwargs['op'], 'CGroupsCleanUp')
-        self.assertFalse(kwargs['is_success'])
-        self.assertEquals(
-            kwargs['message'],
-            "Failed to process legacy cgroups. Collection of resource usage data will be disabled. [CGroupsException] The daemon's PID ({0}) was already added to the legacy cgroup; this invalidates resource usage data.".format(daemon_pid))
-
-        self.assertFalse(cgroup_configurator.enabled())
-        self.assertEquals(len(CGroupsTelemetry._tracked), 0)
diff --git a/tests/data/cgroups/proc_self_cgroup b/tests/data/cgroups/proc_self_cgroup
new file mode 100644
index 00000000..c6c0734a
--- /dev/null
+++ b/tests/data/cgroups/proc_self_cgroup
@@ -0,0 +1,13 @@
+12:blkio:/system.slice/walinuxagent.service
+11:cpu,cpuacct:/system.slice/walinuxagent.service
+10:devices:/system.slice/walinuxagent.service
+9:pids:/system.slice/walinuxagent.service
+8:memory:/system.slice/walinuxagent.service
+7:freezer:/
+6:hugetlb:/
+5:perf_event:/
+4:net_cls,net_prio:/
+3:cpuset:/
+2:rdma:/
+1:name=systemd:/system.slice/walinuxagent.service
+0::/system.slice/walinuxagent.service
diff --git a/tests/data/cgroups/sys_fs_cgroup_unified_cgroup.controllers b/tests/data/cgroups/sys_fs_cgroup_unified_cgroup.controllers
new file mode 100644
index 00000000..2a03d239
--- /dev/null
+++ b/tests/data/cgroups/sys_fs_cgroup_unified_cgroup.controllers
@@ -0,0 +1,7 @@
+io
+memory
+pids
+perf_event
+rdma
+cpu
+freezer
\ No newline at end of file
diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py
index 67a11d45..f9f4d2ac 100644
--- a/tests/ga/test_update.py
+++ b/tests/ga/test_update.py
@@ -1485,9 +1485,10 @@ class MonitorThreadTest(AgentTestCase):
                 with patch('azurelinuxagent.ga.exthandlers.get_exthandlers_handler'):
                     with patch('azurelinuxagent.ga.remoteaccess.get_remote_access_handler'):
                         with patch('azurelinuxagent.ga.update.initialize_event_logger_vminfo_common_parameters'):
-                            with patch('time.sleep', side_effect=iterator):
-                                with patch('sys.exit'):
-                                    self.update_handler.run()
+                            with patch('azurelinuxagent.common.cgroupapi.CGroupsApi.cgroups_supported', return_value=False):  # skip all cgroup stuff
+                                with patch('time.sleep', side_effect=iterator):
+                                    with patch('sys.exit'):
+                                        self.update_handler.run()
 
     @patch('azurelinuxagent.ga.monitor.get_monitor_handler')
     @patch('azurelinuxagent.ga.env.get_env_handler')