Repository: Azure/WALinuxAgent
Pull request #1530 (2019-05-17 20:17:55)

Defined API for initialization of cgroups
<!-- DO NOT DELETE THIS TEMPLATE -->

## Description

<!--
Please add an informative description that covers that changes made by the pull request. 
This checklist is used to make sure that common issues in a pull request are addressed.
This will expedite the process of getting your pull request merged and avoid extra work on your part to fix issues discovered during the review process.
-->

---

### PR information
- [ ] The title of the PR is clear and informative.
- [ ] There are a small number of commits, each of which has an informative message. This means that previously merged commits do not appear in the history of the PR. For information on cleaning up the commits in your pull request, [see this page](https://github.com/Azure/azure-powershell/blob/master/documentation/development-docs/cleaning-up-commits.md).
- [ ] Except for special cases involving multiple contributors, the PR is started from a fork of the main repository, not a branch.
- [ ] If applicable, the PR references the bug/issue that it fixes in the description.
- [ ] New Unit tests were added for the changes made and Travis.CI is passing.

### Quality of Code and Contribution Guidelines
- [ ] I have read the [contribution guidelines](https://github.com/Azure/WALinuxAgent/blob/master/.github/CONTRIBUTING.md).

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/azure/walinuxagent/1530)
<!-- Reviewable:end -->


codecov[bot]: # [Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530?src=pr&el=h1) Report
> Merging [#1530](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530?src=pr&el=desc) into [cgroups](https://codecov.io/gh/Azure/WALinuxAgent/commit/df97eec2668c9dbff2c63d17038ff98644e5cd2a?src=pr&el=desc) will **increase** coverage by `0.17%`.
> The diff coverage is `44.71%`.

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

```diff
@@             Coverage Diff             @@
##           cgroups    #1530      +/-   ##
===========================================
+ Coverage     60.9%   61.07%   +0.17%     
===========================================
  Files           77       78       +1     
  Lines        10893    10919      +26     
  Branches      1560     1554       -6     
===========================================
+ Hits          6634     6669      +35     
+ Misses        3944     3935       -9     
  Partials       315      315
```


| [Impacted Files](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [azurelinuxagent/common/cgroupstelemetry.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBzdGVsZW1ldHJ5LnB5) | `32.25% <ø> (+6.98%)` | :arrow_up: |
| [azurelinuxagent/ga/monitor.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2dhL21vbml0b3IucHk=) | `60.55% <ø> (+2.27%)` | :arrow_up: |
| [azurelinuxagent/ga/update.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2dhL3VwZGF0ZS5weQ==) | `88.31% <100%> (+0.11%)` | :arrow_up: |
| [azurelinuxagent/daemon/main.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2RhZW1vbi9tYWluLnB5) | `72.91% <100%> (ø)` | :arrow_up: |
| [azurelinuxagent/common/cgroupapi.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBhcGkucHk=) | `34.32% <34.32%> (ø)` | |
| [azurelinuxagent/common/cgroupconfigurator.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBjb25maWd1cmF0b3IucHk=) | `36.47% <50%> (+4.54%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530?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/1530?src=pr&el=footer). Last update [df97eec...0448d34](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530?src=pr&el=h1) Report
> Merging [#1530](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530?src=pr&el=desc) into [cgroups](https://codecov.io/gh/Azure/WALinuxAgent/commit/df97eec2668c9dbff2c63d17038ff98644e5cd2a?src=pr&el=desc) will **increase** coverage by `0.17%`.
> The diff coverage is `44.71%`.

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

```diff
@@             Coverage Diff             @@
##           cgroups    #1530      +/-   ##
===========================================
+ Coverage     60.9%   61.07%   +0.17%     
===========================================
  Files           77       78       +1     
  Lines        10893    10919      +26     
  Branches      1560     1554       -6     
===========================================
+ Hits          6634     6669      +35     
+ Misses        3944     3935       -9     
  Partials       315      315
```


| [Impacted Files](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [azurelinuxagent/common/cgroupstelemetry.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBzdGVsZW1ldHJ5LnB5) | `32.25% <ø> (+6.98%)` | :arrow_up: |
| [azurelinuxagent/ga/monitor.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2dhL21vbml0b3IucHk=) | `60.55% <ø> (+2.27%)` | :arrow_up: |
| [azurelinuxagent/ga/update.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2dhL3VwZGF0ZS5weQ==) | `88.31% <100%> (+0.11%)` | :arrow_up: |
| [azurelinuxagent/daemon/main.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2RhZW1vbi9tYWluLnB5) | `72.91% <100%> (ø)` | :arrow_up: |
| [azurelinuxagent/common/cgroupapi.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBhcGkucHk=) | `34.32% <34.32%> (ø)` | |
| [azurelinuxagent/common/cgroupconfigurator.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBjb25maWd1cmF0b3IucHk=) | `36.47% <50%> (+4.54%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530?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/1530?src=pr&el=footer). Last update [df97eec...0448d34](https://codecov.io/gh/Azure/WALinuxAgent/pull/1530?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

narrieta: @pgombar thanks for your comments. I updated the code, could you take a look at the last commit?

Patch:
diff --git a/azurelinuxagent/common/cgroupapi.py b/azurelinuxagent/common/cgroupapi.py
new file mode 100644
index 00000000..eb230a6e
--- /dev/null
+++ b/azurelinuxagent/common/cgroupapi.py
@@ -0,0 +1,137 @@
+# Copyright 2018 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.6+ and Openssl 1.0+
+
+import errno
+import os
+
+from azurelinuxagent.common import logger
+from azurelinuxagent.common.exception import CGroupsException
+from azurelinuxagent.common.future import ustr
+from azurelinuxagent.common.osutil import get_osutil
+from azurelinuxagent.common.utils import fileutil
+
+CGROUPS_FILE_SYSTEM_ROOT = '/sys/fs/cgroup'
+CGROUP_CONTROLLERS = ["cpu", "memory"]
+VM_AGENT_CGROUP_NAME = "walinuxagent.service"
+EXTENSIONS_ROOT_CGROUP_NAME = "walinuxagent.extensions"
+
+class CGroupsApi(object):
+    """
+    Interface for the cgroups API
+    """
+    def create_agent_cgroups(self):
+        raise NotImplementedError()
+
+    def create_extension_cgroups_root(self):
+        raise NotImplementedError()
+
+
+class FileSystemCgroupsApi(CGroupsApi):
+    """
+    Cgroups interface using the cgroups file system directly
+    """
+    _osutil = get_osutil()
+
+    @staticmethod
+    def _try_mkdir(path):
+        """
+        Try to create a directory, recursively. If it already exists as such, do nothing. Raise the appropriate
+        exception should an error occur.
+
+        :param path: str
+        """
+        if not os.path.isdir(path):
+            try:
+                os.makedirs(path, 0o755)
+            except OSError as e:
+                if e.errno == errno.EEXIST:
+                    if not os.path.isdir(path):
+                        raise CGroupsException("Create directory for cgroup {0}: normal file already exists with that name".format(path))
+                    else:
+                        pass # There was a race to create the directory, but it's there now, and that's fine
+                elif e.errno == errno.EACCES:
+                    # This is unexpected, as the agent runs as root
+                    raise CGroupsException("Create directory for cgroup {0}: permission denied".format(path))
+                else:
+                    raise
+
+    @staticmethod
+    def _foreach_controller(operation, message):
+        """
+        Executes the given operation on all controllers that need to be tracked; outputs 'message' if the controller is not mounted.
+        """
+        mounted_controllers = os.listdir(CGROUPS_FILE_SYSTEM_ROOT)
+
+        for controller in CGROUP_CONTROLLERS:
+            if controller not in mounted_controllers:
+                logger.warn('Controller "{0}" is not mounted. {1}'.format(controller, message))
+            else:
+                operation(controller)
+
+    def create_agent_cgroups(self):
+        cgroup_paths = []
+
+        pid = int(os.getpid())
+
+        def __impl(controller):
+            try:
+                path = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, controller, VM_AGENT_CGROUP_NAME)
+
+                if not os.path.isdir(path):
+                    FileSystemCgroupsApi._try_mkdir(path)
+                    logger.info("Created cgroup {0}".format(path))
+
+                tasks_file = os.path.join(path, 'cgroup.procs')
+                fileutil.append_file(tasks_file, "{0}\n".format(pid))
+
+                logger.info("Added Agent (PID {0}) to cgroup {1}".format(pid, path))
+
+                cgroup_paths.append(path)
+
+            except Exception as e:
+                logger.warn('Cannot create "{0}" cgroup for the agent. Error: {1}'.format(controller, ustr(e)))
+
+        FileSystemCgroupsApi._foreach_controller(__impl, 'Will not create a cgroup for the VM Agent')
+
+        if len(cgroup_paths) == 0:
+            raise CGroupsException("Failed to create any cgroup for the VM Agent")
+
+        return cgroup_paths
+
+    def create_extension_cgroups_root(self):
+        def __impl(controller):
+            path = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, controller, EXTENSIONS_ROOT_CGROUP_NAME)
+
+            if not os.path.isdir(path):
+                FileSystemCgroupsApi._try_mkdir(path)
+                logger.info("Created {0}".format(path))
+
+        FileSystemCgroupsApi._foreach_controller(__impl, 'Will not create a root cgroup for extensions')
+
+
+class SystemdCgroupsApi(CGroupsApi):
+    """
+    Cgroups interface via systemd
+    """
+    def create_agent_cgroups(self):
+        pass
+
+    def create_extension_cgroups_root(self):
+        pass
+
+
+
+
diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py
index 2034edd7..7d4dbd37 100644
--- a/azurelinuxagent/common/cgroupconfigurator.py
+++ b/azurelinuxagent/common/cgroupconfigurator.py
@@ -18,13 +18,14 @@ import errno
 import os
 
 from azurelinuxagent.common import logger, conf
+from azurelinuxagent.common.cgroupapi import FileSystemCgroupsApi, SystemdCgroupsApi
 from azurelinuxagent.common.exception import CGroupsException
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.osutil import get_osutil
 from azurelinuxagent.common.osutil.default import BASE_CGROUPS
 from azurelinuxagent.common.utils import fileutil
 from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION
-
+from azurelinuxagent.common.event import add_event, WALAEventOperation
 
 WRAPPER_CGROUP_NAME = "WALinuxAgent"
 AGENT_CGROUP_NAME = "WALinuxAgent"
@@ -39,15 +40,14 @@ DEFAULT_MEM_LIMIT_MIN_MB = 256  # mb, applies to agent and extensions
 DEFAULT_MEM_LIMIT_MAX_MB = 512  # mb, applies to agent only
 DEFAULT_MEM_LIMIT_PCT = 15  # percent, applies to extensions
 
+
 class CGroupConfigurator(object):
     """
-    This class represents the cgroup folders for the agent or an extension. This is a pretty lightweight object
-    without much state worth preserving; it's not unreasonable to create one just when you need it.
+    This class implements the high-level operations on CGroups (e.g. initialization, creation, etc)
     """
     # whether cgroup support is enabled
     _enabled = True
     _hierarchies = [ "cpu", "memory" ]
-    _use_systemd = None     # Tri-state: None (i.e. "unknown"), True, False
     _osutil = get_osutil()
 
     @staticmethod
@@ -87,12 +87,7 @@ class CGroupConfigurator(object):
         :param str name: Name for the cgroup (usually the full name of the extension)
         :param path_maker: Function which constructs the root path for a given hierarchy where this cgroup lives
         """
-        if not name or name == "":
-            self.name = "Agents+Extensions"
-            self.is_wrapper_cgroup = True
-        else:
-            self.name = name
-            self.is_wrapper_cgroup = False
+        self.name = name
 
         self.cgroups = {}
 
@@ -107,32 +102,34 @@ class CGroupConfigurator(object):
                 self.disable()
                 raise CGroupsException("Hierarchy {0} is not mounted".format(hierarchy))
 
-            cgroup_name = "" if self.is_wrapper_cgroup else self.name
-            cgroup_path = path_maker(hierarchy, cgroup_name)
+            cgroup_path = path_maker(hierarchy, self.name)
             if not os.path.isdir(cgroup_path):
                 CGroupConfigurator._try_mkdir(cgroup_path)
                 logger.info("Created cgroup {0}".format(cgroup_path))
 
             self.cgroups[hierarchy] = cgroup_path
 
+    _is_systemd_return_value = None
+
     @staticmethod
-    def is_systemd_manager():
+    def is_systemd():
         """
-        Determine if systemd is managing system services. Many extensions are structured as a set of services,
-        including the agent itself; systemd expects those services to remain in the cgroups in which it placed them.
-        If this process (presumed to be the agent) is in a cgroup that looks like one created by systemd, we can
-        assume systemd is in use.
+        Determine if systemd is managing system services. If this process (presumed to be the agent) is in a CPU cgroup
+        that looks like one created by systemd, we can assume systemd is in use.
+
+        TODO: We need to re-evaluate whether this the right logic to determine whether Systemd is managing cgroups.
 
         :return: True if systemd is managing system services
         :rtype: Bool
         """
-        if not CGroupConfigurator.enabled():
-            return False
-        if CGroupConfigurator._use_systemd is None:
-            hierarchy = METRIC_HIERARCHIES[0]
-            path = CGroupConfigurator.get_my_cgroup_folder(hierarchy)
-            CGroupConfigurator._use_systemd = path.startswith(CGroupConfigurator._construct_systemd_path_for_hierarchy(hierarchy, ""))
-        return CGroupConfigurator._use_systemd
+        if CGroupConfigurator._is_systemd_return_value is None:
+            if not CGroupConfigurator.enabled():
+                CGroupConfigurator._is_systemd_return_value = False
+            else:
+                path = CGroupConfigurator.get_my_cgroup_folder("cpu")
+                CGroupConfigurator._is_systemd_return_value = path.startswith(CGroupConfigurator._construct_systemd_path_for_hierarchy("cpu", ""))
+
+        return CGroupConfigurator._is_systemd_return_value
 
     @staticmethod
     def _try_mkdir(path):
@@ -165,9 +162,6 @@ class CGroupConfigurator(object):
         if not self.enabled():
             return
 
-        if self.is_wrapper_cgroup:
-            raise CGroupsException("Cannot add a process to the Agents+Extensions wrapper cgroup")
-
         if not self._osutil.check_pid_alive(pid):
             raise CGroupsException('PID {0} does not exist'.format(pid))
         for hierarchy, cgroup in self.cgroups.items():
@@ -214,79 +208,6 @@ class CGroupConfigurator(object):
                 message=msg,
                 log_event=False)
 
-    @staticmethod
-    def _apply_wrapper_limits(path, hierarchy):
-        """
-        Find wrapping limits for the hierarchy and apply them to the cgroup denoted by the path
-
-        :param path: str
-        :param hierarchy: str
-        """
-        pass
-
-    @staticmethod
-    def _setup_wrapper_groups():
-        """
-        For each hierarchy, construct the wrapper cgroup and apply the appropriate limits
-        """
-        for hierarchy in METRIC_HIERARCHIES:
-            # This creates /sys/fs/cgroup/{cpu,memory}/WALinuxAgent
-            root_dir = CGroupConfigurator._construct_custom_path_for_hierarchy(hierarchy, "")
-            CGroupConfigurator._try_mkdir(root_dir)
-            CGroupConfigurator._apply_wrapper_limits(root_dir, hierarchy)
-
-    @staticmethod
-    def setup(suppress_process_add=False):
-        """
-        Only needs to be called once, and should be called from the -daemon instance of the agent.
-            Mount the cgroup fs if necessary
-            Create wrapper cgroups for agent-plus-extensions and set limits on them;
-            Add this process to the "agent" cgroup, if required
-        Actual collection of metrics from cgroups happens in the -run-exthandlers instance
-        """
-        if CGroupConfigurator.enabled():
-            try:
-                CGroupConfigurator._osutil.mount_cgroups()
-                if not suppress_process_add:
-                    # Creates /sys/fs/cgroup/{cpu,memory}/WALinuxAgent wrapper cgroup
-                    CGroupConfigurator._setup_wrapper_groups()
-                    pid = int(os.getpid())
-                    if CGroupConfigurator.is_systemd_manager():
-                        # When daemon is running as a service, it's called walinuxagent.service
-                        # and is created and tracked by systemd, so we don't explicitly add the PID ourselves,
-                        # just track it for our reporting purposes
-                        cg = CGroupConfigurator.for_systemd_service(AGENT_CGROUP_NAME.lower() + ".service")
-                        logger.info("Daemon process id {0} is tracked in systemd cgroup {1}".format(pid, cg.name))
-                        # systemd sets limits; any limits we write would be overwritten
-                    else:
-                        # Creates /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent cgroup
-                        cg = CGroupConfigurator.for_extension(AGENT_CGROUP_NAME)
-                        logger.info("Daemon process id {0} is tracked in cgroup {1}".format(pid, cg.name))
-                        cg.add(pid)
-                        cg.set_limits()
-
-                status = "successfully set up agent cgroup"
-            except CGroupsException as cge:
-                status = cge.msg
-                CGroupConfigurator.disable()
-            except Exception as ge:
-                status = ustr(ge)
-                CGroupConfigurator.disable()
-        else:
-            status = "not supported by platform"
-            CGroupConfigurator.disable()
-
-        logger.info("CGroups: {0}".format(status))
-
-        from azurelinuxagent.common.event import add_event, WALAEventOperation
-        add_event(
-            AGENT_NAME,
-            version=CURRENT_VERSION,
-            op=WALAEventOperation.InitializeCGroups,
-            is_success=CGroupConfigurator.enabled(),
-            message=status,
-            log_event=False)
-
     @staticmethod
     def add_to_extension_cgroup(name, pid):
         """
@@ -485,9 +406,6 @@ class CGroupsLimits(object):
         return threshold[limit] if threshold and limit in threshold else compute_default(name)
 
     def __init__(self, cgroup_name, threshold=None):
-        if not cgroup_name or cgroup_name == "":
-            cgroup_name = "Agents+Extensions"
-
         self.cpu_limit = self._get_value_or_default(cgroup_name, threshold, "cpu", CGroupsLimits.get_default_cpu_limits)
         self.memory_limit = self._get_value_or_default(cgroup_name, threshold, "memory",
                                                        CGroupsLimits.get_default_memory_limits)
@@ -511,3 +429,74 @@ class CGroupsLimits(object):
         if AGENT_CGROUP_NAME.lower() in cgroup_name.lower():
             mem_limit = min(DEFAULT_MEM_LIMIT_MAX_MB, mem_limit)
         return mem_limit
+
+
+class CGroupConfigurator_tmp(object):
+    #
+    # TODO: code from CGroupConfigurator is being moved to this class
+    #
+    class __impl(object):
+        def __init__(self):
+            """
+            Ensures the cgroups file system is mounted and selects the correct API to interact with it
+            """
+            self.cgroups_api = None
+
+            if CGroupConfigurator.enabled():
+                try:
+                    CGroupConfigurator._osutil.mount_cgroups()
+                    self.cgroups_api = SystemdCgroupsApi() if CGroupConfigurator.is_systemd() else FileSystemCgroupsApi()
+                    status = "The cgroup filesystem is ready to use"
+                except Exception as e:
+                    status = ustr(e)
+                    CGroupConfigurator.disable()
+            else:
+                status = "Cgroups are not supported by the platform"
+
+            logger.info("CGroups Status: {0}".format(status))
+
+            add_event(
+                AGENT_NAME,
+                version=CURRENT_VERSION,
+                op=WALAEventOperation.InitializeCGroups,
+                is_success=CGroupConfigurator.enabled(),
+                message=status,
+                log_event=False)
+
+        @staticmethod
+        def _invoke_cgroup_operation(operation, error_message):
+            """
+            Ensures the given operation is invoked only if cgroups are enabled and traps any errors on the operation
+            """
+            if not CGroupConfigurator.enabled():
+                return
+
+            try:
+                operation()
+            except Exception as e:
+                logger.warn("{0}. Error: {1}".format(error_message, ustr(e)))
+
+        def create_agent_cgroups(self, track_cgroups):
+            def __impl():
+                cgroups = self.cgroups_api.create_agent_cgroups()
+
+                if track_cgroups:
+                    # TODO: Add to tracking list
+                    pass
+
+            self._invoke_cgroup_operation(__impl, "Failed to create a cgroup for the VM Agent; resource usage for the Agent will not be tracked")
+
+        def create_extension_cgroups_root(self):
+            def __impl():
+                self.cgroups_api.create_extension_cgroups_root()
+
+            self._invoke_cgroup_operation(__impl, "Failed to create a root cgroup for extensions; resource usage for extensions will not be tracked")
+
+    # unique instance for the singleton (TODO: find a better pattern for a singleton)
+    __instance = None
+
+    @staticmethod
+    def get_instance():
+        if CGroupConfigurator_tmp.__instance is None:
+            CGroupConfigurator_tmp.__instance = CGroupConfigurator_tmp.__impl()
+        return CGroupConfigurator_tmp.__instance
diff --git a/azurelinuxagent/common/cgroupstelemetry.py b/azurelinuxagent/common/cgroupstelemetry.py
index 0d43b61d..c9a8629d 100644
--- a/azurelinuxagent/common/cgroupstelemetry.py
+++ b/azurelinuxagent/common/cgroupstelemetry.py
@@ -20,11 +20,6 @@ from azurelinuxagent.common import logger
 from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCGroup
 from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator, AGENT_CGROUP_NAME
 
-related_services = {
-    "Microsoft.OSTCExtensions.LinuxDiagnostic":    ["omid", "omsagent-LAD", "mdsd-lde"],
-    "Microsoft.Azure.Diagnostics.LinuxDiagnostic": ["omid", "omsagent-LAD", "mdsd-lde"],
-}
-
 class CGroupsTelemetry(object):
     """
     Encapsulate the cgroup-based telemetry for the agent or one of its extensions, or for the aggregation across
@@ -87,28 +82,7 @@ class CGroupsTelemetry(object):
         if not CGroupsTelemetry.is_tracked(name):
             cgroup = CGroupConfigurator.for_extension(name) if cgroup is None else cgroup
             logger.info("Now tracking cgroup {0}".format(name))
-            cgroup.set_limits()
             CGroupsTelemetry.track_cgroup(cgroup)
-        if CGroupConfigurator.is_systemd_manager():
-            if name in related_services:
-                for service_name in related_services[name]:
-                    CGroupsTelemetry.track_systemd_service(service_name)
-
-    @staticmethod
-    def track_agent():
-        """
-        Create and track the correct cgroup for the agent itself. The actual cgroup depends on whether systemd
-        is in use, but the caller doesn't need to know that.
-        """
-        if not CGroupConfigurator.enabled():
-            return
-        if CGroupConfigurator.is_systemd_manager():
-            logger.info("Tracking systemd cgroup for {0}".format(AGENT_CGROUP_NAME))
-            CGroupsTelemetry.track_systemd_service(AGENT_CGROUP_NAME)
-        else:
-            logger.info("Tracking cgroup for {0}".format(AGENT_CGROUP_NAME))
-            # This creates /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent
-            CGroupsTelemetry.track_cgroup(CGroupConfigurator.for_extension(AGENT_CGROUP_NAME))
 
     @staticmethod
     def is_tracked(name):
@@ -145,34 +119,6 @@ class CGroupsTelemetry(object):
 
         return results, limits
 
-    @staticmethod
-    def update_tracked(ext_handlers):
-        """
-        Track CGroups for all enabled extensions.
-        Track CGroups for services created by enabled extensions.
-        Stop tracking CGroups for not-enabled extensions.
-
-        :param List(ExtHandler) ext_handlers:
-        """
-        if not CGroupConfigurator.enabled():
-            return
-
-        not_enabled_extensions = set()
-        for extension in ext_handlers:
-            if extension.properties.state == u"enabled":
-                CGroupsTelemetry.track_extension(extension.name)
-            else:
-                not_enabled_extensions.add(extension.name)
-
-        names_now_tracked = set(CGroupsTelemetry._tracked.keys())
-        if CGroupsTelemetry.tracked_names != names_now_tracked:
-            now_tracking = " ".join("[{0}]".format(name) for name in sorted(names_now_tracked))
-            if len(now_tracking):
-                logger.info("After updating cgroup telemetry, tracking {0}".format(now_tracking))
-            else:
-                logger.warn("After updating cgroup telemetry, tracking no cgroups.")
-            CGroupsTelemetry.tracked_names = names_now_tracked
-
     def __init__(self, name, cgroup=None):
         """
         Create the necessary state to collect metrics for the agent, one of its extensions, or the aggregation across
diff --git a/azurelinuxagent/daemon/main.py b/azurelinuxagent/daemon/main.py
index 6f7333b8..a872be8e 100644
--- a/azurelinuxagent/daemon/main.py
+++ b/azurelinuxagent/daemon/main.py
@@ -26,7 +26,7 @@ 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.cgroupconfigurator import CGroupConfigurator_tmp
 from azurelinuxagent.common.event import add_event, WALAEventOperation
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.osutil import get_osutil
@@ -68,7 +68,7 @@ class DaemonHandler(object):
         self.check_pid()
         self.initialize_environment()
 
-        CGroupConfigurator.setup()
+        CGroupConfigurator_tmp.get_instance().create_agent_cgroups(track_cgroups=False)
 
         # If FIPS is enabled, set the OpenSSL environment variable
         # Note:
diff --git a/azurelinuxagent/ga/monitor.py b/azurelinuxagent/ga/monitor.py
index 69747d43..106d92a4 100644
--- a/azurelinuxagent/ga/monitor.py
+++ b/azurelinuxagent/ga/monitor.py
@@ -132,7 +132,6 @@ class MonitorHandler(object):
     def run(self):
         self.init_protocols()
         self.init_sysinfo()
-        self.init_cgroups()
         self.start()
 
     def stop(self):
@@ -400,26 +399,6 @@ class MonitorHandler(object):
 
             self.last_telemetry_heartbeat = datetime.datetime.utcnow()
 
-    @staticmethod
-    def init_cgroups():
-        # Track metrics for the wrapper cgroup and for the agent cgroup
-        try:
-            # This creates the wrapper cgroup for everything under agent,
-            # /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/
-            # There is no need in tracking this cgroup, as it only serves
-            # as an umbrella for the agent and extensions cgroups
-            CGroupConfigurator.for_extension("")
-            # This creates the agent's cgroup (for the daemon and extension handler)
-            # /sys/fs/cgroup/{cpu,memory}/WALinuxAgent/WALinuxAgent
-            # If the system is using systemd, it would have already been set up under /system.slice
-            CGroupsTelemetry.track_agent()
-        except Exception as e:
-            # when a hierarchy is not mounted, we raise an exception
-            # and we should therefore only issue a warning, since this
-            # is not unexpected
-            logger.warn("Monitor: cgroups not initialized: {0}", ustr(e))
-            logger.verbose(traceback.format_exc())
-
     def send_cgroup_telemetry(self):
         if self.last_cgroup_telemetry is None:
             self.last_cgroup_telemetry = datetime.datetime.utcnow()
@@ -467,13 +446,6 @@ class MonitorHandler(object):
                 logger.warn("Monitor: failed to collect cgroups performance metrics: {0}", ustr(e))
                 logger.verbose(traceback.format_exc())
 
-            # Look for extension cgroups we're not already tracking and track them
-            try:
-                CGroupsTelemetry.update_tracked(self.protocol.client.get_current_handlers())
-            except Exception as e:
-                logger.warn("Monitor: failed to update cgroups tracked extensions: {0}", ustr(e))
-                logger.verbose(traceback.format_exc())
-
             self.last_cgroup_telemetry = datetime.datetime.utcnow()
 
     def log_altered_network_configuration(self):
diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py
index bb66614c..548df4a9 100644
--- a/azurelinuxagent/ga/update.py
+++ b/azurelinuxagent/ga/update.py
@@ -39,6 +39,7 @@ import azurelinuxagent.common.logger as logger
 import azurelinuxagent.common.utils.fileutil as fileutil
 import azurelinuxagent.common.utils.restutil as restutil
 import azurelinuxagent.common.utils.textutil as textutil
+from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator_tmp
 
 from azurelinuxagent.common.event import add_event, add_periodic, \
                                     elapsed_milliseconds, \
@@ -274,6 +275,7 @@ class UpdateHandler(object):
             self._emit_restart_event()
             self._ensure_partition_assigned()
             self._ensure_readonly_files()
+            self._ensure_cgroups_initialized()
 
             goal_state_interval = GOAL_STATE_INTERVAL \
                 if conf.get_extensions_enabled() \
@@ -447,6 +449,11 @@ class UpdateHandler(object):
             for path in glob.iglob(os.path.join(conf.get_lib_dir(), g)):
                 os.chmod(path, stat.S_IRUSR)
 
+    def _ensure_cgroups_initialized(self):
+        configurator = CGroupConfigurator_tmp.get_instance()
+        configurator.create_agent_cgroups(track_cgroups=True)
+        configurator.create_extension_cgroups_root()
+
     def _evaluate_agent_health(self, latest_agent):
         """
         Evaluate the health of the selected agent: If it is restarting


Test patch:
diff --git a/tests/common/test_cgroupapi.py b/tests/common/test_cgroupapi.py
new file mode 100644
index 00000000..6b903972
--- /dev/null
+++ b/tests/common/test_cgroupapi.py
@@ -0,0 +1,33 @@
+# Copyright 2018 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+from __future__ import print_function
+
+from azurelinuxagent.common.cgroupapi import FileSystemCgroupsApi
+from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator
+from tests.tools import *
+
+
+@skip_if_predicate_false(CGroupConfigurator.enabled, "CGroups not supported in this environment")
+class TestCGroupConfigurator(AgentTestCase):
+    #
+    # TODO -- Need to write actual tests
+    #
+    def test_dummy(self):
+        api = FileSystemCgroupsApi()
+        api.create_agent_cgroups()
+        self.assertTrue(True)
diff --git a/tests/common/test_cgroups.py b/tests/common/test_cgroups.py
index 6b597339..d5e45b87 100644
--- a/tests/common/test_cgroups.py
+++ b/tests/common/test_cgroups.py
@@ -18,7 +18,7 @@
 from __future__ import print_function
 
 from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCGroup
-from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator, CGroupsLimits, BASE_CGROUPS,  DEFAULT_MEM_LIMIT_MIN_MB
+from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator_tmp, CGroupConfigurator, CGroupsLimits, BASE_CGROUPS,  DEFAULT_MEM_LIMIT_MIN_MB
 from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry
 from azurelinuxagent.common.exception import CGroupsException
 from azurelinuxagent.common.version import AGENT_NAME
@@ -69,11 +69,20 @@ def i_am_root():
     return os.geteuid() == 0
 
 
+@skip_if_predicate_false(CGroupConfigurator.enabled, "CGroups not supported in this environment")
+class TestCGroupConfigurator(AgentTestCase):
+    #
+    # TODO -- Need to write actual tests
+    #
+    def test_initialize(self):
+        CGroupConfigurator_tmp.get_instance().create_extension_cgroups_root()
+
+
 @skip_if_predicate_false(CGroupConfigurator.enabled, "CGroups not supported in this environment")
 class TestCGroups(AgentTestCase):
     @classmethod
     def setUpClass(cls):
-        CGroupConfigurator.setup(True)
+        CGroupConfigurator_tmp.get_instance()
         super(AgentTestCase, cls).setUpClass()
 
     def test_cgroup_utilities(self):
@@ -303,10 +312,6 @@ class TestCGroupsLimits(AgentTestCase):
         self.assertEqual(limits.cpu_limit, CGroupsLimits.get_default_cpu_limits(cgroup_name ))
         self.assertEqual(limits.memory_limit, CGroupsLimits.get_default_memory_limits(cgroup_name ))
 
-        limits = CGroupsLimits(None)
-        self.assertEqual(limits.cpu_limit, CGroupsLimits.get_default_cpu_limits(cgroup_name))
-        self.assertEqual(limits.memory_limit, CGroupsLimits.get_default_memory_limits(cgroup_name))
-
     @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_total_mem", return_value=1024)
     def test_with_limits_passed(self, patched_get_total_mem):
         cpu_limit = 50