Repository: RedHatInsights/insights-core

Issue:
Mount options can contain quoted values with embedded commas
If a mount option is quoted and contains embedded commas it will not be parsed correctly.  For example context will be split in the current [ProcMounts](https://github.com/RedHatInsights/insights-core/blob/master/insights/parsers/mount.py#L280) parser:

```
tmpfs /var/lib/containers/storage/overlay-containers/ff7e79fc09c/userdata/shm tmpfs rw,nosuid,nodev,noexec,relatime,context="system_u:object_r:container_file_t:s0:c184,c371",size=64000k 0 0
```

The split options will be `[..., 'context="system_u:object_r:container_file_t:s0:c184', 'c371"',...]`.

This is actually caused by the shared function [optlist_to_dict](https://github.com/RedHatInsights/insights-core/blob/master/insights/parsers/__init__.py#L39) which doesn't handle quoted values.


Discussion:

Resolving patch:
diff --git a/insights/parsers/mount.py b/insights/parsers/mount.py
--- a/insights/parsers/mount.py
+++ b/insights/parsers/mount.py
@@ -43,7 +43,7 @@
 from insights.core import CommandParser
 from insights.core.exceptions import ParseException, SkipComponent
 from insights.core.plugins import parser
-from insights.parsers import get_active_lines, keyword_search, optlist_to_dict
+from insights.parsers import get_active_lines, keyword_search
 from insights.specs import Specs
 
 
@@ -267,7 +267,7 @@ def _parse_mounts(self, content):
             mount['mount_point'] = line_sp[0]
             mount['mount_type'] = line_sp[1].split()[0]
             line_sp = _customized_split(raw=line, l=mnt_pt_sp[1], sep=None, check=False)
-            mount['mount_options'] = MountOpts(optlist_to_dict(line_sp[0].strip('()')))
+            mount['mount_options'] = MountOpts(_parse_mount_options(line_sp[0].strip('()')))
             if len(line_sp) == 2:
                 mount['mount_label'] = line_sp[1]
 
@@ -333,7 +333,7 @@ def _parse_mounts(self, content):
             line_sp = _customized_split(raw=line, l=line_sp[1], num=3, reverse=True)
             mount['mount_label'] = line_sp[-2:]
             line_sp = _customized_split(raw=line, l=line_sp[0], reverse=True)
-            mount['mount_options'] = MountOpts(optlist_to_dict(line_sp[1]))
+            mount['mount_options'] = MountOpts(_parse_mount_options(line_sp[1]))
             line_sp = _customized_split(raw=line, l=line_sp[0], reverse=True)
             mount['mount_type'] = mount['filesystem_type'] = line_sp[1]
             mount['mount_point'] = line_sp[0]
@@ -400,7 +400,7 @@ def _parse_mounts(self, content):
             optional_fields = line_left_sp[5].split()[1] if ' ' in line_left_sp[5] else ''
             mntopt = line_left_sp[5].split()[0] if ' ' in line_left_sp[5] else line_left_sp[5]
             unioned_options = ','.join([mntopt, line_right_sp[2]])
-            mount['mount_options'] = MountOpts(optlist_to_dict(unioned_options))
+            mount['mount_options'] = MountOpts(_parse_mount_options(unioned_options))
             mount['mount_addtlinfo'] = MountAddtlInfo({
                     'mount_id': line_left_sp[0],
                     'parent_id': line_left_sp[1],
@@ -414,6 +414,43 @@ def _parse_mounts(self, content):
         self.mounts = dict([mnt['mount_point'], rows[idx]] for idx, mnt in enumerate(rows))
 
 
+def _parse_mount_options(mount_options):
+    """
+    Parse mount options including quoted values with embedded commas.
+
+    Args:
+        mount_options (str): String of comma separated mount options and values
+
+    Returns:
+        dict: Dictionary of mount options
+
+    """
+    opts = dict()
+    sp_opts = mount_options.split(',')
+    start_ndx = 0
+    for i, opt in enumerate(sp_opts):
+        # Look for option="... start of quoted value
+        if '="' in opt:
+            start_ndx = i
+
+        # Look for closing quote of option="..." and if found recombine value
+        elif '"' in opt:
+            last_ndx = i
+            comb_opt = ','.join(sp_opts[start_ndx:last_ndx + 1])
+            opt_name, opt_value = comb_opt.split('=', 1)
+            # Remove leading and trailing quotes
+            opts[opt_name] = opt_value[1:-1]
+
+        # Else just a normal option or option=value
+        else:
+            if '=' in opt:
+                opt_name, opt_value = opt.split('=', 1)
+                opts[opt_name] = opt_value
+            else:
+                opts[opt] = True
+    return opts
+
+
 def _customized_split(raw, l, sep=None, num=2, reverse=False, check=True):
     if num >= 2:
         if reverse is False:


Test patch:
diff --git a/insights/tests/parsers/test_mount.py b/insights/tests/parsers/test_mount.py
--- a/insights/tests/parsers/test_mount.py
+++ b/insights/tests/parsers/test_mount.py
@@ -58,6 +58,11 @@
 /etc/auto.misc /misc autofs rw,relatime,fd=7,pgrp=1936,timeout=300,minproto=5,maxproto=5,indirect 0 0
 """.strip()
 
+PROC_MOUNT_QUOTES = """
+/dev/mapper/rootvg-rootlv / ext4 rw,relatime,barrier=1,data=ordered 0 0
+tmpfs /var/lib/containers/storage/overlay-containers/ff7e79fc09c/userdata/shm tmpfs rw,nosuid,nodev,noexec,relatime,context="system_u:object_r:container_file_t:s0:c184,c371",size=64000k 0 0
+"""
+
 PROCMOUNT_ERR_DATA = """
 rootfs / rootfs rw 0 0
 sysfs /sys sysfs rw,relatime
@@ -266,6 +271,14 @@ def test_proc_mount():
     ]
 
 
+def test_proc_mount_quotes():
+    results = ProcMounts(context_wrap(PROC_MOUNT_QUOTES))
+    assert results is not None
+    assert len(results) == 2
+    device = results['/var/lib/containers/storage/overlay-containers/ff7e79fc09c/userdata/shm']
+    assert device.mount_options.context == "system_u:object_r:container_file_t:s0:c184,c371"
+
+
 def test_proc_mount_exception1():
     with pytest.raises(SkipComponent) as e:
         ProcMounts(context_wrap(PROC_EXCEPTION1))