Repository: RedHatInsights/insights-core

Issue:
Several parsers that parse json are throwing parse exceptions in production.
The below exceptions are occurring in production due to blank spec output.

ParseException("insights.parsers.ceph_cmd_json_parsing.CephS couldn't parse json.",)
ParseException("insights.parsers.ceph_cmd_json_parsing.CephDfDetail couldn't parse json.",)
ParseException("insights.parsers.ceph_cmd_json_parsing.CephOsdDump couldn't parse json.",)
ParseException("insights.parsers.ceph_cmd_json_parsing.CephHealthDetail couldn't parse json.",)
ParseException("insights.parsers.ceph_cmd_json_parsing.CephOsdTree couldn't parse json.",)

ParseException("insights.parsers.ndctl_list.NdctlListNi couldn't parse json.",)

ParseException("insights.parsers.rhsm_releasever.RhsmReleaseVer couldn't parse json.",)


Discussion:

Resolving patch:
diff --git a/insights/core/__init__.py b/insights/core/__init__.py
--- a/insights/core/__init__.py
+++ b/insights/core/__init__.py
@@ -750,11 +750,15 @@ def parse_content(self, content):
         try:
             self.data = json.loads(''.join(content))
         except:
-            tb = sys.exc_info()[2]
-            cls = self.__class__
-            name = ".".join([cls.__module__, cls.__name__])
-            msg = "%s couldn't parse json." % name
-            six.reraise(ParseException, ParseException(msg), tb)
+            # If content is empty then raise a skip exception instead of a parse exception.
+            if not content:
+                raise SkipException("Empty output.")
+            else:
+                tb = sys.exc_info()[2]
+                cls = self.__class__
+                name = ".".join([cls.__module__, cls.__name__])
+                msg = "%s couldn't parse json." % name
+                six.reraise(ParseException, ParseException(msg), tb)
 
 
 class ScanMeta(type):
diff --git a/insights/parsers/engine_db_query.py b/insights/parsers/engine_db_query.py
--- a/insights/parsers/engine_db_query.py
+++ b/insights/parsers/engine_db_query.py
@@ -5,7 +5,6 @@
 Parses the output of the command `engine-db-query` returned in JSON format.
 """
 from insights.core import CommandParser, JSONParser
-from insights.parsers import SkipException
 from insights.core.plugins import parser
 from insights.specs import Specs
 
@@ -46,9 +45,6 @@ class EngineDBQueryVDSMversion(CommandParser, JSONParser):
         True
     """
     def parse_content(self, content):
-        if not content:
-            raise SkipException("Empty output.")
-
         super(EngineDBQueryVDSMversion, self).parse_content(content)
 
     @property


Test patch:
diff --git a/insights/parsers/tests/__init__.py b/insights/parsers/tests/__init__.py
--- a/insights/parsers/tests/__init__.py
+++ b/insights/parsers/tests/__init__.py
@@ -1,6 +1,7 @@
 import doctest
 from doctest import (DebugRunner, DocTestFinder, DocTestRunner,
                      OutputChecker)
+import pytest
 import re
 import sys
 
@@ -35,3 +36,13 @@ def ic_testmod(m, name=None, globs=None, verbose=None,
         runner.summarize()
 
     return doctest.TestResults(runner.failures, runner.tries)
+
+
+@pytest.fixture()
+def test_empty_skip(parser_obj):
+    from insights.parsers import SkipException
+    from insights.tests import context_wrap
+
+    with pytest.raises(SkipException) as ex:
+        parser_obj(context_wrap(""))
+    return str(ex)
diff --git a/insights/parsers/tests/test_awx_manage.py b/insights/parsers/tests/test_awx_manage.py
--- a/insights/parsers/tests/test_awx_manage.py
+++ b/insights/parsers/tests/test_awx_manage.py
@@ -1,9 +1,10 @@
 import doctest
 import pytest
 
-from insights.parsers import awx_manage, SkipException
 from insights.core import ContentException, ParseException
+from insights.parsers import awx_manage, SkipException
 from insights.parsers.awx_manage import AnsibleTowerLicenseType, AnsibleTowerLicense
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
 
 GOOD_LICENSE = """
@@ -58,8 +59,7 @@ def test_ansible_tower_license_data():
 
 
 def test_ansible_tower_license__data_ab_type():
-    with pytest.raises(ParseException):
-        AnsibleTowerLicense(context_wrap(NG_COMMAND_0))
+    assert 'Empty output.' in test_empty_skip(AnsibleTowerLicense)
 
     with pytest.raises(ContentException):
         AnsibleTowerLicense(context_wrap(NG_COMMAND_1))
diff --git a/insights/parsers/tests/test_ceph_cmd_json_parsing.py b/insights/parsers/tests/test_ceph_cmd_json_parsing.py
--- a/insights/parsers/tests/test_ceph_cmd_json_parsing.py
+++ b/insights/parsers/tests/test_ceph_cmd_json_parsing.py
@@ -1,8 +1,10 @@
 import doctest
 import pytest
-from insights.parsers import ceph_cmd_json_parsing, ParseException, SkipException
+
+from insights.parsers import ceph_cmd_json_parsing, ParseException
 from insights.parsers.ceph_cmd_json_parsing import CephOsdDump, CephOsdDf, CephS, CephECProfileGet, CephCfgInfo, \
     CephHealthDetail, CephDfDetail, CephOsdTree, CephReport
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
 
 CEPH_OSD_DUMP_INFO = """
@@ -163,7 +165,7 @@
 {
     "name": "osd.1",
     "cluster": "ceph",
-    "debug_none": "0\/5",
+    "debug_none": "0/5",
     "heartbeat_interval": "5",
     "heartbeat_file": "",
     "heartbeat_inject_failure": "0",
@@ -493,8 +495,6 @@
 }
 """.strip()
 
-CEPH_REPORT_EMPTY = """""".strip()
-
 
 def test_ceph_doc_examples():
     env = {
@@ -534,6 +534,9 @@ def test_ceph_osd_dump(self):
         }
         assert result['pools'][0]['min_size'] == 2
 
+    def test_ceph_osd_dump_empty(self):
+        assert 'Empty output.' in test_empty_skip(CephOsdDump)
+
 
 class TestCephOsdDf():
     def test_ceph_osd_df(self):
@@ -570,6 +573,9 @@ def test_ceph_osd_df(self):
         }
         assert result['nodes'][0]['pgs'] == 945
 
+    def test_ceph_os_df_empty(self):
+        assert 'Empty output.' in test_empty_skip(CephOsdDf)
+
 
 class TestCephS():
     def test_ceph_s(self):
@@ -600,6 +606,9 @@ def test_ceph_s(self):
         }
         assert result['pgmap']['pgs_by_state'][0]['state_name'] == 'active+clean'
 
+    def test_ceph_s_empty(self):
+        assert 'Empty output.' in test_empty_skip(CephS)
+
 
 class TestCephECProfileGet():
     def test_ceph_ec_profile_get(self):
@@ -614,9 +623,12 @@ def test_ceph_ec_profile_get(self):
         assert result['k'] == "2"
         assert result['m'] == "1"
 
+    def test_ceph_ec_profile_get_empty(self):
+        assert 'Empty output.' in test_empty_skip(CephECProfileGet)
+
 
 class TestCephCfgInfo():
-    def test_cephcfginfo(self):
+    def test_ceph_cfg_info(self):
         result = CephCfgInfo(context_wrap(CEPHINFO))
 
         assert result.data == {
@@ -637,6 +649,9 @@ def test_cephcfginfo(self):
 
         assert result.max_open_files == '131072'
 
+    def test_ceph_cfg_info_empty(self):
+        assert 'Empty output.' in test_empty_skip(CephCfgInfo)
+
 
 class TestCephHealthDetail():
     def test_ceph_health_detail(self):
@@ -656,6 +671,9 @@ def test_ceph_health_detail(self):
         }
         assert result['overall_status'] == 'HEALTH_OK'
 
+    def test_ceph_health_detail_empty(self):
+        assert 'Empty output.' in test_empty_skip(CephHealthDetail)
+
 
 class TestCephDfDetail():
     def test_ceph_df_detail(self):
@@ -705,6 +723,9 @@ def test_ceph_df_detail(self):
         }
         assert result['stats']['total_avail_bytes'] == 16910123008
 
+    def test_ceph_df_detail_empty(self):
+        assert 'Empty output.' in test_empty_skip(CephDfDetail)
+
 
 class TestCephOsdTree():
     def test_ceph_osd_tree(self):
@@ -836,6 +857,9 @@ def test_ceph_osd_tree(self):
 
         assert len(result['nodes'][0]['children']) == 4
 
+    def test_ceph_osd_tree_empty(self):
+        assert 'Empty output.' in test_empty_skip(CephOsdTree)
+
 
 class TestCephReport():
     def test_ceph_report(self):
@@ -852,7 +876,5 @@ def test_invalid_json(self):
             CephReport(context_wrap(CEPH_REPORT_INVALID_JSON))
         assert "Could not parse json." in str(e)
 
-    def test_invalid_empty(self):
-        with pytest.raises(SkipException) as e:
-            CephReport(context_wrap(CEPH_REPORT_EMPTY))
-        assert "Empty output." in str(e)
+    def test_ceph_report_empty(self):
+        assert 'Empty output.' in test_empty_skip(CephReport)
diff --git a/insights/parsers/tests/test_cloud_cfg.py b/insights/parsers/tests/test_cloud_cfg.py
--- a/insights/parsers/tests/test_cloud_cfg.py
+++ b/insights/parsers/tests/test_cloud_cfg.py
@@ -1,5 +1,7 @@
 import doctest
+
 from insights.parsers import cloud_cfg
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
 
 
@@ -20,6 +22,10 @@ def test_cloud_cfg():
     assert result.data['config'][0]['name'] == 'eth0'
 
 
+def test_cloud_cfg_empty():
+    assert 'Empty output.' in test_empty_skip(cloud_cfg.CloudCfg)
+
+
 def test_doc_examples():
     env = {
         'cloud_cfg': cloud_cfg.CloudCfg(context_wrap(CONFIG_2)),
diff --git a/insights/parsers/tests/test_cni_podman_bridge_conf.py b/insights/parsers/tests/test_cni_podman_bridge_conf.py
--- a/insights/parsers/tests/test_cni_podman_bridge_conf.py
+++ b/insights/parsers/tests/test_cni_podman_bridge_conf.py
@@ -1,8 +1,9 @@
 import doctest
 
-from insights.tests import context_wrap
 from insights.parsers import cni_podman_bridge_conf
 from insights.parsers.cni_podman_bridge_conf import CNIPodmanBridgeConf
+from insights.parsers.tests import test_empty_skip
+from insights.tests import context_wrap
 
 PODMAN_CNI_FILE = '''
 {
@@ -61,3 +62,7 @@ def test_cni_podman_bridge_conf():
     conf = CNIPodmanBridgeConf(context_wrap(PODMAN_CNI_FILE))
     assert len(conf["plugins"]) == 4
     assert conf["plugins"][3]["type"] == "tuning"
+
+
+def test_cni_podman_bridge_conf_empty():
+    assert 'Empty output.' in test_empty_skip(CNIPodmanBridgeConf)
diff --git a/insights/parsers/tests/test_engine_db_query.py b/insights/parsers/tests/test_engine_db_query.py
--- a/insights/parsers/tests/test_engine_db_query.py
+++ b/insights/parsers/tests/test_engine_db_query.py
@@ -1,6 +1,8 @@
 import doctest
 import pytest
-from insights.parsers import engine_db_query, ParseException, SkipException
+
+from insights.parsers import engine_db_query, ParseException
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
 
 
@@ -91,9 +93,7 @@ def test_edbq():
     assert output.result == [{'vds_name': 'hosto', 'rpm_version': 'vdsm-4.40.20-33.git1b7dedcf3.fc30'}, {'vds_name': 'hosto2', 'rpm_version': 'vdsm-4.40.13-38.gite9bae3c68.fc30'}]
 
     # No content
-    with pytest.raises(SkipException) as e:
-        engine_db_query.EngineDBQueryVDSMversion(context_wrap(""))
-    assert "Empty output." in str(e)
+    assert 'Empty output.' in test_empty_skip(engine_db_query.EngineDBQueryVDSMversion)
 
     # Error
     with pytest.raises(ParseException) as e:
diff --git a/insights/parsers/tests/test_freeipa_healthcheck_log.py b/insights/parsers/tests/test_freeipa_healthcheck_log.py
--- a/insights/parsers/tests/test_freeipa_healthcheck_log.py
+++ b/insights/parsers/tests/test_freeipa_healthcheck_log.py
@@ -1,6 +1,8 @@
 import doctest
+
 from insights.parsers import freeipa_healthcheck_log
 from insights.parsers.freeipa_healthcheck_log import FreeIPAHealthCheckLog
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
 
 LONG_FREEIPA_HEALTHCHECK_LOG_OK = """
@@ -96,6 +98,10 @@ def test_freeipa_healthcheck_get_results_not_ok():
         assert result['source'] == 'ipahealthcheck.system.filesystemspace'
 
 
+def test_freeipa_healthcheck_log_empty():
+    assert 'Empty output.' in test_empty_skip(FreeIPAHealthCheckLog)
+
+
 def test_freeipa_healthcheck_log__documentation():
     env = {
         'healthcheck': FreeIPAHealthCheckLog(context_wrap(FREEIPA_HEALTHCHECK_LOG_DOCS_EXAMPLE)),
diff --git a/insights/parsers/tests/test_httpd_open_nfs.py b/insights/parsers/tests/test_httpd_open_nfs.py
--- a/insights/parsers/tests/test_httpd_open_nfs.py
+++ b/insights/parsers/tests/test_httpd_open_nfs.py
@@ -1,7 +1,9 @@
+import doctest
+
 from insights.parsers import httpd_open_nfs
 from insights.parsers.httpd_open_nfs import HttpdOnNFSFilesCount
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
-import doctest
 
 http_nfs = """
 {"http_ids": [1787, 2399], "nfs_mounts": ["/data", "/www"], "open_nfs_files": 1000}
@@ -16,6 +18,10 @@ def test_http_nfs():
     assert httpd_nfs_counting.data.get("open_nfs_files") == 1000
 
 
+def test_empty():
+    assert 'Empty output.' in test_empty_skip(HttpdOnNFSFilesCount)
+
+
 def test_http_nfs_documentation():
     env = {
         'httpon_nfs': HttpdOnNFSFilesCount(context_wrap(http_nfs))
diff --git a/insights/parsers/tests/test_ndctl_list.py b/insights/parsers/tests/test_ndctl_list.py
--- a/insights/parsers/tests/test_ndctl_list.py
+++ b/insights/parsers/tests/test_ndctl_list.py
@@ -1,6 +1,8 @@
 import doctest
+
 from insights.parsers import ndctl_list
 from insights.parsers.ndctl_list import NdctlListNi
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
 
 NDCTL_OUTPUT = """
@@ -35,7 +37,7 @@
 """.strip()
 
 
-def test_netstat_doc_examples():
+def test_ndctl_list_doc_examples():
     env = {
         'ndctl_list': NdctlListNi(context_wrap(NDCTL_OUTPUT))
     }
@@ -49,3 +51,7 @@ def test_get_dev_attr():
     assert 'map' in ndctl.get_blockdev('pmem1')
     assert ndctl.get_blockdev('pmem1').get('map') == 'mem'
     assert ndctl.get_blockdev('pmem2') == {}
+
+
+def test_empty():
+    assert 'Empty output.' in test_empty_skip(NdctlListNi)
diff --git a/insights/parsers/tests/test_rhsm_releasever.py b/insights/parsers/tests/test_rhsm_releasever.py
--- a/insights/parsers/tests/test_rhsm_releasever.py
+++ b/insights/parsers/tests/test_rhsm_releasever.py
@@ -1,8 +1,9 @@
 import doctest
 import pytest
-import insights.parsers.rhsm_releasever as rhsm_releasever_module
-from insights.parsers import SkipException
+
+from insights.parsers import rhsm_releasever as rhsm_releasever_module, SkipException
 from insights.parsers.rhsm_releasever import RhsmReleaseVer
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
 
 RHEL_MAJ_MIN = '{"releaseVer": "6.10"}'
@@ -45,10 +46,14 @@ def test_rhsm_releasever():
     assert relver.minor is None
 
     with pytest.raises(SkipException) as e_info:
-        relver = RhsmReleaseVer(context_wrap(RHEL_EMPTY))
+        RhsmReleaseVer(context_wrap(RHEL_EMPTY))
     assert "releaseVer is not in data" in str(e_info.value)
 
 
+def test_empty():
+    assert 'Empty output.' in test_empty_skip(RhsmReleaseVer)
+
+
 def test_doc_examples():
     env = {
         'rhsm_releasever': RhsmReleaseVer(context_wrap(RHEL_MAJ_MIN)),
diff --git a/insights/parsers/tests/test_rhv_log_collector_analyzer.py b/insights/parsers/tests/test_rhv_log_collector_analyzer.py
--- a/insights/parsers/tests/test_rhv_log_collector_analyzer.py
+++ b/insights/parsers/tests/test_rhv_log_collector_analyzer.py
@@ -1,4 +1,5 @@
 from insights.parsers.rhv_log_collector_analyzer import RhvLogCollectorJson
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
 
 RHV_ANALYZER_JSON = """
@@ -136,3 +137,6 @@ def test_rhv_log_collector_json(self):
             ]
         }
         assert result['rhv-log-collector-analyzer'][0]['file'] == 'cluster_query_migration_policy_check_legacy.sql'
+
+    def test_empty(self):
+        assert 'Empty output.' in test_empty_skip(RhvLogCollectorJson)
diff --git a/insights/parsers/tests/test_tags.py b/insights/parsers/tests/test_tags.py
--- a/insights/parsers/tests/test_tags.py
+++ b/insights/parsers/tests/test_tags.py
@@ -1,4 +1,5 @@
 from insights.parsers.tags import Tags
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
 
 tags_json_content = """
@@ -14,3 +15,7 @@ def test_tags_json():
     assert result.data['owner'] == "test"
     assert result.data['exclude'] == "true"
     assert result.data['group'] == "app-db-01"
+
+
+def test_tags_empty():
+    assert 'Empty output.' in test_empty_skip(Tags)
diff --git a/insights/parsers/tests/test_teamdctl_config_dump.py b/insights/parsers/tests/test_teamdctl_config_dump.py
--- a/insights/parsers/tests/test_teamdctl_config_dump.py
+++ b/insights/parsers/tests/test_teamdctl_config_dump.py
@@ -1,7 +1,9 @@
-from insights.parsers.teamdctl_config_dump import TeamdctlConfigDump
+import doctest
+
 from insights.parsers import teamdctl_config_dump
+from insights.parsers.teamdctl_config_dump import TeamdctlConfigDump
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
-import doctest
 
 TEAMDCTL_CONFIG_DUMP_INFO = """
 {
@@ -38,6 +40,10 @@ def test_teamdctl_state_dump():
     assert result.runner_hwaddr_policy == 'only_active'
 
 
+def test_teamdctl_state_dump_empty():
+    assert 'Empty output.' in test_empty_skip(TeamdctlConfigDump)
+
+
 def test_nmcli_doc_examples():
     env = {
             'teamdctl_config_dump': TeamdctlConfigDump(context_wrap(TEAMDCTL_CONFIG_DUMP_INFO)),
diff --git a/insights/parsers/tests/test_teamdctl_state_dump.py b/insights/parsers/tests/test_teamdctl_state_dump.py
--- a/insights/parsers/tests/test_teamdctl_state_dump.py
+++ b/insights/parsers/tests/test_teamdctl_state_dump.py
@@ -1,4 +1,5 @@
 from insights.parsers.teamdctl_state_dump import TeamdctlStateDump
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
 
 TEAMDCTL_STATE_DUMP_INFO = """
@@ -110,3 +111,7 @@ def test_teamdctl_state_dump_none():
     assert result['setup']['runner_name'] == 'activebackup'
     assert result.runner_type == 'activebackup'
     assert result.team_ifname is None
+
+
+def test_teamdctl_state_dump_empty():
+    assert 'Empty output.' in test_empty_skip(TeamdctlStateDump)
diff --git a/insights/parsers/tests/test_version_info.py b/insights/parsers/tests/test_version_info.py
--- a/insights/parsers/tests/test_version_info.py
+++ b/insights/parsers/tests/test_version_info.py
@@ -1,5 +1,7 @@
 import doctest
+
 from insights.parsers import version_info
+from insights.parsers.tests import test_empty_skip
 from insights.tests import context_wrap
 
 
@@ -22,6 +24,10 @@ def test_version_info():
     assert ret.client_version == '3.1.1'
 
 
+def test_version_info_empty():
+    assert 'Empty output.' in test_empty_skip(version_info.VersionInfo)
+
+
 def test_doc_examples():
     env = {
         'ver': version_info.VersionInfo(context_wrap(VER_INFO_2)),
diff --git a/insights/parsers/tests/test_virt_uuid_facts.py b/insights/parsers/tests/test_virt_uuid_facts.py
--- a/insights/parsers/tests/test_virt_uuid_facts.py
+++ b/insights/parsers/tests/test_virt_uuid_facts.py
@@ -1,6 +1,7 @@
 import doctest
 
 from insights.parsers import virt_uuid_facts
+from insights.parsers.tests import test_empty_skip
 from insights.parsers.virt_uuid_facts import VirtUuidFacts
 from insights.tests import context_wrap
 
@@ -20,6 +21,10 @@ def test_virt_uuid_facts():
     assert result.data['virt.uuid'] == '4546B285-6C41-5D6R-86G5-0BFR4B3625FS'
 
 
+def test_virt_uuid_facts_empty():
+    assert 'Empty output.' in test_empty_skip(VirtUuidFacts)
+
+
 def test_virt_uuid_facts_doc_examples():
     env = {
         'VirtUuidFacts': VirtUuidFacts,