Search code examples
pythondictionaryconfigiscsi

parsing linux iscsi multipath.conf into python nested dictionaries


I writing a script that involves adding/removing multipath "objects" from the standard multipath.conf configuration file, example below:

# This is a basic configuration file with some examples, for device mapper
# multipath.

## Use user friendly names, instead of using WWIDs as names.

defaults {
                user_friendly_names yes
         }

##
devices {
        device {
                vendor "SolidFir"
                product "SSD SAN"
                path_grouping_policy multibus
                getuid_callout "/lib/udev/scsi_id --whitelisted --device=/dev/%n"
                path_selector "service-time 0"
                path_checker tur
                hardware_handler "0"
                failback immediate
                rr_weight uniform
                rr_min_io 1000
                rr_min_io_rq 1
                features "0"
                no_path_retry 24
                prio const
        }
}

multipaths {
        multipath {
                wwid 36f47acc1000000006167347a00000041
                alias dwqa-ora-fs
        }
        multipath {
                wwid 36f47acc1000000006167347a00000043
                alias dwqa-ora-grid
        }
        multipath {
                wwid 36f47acc1000000006167347a00000044
                alias dwqa-ora-dwqa1
        }
        multipath {
                wwid 36f47acc1000000006167347a000000ae
                alias dwqa-ora-dwh2d10-1
        }
        multipath {
                wwid 36f47acc1000000006167347a000000f9
                alias dwqa-ora-testdg-1
        }
}

So what I'm trying to do is read this file in and store it in a nested python dictionary (or list of nested dictionaries). We can ignore the comments lines (starting with #) for now. I have not come up with a clear/concise solution for this.

Here is my partial solution (doesn't give me the expected output yet, but it's close)

def nonblank_lines(f):
    for l in f:
        line = l.rstrip()
        if line:
            yield line

def __parse_conf__(self):
    conf = []
    with open(self.conf_file_path) as f:
        for line in nonblank_lines(f):
            if line.strip().endswith("{"): # opening bracket, start of new list of dictionaries
                current_dictionary_key = line.split()[0]
                current_dictionary = { current_dictionary_key : None }
                conf.append(current_dictionary)

            elif line.strip().endswith("}"): # closing bracket, end of new dictionary
                pass
                # do nothing...

            elif not line.strip().startswith("#"):
                if current_dictionary.values() == [None]:
                    # New dictionary... we should be appending to this one
                    current_dictionary[current_dictionary_key] = [{}]
                    current_dictionary = current_dictionary[current_dictionary_key][0]
                key = line.strip().split()[0]
                val = " ".join(line.strip().split()[1:])
                current_dictionary[key] = val

And this is the resulting dictionary (the list 'conf'):

[{'defaults': [{'user_friendly_names': 'yes'}]},
 {'devices': None},
 {'device': [{'failback': 'immediate',
              'features': '"0"',
              'getuid_callout': '"/lib/udev/scsi_id --whitelisted --device=/dev/%n"',
              'hardware_handler': '"0"',
              'no_path_retry': '24',
              'path_checker': 'tur',
              'path_grouping_policy': 'multibus',
              'path_selector': '"service-time 0"',
              'prio': 'const',
              'product': '"SSD SAN"',
              'rr_min_io': '1000',
              'rr_min_io_rq': '1',
              'rr_weight': 'uniform',
              'vendor': '"SolidFir"'}]},
 {'multipaths': None},
 {'multipath': [{'alias': 'dwqa-ora-fs',
                 'wwid': '36f47acc1000000006167347a00000041'}]},
 {'multipath': [{'alias': 'dwqa-ora-grid',
                 'wwid': '36f47acc1000000006167347a00000043'}]},
 {'multipath': [{'alias': 'dwqa-ora-dwqa1',
                 'wwid': '36f47acc1000000006167347a00000044'}]},
 {'multipath': [{'alias': 'dwqa-ora-dwh2d10-1',
                 'wwid': '36f47acc1000000006167347a000000ae'}]},
 {'multipath': [{'alias': 'dwqa-ora-testdg-1',
                 'wwid': '36f47acc1000000006167347a000000f9'}]},
 {'multipath': [{'alias': 'dwqa-ora-testdp10-1',
                 'wwid': '"SSolidFirSSD SAN 6167347a00000123f47acc0100000000"'}]}]

Obviously the "None"s should be replaced with nested dictionary below it, but I can't get this part to work.

Any suggestions? Or better ways to parse this file and store it in a python data structure?


Solution

  • Try something like this:

    def parse_conf(conf_lines):
        config = []
    
        # iterate on config lines
        for line in conf_lines:
            # remove left and right spaces
            line = line.rstrip().strip()
    
            if line.startswith('#'):
                # skip comment lines
                continue
            elif line.endswith('{'):
                # new dict (notice the recursion here)
                config.append({line.split()[0]: parse_conf(conf_lines)})
            else:
                # inside a dict
                if line.endswith('}'):
                    # end of current dict
                    break
                else:
                    # parameter line
                    line = line.split()
                    if len(line) > 1:
                        config.append({line[0]: " ".join(line[1:])})
        return config
    

    The function will get into the nested levels on the configuration file (thanks to recursion and the fact that the conf_lines object is an iterator) and make a list of dictionaries that contain other dictionaries. Unfortunately, you have to put every nested dictionary inside a list again, because in the example file you show how multipath can repeat, but in Python dictionaries a key must be unique. So you make a list.

    You can test it with your example configuration file, like this:

    with open('multipath.conf','r') as conf_file:
        config = parse_conf(conf_file)
    
        # show multipath config lines as an example
        for item in config:
            if 'multipaths' in item:
                for multipath in item['multipaths']:
                    print multipath
                    # or do something more useful
    

    And the output would be:

    {'multipath': [{'wwid': '36f47acc1000000006167347a00000041'}, {'alias': 'dwqa-ora-fs'}]}
    {'multipath': [{'wwid': '36f47acc1000000006167347a00000043'}, {'alias': 'dwqa-ora-grid'}]}
    {'multipath': [{'wwid': '36f47acc1000000006167347a00000044'}, {'alias': 'dwqa-ora-dwqa1'}]}
    {'multipath': [{'wwid': '36f47acc1000000006167347a000000ae'}, {'alias': 'dwqa-ora-dwh2d10-1'}]}
    {'multipath': [{'wwid': '36f47acc1000000006167347a000000f9'}, {'alias': 'dwqa-ora-testdg-1'}]}