pvl/dhcp/config.py
author Tero Marttila <terom@paivola.fi>
Tue, 17 Dec 2013 01:15:37 +0200
changeset 299 df653511caf9
parent 256 63b285ceae41
child 476 4b792c44cf05
permissions -rw-r--r--
pvl.hosts.Host.fqnd(): assume any host with a . is a fqdn
"""
    Simple parser for ISC dhcpd config files.
"""

import logging; log = logging.getLogger('pvl.dhcp.config')

class DHCPConfigParser (object) :
    """
        Simplistic parser for a dhcpd.leases file.

        Doesn't implement the full spec, but a useful approximation.
    """

    @classmethod
    def load (cls, file) :
        return cls().parse_file(file)

    def __init__ (self) :
        self.stack = []
        self.block = None
        self.items = []
        self.blocks = []
    
    @classmethod
    def split (cls, line) :
        """
            Split given line-data.
            
            >>> split = DHCPConfigParser.split
            >>> split('foo bar')
            ['foo', 'bar']
            >>> split('"foo"')
            ['foo']
            >>> split('foo "asdf quux" bar')
            ['foo', 'asdf quux', 'bar']
            >>> split('foo "asdf quux"')
            ['foo', 'asdf quux']
        """

        # parse out one str
        if '"' in line :
            log.debug("%s", line)

            # crude
            pre, line = line.split('"', 1)
            data, post = line.rsplit('"', 1)

            return pre.split() + [data] + post.split()
        else :
            return line.split()

    @classmethod
    def lex (self, line) :
        """
            Yield tokens from the given lines.

            >>> lex = DHCPConfigParser.lex
            >>> list(lex('foo;'))
            [('item', ['foo'])]
            >>> list(item for line in ['foo {', ' bar;', '}'] for item in lex(line))
            [('open', ['foo']), ('item', ['bar']), ('close', None)]

        """

        log.debug("%s", line)

        # comments?
        if '#' in line :
            line, comment = line.split('#', 1)
        else :
            comment = None

        # clean?
        line = line.strip()

        # parse
        if not line :
            # ignore, empty/comment
            return
        
        elif line.startswith('uid') :
            # XXX: too hard to parse properly
            return

        elif '{' in line :
            decl, line = line.split('{', 1)

            # we are in a new decl
            yield 'open', self.split(decl)
       
        elif ';' in line :
            param, line = line.split(';', 1)
            
            # a stanza
            yield 'item', self.split(param)
        
        elif '}' in line :
            close, line = line.split('}', 1)

            if close.strip() :
                log.warn("Predata on close: %s", close)

            # end
            yield 'close', None
    
        else :
            log.warn("Unknown line: %s", line)
            return

        # got the whole line?
        if line.strip() :
            log.warn("Data remains: %s", line)

    def push_block (self, block) :
        """
            Open new block.
        """

        self.stack.append((self.block, self.items, self.blocks))

        self.block = block
        self.items = []
        self.blocks = []

    def feed_item (self, item) :
        """
            Add item to block
        """

        self.items.append(item)

    def pop_block (self) :
        """
            Close block. Returns
                (block, [items])
        """

        assert self.block

        block = (self.block, self.items, self.blocks)

        self.block, self.items, self.blocks = self.stack.pop(-1)

        self.blocks.append(block)

        return block

    def parse_line (self, line) :
        """
            Parse given line, yielding any complete blocks that come out.

            Yields (block, [ lines ]) tuples.

            >>> parser = DHCPConfigParser()
            >>> list(parser.parse_lines(['foo {', ' bar;', ' quux asdf;', '}']))
            [(['foo'], [['bar'], ['quux', 'asdf']], [])]

            >>> parser = DHCPConfigParser()
            >>> list(parser.parse_line('foo {'))
            []
            >>> list(parser.parse_lines([' bar;', ' quux asdf;']))
            []
            >>> list(parser.parse_line('}'))
            [(['foo'], [['bar'], ['quux', 'asdf']], [])]
        """

        for token, args in self.lex(line) :
            #log.debug("%s: %s [block=%s]", token, args, self.block)

            if token == 'open' :
                # open new block
                block = args

                if self.block :
                    log.debug("nested block: %s > %s", self.block, block)
                else :
                    log.debug("open block: %s", block)

                self.push_block(block)
            
            elif token == 'close' :
                log.debug("close block: %s", self.block)

                # collected block items
                yield self.pop_block()

            # must be within block!
            elif token == 'item' :
                item = args

                log.debug("block %s item: %s", self.block, item)
                self.feed_item(item)

            else :
                # ???
                raise KeyError("Unknown token: {0}: {1}".format(token, args))
    
    def parse_lines (self, lines) :
        """
            Trivial wrapper around parse to parse multiple lines.
        """

        for line in lines :
            for item in self.parse_line(line) :
                yield item

    def parse_file (self, file) :
        """
            Parse an entire file, returning (items, blocks) lists.

            >>> DHCPConfigParser().parse_file(['foo;', 'barfoo {', 'bar;', '}'])
            ([['foo']], [(['barfoo'], [['bar']], [])])
        """

        for line in file :
            for item in self.parse_line(line) :
                log.debug("%s", item)

        assert not self.block

        return self.items, self.blocks