Repository: RedHatInsights/insights-core

Issue:
"rpm -qa" are failing database corruption and no packages are output
Rpm commands are failing with messages similar to:

```
# cat insights-xxx/insights_commands/rpm_-qa_--root_._--qf_name_NAME_epoch_EPOCH_version_VERSION_release_RELEASE_arch_ARCH_installtime_INSTALLTIME_date_buildtime_BUILDTIME_vendor_VENDOR_buildhost_BUILDHOST_sigpgp_SIGPGP_pgpsig
error: rpmdb: BDB0113 Thread/process 20263/140251984590912 failed: BDB1507 Thread died in Berkeley DB library
error: db5 error(-30973) from dbenv->failchk: BDB0087 DB_RUNRECOVERY: Fatal error, run database recovery
error: cannot open Packages index using db5 -  (-30973)
error: cannot open Packages database in /var/lib/rpm
error: rpmdb: BDB0113 Thread/process 20263/140251984590912 failed: BDB1507 Thread died in Berkeley DB library
error: db5 error(-30973) from dbenv->failchk: BDB0087 DB_RUNRECOVERY: Fatal error, run database recovery
error: cannot open Packages database in /var/lib/rpm
```
* See also https://access.redhat.com/solutions/3330211

Which causes false positive when the condition requires checking the package is not installed.




Discussion:

Resolving patch:
diff --git a/insights/parsers/installed_rpms.py b/insights/parsers/installed_rpms.py
--- a/insights/parsers/installed_rpms.py
+++ b/insights/parsers/installed_rpms.py
@@ -153,6 +153,14 @@ def __contains__(self, package_name):
         """
         Checks if package name is in list of installed RPMs.
 
+        .. note::
+            The :attr:`packages` could be empty, e.g. when rpm database corrupt.
+            When doing exclusion check, make sure the ``packages`` is NOT
+            empty, e.g.::
+
+                >>> if rpms.packages and "pkg_name" not in rpms:
+                >>>     pass
+
         Args:
             package_name (str): RPM package name such as 'bash'
 
@@ -217,16 +225,26 @@ class InstalledRpms(CommandParser, RpmList):
     related information.
     """
     def __init__(self, *args, **kwargs):
-        self.errors = []
+        self.errors = list()
         """list: List of input lines that indicate an error acquiring the data on the client."""
-        self.unparsed = []
+        self.unparsed = list()
         """list: List of input lines that raised an exception during parsing."""
-        self.packages = defaultdict(list)
-        """dict (InstalledRpm): Dictionary of RPMs keyed by package name."""
+        self.packages = dict()
+        """
+        dict (InstalledRpm): Dictionary of RPMs keyed by package name.
 
+        .. note::
+            The ``packages`` could be empty, e.g. when rpm database corrupt.
+            When doing exclusion check, make sure the ``packages`` is NOT
+            empty, e.g.::
+
+                >>> if rpms.packages and "pkg_name" not in rpms.packages:
+                >>>     pass
+        """
         super(InstalledRpms, self).__init__(*args, **kwargs)
 
     def parse_content(self, content):
+        packages = defaultdict(list)
         for line in get_active_lines(content, comment_char='COMMAND>'):
             if line.startswith('error:') or line.startswith('warning:'):
                 self.errors.append(line)
@@ -234,23 +252,28 @@ def parse_content(self, content):
                 try:
                     # Try to parse from JSON input
                     rpm = InstalledRpm.from_json(line)
-                    self.packages[rpm.name].append(rpm)
+                    packages[rpm.name].append(rpm)
                 except Exception:
                     # If that fails, try to parse from line input
                     if line.strip():
                         try:
                             rpm = InstalledRpm.from_line(line)
-                            self.packages[rpm.name].append(rpm)
+                            packages[rpm.name].append(rpm)
                         except Exception:
                             # Both ways failed
                             self.unparsed.append(line)
         # Don't want defaultdict's behavior after parsing is complete
-        self.packages = dict(self.packages)
+        self.packages = dict(packages)
 
     @property
     def corrupt(self):
         """bool: True if RPM database is corrupted, else False."""
-        return any('rpmdbNextIterator' in s for s in self.errors)
+        _corrupts = [
+            'error: rpmdbNextIterator',
+            'error: rpmdb: BDB0113',
+            'error: db5 error',
+        ]
+        return any(c in s for s in self.errors for c in _corrupts)
 
 
 p = re.compile(r"(\d+|[a-z]+|\.|-|_)")


Test patch:
diff --git a/insights/parsers/tests/test_installed_rpms.py b/insights/parsers/tests/test_installed_rpms.py
--- a/insights/parsers/tests/test_installed_rpms.py
+++ b/insights/parsers/tests/test_installed_rpms.py
@@ -83,6 +83,16 @@
 yum-security-1.1.16-21.el5.noarch
 '''.strip()
 
+ERROR_DB_NO_PKG = """
+error: rpmdb: BDB0113 Thread/process 20263/140251984590912 failed: BDB1507 Thread died in Berkeley DB library
+error: db5 error(-30973) from dbenv->failchk: BDB0087 DB_RUNRECOVERY: Fatal error, run database recovery
+error: cannot open Packages index using db5 -  (-30973)
+error: cannot open Packages database in /var/lib/rpm
+error: rpmdb: BDB0113 Thread/process 20263/140251984590912 failed: BDB1507 Thread died in Berkeley DB library
+error: db5 error(-30973) from dbenv->failchk: BDB0087 DB_RUNRECOVERY: Fatal error, run database recovery
+error: cannot open Packages database in /var/lib/rpm
+""".strip()
+
 ORACLEASM_RPMS = '''
 oracleasm-2.6.18-164.el5-2.0.5-1.el5.x86_64
 oracleasmlib-2.0.4-1.el5.x86_64
@@ -205,8 +215,17 @@ def test_garbage():
 
 def test_corrupt_db():
     rpms = InstalledRpms(context_wrap(ERROR_DB))
+    assert rpms.corrupt is True
     assert "yum-security" in rpms.packages
+    assert "yum-security" in rpms
+
+    rpms = InstalledRpms(context_wrap(ERROR_DB_NO_PKG))
     assert rpms.corrupt is True
+    assert not rpms.packages
+    assert "kernel" not in rpms
+    assert "kernel" not in rpms.packages
+    assert rpms.newest("kernel") is None
+    assert rpms.oldest("kernel") is None
 
 
 def test_rpm_manifest():