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