Repository: RedHatInsights/insights-core

Issue:
Need to enhance parser "ls_parser" to parse the output of  command 'ls -laRZ <dir-name>' in RHEL-8 
The parser **ls_parser** needs to be enhanced to support parsing the output of command `ls` with an option `Z` (for selinux context) in a RHEL8 system. In RHEL8, additional fields have been introduced in the output of command `ls -laRZ`. For example:

RHEL7: 

```
#/bin/ls -laRZ /var/lib/nova/instances
/var/lib/nova/instances:
drwxr-xr-x. nova nova system_u:object_r:nova_var_lib_t:s0 .
drwxr-xr-x. nova nova system_u:object_r:nova_var_lib_t:s0 ..
drwxr-xr-x. root root unconfined_u:object_r:nova_var_lib_t:s0 xxxx-xxxx-xxxx-xxxx
```

RHEL8: 

```
#/bin/ls -laRZ /var/lib/nova/instances
/var/lib/nova/instances:
total 0
drwxr-xr-x. 3 root root unconfined_u:object_r:var_lib_t:s0 50 Apr  8 16:41 .
drwxr-xr-x. 3 root root unconfined_u:object_r:var_lib_t:s0 23 Apr  8 16:29 ..
drwxr-xr-x. 2 root root unconfined_u:object_r:var_lib_t:s0 54 Apr  8 16:41 xxxx-xxxx-xxxx-xxxx
```


Discussion:

Resolving patch:
diff --git a/insights/core/ls_parser.py b/insights/core/ls_parser.py
--- a/insights/core/ls_parser.py
+++ b/insights/core/ls_parser.py
@@ -76,7 +76,7 @@ def parse_selinux(parts):
 
     Returns:
         A dict containing owner, group, se_user, se_role, se_type, se_mls, and
-        name. If the raw name was a symbolic link, link is always included.
+        name. If the raw name was a symbolic link, link is also included.
 
     """
 
@@ -98,6 +98,47 @@ def parse_selinux(parts):
     return result
 
 
+def parse_rhel8_selinux(parts):
+    """
+    Parse part of an ls output line that is selinux on RHEL8.
+
+    Args:
+        parts (list): A four element list of strings representing the initial
+            parts of an ls line after the permission bits. The parts are link
+            count, owner, group, and everything else
+
+    Returns:
+        A dict containing links, owner, group, se_user, se_role, se_type,
+        se_mls, size, date, and name. If the raw name was a symbolic link,
+        link is also included.
+
+    """
+
+    links, owner, group, last = parts
+
+    selinux = parts[3].split(":")
+    lsel = len(selinux)
+    selinux, size, last = parts[-1].split(None, 2)
+    selinux = selinux.split(":")
+    date = last[:12]
+    path, link = parse_path(last[13:])
+    result = {
+        "links": int(links),
+        "owner": owner,
+        "group": group,
+        "se_user": selinux[0],
+        "se_role": selinux[1] if lsel > 1 else None,
+        "se_type": selinux[2] if lsel > 2 else None,
+        "se_mls": selinux[3] if lsel > 3 else None,
+        "size": int(size),
+        "name": path,
+        "date": date,
+    }
+    if link:
+        result["link"] = link
+    return result
+
+
 PASS_KEYS = set(["name", "total"])
 DELAYED_KEYS = ["entries", "files", "dirs", "specials"]
 
@@ -137,6 +178,10 @@ def _load(self):
         files = []
         specials = []
         for line in self.body:
+            # we can't split(None, 5) here b/c rhel 6/7 selinux lines only have
+            # 4 parts before the path, and the path itself could contain
+            # spaces. Unfortunately, this means we have to split the line again
+            # below
             parts = line.split(None, 4)
             perms = parts[0]
             typ = perms[0]
@@ -145,7 +190,13 @@ def _load(self):
                 "perms": perms[1:]
             }
             if parts[1][0].isdigit():
-                rest = parse_non_selinux(parts[1:])
+                # We have to split the line again to see if this is a RHEL8
+                # selinux stanza. This assumes that the context section will
+                # always have at least two pieces separated by ':'.
+                if ":" in line.split()[4]:
+                    rest = parse_rhel8_selinux(parts[1:])
+                else:
+                    rest = parse_non_selinux(parts[1:])
             else:
                 rest = parse_selinux(parts[1:])
 
@@ -220,5 +271,6 @@ def parse(lines, root=None):
             continue
         entries.append(line)
     name = name or root
-    doc[name] = Directory(name, total or len(entries), entries)
+    total = total if total is not None else len(entries)
+    doc[name] = Directory(name, total, entries)
     return doc


Test patch:
diff --git a/insights/tests/test_ls_parser.py b/insights/tests/test_ls_parser.py
--- a/insights/tests/test_ls_parser.py
+++ b/insights/tests/test_ls_parser.py
@@ -112,6 +112,14 @@
 -rw-r--r--. root root system_u:object_r:boot_t:s0      initramfs-0-rescue
 """
 
+RHEL8_SELINUX_DIRECTORY = """
+/var/lib/nova/instances:
+total 0
+drwxr-xr-x. 3 root root unconfined_u:object_r:var_lib_t:s0 50 Apr  8 16:41 .
+drwxr-xr-x. 3 root root unconfined_u:object_r:var_lib_t:s0 23 Apr  8 16:29 ..
+drwxr-xr-x. 2 root root unconfined_u:object_r:var_lib_t:s0 54 Apr  8 16:41 abcd-efgh-ijkl-mnop
+"""
+
 FILES_CREATED_WITH_SELINUX_DISABLED = """
 /dev/mapper:
 total 2
@@ -285,3 +293,22 @@ def test_bad_line():
     assert res["date"] == "Aug  4 16:56"
     assert res["name"] == "dm-10"
     assert res["dir"] == "/tmp"
+
+
+def test_rhel8_selinux():
+    results = parse(RHEL8_SELINUX_DIRECTORY.splitlines(), "/var/lib/nova/instances")
+    assert len(results) == 1
+    assert results["/var/lib/nova/instances"]["name"] == "/var/lib/nova/instances", results["/var/lib/nova/instances"]["name"]
+    res = results["/var/lib/nova/instances"]["entries"]["abcd-efgh-ijkl-mnop"]
+    assert results["/var/lib/nova/instances"]["total"] == 0, results["/var/lib/nova/instances"]["total"]
+    assert res["type"] == "d"
+    assert res["links"] == 2
+    assert res["owner"] == "root"
+    assert res["group"] == "root"
+    assert res["se_user"] == "unconfined_u"
+    assert res["se_role"] == "object_r"
+    assert res["se_type"] == "var_lib_t"
+    assert res["se_mls"] == "s0"
+    assert res["date"] == "Apr  8 16:41"
+    assert res["name"] == "abcd-efgh-ijkl-mnop"
+    assert res["dir"] == "/var/lib/nova/instances"