Repository: Azure/WALinuxAgent
Pull request #1556 (2019-06-14 01:58:47)

Handle systemd failures when invoking extensions
<!-- DO NOT DELETE THIS TEMPLATE -->

## Description

Issue # <!-- if any -->
<!--
Please add an informative description that covers that changes made by the pull request. 
This checklist is used to make sure that common issues in a pull request are addressed.
This will expedite the process of getting your pull request merged and avoid extra work on your part to fix issues discovered during the review process.
-->

---

### PR information
- [ ] The title of the PR is clear and informative.
- [ ] There are a small number of commits, each of which has an informative message. This means that previously merged commits do not appear in the history of the PR. For information on cleaning up the commits in your pull request, [see this page](https://github.com/Azure/azure-powershell/blob/master/documentation/development-docs/cleaning-up-commits.md).
- [ ] 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/1556)
<!-- Reviewable:end -->


codecov[bot]: # [Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/1556?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`develop@3816e53`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `61.11%`.

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

```diff
@@            Coverage Diff             @@
##             develop    #1556   +/-   ##
==========================================
  Coverage           ?   63.21%           
==========================================
  Files              ?       78           
  Lines              ?    11048           
  Branches           ?     1554           
==========================================
  Hits               ?     6984           
  Misses             ?     3725           
  Partials           ?      339
```


| [Impacted Files](https://codecov.io/gh/Azure/WALinuxAgent/pull/1556?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [azurelinuxagent/common/event.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1556/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9ldmVudC5weQ==) | `82.75% <100%> (ø)` | |
| [azurelinuxagent/common/cgroupapi.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1556/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBhcGkucHk=) | `62.6% <58.82%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/1556?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/1556?src=pr&el=footer). Last update [3816e53...a6cfe58](https://codecov.io/gh/Azure/WALinuxAgent/pull/1556?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/1556?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`develop@3816e53`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `61.11%`.

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

```diff
@@            Coverage Diff             @@
##             develop    #1556   +/-   ##
==========================================
  Coverage           ?   63.21%           
==========================================
  Files              ?       78           
  Lines              ?    11048           
  Branches           ?     1554           
==========================================
  Hits               ?     6984           
  Misses             ?     3725           
  Partials           ?      339
```


| [Impacted Files](https://codecov.io/gh/Azure/WALinuxAgent/pull/1556?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [azurelinuxagent/common/event.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1556/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9ldmVudC5weQ==) | `82.75% <100%> (ø)` | |
| [azurelinuxagent/common/cgroupapi.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1556/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9jZ3JvdXBhcGkucHk=) | `62.6% <58.82%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Azure/WALinuxAgent/pull/1556?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/1556?src=pr&el=footer). Last update [3816e53...a6cfe58](https://codecov.io/gh/Azure/WALinuxAgent/pull/1556?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 f373256c..36b16a76 100644
--- a/azurelinuxagent/common/cgroupapi.py
+++ b/azurelinuxagent/common/cgroupapi.py
@@ -17,13 +17,17 @@
 import errno
 import os
 import subprocess
+import time
 import uuid
 
 from azurelinuxagent.common import logger
-from azurelinuxagent.common.cgroup import CpuCgroup, MemoryCgroup, CGroup
+from azurelinuxagent.common.cgroup import CGroup
+from azurelinuxagent.common.event import add_event, WALAEventOperation
 from azurelinuxagent.common.exception import CGroupsException
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.utils import fileutil, shellutil
+from azurelinuxagent.common.utils.processutil import read_output
+from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION
 
 CGROUPS_FILE_SYSTEM_ROOT = '/sys/fs/cgroup'
 CGROUP_CONTROLLERS = ["cpu", "memory"]
@@ -423,14 +427,53 @@ After=system-{1}.slice""".format(extension_name, EXTENSIONS_ROOT_CGROUP_NAME)
             env=env,
             preexec_fn=os.setsid)
 
-        logger.info("Started extension using scope '{0}'", scope_name)
+        # Wait a bit and check if we completed with error
+        time.sleep(1)
+        return_code = process.poll()
+
+        if return_code is not None and return_code != 0:
+            process_output = read_output(stdout, stderr)
+
+            # When systemd-run successfully invokes a command, thereby creating its unit, it will output the
+            # unit's name. Since the scope name is only known to systemd-run, and not to the extension itself,
+            # if scope_name appears in the output, we are certain systemd-run managed to run.
+            if scope_name not in process_output:
+                logger.warn('Failed to run systemd-run for unit {0}.scope '
+                            'Process exited with code {1} and output {2}'.format(scope_name,
+                                                                                 return_code,
+                                                                                 process_output))
+
+                add_event(AGENT_NAME,
+                          version=CURRENT_VERSION,
+                          op=WALAEventOperation.InvokeCommandUsingSystemd,
+                          is_success=False,
+                          message='Failed to run systemd-run for unit {0}.scope. '
+                                  'Process exited with code {1} and output {2}'.format(scope_name,
+                                                                                       return_code,
+                                                                                       process_output))
+
+                # Try starting the process without systemd-run
+                process = subprocess.Popen(
+                    command,
+                    shell=shell,
+                    cwd=cwd,
+                    env=env,
+                    stdout=stdout,
+                    stderr=stderr,
+                    preexec_fn=os.setsid)
+
+                return process, []
 
         cgroups = []
 
+        logger.info("Started extension using scope '{0}'", scope_name)
+
         def create_cgroup(controller):
-            cpu_cgroup_path = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, controller, 'system.slice', scope_name + ".scope")
-            cgroups.append(CGroup.create(cpu_cgroup_path, controller, extension_name))
+            cgroup_path = os.path.join(CGROUPS_FILE_SYSTEM_ROOT, controller, 'system.slice', scope_name + ".scope")
+            cgroups.append(CGroup.create(cgroup_path, controller, extension_name))
 
-        self._foreach_controller(create_cgroup, 'Cannot create cgroup for extension {0}; resource usage will not be tracked.'.format(extension_name))
+        self._foreach_controller(create_cgroup,
+                                 'Cannot create cgroup for extension {0}; resource usage will not be tracked.'.format(
+                                     extension_name))
 
         return process, cgroups
diff --git a/azurelinuxagent/common/cgroupstelemetry.py b/azurelinuxagent/common/cgroupstelemetry.py
index e680a6e2..b323aec6 100644
--- a/azurelinuxagent/common/cgroupstelemetry.py
+++ b/azurelinuxagent/common/cgroupstelemetry.py
@@ -92,7 +92,11 @@ class CGroupsTelemetry(object):
         collected_metrics = {}
 
         for name, cgroup_metrics in CGroupsTelemetry._cgroup_metrics.items():
-            collected_metrics[name] = CGroupsTelemetry._process_cgroup_metric(cgroup_metrics)
+            perf_metric = CGroupsTelemetry._process_cgroup_metric(cgroup_metrics)
+
+            if perf_metric:
+                collected_metrics[name] = perf_metric
+
             cgroup_metrics.clear()
 
         # Doing cleanup after the metrics have already been collected.
diff --git a/azurelinuxagent/common/event.py b/azurelinuxagent/common/event.py
index 583d6400..866492ae 100644
--- a/azurelinuxagent/common/event.py
+++ b/azurelinuxagent/common/event.py
@@ -65,6 +65,7 @@ class WALAEventOperation:
     Install = "Install"
     InitializeCGroups = "InitializeCGroups"
     InitializeHostPlugin = "InitializeHostPlugin"
+    InvokeCommandUsingSystemd = "InvokeCommandUsingSystemd"
     Log = "Log"
     Partition = "Partition"
     ProcessGoalState = "ProcessGoalState"
diff --git a/azurelinuxagent/common/utils/processutil.py b/azurelinuxagent/common/utils/processutil.py
index d4d48b48..ecddc30c 100644
--- a/azurelinuxagent/common/utils/processutil.py
+++ b/azurelinuxagent/common/utils/processutil.py
@@ -16,16 +16,27 @@
 #
 # Requires Python 2.6+ and Openssl 1.0+
 #
-import os
-import signal
-from errno import ESRCH
 
-from azurelinuxagent.common.exception import ExtensionError
 from azurelinuxagent.common.future import ustr
 
 TELEMETRY_MESSAGE_MAX_LEN = 3200
 
 
+def read_output(stdout, stderr):
+    try:
+        stdout.seek(0)
+        stderr.seek(0)
+
+        stdout = ustr(stdout.read(TELEMETRY_MESSAGE_MAX_LEN), encoding='utf-8',
+                      errors='backslashreplace')
+        stderr = ustr(stderr.read(TELEMETRY_MESSAGE_MAX_LEN), encoding='utf-8',
+                      errors='backslashreplace')
+
+        return format_stdout_stderr(stdout, stderr)
+    except Exception as e:
+        return format_stdout_stderr("", "Cannot read stdout/stderr: {0}".format(str(e)))
+
+
 def format_stdout_stderr(stdout, stderr, max_len=TELEMETRY_MESSAGE_MAX_LEN):
     """
     Format stdout and stderr's output to make it suitable in telemetry.
diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py
index d128a957..cacd3e32 100644
--- a/azurelinuxagent/ga/exthandlers.py
+++ b/azurelinuxagent/ga/exthandlers.py
@@ -51,7 +51,7 @@ from azurelinuxagent.common.protocol.restapi import ExtHandlerStatus, \
     get_properties, \
     set_properties
 from azurelinuxagent.common.utils.flexible_version import FlexibleVersion
-from azurelinuxagent.common.utils.processutil import format_stdout_stderr, TELEMETRY_MESSAGE_MAX_LEN
+from azurelinuxagent.common.utils.processutil import read_output
 from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION, GOAL_STATE_AGENT_VERSION, \
     DISTRO_NAME, DISTRO_VERSION, PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO
 
@@ -1166,22 +1166,10 @@ class ExtHandlerInstance(object):
             time.sleep(1)
             retry -= 1
 
-        def read_output():
-            try:
-                stdout_file.seek(0)
-                stderr_file.seek(0)
-
-                stdout = ustr(stdout_file.read(TELEMETRY_MESSAGE_MAX_LEN), encoding='utf-8', errors='backslashreplace')
-                stderr = ustr(stderr_file.read(TELEMETRY_MESSAGE_MAX_LEN), encoding='utf-8', errors='backslashreplace')
-
-                return format_stdout_stderr(stdout, stderr)
-            except Exception as e:
-                return format_stdout_stderr("", "Cannot read stdout/stderr: {0}".format(str(e)))
-
         # timeout expired
         if retry == 0:
             os.killpg(os.getpgid(process.pid), signal.SIGKILL)
-            raise ExtensionError("Timeout({0}): {1}\n{2}".format(timeout, cmd, read_output()),
+            raise ExtensionError("Timeout({0}): {1}\n{2}".format(timeout, cmd, read_output(stdout_file, stderr_file)),
                                  code=ExtensionErrorCodes.PluginHandlerScriptTimedout)
 
         # process completed or forked; sleep 1 sec to give the child process (if any) a chance to start
@@ -1189,9 +1177,12 @@ class ExtHandlerInstance(object):
 
         return_code = process.wait()
         if return_code != 0:
-            raise ExtensionError("Non-zero exit code: {0}, {1}\n{2}".format(return_code, cmd, read_output()), code=code)
+            raise ExtensionError("Non-zero exit code: {0}, {1}\n{2}".format(return_code,
+                                                                            cmd,
+                                                                            read_output(stdout_file, stderr_file)),
+                                 code=code)
 
-        return read_output()
+        return read_output(stdout_file, stderr_file)
 
     def load_manifest(self):
         man_file = self.get_manifest_file()


Test patch:
diff --git a/tests/common/test_cgroupapi.py b/tests/common/test_cgroupapi.py
index d0cc4c6b..cdb175d0 100644
--- a/tests/common/test_cgroupapi.py
+++ b/tests/common/test_cgroupapi.py
@@ -190,7 +190,7 @@ class FileSystemCgroupsApiTestCase(AgentTestCase):
     def test_remove_extension_cgroups_should_log_a_warning_when_the_cgroup_contains_active_tasks(self):
         api = FileSystemCgroupsApi()
         api.create_extension_cgroups_root()
-        extension_cgroups = api.create_extension_cgroups("Microsoft.Compute.TestExtension-1.2.3")
+        api.create_extension_cgroups("Microsoft.Compute.TestExtension-1.2.3")
 
         with patch("azurelinuxagent.common.cgroupapi.logger.warn") as mock_logger_warn:
             with patch("azurelinuxagent.common.cgroupapi.os.rmdir", side_effect=OSError(16, "Device or resource busy")):
@@ -317,3 +317,90 @@ class SystemdCgroupsApiTestCase(AgentTestCase):
 
             self.assertTrue(cpu_found, 'start_extension_command did not return a cpu cgroup')
             self.assertTrue(memory_found, 'start_extension_command did not return a memory cgroup')
+
+    @skip_if_predicate_false(i_am_root, "Test does not run when normal user")
+    def test_start_extension_command_should_use_systemd_if_not_failing(self):
+        original_popen = subprocess.Popen
+
+        def mock_popen(*args, **kwargs):
+            return original_popen(*args, **kwargs)
+
+        with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout:
+            with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr:
+                with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen) as patch_mock_popen:
+                    process, extension_cgroups = SystemdCgroupsApi().start_extension_command(
+                        extension_name="Microsoft.Compute.TestExtension-1.2.3",
+                        command="date",
+                        shell=True,
+                        cwd=self.tmp_dir,
+                        env={},
+                        stdout=stdout,
+                        stderr=stderr)
+
+                    process.wait()
+
+                    ret = process.poll()
+
+                    # We should have invoked the extension command only once and succeeded
+                    self.assertEquals(0, ret)
+                    self.assertEquals(1, patch_mock_popen.call_count)
+
+                    # Assert that the extension's cgroups were created as well
+                    self.assertEqual(len(extension_cgroups), 2, 'start_extension_command did not return the expected number of cgroups')
+
+                    cpu_found = memory_found = False
+
+                    for cgroup in extension_cgroups:
+                        match = re.match(r'^/sys/fs/cgroup/(cpu|memory)/system.slice/Microsoft.Compute.TestExtension_1\.2\.3\_([a-f0-9-]+)\.scope$', cgroup.path)
+
+                        self.assertTrue(match is not None, "Unexpected path for cgroup: {0}".format(cgroup.path))
+
+                        if match.group(1) == 'cpu':
+                            cpu_found = True
+                        if match.group(1) == 'memory':
+                            memory_found = True
+
+                    self.assertTrue(cpu_found, 'start_extension_command did not return a cpu cgroup')
+                    self.assertTrue(memory_found, 'start_extension_command did not return a memory cgroup')
+
+    @skip_if_predicate_false(i_am_root, "Test does not run when normal user")
+    def test_start_extension_command_should_not_use_systemd_if_failing(self):
+        original_popen = subprocess.Popen
+
+        def mock_popen(*args, **kwargs):
+            # Inject a syntax error to the call
+            systemd_command = args[0].replace('systemd-run', 'systemd-run syntax_error')
+            new_args = (systemd_command,)
+            return original_popen(new_args, **kwargs)
+
+        with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stdout:
+            with tempfile.TemporaryFile(dir=self.tmp_dir, mode="w+b") as stderr:
+                with patch("azurelinuxagent.common.cgroupapi.logger.warn") as mock_logger_warn:
+                    with patch("azurelinuxagent.common.cgroupapi.subprocess.Popen", side_effect=mock_popen) as patch_mock_popen:
+
+                        # We expect this call to fail because of the syntax error
+                        process, extension_cgroups = SystemdCgroupsApi().start_extension_command(
+                            extension_name="Microsoft.Compute.TestExtension-1.2.3",
+                            command="date",
+                            shell=True,
+                            cwd=self.tmp_dir,
+                            env={},
+                            stdout=stdout,
+                            stderr=stderr)
+
+                        process.wait()
+
+                        args, kwargs = mock_logger_warn.call_args
+                        message = args[0]
+                        self.assertIn("Failed to find executable syntax_error: No such file or directory", message)
+                        self.assertIn("Failed to run systemd-run for unit Microsoft.Compute.TestExtension_1.2.3", message)
+
+                        # We expect the process to ultimately succeed since the fallback option worked
+                        ret = process.poll()
+                        self.assertEquals(0, ret)
+
+                        # We expect two calls to Popen, first for the systemd-run call, second for the fallback option
+                        self.assertEquals(2, patch_mock_popen.call_count)
+
+                        # No cgroups should have been created
+                        self.assertEquals(extension_cgroups, [])
diff --git a/tests/common/test_cgroupstelemetry.py b/tests/common/test_cgroupstelemetry.py
index 6a7de45a..9fa9eabe 100644
--- a/tests/common/test_cgroupstelemetry.py
+++ b/tests/common/test_cgroupstelemetry.py
@@ -313,7 +313,10 @@ class TestCGroupsTelemetry(AgentTestCase):
             self.assertListEqual(generate_metric_list(cpu_percent_values),
                                  collected_metrics[name]["cpu"]["cur_cpu"][0:5])
 
-    def test_cgroup_tracking(self):
+    # mocking get_proc_stat to make it run on Mac and other systems
+    # this test does not need to read the values of the /proc/stat file
+    @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat")
+    def test_cgroup_tracking(self, *args):
         num_extensions = 5
         num_controllers = 2
         for i in range(num_extensions):
@@ -330,7 +333,10 @@ class TestCGroupsTelemetry(AgentTestCase):
 
         self.assertEqual(num_extensions * num_controllers, len(CGroupsTelemetry._tracked))
 
-    def test_cgroup_pruning(self):
+    # mocking get_proc_stat to make it run on Mac and other systems
+    # this test does not need to read the values of the /proc/stat file
+    @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat")
+    def test_cgroup_pruning(self, *args):
         num_extensions = 5
         num_controllers = 2
         for i in range(num_extensions):
@@ -355,7 +361,10 @@ class TestCGroupsTelemetry(AgentTestCase):
 
         self.assertEqual(0, len(CGroupsTelemetry._tracked))
 
-    def test_cgroup_is_tracked(self):
+    # mocking get_proc_stat to make it run on Mac and other systems
+    # this test does not need to read the values of the /proc/stat file
+    @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat")
+    def test_cgroup_is_tracked(self, *args):
         num_extensions = 5
         for i in range(num_extensions):
             dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i))
@@ -372,7 +381,10 @@ class TestCGroupsTelemetry(AgentTestCase):
         self.assertFalse(CGroupsTelemetry.is_tracked("not_present_cpu_dummy_path"))
         self.assertFalse(CGroupsTelemetry.is_tracked("not_present_memory_dummy_path"))
 
-    def test_process_cgroup_metric_with_incorrect_cgroups_mounted(self):
+    # mocking get_proc_stat to make it run on Mac and other systems
+    # this test does not need to read the values of the /proc/stat file
+    @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_stat")
+    def test_process_cgroup_metric_with_incorrect_cgroups_mounted(self, *args):
         num_extensions = 5
         for i in range(num_extensions):
             dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i))
@@ -496,6 +508,35 @@ class TestCGroupsTelemetry(AgentTestCase):
                             self.assertEqual(len(max_memory_levels._data), 0)
                             self.assertEqual(len(current_cpu_usage._data), 0)
 
+    @patch("azurelinuxagent.common.cgroup.CpuCgroup.collect")
+    @patch("azurelinuxagent.common.cgroup.MemoryCgroup._get_memory_usage")
+    @patch("azurelinuxagent.common.cgroup.MemoryCgroup._get_memory_max_usage")
+    @patch("azurelinuxagent.common.cgroup.CpuCgroup._get_current_cpu_total")
+    @patch("azurelinuxagent.common.osutil.default.DefaultOSUtil.get_total_cpu_ticks_since_boot")
+    def test_extension_temetry_not_sent_for_empty_perf_metrics(self, *args):
+        num_extensions = 5
+        for i in range(num_extensions):
+            dummy_cpu_cgroup = CGroup.create("dummy_cpu_path_{0}".format(i), "cpu", "dummy_extension_{0}".format(i))
+            CGroupsTelemetry.track_cgroup(dummy_cpu_cgroup)
+
+            dummy_memory_cgroup = CGroup.create("dummy_memory_path_{0}".format(i), "memory",
+                                                "dummy_extension_{0}".format(i))
+            CGroupsTelemetry.track_cgroup(dummy_memory_cgroup)
+
+        with patch("azurelinuxagent.common.cgroupstelemetry.CGroupsTelemetry._process_cgroup_metric") as \
+                patch_process_cgroup_metric:
+            with patch("azurelinuxagent.common.cgroup.CGroup.is_active") as patch_is_active:
+
+                patch_is_active.return_value = False
+                patch_process_cgroup_metric.return_value = {}
+                poll_count = 1
+
+                for data_count in range(poll_count, 10):
+                    CGroupsTelemetry.poll_all_tracked()
+
+                collected_metrics = CGroupsTelemetry.report_all_tracked()
+                self.assertEqual(0, len(collected_metrics))
+
     @skip_if_predicate_false(i_am_root, "This test will only run as root")
     @skip_if_predicate_false(CGroupConfigurator.get_instance().enabled, "Does not run when Cgroups are not enabled")
     def test_telemetry_with_tracked_cgroup(self):
diff --git a/tests/ga/test_exthandlers.py b/tests/ga/test_exthandlers.py
index 9de2fe07..159c5107 100644
--- a/tests/ga/test_exthandlers.py
+++ b/tests/ga/test_exthandlers.py
@@ -540,7 +540,7 @@ sys.stderr.write("E" * 5 * 1024 * 1024)
         # Mocking the call to file.read() is difficult, so instead we mock the call to format_stdout_stderr, which takes the
         # return value of the calls to file.read(). The intention of the test is to verify we never read (and load in memory)
         # more than a few KB of data from the files used to capture stdout/stderr
-        with patch('azurelinuxagent.ga.exthandlers.format_stdout_stderr', side_effect=format_stdout_stderr) as mock_format:
+        with patch('azurelinuxagent.common.utils.processutil.format_stdout_stderr', side_effect=format_stdout_stderr) as mock_format:
             output = self.ext_handler_instance.launch_command(command)
 
         self.assertGreaterEqual(len(output), 1024)