Repository: saltstack/salt
Pull request: Update dpkg_lowpkg to handle deb files that have spaces in `dpkg -I` output
### What does this PR do?
use re.match to find Name/Architecture/Version instead of startswith, some dpkg -I output has spaces between the description and colon

### What issues does this PR fix or reference?
Fixes #54739 

### Previous Behavior
Deb packages that returned output of `dpkg -I` with spaces between the row name and the ':' would not install

for example
```
# dpkg -I /var/cache/salt/minion/files/base/apps/mcafee/agent/files/MFEdx-ma-5.0.0-568.x86_64.deb
 new Debian package, version 2.0.
 size 1333890 bytes: control archive=1142 bytes.
     260 bytes,     9 lines      control
    1788 bytes,    61 lines   *  postinst             #!/bin/sh
     342 bytes,    18 lines   *  postrm               #!/bin/sh
     401 bytes,    20 lines   *  preinst              #!/bin/sh
     588 bytes,    28 lines   *  prerm                #!/bin/sh
 Package        : MFEdx
 Version        : 5.0.0
 Maintainer     : McAfee, Inc.
 Description    : McAfee Data Exchange Layer
 Release        : 568
 Architecture   : all
 Section        : Network/Agent
 Summary        : McAfee Data Exchange Layer
 Pre-Depends    : MFErt
```

### New Behavior
Packages with this type of output are able to be installed

### Tests written?
Yes

### Commits signed with GPG?
No

Please review [Salt's Contributing Guide](https://docs.saltstack.com/en/latest/topics/development/contributing.html) for best practices.

See GitHub's [page on GPG signing](https://help.github.com/articles/signing-commits-using-gpg/) for more information about signing commits with GPG.


Problem statement:
Unable to use pkg.install with 'sources' parameter in Ubuntu on some deb files
### Description of Issue
Unable to install some deb packages through pkg.install using the 'sources' parameter.

the dpkg_lowpkg module attempts to use 'startswith' to find the information from the output of `dpkg -I ...`

dpkg -I <path/to/deb> returns info with spaces on some packages (McAfee Agent Packages in my case):

```
package.deb
 new Debian package, version 2.0.
 size 1333890 bytes: control archive=1142 bytes.
     260 bytes,     9 lines      control
    1788 bytes,    61 lines   *  postinst             #!/bin/sh
     342 bytes,    18 lines   *  postrm               #!/bin/sh
     401 bytes,    20 lines   *  preinst              #!/bin/sh
     588 bytes,    28 lines   *  prerm                #!/bin/sh
 Package        : <package_name>
 Version        : <version_number>
 Maintainer     : <maintainer>
 Description    : <description>
 Release        : <release_number>
 Architecture   : <arch>
 Section        : <section info>
 Summary        : <summary>
 Pre-Depends    : <depends>
```

The spaces mess up the 'startswith' which uses the 'Architecture:', 'Package:', etc..

### Setup
salt-call pkg.install sources="[{'pkgname':'/path/to/install.deb'}]"

### Steps to Reproduce Issue
salt-call pkg.install sources="[{'pkgname':'/path/to/install.deb'}]"
```
Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/salt/cli/caller.py", line 237, in call
    ret['return'] = self.minion.executors[fname](self.opts, data, func, args, kwargs)
  File "/usr/lib/python2.7/dist-packages/salt/executors/direct_call.py", line 12, in execute
    return func(*args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/salt/modules/aptpkg.py", line 618, in install
    deb_info = __salt__['lowpkg.bin_pkg_info'](pkg_source)
  File "/usr/lib/python2.7/dist-packages/salt/modules/dpkg.py", line 106, in bin_pkg_info
    'Unable to get {0} for {1}'.format(', '.join(missing), path)
CommandExecutionError: Unable to get arch for /var/cache/salt/minion/files/base/install.deb
Error running 'pkg.install': Unable to get arch for /var/cache/salt/minion/files/base/install.deb
```

### Versions Report
```
Salt Version:
           Salt: 2019.2.0

Dependency Versions:
           cffi: Not Installed
       cherrypy: Not Installed
       dateutil: 2.6.1
      docker-py: Not Installed
          gitdb: Not Installed
      gitpython: Not Installed
          ioflo: Not Installed
         Jinja2: 2.10
        libgit2: Not Installed
        libnacl: Not Installed
       M2Crypto: Not Installed
           Mako: 1.0.7
   msgpack-pure: Not Installed
 msgpack-python: 0.5.6
   mysql-python: Not Installed
      pycparser: Not Installed
       pycrypto: 2.6.1
   pycryptodome: Not Installed
         pygit2: Not Installed
         Python: 2.7.15+ (default, Jul  9 2019, 16:51:35)
   python-gnupg: 0.4.1
         PyYAML: 3.12
          PyZMQ: 16.0.2
           RAET: Not Installed
          smmap: Not Installed
        timelib: Not Installed
        Tornado: 4.5.3
            ZMQ: 4.2.5

System Versions:
           dist: Ubuntu 18.04 bionic
         locale: UTF-8
        machine: x86_64
        release: 4.15.0-1044-aws
         system: Linux
        version: Ubuntu 18.04 bionic
```

Hints from issue discussion:
Thanks for reporting and addressing the issue! 
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

If this issue is closed prematurely, please leave a comment and we will gladly reopen the issue.

Thank you for updating this issue. It is no longer marked as stale.

awaiting PR commit

Commit message: Merge branch 'master' into dpkg_output_fix

Patch to review:
diff --git a/salt/modules/dpkg_lowpkg.py b/salt/modules/dpkg_lowpkg.py
index e267eea51bb6..4dcf2640ac3b 100644
--- a/salt/modules/dpkg_lowpkg.py
+++ b/salt/modules/dpkg_lowpkg.py
@@ -87,11 +87,11 @@ def bin_pkg_info(path, saltenv="base"):
     ret = {}
     for line in result["stdout"].splitlines():
         line = line.strip()
-        if line.startswith("Package:"):
+        if re.match(r"^Package[ ]*:", line):
             ret["name"] = line.split()[-1]
-        elif line.startswith("Version:"):
+        elif re.match(r"^Version[ ]*:", line):
             ret["version"] = line.split()[-1]
-        elif line.startswith("Architecture:"):
+        elif re.match(r"^Architecture[ ]*:", line):
             ret["arch"] = line.split()[-1]
 
     missing = [x for x in ("name", "version", "arch") if x not in ret]
diff --git a/tests/unit/modules/test_dpkg_lowpkg.py b/tests/unit/modules/test_dpkg_lowpkg.py
index 2e8577aca8d6..f4aee1f57210 100644
--- a/tests/unit/modules/test_dpkg_lowpkg.py
+++ b/tests/unit/modules/test_dpkg_lowpkg.py
@@ -12,10 +12,11 @@
 # Import Salt Libs
 import salt.modules.dpkg_lowpkg as dpkg
 
+# Import Salt Testing Libs
 # Import Salt Testing Libs
 from tests.support.mixins import LoaderModuleMockMixin
-from tests.support.mock import MagicMock, patch
-from tests.support.unit import TestCase
+from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch
+from tests.support.unit import TestCase, skipIf
 
 DPKG_ERROR_MSG = """dpkg-query: package 'httpd' is not installed
 Use dpkg --contents (= dpkg-deb --contents) to list archive files contents.
@@ -211,6 +212,64 @@ def test_file_dict(self):
         with patch.dict(dpkg.__salt__, {"cmd.run_all": mock}):
             self.assertEqual(dpkg.file_dict("httpd"), "Error:  " + DPKG_ERROR_MSG)
 
+    def test_bin_pkg_info_spaces(self):
+        """
+        Test the bin_pkg_info function
+        """
+        file_proto_mock = MagicMock(return_value=True)
+        with patch.dict(dpkg.__salt__, {"config.valid_fileproto": file_proto_mock}):
+            cache_mock = MagicMock(return_value="/path/to/some/package.deb")
+            with patch.dict(dpkg.__salt__, {"cp.cache_file": cache_mock}):
+                dpkg_info_mock = MagicMock(
+                    return_value={
+                        "retcode": 0,
+                        "stderr": "",
+                        "stdout": (
+                            " new Debian package, version 2.0\n"
+                            " size 123456 bytes: control archive: 4029  bytes.\n"
+                            " Package          : package_name\n"
+                            " Version          : 1.0\n"
+                            " Section          : section_name\n"
+                            " Priority         : priority\n"
+                            " Architecture     : all\n"
+                            " Description      : some package\n"
+                        ),
+                    }
+                )
+                with patch.dict(dpkg.__salt__, {"cmd.run_all": dpkg_info_mock}):
+                    self.assertEqual(
+                        dpkg.bin_pkg_info("package.deb")["name"], "package_name"
+                    )
+
+    def test_bin_pkg_info_no_spaces(self):
+        """
+        Test the bin_pkg_info function
+        """
+        file_proto_mock = MagicMock(return_value=True)
+        with patch.dict(dpkg.__salt__, {"config.valid_fileproto": file_proto_mock}):
+            cache_mock = MagicMock(return_value="/path/to/some/package.deb")
+            with patch.dict(dpkg.__salt__, {"cp.cache_file": cache_mock}):
+                dpkg_info_mock = MagicMock(
+                    return_value={
+                        "retcode": 0,
+                        "stderr": "",
+                        "stdout": (
+                            " new Debian package, version 2.0\n"
+                            " size 123456 bytes: control archive: 4029  bytes.\n"
+                            " Package: package_name\n"
+                            " Version: 1.0\n"
+                            " Section: section_name\n"
+                            " Priority: priority\n"
+                            " Architecture: all\n"
+                            " Description: some package\n"
+                        ),
+                    }
+                )
+                with patch.dict(dpkg.__salt__, {"cmd.run_all": dpkg_info_mock}):
+                    self.assertEqual(
+                        dpkg.bin_pkg_info("package.deb")["name"], "package_name"
+                    )
+
     def test_info(self):
         """
         Test package info


Code review comments:
diff_hunk: @@ -12,10 +12,11 @@
 # Import Salt Libs
 import salt.modules.dpkg_lowpkg as dpkg
 
+# Import Salt Testing Libs
 # Import Salt Testing Libs
 from tests.support.mixins import LoaderModuleMockMixin
-from tests.support.mock import MagicMock, patch
-from tests.support.unit import TestCase
+from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch
path: tests/unit/modules/test_dpkg_lowpkg.py
text: @user1:
Please leave this import as it was before, ie, remove the `NO_MOCK*` imports