Lukas Bednar has uploaded a new change for review.

Change subject: Implementation of machine dialog parser for python
......................................................................

Implementation of machine dialog parser for python

Change-Id: Ia7b715a39840e548ef768ac9e77004cc0d98a9c0
Signed-off-by: Lukas Bednar <lbed...@redhat.com>
---
A src/otopi/machine_dialog_parser.py
1 file changed, 508 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.ovirt.org:29418/otopi refs/changes/80/28180/1

diff --git a/src/otopi/machine_dialog_parser.py 
b/src/otopi/machine_dialog_parser.py
new file mode 100644
index 0000000..d7faebe
--- /dev/null
+++ b/src/otopi/machine_dialog_parser.py
@@ -0,0 +1,508 @@
+#
+# otopi -- plugable installer
+# Copyright (C) 2012-2013 Red Hat, Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+
+
+"""
+Module implements machine dialog parser.
+Please refer to README.dialog.
+"""
+
+
+import re
+import logging
+import gettext
+_ = lambda m: gettext.dgettext(message=m, domain='otopi')
+
+
+from . import constants
+from . import util
+
+
+class ParseError(Exception):
+    pass
+
+
+class UnexpectedEOF(ParseError):
+    pass
+
+
+class HeadDoesNotMatch(ParseError):
+    pass
+
+
+class DialogError(ParseError):
+    pass
+
+
+class UnexpectedEventError(DialogError):
+    pass
+
+
+class Event(object):
+    """
+    Base class for otopi dialog events.
+    """
+    head_regex = None
+
+    @classmethod
+    def _parse_head(cls, head):
+        match = cls.head_regex.match(head)
+        if match is None:
+            raise HeadDoesNotMatch(head, cls)
+        return match.groupdict()
+
+    @classmethod
+    def create(cls, head, stream):
+        return cls(**cls._parse_head(head))
+
+    def __str__(self):
+        params = [("%s=%s" % (k, v)) for k, v in self.__dict__.iteritems()]
+        return "%s(%s)" % (self.__class__.__name__, ", ".join(params))
+
+
+@util.export
+class Replyable(object):
+    """
+    Add 'value' attribute to Event, in order to replay.
+    """
+    def __init__(self, *args, **kwargs):
+        super(Replyable, self).__init__()
+        self.value = None
+
+    def response(self):
+        return self._response()
+
+    def _response(self):
+        raise NotImplementedError()
+
+
+@util.export
+class Abortable(Replyable):
+    """
+    Add 'abort' atribute to Event, in order to abort event.
+    """
+    def __init__(self, *args, **kwargs):
+        super(Abortable, self).__init__()
+        self.abort = False
+
+    def _abort(self):
+        raise NotImplementedError()
+
+    def response(self):
+        if self.abort:
+            return self._abort()
+        return super(Abortable, self).response()
+
+
+@util.export
+class NoteEvent(Event):
+    """
+    ^#+ (.*)\n$
+        Group1 - message.
+
+    Every line that begins with '#' is ignored by manager. Notes are
+    used to interact with humans.
+    """
+
+    head_regex = re.compile('^#+ (?P<note>.*)$')
+
+    def __init__(self, note):
+        super(NoteEvent, self).__init__()
+        self.note = note
+
+
+@util.export
+class TerminateEvent(Event):
+    """
+    ^***TERMINATE\n$
+
+    Termiante dialog.
+    """
+    head_regex = re.compile('^[*]{3}TERMINATE$')
+
+
+@util.export
+class LogEvent(Event):
+    """
+    ^***L:INFO (.*)\n$
+    ^***L:WARNING (.*)\n$
+    ^***L:ERROR (.*)\n$
+        Group1 - message.
+    """
+    head_regex = re.compile('^[*]{3}L:(?P<severity>INFO|WARNING|ERROR) '
+                            '(?P<message>.*)$')
+
+    def __init__(self, severity, message):
+        super(LogEvent, self).__init__()
+        self.severity = severity
+        self.message = message
+
+
+class QueryEvent(Event):
+    """
+    Query based events.
+    """
+    def __init__(self, name):
+        super(QueryEvent, self).__init__()
+        self.name = name
+
+
+@util.export
+class QueryString(QueryEvent, Replyable):
+    """
+    ^***Q:STRING (.*)\n$
+        Group1: variable name.
+        Response:
+            Single line response.
+    """
+    head_regex = re.compile('^[*]{3}Q:STRING (?P<name>.*)$')
+
+    def _response(self):
+        if not isinstance(self.value, basestring) or '\n' in self.value:
+            msg = "QueryString.value must be single-line string, got: %s"
+            raise TypeError(msg % self.value)
+        return self.value
+
+
+@util.export
+class QueryMultiString(QueryEvent, Abortable):
+    """
+    ^***Q:MULTI-STRING (.*) (.*) (.*)\n$
+        Group1: variable name.
+        Group2: boundary.
+        Group3: abort boundary.
+        Response:
+            Multiple line response.
+            Boundary at own line marks end.
+    """
+    head_regex = re.compile('^[*]{3}Q:MULTI-STRING '
+                            '(?P<name>[^ ]+) '
+                            '(?P<boundary>[^ ]+) '
+                            '(?P<abort_boundary>[^ ]+)$')
+
+    def __init__(self, name, boundary, abort_boundary):
+        super(QueryMultiString, self).__init__(name)
+        self.boundary = boundary
+        self.abort_boundary = abort_boundary
+
+    def _response(self):
+        return "%s%s" % (''.join(self.value), self.boundary)
+
+    def _abort(self):
+        return self.abort_boundary
+
+
+@util.export
+class QueryValue(QueryEvent, Abortable):
+    """
+    ^***Q:VALUE (.*)\n$
+        Group1: variable name.
+        Response:
+            ^VALUE (.*)=(.*):(.*)\n$
+            Group1: variable name.
+            Group2: variable type.
+            Group3: variable value.
+        Response:
+            ^ABORT (.*)\n$
+            Group1: variable name.
+    """
+    head_regex = re.compile('^[*]{3}Q:VALUE (?P<name>.*)$')
+
+    def _response(self):
+        type_ = type(self.value).__name__
+        if type_ == 'NoneType':
+            type_ = 'none'
+        return "VALUE %s=%s:%s" % (self.name, type, self.value)
+
+    def _abort(self):
+        return "ABORT %s" % self.name
+
+
+@util.export
+class ConfirmEvent(Event, Abortable):
+    """
+    ^***CONFIRM (.*) (.*)$
+        Group1: id.
+        Group2: description.
+        Response:
+            ^CONFIRM (.*)=(yes|no)\n$
+            Group1: id
+            Group2: response
+        Response:
+            ^ABORT (.*)\n$
+            Group1: variable name.
+    """
+    head_regex = re.compile('^[*]{3}CONFIRM (?P<id_>[^ ]+) '
+                            '(?P<description>.*)$')
+    options = ('yes', 'no')
+
+    def __init__(self, id_, description):
+        super(ConfirmEvent, self).__init__()
+        self.id_ = id_
+        self.description = description
+
+    def _response(self):
+        if (not isinstance(self.value, basestring) or
+                self.value not in self.options):
+            msg = "ConfirmEvent.value must be 'yes' or 'no' got: %s"
+            raise TypeError(msg % self.value)
+        return "CONFIRM %s=%s" % (self.id_, self.value)
+
+    def _abort(self):
+        return "ABORT %s" % self.id_
+
+
+class DisplayEvent(Event):
+    """
+    Display based events.
+    """
+    def __init__(self, name):
+        super(DisplayEvent, self).__init__()
+        self.name = name
+
+
+@util.export
+class DisplayValue(DisplayEvent):
+    """
+    ^***D:VALUE (.*)=(.*):(.*)\n$
+        Group1: variable name.
+        Group2: type.
+        Group3: value.
+    """
+    head_regex = re.compile('^[*]{3}D:VALUE '
+                            '(?P<name>[^=]+)='
+                            '(?P<type_>[^:]+):'
+                            '(?P<value>.*)$')
+    types = {'none': lambda x: None,
+             'str': str,
+             'int': int,
+             'bool': lambda x: x.lower() == 'true'}
+
+    def __init__(self, name, type_, value):
+        super(DisplayValue, DisplayEvent).__init__(name)
+        self.value = self.types[type_.lower()](value)
+
+
+@util.export
+class DisplayMultiString(DisplayEvent):
+    """
+    ^***D:MULTI-STRING (.*) (.*)\n$
+    (^.*\n$)*
+    ^(.*)\n$
+        Group1: variable name.
+        Group2: boundary.
+        Group3: content.
+        Group4: boundary.
+    """
+    head_regex = re.compile('^[*]{3}D:MULTI-STRING '
+                            '(?P<name>[^ ]+) '
+                            '(?P<boundary>.*)$')
+
+    def __init__(self, name, boundary, value=None):
+        super(DisplayValue, self).__init__(name)
+        self.boundary = boundary
+        self.value = value
+
+    def _process_multi_line(self, stream):
+        self.value = []
+        while True:
+            line = stream.readline()
+            if not line:
+                raise UnexpectedEOF()
+            if line.strip() != self.boundary:
+                self.value.append(line)
+            else:
+                break
+
+    @classmethod
+    def create(cls, head, stream):
+        event = super(DisplayMultiString, cls).create(head, stream)
+        event._process_multi_line(stream)
+        return event
+
+
+@util.export
+class MachineDialogParser(object):
+    """
+    Machine dialog parser.
+    """
+    logger = logging.getLogger(name=constants.Log.LOGGER_BASE)
+    event_types = (NoteEvent,
+                   TerminateEvent,
+                   LogEvent,
+                   QueryString,
+                   QueryMultiString,
+                   QueryValue,
+                   ConfirmEvent,
+                   DisplayValue,
+                   DisplayMultiString)
+
+    def __init__(self, input_=None, output=None, filter_=tuple()):
+        """
+        Keyword arguments:
+        input_ -- file like object
+        output -- file like object
+        filter_ -- list of events which should be skipped by parser.
+        """
+        super(MachineDialogParser, self).__init__()
+        self.output = None
+        self.input_ = None
+        self.set_streams(input_, output)
+        self.filter_ = None
+        self.set_filter(filter_)
+
+    def _write(self, data):
+        """
+        Writes data to output stream
+
+        Keyword arguments:
+        data -- string to be written
+        """
+        self.logger.debug("writing data {{{\n%s\n}}}", data)
+        self.output.write(data)
+        self.output.write('\n')
+        self.output.flush()
+
+    def set_filter(self, filter_):
+        """
+        Set list of events which should be skipped by parser.
+
+        Keyword arguments:
+        filter_ -- list of events
+        """
+        for event in filter_:
+            if not issubclass(event, Event):
+                raise TypeError("%s is not Event's subclass" % event)
+            if issubclass(event, Replyable):
+                if not issubclass(event, Abortable):
+                    raise TypeError("%s requires response" % event)
+
+    def set_streams(self, input_, output):
+        self.input_ = input_
+        self.output = output
+
+    def _filter(self, event):
+        if event.__class__ in self.filter_:
+            self.logger.debug("Event is in filter, skipping: %s", event)
+            if isinstance(event, Abortable):
+                self.logger.warn(
+                    "Skipped event requires response, aborting: %s", event)
+                event.abort = True
+                self.send_response(event)
+            return True
+        return False
+
+    def next_event(self):
+        """
+        Returns instance of Event
+        """
+        line = self.input_.readline()
+        if not line:
+            raise UnexpectedEOF()
+        for event_type in self.event_types:
+            try:
+                event = event_type.create(self.input_)
+                if self._filter(event):
+                    continue
+                self.logger.debug("Next event: %s", event)
+                return event
+            except HeadDoesNotMatch:
+                continue
+        raise HeadDoesNotMatch(line, self.event_types)
+
+    def send_response(self, event):
+        """
+        Sends response for Replyable events.
+
+        Keyword arguments:
+        event -- instance of Replyable event.
+        """
+        self._write(event.response())
+
+    # NOTE: all these methods doesn't fit here,
+    # I would move it to separate class.
+    def cli_env_get(self, key):
+        """
+        Keyword arguments:
+        key -- name of variable
+        Returns value for environment variable
+        """
+        cmd = 'env-get -k %s' % key
+        self._write(cmd)
+
+        event = self.next_event()
+        if not isinstance(event, DisplayEvent):
+            raise UnexpectedEventError(event)
+        return event.value
+
+    def cli_env_set(self, key, value):
+        """
+        Sets given value for given environment variable
+
+        Keyword arguments:
+        key -- name of variable
+        value -- value to be set
+        """
+        cmd = 'env-query'
+        if isinstance(value, (list, tuple)):
+            cmd += '-multi'
+        cmd += " -k %s" % key
+        self._write(cmd)
+
+        event = self.next_event()
+        if not isinstance(event, QueryEvent):
+            raise UnexpectedEventError(event)
+        event.value = value
+        self.send_response(event)
+
+    def cli_download_log(self):
+        """
+        Returns log
+        """
+        self._write('log')
+        event = self.next_event()
+        if isinstance(event, DisplayMultiString):
+            return event.value
+        raise UnexpectedEventError(event)
+
+    def cli_noop(self):
+        """
+        noop command
+        """
+        self._write('noop')
+
+    def cli_quit(self):
+        """
+        quit command
+        """
+        self._write('quit')
+
+    def cli_install(self):
+        """
+        install command
+        """
+        self._write('install')
+
+    def cli_abort(self):
+        """
+        abort command
+        """
+        self._write('abort')
+
+# vim: expandtab tabstop=4 shiftwidth=4


-- 
To view, visit http://gerrit.ovirt.org/28180
To unsubscribe, visit http://gerrit.ovirt.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ia7b715a39840e548ef768ac9e77004cc0d98a9c0
Gerrit-PatchSet: 1
Gerrit-Project: otopi
Gerrit-Branch: master
Gerrit-Owner: Lukas Bednar <lbed...@redhat.com>
_______________________________________________
Engine-patches mailing list
Engine-patches@ovirt.org
http://lists.ovirt.org/mailman/listinfo/engine-patches

Reply via email to