Repository: Azure/WALinuxAgent
Pull request #1858 (2020-04-18 00:04:28)

Use run_command instead of run_get_output for OSUtil functions
<!-- DO NOT DELETE THIS TEMPLATE -->

## Description
Changed the internals of certain OSUtil functions to use shellutil.run_command instead of run_get_output or run() because the former is securer as it doesn't create a new shell during Popen. i.e. shell=False in Popen

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).

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

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

```diff
@@             Coverage Diff             @@
##           develop    #1858      +/-   ##
===========================================
- Coverage    69.47%   69.46%   -0.01%     
===========================================
  Files           82       82              
  Lines        11469    11469              
  Branches      1619     1609      -10     
===========================================
- Hits          7968     7967       -1     
  Misses        3164     3164              
- Partials       337      338       +1     
```


| [Impacted Files](https://codecov.io/gh/Azure/WALinuxAgent/pull/1858?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [azurelinuxagent/common/osutil/freebsd.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1858/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9vc3V0aWwvZnJlZWJzZC5weQ==) | `43.21% <0.00%> (+0.53%)` | :arrow_up: |
| [azurelinuxagent/common/osutil/nsbsd.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1858/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9vc3V0aWwvbnNic2QucHk=) | `41.66% <0.00%> (+0.96%)` | :arrow_up: |
| [azurelinuxagent/common/osutil/openbsd.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1858/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9vc3V0aWwvb3BlbmJzZC5weQ==) | `22.51% <0.00%> (+0.38%)` | :arrow_up: |
| [azurelinuxagent/common/osutil/openwrt.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1858/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9vc3V0aWwvb3BlbndydC5weQ==) | `38.27% <0.00%> (+0.92%)` | :arrow_up: |
| [azurelinuxagent/common/osutil/gaia.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1858/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9vc3V0aWwvZ2FpYS5weQ==) | `27.33% <8.33%> (-1.90%)` | :arrow_down: |
| [azurelinuxagent/common/osutil/default.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1858/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9vc3V0aWwvZGVmYXVsdC5weQ==) | `59.97% <41.17%> (+0.18%)` | :arrow_up: |
| [azurelinuxagent/common/osutil/bigip.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1858/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L2NvbW1vbi9vc3V0aWwvYmlnaXAucHk=) | `85.59% <71.42%> (-0.70%)` | :arrow_down: |
| [azurelinuxagent/pa/provision/default.py](https://codecov.io/gh/Azure/WALinuxAgent/pull/1858/diff?src=pr&el=tree#diff-YXp1cmVsaW51eGFnZW50L3BhL3Byb3Zpc2lvbi9kZWZhdWx0LnB5) | `73.97% <0.00%> (-1.03%)` | :arrow_down: |

------

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

anhvoms: LGTM

Patch:
diff --git a/azurelinuxagent/common/osutil/bigip.py b/azurelinuxagent/common/osutil/bigip.py
index 945c89bb..61d3c695 100644
--- a/azurelinuxagent/common/osutil/bigip.py
+++ b/azurelinuxagent/common/osutil/bigip.py
@@ -149,14 +149,11 @@ class BigIpOSUtil(DefaultOSUtil):
             logger.info("User {0} already exists, skip useradd", username)
             return None
 
-        cmd = "/usr/bin/tmsh create auth user %s partition-access add { all-partitions { role admin } } shell bash" % (username)
-        retcode, out = shellutil.run_get_output(cmd, log_cmd=True, chk_err=True)
-        if retcode != 0:
-            raise OSUtilError(
-                "Failed to create user account:{0}, retcode:{1}, output:{2}".format(username, retcode, out)
-            )
+        cmd = ['/usr/bin/tmsh', 'create', 'auth', 'user', username, 'partition-access', 'add', '{', 'all-partitions',
+               '{', 'role', 'admin', '}', '}', 'shell', 'bash']
+        self._run_command_raising_OSUtilError(cmd, err_msg="Failed to create user account:{0}".format(username))
         self._save_sys_config()
-        return retcode
+        return 0
 
     def chpasswd(self, username, password, crypt_id=6, salt_len=10):
         """Change a user's password with tmsh
@@ -177,12 +174,9 @@ class BigIpOSUtil(DefaultOSUtil):
         """
 
         # Start by setting the password of the user provided account
-        cmd = "/usr/bin/tmsh modify auth user {0} password '{1}'".format(username, password)
-        ret, output = shellutil.run_get_output(cmd, log_cmd=False, chk_err=True)
-        if ret != 0:
-            raise OSUtilError(
-                "Failed to set password for {0}: {1}".format(username, output)
-            )
+        self._run_command_raising_OSUtilError(
+            ['/usr/bin/tmsh', 'modify', 'auth', 'user', username, 'password', password],
+            err_msg="Failed to set password for {0}".format(username))
 
         # Next, set the password of the built-in 'admin' account to be have
         # the same password as the user provided account
@@ -190,14 +184,11 @@ class BigIpOSUtil(DefaultOSUtil):
         if userentry is None:
             raise OSUtilError("The 'admin' user account was not found!")
 
-        cmd = "/usr/bin/tmsh modify auth user 'admin' password '{0}'".format(password)
-        ret, output = shellutil.run_get_output(cmd, log_cmd=False, chk_err=True)
-        if ret != 0:
-            raise OSUtilError(
-                "Failed to set password for 'admin': {0}".format(output)
-            )
+        self._run_command_raising_OSUtilError(
+            ['/usr/bin/tmsh', 'modify', 'auth', 'user', 'admin', 'password', password],
+            err_msg="Failed to set password for admin")
         self._save_sys_config()
-        return ret
+        return 0
 
     def del_account(self, username):
         """Deletes a user account.
@@ -211,8 +202,8 @@ class BigIpOSUtil(DefaultOSUtil):
         :param username:
         :return:
         """
-        shellutil.run("> /var/run/utmp")
-        shellutil.run("/usr/bin/tmsh delete auth user " + username)
+        self._run_command_without_raising(["touch", "/var/run/utmp"])
+        self._run_command_without_raising(['/usr/bin/tmsh', 'delete', 'auth', 'user', username])
 
     def get_dvd_device(self, dev_dir='/dev'):
         """Find BIG-IP's CD/DVD device
diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py
index f4509d1d..7bd1bbb1 100644
--- a/azurelinuxagent/common/osutil/default.py
+++ b/azurelinuxagent/common/osutil/default.py
@@ -423,28 +423,23 @@ class DefaultOSUtil(object):
             return
 
         if expiration is not None:
-            cmd = "useradd -m {0} -e {1}".format(username, expiration)
+            cmd = ["useradd", "-m", username, "-e", expiration]
         else:
-            cmd = "useradd -m {0}".format(username)
+            cmd = ["useradd", "-m", username]
         
         if comment is not None:
-            cmd += " -c {0}".format(comment)
-        retcode, out = shellutil.run_get_output(cmd)
-        if retcode != 0:
-            raise OSUtilError(("Failed to create user account:{0}, "
-                               "retcode:{1}, "
-                               "output:{2}").format(username, retcode, out))
+            cmd.extend(["-c", comment])
+
+        self._run_command_raising_OSUtilError(cmd, err_msg="Failed to create user account:{0}".format(username))
 
     def chpasswd(self, username, password, crypt_id=6, salt_len=10):
         if self.is_sys_user(username):
             raise OSUtilError(("User {0} is a system user, "
                                "will not set password.").format(username))
         passwd_hash = textutil.gen_password_hash(password, crypt_id, salt_len)
-        cmd = "usermod -p '{0}' {1}".format(passwd_hash, username)
-        ret, output = shellutil.run_get_output(cmd, log_cmd=False)
-        if ret != 0:
-            raise OSUtilError(("Failed to set password for {0}: {1}"
-                               "").format(username, output))
+
+        self._run_command_raising_OSUtilError(["usermod", "-p", passwd_hash, username],
+                                              err_msg="Failed to set password for {0}".format(username))
     
     def get_users(self):
         return getpwall()
@@ -1124,7 +1119,7 @@ class DefaultOSUtil(object):
 
     def set_hostname(self, hostname):
         fileutil.write_file('/etc/hostname', hostname)
-        shellutil.run("hostname {0}".format(hostname), chk_err=False)
+        self._run_command_without_raising(["hostname", hostname], log_error=False)
 
     def set_dhcp_hostname(self, hostname):
         autosend = r'^[^#]*?send\s*host-name.*?(<hostname>|gethostname[(,)])'
@@ -1292,8 +1287,9 @@ class DefaultOSUtil(object):
     def del_account(self, username):
         if self.is_sys_user(username):
             logger.error("{0} is a system user. Will not delete it.", username)
-        shellutil.run("> /var/run/utmp")
-        shellutil.run("userdel -f -r " + username)
+
+        self._run_command_without_raising(["touch", "/var/run/utmp"])
+        self._run_command_without_raising(['userdel', '-f', '-r', username])
         self.conf_sudoer(username, remove=True)
 
     def decode_customdata(self, data):
@@ -1428,3 +1424,25 @@ class DefaultOSUtil(object):
         # Original implementation of run() does a blanket catch, so mimicking the behaviour here
         except Exception:
             pass
+
+    @staticmethod
+    def _run_multiple_commands_without_raising(commands, log_error=True, continue_on_error=False):
+        for cmd in commands:
+            try:
+                shellutil.run_command(cmd, log_error=log_error)
+            # Original implementation of run() does a blanket catch, so mimicking the behaviour here
+            except Exception:
+                if continue_on_error:
+                    continue
+                break
+
+    @staticmethod
+    def _run_command_raising_OSUtilError(cmd, err_msg, cmd_input=None):
+        # This method runs shell command using the new secure shellutil.run_command and raises OSUtilErrors on failures.
+        try:
+            return shellutil.run_command(cmd, log_error=True, cmd_input=cmd_input)
+        except shellutil.CommandError as e:
+            raise OSUtilError(
+                "{0}, Retcode: {1}, Output: {2}, Error: {3}".format(err_msg, e.returncode, e.stdout, e.stderr))
+        except Exception as e:
+            raise OSUtilError("{0}, Retcode: {1}, Error: {2}".format(err_msg, -1, ustr(e)))
diff --git a/azurelinuxagent/common/osutil/freebsd.py b/azurelinuxagent/common/osutil/freebsd.py
index 2e298ca5..0686d3ba 100644
--- a/azurelinuxagent/common/osutil/freebsd.py
+++ b/azurelinuxagent/common/osutil/freebsd.py
@@ -40,7 +40,7 @@ class FreeBSDOSUtil(DefaultOSUtil):
         conf_file = fileutil.read_file(rc_file_path).split("\n")
         textutil.set_ini_config(conf_file, "hostname", hostname)
         fileutil.write_file(rc_file_path, "\n".join(conf_file))
-        shellutil.run("hostname {0}".format(hostname), chk_err=False)
+        self._run_command_without_raising(["hostname", hostname], log_error=False)
 
     def restart_ssh_service(self):
         return shellutil.run('service sshd restart', chk_err=False)
@@ -54,22 +54,19 @@ class FreeBSDOSUtil(DefaultOSUtil):
             logger.warn("User {0} already exists, skip useradd", username)
             return
         if expiration is not None:
-            cmd = "pw useradd {0} -e {1} -m".format(username, expiration)
+            cmd = ["pw", "useradd", username, "-e", expiration, "-m"]
         else:
-            cmd = "pw useradd {0} -m".format(username)
+            cmd = ["pw", "useradd", username, "-m"]
         if comment is not None:
-            cmd += " -c {0}".format(comment)
-        retcode, out = shellutil.run_get_output(cmd)
-        if retcode != 0:
-            raise OSUtilError(("Failed to create user account:{0}, "
-                               "retcode:{1}, "
-                               "output:{2}").format(username, retcode, out))
+            cmd.extend(["-c", comment])
+
+        self._run_command_raising_OSUtilError(cmd, err_msg="Failed to create user account:{0}".format(username))
 
     def del_account(self, username):
         if self.is_sys_user(username):
             logger.error("{0} is a system user. Will not delete it.", username)
-        shellutil.run('> /var/run/utx.active')
-        shellutil.run('rmuser -y ' + username)
+        self._run_command_without_raising(['touch', '/var/run/utx.active'])
+        self._run_command_without_raising(['rmuser', '-y', username])
         self.conf_sudoer(username, remove=True)
 
     def chpasswd(self, username, password, crypt_id=6, salt_len=10):
@@ -77,11 +74,8 @@ class FreeBSDOSUtil(DefaultOSUtil):
             raise OSUtilError(("User {0} is a system user, "
                                "will not set password.").format(username))
         passwd_hash = textutil.gen_password_hash(password, crypt_id, salt_len)
-        cmd = "echo '{0}'|pw usermod {1} -H 0 ".format(passwd_hash, username)
-        ret, output = shellutil.run_get_output(cmd, log_cmd=False)
-        if ret != 0:
-            raise OSUtilError(("Failed to set password for {0}: {1}"
-                               "").format(username, output))
+        self._run_command_raising_OSUtilError(['pw', 'usermod', username, '-H', '0'], cmd_input=passwd_hash,
+                                              err_msg="Failed to set password for {0}".format(username))
 
     def del_root_password(self):
         err = shellutil.run('pw usermod root -h -')
diff --git a/azurelinuxagent/common/osutil/gaia.py b/azurelinuxagent/common/osutil/gaia.py
index a783c59a..943032c7 100644
--- a/azurelinuxagent/common/osutil/gaia.py
+++ b/azurelinuxagent/common/osutil/gaia.py
@@ -37,12 +37,22 @@ class GaiaOSUtil(DefaultOSUtil):
     def __init__(self):
         super(GaiaOSUtil, self).__init__()
 
-    def _run_clish(self, cmd, log_cmd=True):
+    def _run_clish(self, cmd):
+        ret = 0
+        out = ""
         for i in xrange(10):
-            ret, out = shellutil.run_get_output(
-                "/bin/clish -s -c '" + cmd + "'", log_cmd=log_cmd)
-            if not ret:
+            try:
+                final_command = ["/bin/clish", "-s", "-c", "'{0}'".format(cmd)]
+                out = shellutil.run_command(final_command, log_error=True)
+                ret = 0
                 break
+            except shellutil.CommandError as e:
+                ret = e.returncode
+                out = e.stdout
+            except Exception as e:
+                ret = -1
+                out = ustr(e)
+
             if 'NMSHST0025' in out:  # Entry for [hostname] already present
                 ret = 0
                 break
@@ -56,7 +66,7 @@ class GaiaOSUtil(DefaultOSUtil):
         logger.info('chpasswd')
         passwd_hash = textutil.gen_password_hash(password, crypt_id, salt_len)
         ret, out = self._run_clish(
-            'set user admin password-hash ' + passwd_hash, log_cmd=False)
+            'set user admin password-hash ' + passwd_hash)
         if ret != 0:
             raise OSUtilError(("Failed to set password for {0}: {1}"
                                "").format('admin', out))
diff --git a/azurelinuxagent/common/osutil/iosxe.py b/azurelinuxagent/common/osutil/iosxe.py
index 0d45b0e2..c395851a 100644
--- a/azurelinuxagent/common/osutil/iosxe.py
+++ b/azurelinuxagent/common/osutil/iosxe.py
@@ -19,8 +19,10 @@
 
 import azurelinuxagent.common.logger as logger
 import azurelinuxagent.common.utils.shellutil as shellutil
+from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.osutil.default import DefaultOSUtil
 from azurelinuxagent.common.osutil.redhat import Redhat6xOSUtil
+from azurelinuxagent.common.utils import textutil
 
 '''
 The IOSXE distribution is a variant of the Centos distribution, 
@@ -41,9 +43,11 @@ class IosxeOSUtil(DefaultOSUtil):
         Due to a bug in systemd in Centos-7.0, if this call fails, fallback
         to hostname.
         """
-        hostnamectl_cmd = "hostnamectl set-hostname {0} --static".format(hostname)
-        if shellutil.run(hostnamectl_cmd, chk_err=False) != 0:
-            logger.warn("[{0}] failed, attempting fallback".format(hostnamectl_cmd))
+        hostnamectl_cmd = ["hostnamectl", "set-hostname", hostname, "--static"]
+        try:
+            shellutil.run_command(hostnamectl_cmd)
+        except Exception as e:
+            logger.warn("[{0}] failed with error: {1}, attempting fallback".format(' '.join(hostnamectl_cmd), ustr(e)))
             DefaultOSUtil.set_hostname(self, hostname)
 
     def publish_hostname(self, hostname):
diff --git a/azurelinuxagent/common/osutil/nsbsd.py b/azurelinuxagent/common/osutil/nsbsd.py
index c485a506..4130da43 100644
--- a/azurelinuxagent/common/osutil/nsbsd.py
+++ b/azurelinuxagent/common/osutil/nsbsd.py
@@ -51,11 +51,11 @@ class NSBSDOSUtil(FreeBSDOSUtil):
             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")
+        self._run_command_without_raising(['/usr/Firewall/sbin/setconf', '/usr/Firewall/System/global', 'SystemName', hostname])
+        self._run_command_without_raising(["/usr/Firewall/sbin/enlog"])
+        self._run_command_without_raising(["/usr/Firewall/sbin/enproxy", "-u"])
+        self._run_command_without_raising(["/usr/Firewall/sbin/ensl", "-u"])
+        self._run_command_without_raising(["/usr/Firewall/sbin/ennetwork", "-f"])
 
     def restart_ssh_service(self):
         return shellutil.run('/usr/Firewall/sbin/enservice', chk_err=False)
@@ -86,15 +86,13 @@ class NSBSDOSUtil(FreeBSDOSUtil):
         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))
+        self._run_command_raising_OSUtilError(["/usr/Firewall/sbin/fwpasswd", "-p", password],
+                                              err_msg="Failed to set password for admin")
 
         # password set, activate webadmin and ssh access
-        shellutil.run('setconf /usr/Firewall/ConfigFiles/webadmin ACL any && ensl',
-                      chk_err=False)
+        commands = [['setconf', '/usr/Firewall/ConfigFiles/webadmin', 'ACL', 'any'], ['ensl']]
+        self._run_multiple_commands_without_raising(commands, log_error=False, continue_on_error=False)
+
 
     def deploy_ssh_pubkey(self, username, pubkey):
         """
diff --git a/azurelinuxagent/common/osutil/openbsd.py b/azurelinuxagent/common/osutil/openbsd.py
index 4deab1c4..37fae5c8 100644
--- a/azurelinuxagent/common/osutil/openbsd.py
+++ b/azurelinuxagent/common/osutil/openbsd.py
@@ -51,7 +51,7 @@ class OpenBSDOSUtil(DefaultOSUtil):
 
     def set_hostname(self, hostname):
         fileutil.write_file("/etc/myname", "{}\n".format(hostname))
-        shellutil.run("hostname {0}".format(hostname), chk_err=False)
+        self._run_command_without_raising(["hostname", hostname], log_error=False)
 
     def restart_ssh_service(self):
         return shellutil.run('rcctl restart sshd', chk_err=False)
@@ -71,10 +71,9 @@ class OpenBSDOSUtil(DefaultOSUtil):
 
     def del_account(self, username):
         if self.is_sys_user(username):
-            logger.error("{0} is a system user. Will not delete it.",
-                         username)
-        shellutil.run("> /var/run/utmp")
-        shellutil.run("userdel -r " + username)
+            logger.error("{0} is a system user. Will not delete it.", username)
+        self._run_command_without_raising(["touch", "/var/run/utmp"])
+        self._run_command_without_raising(["userdel", "-r", username])
         self.conf_sudoer(username, remove=True)
 
     def conf_sudoer(self, username, nopasswd=False, remove=False):
@@ -107,17 +106,11 @@ class OpenBSDOSUtil(DefaultOSUtil):
         if self.is_sys_user(username):
             raise OSUtilError(("User {0} is a system user. "
                                "Will not set passwd.").format(username))
-        cmd = "echo -n {0}|encrypt".format(password)
-        ret, output = shellutil.run_get_output(cmd, log_cmd=False)
-        if ret != 0:
-            raise OSUtilError(("Failed to encrypt password for {0}: {1}"
-                               "").format(username, output))
+        output = self._run_command_raising_OSUtilError(['encrypt'], cmd_input=password,
+                                                       err_msg="Failed to encrypt password for {0}".format(username))
         passwd_hash = output.strip()
-        cmd = "usermod -p '{0}' {1}".format(passwd_hash, username)
-        ret, output = shellutil.run_get_output(cmd, log_cmd=False)
-        if ret != 0:
-            raise OSUtilError(("Failed to set password for {0}: {1}"
-                               "").format(username, output))
+        self._run_command_raising_OSUtilError(['usermod', '-p', passwd_hash, username],
+                                              err_msg="Failed to set password for {0}".format(username))
 
     def del_root_password(self):
         ret, output = shellutil.run_get_output('usermod -p "*" root')
diff --git a/azurelinuxagent/common/osutil/openwrt.py b/azurelinuxagent/common/osutil/openwrt.py
index f033b48f..0421da74 100644
--- a/azurelinuxagent/common/osutil/openwrt.py
+++ b/azurelinuxagent/common/osutil/openwrt.py
@@ -47,20 +47,16 @@ class OpenWRTOSUtil(DefaultOSUtil):
             return
 
         if expiration is not None:
-            cmd = "useradd -m {0} -s /bin/ash -e {1}".format(username, expiration)
+            cmd = ["useradd", "-m", username, "-s", "/bin/ash", "-e", expiration]
         else:
-            cmd = "useradd -m {0} -s /bin/ash".format(username)
+            cmd = ["useradd", "-m", username, "-s", "/bin/ash"]
         
         if not os.path.exists("/home"):
             os.mkdir("/home")
 
         if comment is not None:
-            cmd += " -c {0}".format(comment)
-        retcode, out = shellutil.run_get_output(cmd)
-        if retcode != 0:
-            raise OSUtilError(("Failed to create user account:{0}, "
-                               "retcode:{1}, "
-                               "output:{2}").format(username, retcode, out))
+            cmd.extend(["-c", comment])
+        self._run_command_raising_OSUtilError(cmd, err_msg="Failed to create user account:{0}".format(username))
 
     def get_dhcp_pid(self):
         return self._get_dhcp_pid(["pidof", self.dhclient_name])
@@ -146,7 +142,9 @@ class OpenWRTOSUtil(DefaultOSUtil):
 
     def set_hostname(self, hostname):
         fileutil.write_file('/etc/hostname', hostname)
-        shellutil.run("uci set system.@system[0].hostname='{0}' && uci commit system && /etc/init.d/system reload".format(hostname), chk_err=False)
+        commands = [['uci', 'set', 'system.@system[0].hostname={0}'.format(hostname)], ['uci', 'commit', 'system'],
+                    ['/etc/init.d/system', 'reload']]
+        self._run_multiple_commands_without_raising(commands, log_error=False, continue_on_error=False)
 
     def remove_rules_files(self, rules_files=""):
         pass
diff --git a/azurelinuxagent/common/osutil/redhat.py b/azurelinuxagent/common/osutil/redhat.py
index bc24317e..8974eccb 100644
--- a/azurelinuxagent/common/osutil/redhat.py
+++ b/azurelinuxagent/common/osutil/redhat.py
@@ -81,7 +81,7 @@ class Redhat6xOSUtil(DefaultOSUtil):
         fileutil.update_conf_file('/etc/sysconfig/network',
                                   'HOSTNAME',
                                   'HOSTNAME={0}'.format(hostname))
-        shellutil.run("hostname {0}".format(hostname), chk_err=False)
+        self._run_command_without_raising(["hostname", hostname], log_error=False)
 
     def set_dhcp_hostname(self, hostname):
         ifname = self.get_if_name()
@@ -105,9 +105,9 @@ class RedhatOSUtil(Redhat6xOSUtil):
         Due to a bug in systemd in Centos-7.0, if this call fails, fallback
         to hostname.
         """
-        hostnamectl_cmd = "hostnamectl set-hostname {0} --static".format(hostname)
-        if shellutil.run(hostnamectl_cmd, chk_err=False) != 0:
-            logger.warn("[{0}] failed, attempting fallback".format(hostnamectl_cmd))
+        hostnamectl_cmd = ['hostnamectl', 'set-hostname', hostname, '--static']
+        if self._run_command_without_raising(hostnamectl_cmd, log_error=False) != 0:
+            logger.warn("[{0}] failed, attempting fallback".format(' '.join(hostnamectl_cmd)))
             DefaultOSUtil.set_hostname(self, hostname)
 
     def publish_hostname(self, hostname):
diff --git a/azurelinuxagent/common/utils/shellutil.py b/azurelinuxagent/common/utils/shellutil.py
index 2f69b74d..9fc89b00 100644
--- a/azurelinuxagent/common/utils/shellutil.py
+++ b/azurelinuxagent/common/utils/shellutil.py
@@ -132,20 +132,27 @@ class CommandError(Exception):
         self.stderr = stderr
 
 
-def run_command(command, log_error=False):
+def run_command(command, log_error=False, cmd_input=None):
     """
-    Executes the given command and returns its stdout as a string.
-    If there are any errors executing the command it logs details about the failure and raises a RunCommandException;
-    if 'log_error' is True, it also logs details about the error.
+        Executes the given command and returns its stdout as a string. If cmd_input is specified, then we pass the cmd_input
+        to stdin and execute the command. Currently we only support string input for stdin.
+        If there are any errors executing the command it logs details about the failure and raises a RunCommandException;
+        if 'log_error' is True, it also logs details about the error.
 
-    Note: This is the preferred method to execute shell commands over `azurelinuxagent.common.utils.shellutil.run` function.
+        Note: This is the preferred method to execute shell commands over `azurelinuxagent.common.utils.shellutil.run` function.
     """
     def format_command(cmd):
         return " ".join(cmd) if isinstance(cmd, list) else command
 
+    # Currently we only support PIPE for stdin/stdout/stderr, but acceptable options as per python docs are -
+    # PIPE, an existing file descriptor (a positive integer), an existing file object, and None
+    stdin = subprocess.PIPE if cmd_input else None
     try:
-        process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
-        stdout, stderr = process.communicate()
+        # Starting Python 3.4+, you need to encode the string, i.e. you need to pass Bytes to the input rather than string to process.communicate()
+        process_input = cmd_input.encode() if cmd_input else None
+
+        process = subprocess.Popen(command, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
+        stdout, stderr = process.communicate(input=process_input)
         returncode = process.returncode
     except Exception as e:
         if log_error:


Test patch:
diff --git a/tests/common/osutil/test_bigip.py b/tests/common/osutil/test_bigip.py
index 43516167..07fd3b9b 100644
--- a/tests/common/osutil/test_bigip.py
+++ b/tests/common/osutil/test_bigip.py
@@ -76,7 +76,7 @@ class TestBigIpOSUtil_save_sys_config(AgentTestCase):
 class TestBigIpOSUtil_useradd(AgentTestCase):
 
     @patch.object(osutil.BigIpOSUtil, 'get_userentry', return_value=None)
-    @patch.object(shellutil, "run_get_output")
+    @patch.object(shellutil, "run_command")
     def test_success(self, *args):
         args[0].return_value = (0, None)
         result = osutil.BigIpOSUtil.useradd(
@@ -103,7 +103,7 @@ class TestBigIpOSUtil_useradd(AgentTestCase):
 
 class TestBigIpOSUtil_chpasswd(AgentTestCase):
 
-    @patch.object(shellutil, "run_get_output", return_value=(0, None))
+    @patch.object(shellutil, "run_command")
     @patch.object(osutil.BigIpOSUtil, 'get_userentry', return_value=True)
     @patch.object(osutil.BigIpOSUtil, 'is_sys_user', return_value=False)
     @patch.object(osutil.BigIpOSUtil, '_save_sys_config', return_value=None)
diff --git a/tests/utils/test_shell_util.py b/tests/utils/test_shell_util.py
index a8f545b5..27cd0933 100644
--- a/tests/utils/test_shell_util.py
+++ b/tests/utils/test_shell_util.py
@@ -202,6 +202,17 @@ class RunCommandTestCase(AgentTestCase):
             self.assertIn(command, args, msg="The command was not logged")
             self.assertTrue(any("No such file or directory" in str(a) for a in args), msg="The command's stderr was not logged")
 
+    def test_run_command_it_should_read_from_stdin_if_cmd_input_is_set(self):
+        import random
+        command = ["cat"]
+        random_hash = ''.join(random.choice('0123456789ABCDEF') for _ in range(16))
+        try:
+            output = shellutil.run_command(command, cmd_input=random_hash)
+        except:
+            self.fail("No exception should've been thrown when trying to read from stdin in run_command")
+
+        self.assertEqual(output, random_hash, "We're reading from stdin and printing it shell, output should match")
+
 
 if __name__ == '__main__':
     unittest.main()