Source code for insights.parsers.nfs_exports

"""
NFS exports configuration
=========================

NFSExports and NFSExportsD provide a parsed output of the content of an exports
file as defined in ``man exports(5)``.  The content is parsed into a
dictionary, where the key is the export path and the value is another
dictionary, where the key is the hostname and the value is the option list,
parsed into an actual list.

The default (``"-"``) hostname is not specially handled, nor are wildcards.

If export paths are defined multiple times in a file, only the first one is
parsed.  All subsequent redefinitions are not parsed and the raw line is added
to the ``ignored_lines`` member.

All raw lines are kept in ``raw_lines``, which is a ``dict`` where the key is
the export path and the value is the stripped raw line.

Parsers included in this module are:

NFSExports - file ``nfs_exports``
---------------------------------

NFSExportsD - files in the ``nfs_exports.d`` directory
------------------------------------------------------

Sample content of the ``/etc/exports`` file::

    /home/utcs/shared/ro                    @group(ro,sync)   ins1.example.com(rw,sync,no_root_squash) ins2.example.com(rw,sync,no_root_squash)
    /home/insights/shared/rw                @group(rw,sync)   ins1.example.com(rw,sync,no_root_squash) ins2.example.com(ro,sync,no_root_squash)
    /home/insights/shared/special/all/mail  @group(rw,sync,no_root_squash)
    /home/insights/ins/special/all/config   @group(ro,sync,no_root_squash)  ins1.example.com(rw,sync,no_root_squash)
    #/home/insights                          ins1.example.com(rw,sync,no_root_squash)
    /home/example                           @group(rw,sync,root_squash) ins1.example.com(rw,sync,no_root_squash) ins2.example.com(rw,sync,no_root_squash)
    # A duplicate host for this exported path
    /home/example                           ins2.example.com(rw,sync,no_root_squash)

Examples:
    >>> type(exports)
    <class 'insights.parsers.nfs_exports.NFSExports'>
    >>> type(exports.data) == type({})
    True
    >>> exports.raw_lines['/home/insights/shared/rw']  # List of lines that define this path
    ['/home/insights/shared/rw                @group(rw,sync)   ins1.example.com(rw,sync,no_root_squash) ins2.example.com(ro,sync,no_root_squash)']
    >>> exports.raw_lines['/home/example']  # Lines are stored even if they contain duplicate hosts
    ['/home/example                           @group(rw,sync,root_squash) ins1.example.com(rw,sync,no_root_squash) ins2.example.com(rw,sync,no_root_squash)', '/home/example                           ins2.example.com(rw,sync,no_root_squash)']
    >>> exports.ignored_exports
    {'/home/example': {'ins2.example.com': ['rw', 'sync', 'no_root_squash']}}
    >>> sorted(list(exports.all_options()))
    ['no_root_squash', 'ro', 'root_squash', 'rw', 'sync']
    >>> sorted(list(exports.export_paths()))
    ['/home/example', '/home/insights/ins/special/all/config', '/home/insights/shared/rw', '/home/insights/shared/special/all/mail', '/home/utcs/shared/ro']
"""

from itertools import chain

from .. import Parser, parser
from . import get_active_lines
from insights.specs import Specs


[docs] class NFSExportsBase(Parser): """ Class to parse ``/etc/exports`` and ``/etc/exports.d/*.exports``. Exports are stored keyed on the path of the export, and then the host definition. The flags are stored as a list. NFS allows the same path to be listed on multiple lines and in multiple files, but an exported path can only have one definition for a given host. Attributes: data (dict): Key is export path, value is a dict, where the key is the client host and the value is a list of options. ignored_exports (dict): A dictionary of exported paths that have host definitions that conflicted with a previous definition. ignored_lines (dict): A synonym for the above `ignored_exports` dictionary, for historical reasons. raw_lines (dict of lists): The list of the raw lines that define each exported path, including any lines that may have ignored exports. """ def _parse_host(self, content): if "(" in content: host, options = content.split("(") options = options.rstrip(")").split(",") else: host, options = content, [] return host, options def _parse_line(self, line): split = [i.strip() for i in line.split()] path, hosts = split[0], dict(self._parse_host(s) for s in split[1:]) return path, hosts
[docs] def parse_content(self, content): # Exports can be duplicated, but the path-host tuple cannot: the # first read will be stored and all later path-host tuples cause # `exportfs` to generate a warning when setting up the export. self.data = {} self.ignored_exports = {} self.ignored_lines = self.ignored_exports self.raw_lines = {} for line in get_active_lines(content): path, hosts = self._parse_line(line) if path not in self.data: # New path, just add the hosts. self.data[path] = hosts self.raw_lines[path] = [line] else: # Add to raw lines even if some (or all) hosts are ignored. self.raw_lines[path].append(line) # Have to check each path-host tuple for host, flags in hosts.items(): if host not in self.data[path]: # Only add if it doesn't already exist. self.data[path][host] = flags else: if path not in self.ignored_exports: self.ignored_exports[path] = {host: flags} else: self.ignored_exports[path][host] = flags
[docs] def export_paths(self): """Returns the set of all export paths as strings""" return set(self.data.keys())
[docs] def all_options(self): """Returns the set of all options used in all export entries""" items = chain.from_iterable(hosts.values() for hosts in self.data.values()) return set(chain.from_iterable(items))
def __iter__(self): return iter(self.data.items())
[docs] @parser(Specs.nfs_exports) class NFSExports(NFSExportsBase): """Subclass to attach ``nfs_exports`` spec to""" pass
[docs] @parser(Specs.nfs_exports_d) class NFSExportsD(NFSExportsBase): """Subclass to attach ``nfs_exports.d`` spec to""" pass