Source code for insights.parsers.lsof

"""
Lsof - command ``/usr/sbin/lsof``
=================================

This parser reads the output of the ``/usr/sbin/lsof`` command and makes each
line available as a dictionary keyed on the fields in the lsof output (with
names in upper case).

Because of the large quantity of output from this command, this class is based
on the ``Scannable`` parser class.  There are several ways to use this:

* If you simply want to know whether a search matched, use the ``any`` method.
* If you want all lines that match, use the ``collect`` method.

The way these scanner functions work is:

1. You provide a function, which returns True if a match is found, and an
   attribute name.
2. The parser runs every scanner function across every line of data read and
   successfully parsed.
3. The attribute then contains the result of the match (True or False in the
   case of the ``any`` method, or the list of matching rows in the case of
   the ``collect`` method.

As an easier way of finding all the lines that match by key=value pairs, use
the ``collect_keys`` method, giving the name of the scanner attribute to set
and one or more 'key=value' pairs in the method call.  This then returns the
list of rows for which the data in all those keywords matched the respective
given values.  (**Note**: the ``SIZE/OFF`` column is searched for using the
key ``SIZE_OFF`` - see example below)

Sample output::

    COMMAND     PID  TID           USER   FD      TYPE             DEVICE  SIZE/OFF       NODE NAME
    systemd       1                root  cwd       DIR              253,1      4096        128 /
    systemd       1                root  rtd       DIR              253,1      4096        128 /
    systemd       1                root  txt       REG              253,1   1230920    1440410 /usr/lib/systemd/systemd
    systemd       1                root  mem       REG              253,1     37152  135529970 /usr/lib64/libnss_sss.so.2
    abrt-watc  8619                root    0r      CHR                1,3       0t0       4674 /dev/null
    wpa_suppl   641                root    0r      CHR                1,3       0t0       4674 /dev/null
    polkitd     642             polkitd    0u      CHR                1,3       0t0       4674 /dev/null
    polkitd     642             polkitd    1u      CHR                1,3       0t0       4674 /dev/null

Examples:

    >>> Lsof.any('systemd_commands', lambda x: 'systemd' in x['COMMAND'])
    >>> Lsof.collect('polkitd_user', lambda x: x['USER'] == 'polkitd')
    >>> Lsof.collect_keys('root_stdin', USER='root', FD='0r', SIZE_OFF='0t0')
    >>> l = shared[Lsof]
    >>> l.systemd_commands
    True
    >>> len(l.polkitd_user)
    2
    >>> l.polkitd_user[0]['DEVICE']
    '1,3'
    >>> len(l.root_stdin)
    2
    >>> l.root_stdin[0]['COMMAND']
    'abrt-watc'

"""
from insights.core import CommandParser, Scannable
from insights.core.exceptions import SkipComponent
from insights.core.filters import add_filter
from insights.core.plugins import parser
from insights.specs import Specs

add_filter(Specs.lsof, ['COMMAND'])


[docs] @parser(Specs.lsof) class Lsof(CommandParser, Scannable): """ A parser for the output of ``/usr/sbin/lsof`` - determines the column widths from the first row and then puts the data in each row into a dictionary keyed on the column name and found by the locations of each column. Leading and trailing spaces are stripped from data. """ def _calc_indexes(self, line): self.headings = [c.strip() for c in line.split(None)] self.indexes = {} for heading in self.headings: index = line.index(heading) # (start, end) self.indexes[heading] = (index, index + len(heading)) def _start(self, content): """ Consumes lines from content until the HEADER is found and processed. Returns an iterator over the remaining lines. """ start = len(content) for n, line in enumerate(content): if 'COMMAND ' in line: start = n + 1 break if start >= len(content): raise SkipComponent content = iter(content[start:]) self._calc_indexes(line) return content def _parse_line(self, line): """ Given a line, returns a dictionary for that line. Requires _start to be called first. """ # Split NAME seperately as it can have extra whitespaces command = line[self.indexes['NAME'][0]:].strip() rest = line[:self.indexes['NAME'][0]] rdict = dict.fromkeys(self.headings, '') rowsplit = [i.strip() for i in rest.split(None, len(self.headings) - 2)] if len(rowsplit) < len(self.headings) - 1: rowsplit = iter(rowsplit) for heading in self.headings[:-1]: # Use value if (start, end) index of heading is not empty if line[slice(*self.indexes[heading])].strip(): rdict[heading] = next(rowsplit, '') else: rdict = dict(zip(self.headings, rowsplit)) rdict['NAME'] = command return rdict
[docs] def parse(self, content): """ Parse the content for the entire input file. """ for line in self._start(content): yield self._parse_line(line)
[docs] @classmethod def collect_keys(cls, result_key, **kwargs): """ Store a list of lines having keyword=value matches in the given attribute name. Keyword argument names that exist as column names in the data are searched for in the lines of the file and stored in the ``result_key`` attribute. All columns must match (i.e. this is a bolean AND search). Call this class method before using the class data. Examples: collect_keys('root_block_devs', USER='root', TYPE='BLK') """ def scanner(self, obj): # Have to set the attribute so it exists, even if it has no rows if not hasattr(self, result_key): setattr(self, result_key, []) # Minor hack - search for 'SIZE/OFF' as 'SIZE_OFF'. if 'SIZE_OFF' in kwargs: kwargs['SIZE/OFF'] = kwargs['SIZE_OFF'] # Filter the keywords for the list of column names requested. for key in [k for k in kwargs if k in obj]: # If we have a keyword whose value doesn't match the given # item, then skip out now. if obj[key] != kwargs[key]: return # OK, save the item now. getattr(self, result_key).append(obj) cls._scan(result_key, scanner)