Repository: Azure/WALinuxAgent
Pull request #1333 (2018-09-05 08:27:56)

Add NSBSD os support
<!-- DO NOT DELETE THIS TEMPLATE -->

## Description

Add support for NSBSD os to Azure Agent.

NSBSD is used on Stormshield Network Security products :

[Azure marketplace product page](https://azuremarketplace.microsoft.com/en-US/marketplace/apps/stormshield.stormshield-network-security-for-cloud)

[Corporate website](https://www.stormshield.com/products-services/products/network-security/)

The NSBSD os is based on FreeBSD with some restrictions, custom user management, custom network configuration, etc... The mapping to the custom functions has been added in the derived OSUtil class.

<!--
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
- [X] The title of the PR is clear and informative.
- [X] 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).
- [X] 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
- [X] I have read the [contribution guidelines](https://github.com/Azure/WALinuxAgent/blob/master/.github/CONTRIBUTING.md).

msftclas: [![CLA assistant check](https://cla.opensource.microsoft.com/pull/badge/not_signed)](https://cla.opensource.microsoft.com/Azure/WALinuxAgent?pullRequest=1333) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla.opensource.microsoft.com/Azure/WALinuxAgent?pullRequest=1333) before we can accept your contribution.<br/><br/>:x: remip2 [sign now](https://cla.opensource.microsoft.com/Azure/WALinuxAgent?pullRequest=1333)<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla.opensource.microsoft.com/check/Azure/WALinuxAgent?pullRequest=1333) it.</sub>

Patch:
diff --git a/README.md b/README.md
index 1c843884..8fe747ed 100644
--- a/README.md
+++ b/README.md
@@ -206,6 +206,7 @@ OS.SshDir=/etc/ssh
 HttpProxy.Host=None
 HttpProxy.Port=None
 CGroups.EnforceLimits=y
+CGroups.Excluded=customscript,runcommand
 ```
 
 The various configuration options are described in detail below. Configuration
@@ -461,6 +462,13 @@ _Default: y_
 If set, the agent will attempt to set cgroups limits for cpu and memory for the agent process itself
 as well as extension processes. See the wiki for further details on this.
 
+#### __CGroups.Excluded__
+
+_Type: String_  
+_Default: customscript,runcommand_
+
+The list of extensions which will be excluded from cgroups limits. This should be comma separated. 
+
 ### Telemetry
 
 WALinuxAgent collects usage data and sends it to Microsoft to help improve our products and services. The data collected is used to track service health and
@@ -489,7 +497,7 @@ Run once:
 2. Create the pbuilder environment
 
    ```bash
-   sudo pbuilder create --debootstrapopts --variant=buildd`
+   sudo pbuilder create --debootstrapopts --variant=buildd
    ```
 
 3. Obtain `waagent.dsc` from a downstream package repo
@@ -505,7 +513,7 @@ To compile the package, from the top-most directory:
 2. Build the package
 
    ```bash
-   sudo pbuilder build {waagent.dsc}
+   sudo pbuilder build waagent.dsc
    ```
 
 3. Fetch the built package, usually from `/var/cache/pbuilder/result`
diff --git a/azurelinuxagent/common/cgroups.py b/azurelinuxagent/common/cgroups.py
index 5914cc7b..91e956ff 100644
--- a/azurelinuxagent/common/cgroups.py
+++ b/azurelinuxagent/common/cgroups.py
@@ -17,7 +17,6 @@
 import errno
 import os
 import re
-import traceback
 
 import time
 
@@ -32,7 +31,6 @@ from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION
 WRAPPER_CGROUP_NAME = "Agent+Extensions"
 METRIC_HIERARCHIES = ['cpu', 'memory']
 MEMORY_DEFAULT = -1
-UNLIMITED_INSTANCES = ['customscript', 'runcommand']
 
 # percentage of a single core
 DEFAULT_CPU_LIMIT_AGENT = 10
@@ -452,9 +450,8 @@ class CGroups(object):
             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)
-                        )
+                        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:
@@ -490,7 +487,7 @@ class CGroups(object):
         if self.name is None:
             return
 
-        for ext in UNLIMITED_INSTANCES:
+        for ext in conf.get_cgroups_excluded():
             if ext in self.name.lower():
                 logger.info('No cgroups limits for {0}'.format(self.name))
                 return
@@ -742,6 +739,7 @@ class CGroups(object):
             total_units = float(self.get_parameter('cpu', 'cpu.cfs_period_us'))
             limit_units = int(self._convert_cpu_limit_to_fraction(limit) * total_units)
             cpu_shares_file = self._get_cgroup_file('cpu', 'cpu.cfs_quota_us')
+            logger.verbose("writing {0} to {1}".format(limit_units, cpu_shares_file))
             fileutil.write_file(cpu_shares_file, '{0}\n'.format(limit_units))
         else:
             raise CGroupsException("CPU hierarchy not available in this cgroup")
@@ -775,7 +773,7 @@ class CGroups(object):
         if 'memory' in self.cgroups:
             value = self._format_memory_value(unit, limit)
             memory_limit_file = self._get_cgroup_file('memory', 'memory.limit_in_bytes')
-            with open(memory_limit_file, 'w+') as f:
-                f.write("{0}\n".format(value))
+            logger.verbose("writing {0} to {1}".format(value, memory_limit_file))
+            fileutil.write_file(memory_limit_file, '{0}\n'.format(value))
         else:
             raise CGroupsException("Memory hierarchy not available in this cgroup")
diff --git a/azurelinuxagent/common/conf.py b/azurelinuxagent/common/conf.py
index 1ba0610e..afec84c6 100644
--- a/azurelinuxagent/common/conf.py
+++ b/azurelinuxagent/common/conf.py
@@ -131,7 +131,8 @@ __STRING_OPTIONS__ = {
     "ResourceDisk.MountPoint": "/mnt/resource",
     "ResourceDisk.MountOptions": None,
     "ResourceDisk.Filesystem": "ext3",
-    "AutoUpdate.GAFamily": "Prod"
+    "AutoUpdate.GAFamily": "Prod",
+    "CGroups.Excluded": "customscript,runcommand",
 }
 
 
@@ -367,3 +368,8 @@ def get_disable_agent_file_path(conf=__conf__):
 
 def get_cgroups_enforce_limits(conf=__conf__):
     return conf.get_switch("CGroups.EnforceLimits", True)
+
+
+def get_cgroups_excluded(conf=__conf__):
+    excluded_value = conf.get("CGroups.Excluded", "customscript, runcommand")
+    return [s for s in [i.strip().lower() for i in excluded_value.split(',')] if len(s) > 0] if excluded_value else []
diff --git a/azurelinuxagent/common/osutil/factory.py b/azurelinuxagent/common/osutil/factory.py
index 0ef37f93..b63e8b11 100644
--- a/azurelinuxagent/common/osutil/factory.py
+++ b/azurelinuxagent/common/osutil/factory.py
@@ -33,6 +33,7 @@ from .alpine import AlpineOSUtil
 from .bigip import BigIpOSUtil
 from .gaia import GaiaOSUtil
 from .iosxe import IosxeOSUtil
+from .nsbsd import NSBSDOSUtil
 
 from distutils.version import LooseVersion as Version
 
@@ -106,9 +107,12 @@ def get_osutil(distro_name=DISTRO_NAME,
     elif distro_name == "gaia":
         return GaiaOSUtil()
 
-    if distro_name == "iosxe":
+    elif distro_name == "iosxe":
         return IosxeOSUtil()
 
+    elif distro_name == "nsbsd":
+        return NSBSDOSUtil()
+
     else:
         logger.warn("Unable to load distro implementation for {0}. Using "
                     "default distro implementation instead.",
diff --git a/azurelinuxagent/common/osutil/nsbsd.py b/azurelinuxagent/common/osutil/nsbsd.py
new file mode 100644
index 00000000..db822157
--- /dev/null
+++ b/azurelinuxagent/common/osutil/nsbsd.py
@@ -0,0 +1,159 @@
+#
+# Copyright 2018 Stormshield
+#
+# 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.
+#
+
+import azurelinuxagent.common.utils.fileutil as fileutil
+import azurelinuxagent.common.utils.shellutil as shellutil
+import azurelinuxagent.common.utils.textutil as textutil
+import azurelinuxagent.common.logger as logger
+from azurelinuxagent.common.exception import OSUtilError
+from azurelinuxagent.common.osutil.freebsd import FreeBSDOSUtil
+from azurelinuxagent.common.future import ustr
+import azurelinuxagent.common.conf as conf
+import os
+import time
+
+class NSBSDOSUtil(FreeBSDOSUtil):
+
+    resolver = None
+
+    def __init__(self):
+        super(NSBSDOSUtil, self).__init__()
+
+        if self.resolver is None:
+            # NSBSD doesn't have a system resolver, configure a python one
+
+            try:
+                import dns.resolver
+            except ImportError:
+                raise OSUtilError("Python DNS resolver not available. Cannot proceed!")
+
+            self.resolver = dns.resolver.Resolver()
+            servers = []
+            cmd = "getconf /usr/Firewall/ConfigFiles/dns Servers | tail -n +2"
+            ret, output = shellutil.run_get_output(cmd)
+            for server in output.split("\n"):
+                if server == '':
+                    break
+                server = server[:-1] # remove last '='
+                cmd = "grep '{}' /etc/hosts".format(server) + " | awk '{print $1}'"
+                ret, ip = shellutil.run_get_output(cmd)
+                servers.append(ip)
+            self.resolver.nameservers = servers
+            dns.resolver.override_system_resolver(self.resolver)
+
+    def set_hostname(self, hostname):
+        shellutil.run("/usr/Firewall/sbin/setconf /usr/Firewall/System/global SystemName {0}".format(hostname))
+        shellutil.run("/usr/Firewall/sbin/enlog")
+        shellutil.run("/usr/Firewall/sbin/enproxy -u")
+        shellutil.run("/usr/Firewall/sbin/ensl -u")
+        shellutil.run("/usr/Firewall/sbin/ennetwork -f")
+
+    def restart_ssh_service(self):
+        return shellutil.run('/usr/Firewall/sbin/enservice', chk_err=False)
+
+    def conf_sshd(self, disable_password):
+        option = "0" if disable_password else "1"
+
+        shellutil.run('setconf /usr/Firewall/ConfigFiles/system SSH State 1',
+                      chk_err=False)
+        shellutil.run('setconf /usr/Firewall/ConfigFiles/system SSH Password {}'.format(option),
+                      chk_err=False)
+        shellutil.run('enservice', chk_err=False)
+
+        logger.info("{0} SSH password-based authentication methods."
+                    .format("Disabled" if disable_password else "Enabled"))
+
+    def useradd(self, username, expiration=None):
+        """
+        Create user account with 'username'
+        """
+        logger.warn("User creation disabled")
+        return
+
+    def del_account(self, username):
+        logger.warn("User deletion disabled")
+
+    def conf_sudoer(self, username, nopasswd=False, remove=False):
+        logger.warn("Sudo is not enabled")
+
+    def chpasswd(self, username, password, crypt_id=6, salt_len=10):
+        cmd = "/usr/Firewall/sbin/fwpasswd -p {0}".format(password)
+        ret, output = shellutil.run_get_output(cmd, log_cmd=False)
+        if ret != 0:
+            raise OSUtilError(("Failed to set password for admin: {0}"
+                               "").format(output))
+
+        # password set, activate webadmin and ssh access
+        shellutil.run('setconf /usr/Firewall/ConfigFiles/webadmin ACL any && ensl',
+                      chk_err=False)
+
+    def deploy_ssh_pubkey(self, username, pubkey):
+        """
+        Deploy authorized_key
+        """
+        path, thumbprint, value = pubkey
+
+        #overide parameters
+        super(NSBSDOSUtil, self).deploy_ssh_pubkey('admin',
+            ["/usr/Firewall/.ssh/authorized_keys", thumbprint, value])
+
+    def del_root_password(self):
+        logger.warn("Root password deletion disabled")
+
+    def start_dhcp_service(self):
+        shellutil.run("/usr/Firewall/sbin/nstart dhclient", chk_err=False)
+
+    def stop_dhcp_service(self):
+        shellutil.run("/usr/Firewall/sbin/nstop dhclient", chk_err=False)
+
+    def get_dhcp_pid(self):
+        ret = None
+        pidfile = "/var/run/dhclient.pid"
+
+        if os.path.isfile(pidfile):
+            ret = fileutil.read_file(pidfile, encoding='ascii')
+        return ret
+
+    def eject_dvd(self, chk_err=True):
+        pass
+
+    def restart_if(self, ifname):
+        # Restart dhclient only to publish hostname
+        shellutil.run("ennetwork", chk_err=False)
+
+    def set_dhcp_hostname(self, hostname):
+        #already done by the dhcp client
+        pass
+
+    def get_firewall_dropped_packets(self, dst_ip=None):
+        # disable iptables methods
+        return 0
+
+    def get_firewall_will_wait(self):
+        # disable iptables methods
+        return ""
+
+    def _delete_rule(self, rule):
+        # disable iptables methods
+        return
+
+    def remove_firewall(self, dst_ip=None, uid=None):
+        # disable iptables methods
+        return True
+
+    def enable_firewall(self, dst_ip=None, uid=None):
+        # disable iptables methods
+        return True
diff --git a/azurelinuxagent/common/version.py b/azurelinuxagent/common/version.py
index f394df1d..f4868e23 100644
--- a/azurelinuxagent/common/version.py
+++ b/azurelinuxagent/common/version.py
@@ -82,6 +82,9 @@ def get_distro():
         osinfo = ['openbsd', release, '', 'openbsd']
     elif 'Linux' in platform.system():
         osinfo = get_linux_distribution(0, 'alpine')
+    elif 'NS-BSD' in platform.system():
+        release = re.sub('\-.*\Z', '', ustr(platform.release()))
+        osinfo = ['nsbsd', release, '', 'nsbsd']
     else:
         try:
             # dist() removed in Python 3.7
diff --git a/config/alpine/waagent.conf b/config/alpine/waagent.conf
index 77b78859..de581c96 100644
--- a/config/alpine/waagent.conf
+++ b/config/alpine/waagent.conf
@@ -96,3 +96,6 @@ OS.EnableFirewall=y
 
 # Enforce control groups limits on the agent and extensions
 CGroups.EnforceLimits=y
+
+# CGroups which are excluded from limits, comma separated
+CGroups.Excluded=customscript,runcommand
diff --git a/config/bigip/waagent.conf b/config/bigip/waagent.conf
index 2c87918b..22d8e39e 100644
--- a/config/bigip/waagent.conf
+++ b/config/bigip/waagent.conf
@@ -100,3 +100,6 @@ OS.EnableFirewall=y
 
 # Enforce control groups limits on the agent and extensions
 CGroups.EnforceLimits=y
+
+# CGroups which are excluded from limits, comma separated
+CGroups.Excluded=customscript,runcommand
diff --git a/config/coreos/waagent.conf b/config/coreos/waagent.conf
index 45a1a030..7a351f45 100644
--- a/config/coreos/waagent.conf
+++ b/config/coreos/waagent.conf
@@ -122,3 +122,6 @@ OS.EnableFirewall=y
 
 # Enforce control groups limits on the agent and extensions
 CGroups.EnforceLimits=y
+
+# CGroups which are excluded from limits, comma separated
+CGroups.Excluded=customscript,runcommand
diff --git a/config/debian/waagent.conf b/config/debian/waagent.conf
index 82c6834c..d2b899ed 100644
--- a/config/debian/waagent.conf
+++ b/config/debian/waagent.conf
@@ -124,3 +124,6 @@ OS.EnableFirewall=n
 
 # Enforce control groups limits on the agent and extensions
 CGroups.EnforceLimits=y
+
+# CGroups which are excluded from limits, comma separated
+CGroups.Excluded=customscript,runcommand
diff --git a/config/freebsd/waagent.conf b/config/freebsd/waagent.conf
index a0940359..c81186e1 100644
--- a/config/freebsd/waagent.conf
+++ b/config/freebsd/waagent.conf
@@ -120,3 +120,6 @@ OS.EnableFirewall=y
 
 # Enforce control groups limits on the agent and extensions
 CGroups.EnforceLimits=n
+
+# CGroups which are excluded from limits, comma separated
+CGroups.Excluded=customscript,runcommand
diff --git a/config/nsbsd/waagent.conf b/config/nsbsd/waagent.conf
new file mode 100644
index 00000000..7731da51
--- /dev/null
+++ b/config/nsbsd/waagent.conf
@@ -0,0 +1,117 @@
+#
+# Microsoft Azure Linux Agent Configuration
+#
+
+# Enable instance creation
+Provisioning.Enabled=y
+
+# Rely on cloud-init to provision
+Provisioning.UseCloudInit=n
+
+# Password authentication for root account will be unavailable.
+Provisioning.DeleteRootPassword=n
+
+# Generate fresh host key pair.
+Provisioning.RegenerateSshHostKeyPair=n
+
+# Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto".
+# The "auto" option is supported on OpenSSH 5.9 (2011) and later.
+Provisioning.SshHostKeyPairType=rsa
+
+# Monitor host name changes and publish changes via DHCP requests.
+Provisioning.MonitorHostName=y
+
+# Decode CustomData from Base64.
+Provisioning.DecodeCustomData=n
+
+# Execute CustomData after provisioning.
+Provisioning.ExecuteCustomData=n
+
+# Algorithm used by crypt when generating password hash.
+#Provisioning.PasswordCryptId=6
+
+# Length of random salt used when generating password hash.
+#Provisioning.PasswordCryptSaltLength=10
+
+# Format if unformatted. If 'n', resource disk will not be mounted.
+ResourceDisk.Format=n
+
+# File system on the resource disk
+# Typically ext3 or ext4. FreeBSD images should use 'ufs' here.
+ResourceDisk.Filesystem=ufs
+
+# Mount point for the resource disk
+ResourceDisk.MountPoint=/mnt/resource
+
+# Create and use swapfile on resource disk.
+ResourceDisk.EnableSwap=n
+
+# Size of the swapfile.
+ResourceDisk.SwapSizeMB=0
+
+# Comma-seperated list of mount options. See man(8) for valid options.
+ResourceDisk.MountOptions=None
+
+# Enable verbose logging (y|n)  TODO set n
+Logs.Verbose=n
+
+# Is FIPS enabled
+OS.EnableFIPS=n
+
+# Root device timeout in seconds.
+OS.RootDeviceScsiTimeout=300
+
+# If "None", the system default version is used.
+OS.OpensslPath=None
+
+# Set the path to SSH keys and configuration files
+OS.SshDir=/etc/ssh
+
+OS.PasswordPath=/etc/master.passwd
+
+OS.SudoersDir=/usr/local/etc/sudoers.d
+
+# If set, agent will use proxy server to access internet
+#HttpProxy.Host=None
+#HttpProxy.Port=None
+
+# Detect Scvmm environment, default is n
+# DetectScvmmEnv=n
+
+#
+Lib.Dir=/usr/Firewall/var/waagent
+
+#
+# DVD.MountPoint=/mnt/cdrom/secure
+
+#
+# Pid.File=/var/run/waagent.pid
+
+#
+Extension.LogDir=/log/azure
+
+#
+# Home.Dir=/home
+
+# Enable RDMA management and set up, should only be used in HPC images
+# OS.EnableRDMA=y
+
+# Enable or disable goal state processing auto-update, default is enabled
+AutoUpdate.Enabled=n
+
+# Determine the update family, this should not be changed
+# AutoUpdate.GAFamily=Prod
+
+# Determine if the overprovisioning feature is enabled. If yes, hold extension
+# handling until inVMArtifactsProfile.OnHold is false.
+# Default is disabled
+# EnableOverProvisioning=n
+
+# Allow fallback to HTTP if HTTPS is unavailable
+# Note: Allowing HTTP (vs. HTTPS) may cause security risks
+# OS.AllowHTTP=n
+
+# Add firewall rules to protect access to Azure host node services
+# Note:
+# - The default is false to protect the state of existing VMs
+OS.EnableFirewall=n
diff --git a/config/ubuntu/waagent.conf b/config/ubuntu/waagent.conf
index 626a0efc..d2759a45 100644
--- a/config/ubuntu/waagent.conf
+++ b/config/ubuntu/waagent.conf
@@ -110,3 +110,6 @@ OS.EnableFirewall=y
 
 # Enforce control groups limits on the agent and extensions
 CGroups.EnforceLimits=y
+
+# CGroups which are excluded from limits, comma separated
+CGroups.Excluded=customscript,runcommand
diff --git a/config/waagent.conf b/config/waagent.conf
index 706318c9..0c8e804d 100644
--- a/config/waagent.conf
+++ b/config/waagent.conf
@@ -122,3 +122,6 @@ OS.EnableFirewall=y
 
 # Enforce control groups limits on the agent and extensions
 CGroups.EnforceLimits=y
+
+# CGroups which are excluded from limits, comma separated
+CGroups.Excluded=customscript,runcommand


Test patch:
diff --git a/tests/common/test_conf.py b/tests/common/test_conf.py
index 6c3cb3d5..f3c1ab0a 100644
--- a/tests/common/test_conf.py
+++ b/tests/common/test_conf.py
@@ -68,6 +68,7 @@ class TestConf(AgentTestCase):
         "OS.AllowHTTP": False,
         "OS.EnableFirewall": False,
         "CGroups.EnforceLimits": True,
+        "CGroups.Excluded": "customscript,runcommand",
     }
 
     def setUp(self):
@@ -135,3 +136,36 @@ class TestConf(AgentTestCase):
     def test_get_extensions_enabled(self):
         self.assertTrue(get_extensions_enabled(self.conf))
 
+    @patch('azurelinuxagent.common.conf.ConfigurationProvider.get')
+    def assert_get_cgroups_excluded(self, patch_get, config, expected_value):
+        patch_get.return_value = config
+        self.assertEqual(expected_value, conf.get_cgroups_excluded(self.conf))
+
+    def test_get_cgroups_excluded(self):
+        self.assert_get_cgroups_excluded(config=None,
+                                         expected_value=[])
+
+        self.assert_get_cgroups_excluded(config='',
+                                         expected_value=[])
+
+        self.assert_get_cgroups_excluded(config='  ',
+                                         expected_value=[])
+
+        self.assert_get_cgroups_excluded(config='  ,  ,,  ,',
+                                         expected_value=[])
+
+        standard_values = ['customscript', 'runcommand']
+        self.assert_get_cgroups_excluded(config='CustomScript, RunCommand',
+                                         expected_value=standard_values)
+
+        self.assert_get_cgroups_excluded(config='customScript, runCommand  , , ,,',
+                                         expected_value=standard_values)
+
+        self.assert_get_cgroups_excluded(config='  customscript,runcommand  ',
+                                         expected_value=standard_values)
+
+        self.assert_get_cgroups_excluded(config='customscript,, runcommand',
+                                         expected_value=standard_values)
+
+        self.assert_get_cgroups_excluded(config=',,customscript ,runcommand',
+                                         expected_value=standard_values)
diff --git a/tests/data/test_waagent.conf b/tests/data/test_waagent.conf
index 2e4eb3e8..59a1f184 100644
--- a/tests/data/test_waagent.conf
+++ b/tests/data/test_waagent.conf
@@ -132,3 +132,6 @@ OS.EnableFirewall=n
 
 # Enforce control groups limits on the agent and extensions
 CGroups.EnforceLimits=y
+
+# CGroups which are excluded from limits, comma separated
+CGroups.Excluded=customscript,runcommand
diff --git a/tests/test_agent.py b/tests/test_agent.py
index ea87c58b..bff73750 100644
--- a/tests/test_agent.py
+++ b/tests/test_agent.py
@@ -27,6 +27,7 @@ EXPECTED_CONFIGURATION = \
 AutoUpdate.GAFamily = Prod
 Autoupdate.Frequency = 3600
 CGroups.EnforceLimits = True
+CGroups.Excluded = customscript,runcommand
 DVD.MountPoint = /mnt/cdrom/secure
 DetectScvmmEnv = False
 EnableOverProvisioning = True