コード例 #1
0
ファイル: utils.py プロジェクト: jpopelka/gluetool
def load_yaml(filepath, logger=None):
    """
    Load data stored in YAML file, and return their Python representation.

    :param str filepath: Path to a file. ``~`` or ``~<username>`` are expanded before using.
    :param gluetool.log.ContextLogger logger: Logger used for logging.
    :rtype: object
    :returns: structures representing data in the file.
    :raises gluetool.glue.GlueError: if it was not possible to successfully load content of the file.
    """

    if not filepath:
        raise GlueError('File path is not valid: {}'.format(filepath))

    logger = logger or Logging.get_logger()

    real_filepath = normalize_path(filepath)

    logger.debug("attempt to load YAML from '{}' (maps to '{}')".format(
        filepath, real_filepath))

    if not os.path.exists(real_filepath):
        raise GlueError("File '{}' does not exist".format(filepath))

    try:
        with open(real_filepath, 'r') as f:
            data = YAML.load(f)
            logger.debug("loaded YAML data from '{}':\n{}".format(
                filepath, format_dict(data)))

            return data

    except ruamel.yaml.YAMLError as e:
        raise GlueError("Unable to load YAML file '{}': {}".format(
            filepath, str(e)))
コード例 #2
0
ファイル: utils.py プロジェクト: jpopelka/gluetool
def dump_yaml(data, filepath, logger=None):
    """
    Save data stored in variable to YAML file.

    :param object data: Data to store in YAML file
    :param str filepath: Path to an output file.
    :raises gluetool.glue.GlueError: if it was not possible to successfully save data to file.
    """
    if not filepath:
        raise GlueError("File path is not valid: '{}'".format(filepath))

    logger = logger or Logging.get_logger()

    real_filepath = normalize_path(filepath)
    dirpath = os.path.dirname(real_filepath)

    if not os.path.exists(dirpath):
        raise GlueError(
            "Cannot save file in nonexistent directory '{}'".format(dirpath))

    try:

        with open(real_filepath, 'w') as f:
            YAML.dump(data, f)
            f.flush()

    except ruamel.yaml.YAMLError as e:
        raise GlueError("Unable to save YAML file '{}': {}".format(
            filepath, str(e)))
コード例 #3
0
ファイル: utils.py プロジェクト: jpopelka/gluetool
    def __init__(self, filepath, logger=None):
        self.logger = logger or Logging.get_logger()
        logger.connect(self)

        pattern_map = load_yaml(filepath, logger=self.logger)

        if pattern_map is None:
            raise GlueError(
                "pattern map '{}' does not contain any patterns".format(
                    filepath))

        self._compiled_map = []

        for pattern_dict in pattern_map:
            if not isinstance(pattern_dict, dict):
                raise GlueError(
                    "Invalid format: '- <pattern>: <result>' expected, '{}' found"
                    .format(pattern_dict))

            pattern = pattern_dict.keys()[0]
            result = pattern_dict[pattern].strip()

            try:
                pattern = re.compile(pattern)

            except re.error as exc:
                raise GlueError("Pattern '{}' is not valid: {}".format(
                    pattern, str(exc)))

            self._compiled_map.append((pattern, result))
コード例 #4
0
ファイル: utils.py プロジェクト: jpopelka/gluetool
def fetch_url(url, logger=None, success_codes=(200, )):
    """
    "Get me content of this URL" helper.

    Very thin wrapper around urllib. Added value is logging, and converting
    possible errors to :py:class:`gluetool.glue.GlueError` exception.

    :param str url: URL to get.
    :param gluetool.log.ContextLogger logger: Logger used for logging.
    :param tuple success_codes: tuple of HTTP response codes representing successfull request.
    :returns: tuple ``(response, content)`` where ``response`` is what :py:func:`urllib2.urlopen`
      returns, and ``content`` is the payload of the response.
    """

    logger = logger or Logging.get_logger()

    logger.debug("opening URL '{}'".format(url))

    try:
        response = urllib2.urlopen(url)
        code, content = response.getcode(), response.read()

    except urllib2.HTTPError as exc:
        logger.exception(exc)
        raise GlueError("Failed to fetch URL '{}': {}".format(
            url, exc.message))

    log_blob(logger.debug, '{}: {}'.format(url, code), content)

    if code not in success_codes:
        raise GlueError("Unsuccessfull response from '{}'".format(url))

    return response, content
コード例 #5
0
ファイル: utils.py プロジェクト: jpopelka/gluetool
        def _check_types(items):
            if not isinstance(items, list):
                raise GlueError('Only list of strings is accepted')

            if not all((isinstance(s, str) for s in items)):
                raise GlueError(
                    'Only list of strings is accepted, {} found'.format(
                        [type(s) for s in items]))
コード例 #6
0
def test_sentry_fingerprint(current, explicit):
    if not explicit:
        assert GlueError(
            '',
            sentry_fingerprint=explicit).sentry_fingerprint(current) is current

    else:
        assert GlueError('', sentry_fingerprint=explicit).sentry_fingerprint(
            current) == ['GlueError'] + explicit
コード例 #7
0
    def run_pipeline(self):
        Glue = self.Glue

        # no modules
        if not self.pipeline_desc:
            raise GlueError('No module specified, use -l to list available')

        # command-line info
        if Glue.option('info'):
            self.log_cmdline(self.argv, self.pipeline_desc)

        # actually the execution loop is retries+1
        # there is always one execution
        retries = Glue.option('retries')
        for loop_number in range(retries + 1):
            try:
                # Reset pipeline - destroy all modules that exist so far
                Glue.destroy_modules()

                # Print retry info
                if loop_number:
                    Glue.warn('retrying execution (attempt #{} out of {})'.format(loop_number, retries))

                # Run the pipeline
                Glue.run_modules(self.pipeline_desc, register=True)

            except GlueRetryError as e:
                Glue.error(e)
                continue

            break
コード例 #8
0
ファイル: utils.py プロジェクト: jpopelka/gluetool
    def match(self, s, multiple=False):
        """
        Try to match ``s`` by the map. If the match is found - the first one wins - then its
        conversions are applied to the ``s``.

        There can be multiple conversions for a pattern, by default only the product of
        the first one is returned. If ``multiple`` is set to ``True``, list of all products
        is returned instead.

        :rtype: str
        :returns: if matched, output of the corresponding transformation.
        """

        self.debug(
            "trying to match string '{}' with patterns in the map".format(s))

        for pattern, converters in self._compiled_map:
            self.debug("testing pattern '{}'".format(pattern.pattern))

            match = pattern.match(s)
            if match is None:
                continue

            self.debug('  matched!')

            if multiple is not True:
                return converters[0](pattern, s)

            return [converter(pattern, s) for converter in converters]

        raise GlueError(
            "Could not match string '{}' with any pattern".format(s))
コード例 #9
0
ファイル: utils.py プロジェクト: jpopelka/gluetool
def wait(label, check, timeout=None, tick=30, logger=None):
    """
    Wait for a condition to be true.

    :param str label: printable label used for logging.
    :param callable check: called to test the condition. If its return value evaluates as ``True``,
        the condition is assumed to pass the test and waiting ends.
    :param int timeout: fail after this many seconds. ``None`` means test forever.
    :param int tick: test condition every ``tick`` seconds.
    :param gluetool.log.ContextAdapter logger: parent logger whose methods will be used for logging.
    :raises gluetool.glue.GlueError: when ``timeout`` elapses while condition did not pass the check.
    """

    if not isinstance(tick, int):
        raise GlueError('Tick must be an integer')

    if tick < 0:
        raise GlueError('Tick must be a positive integer')

    logger = logger or Logging.get_logger()

    if timeout is not None:
        end_time = time.time() + timeout

    def _timeout():
        return '{} seconds'.format(
            int(end_time - time.time())) if timeout is not None else 'infinite'

    logger.debug(
        "waiting for condition '{}', timeout {}, check every {} seconds".
        format(label, _timeout(), tick))

    while timeout is None or time.time() < end_time:
        logger.debug("calling callback function")
        ret = check()
        if ret:
            logger.debug('check passed, assuming success')
            return ret

        logger.debug("check failed with {}, assuming failure".format(ret))

        logger.debug('{} left, sleeping for {} seconds'.format(
            _timeout(), tick))
        time.sleep(tick)

    raise GlueError(
        "Condition '{}' failed to pass within given time".format(label))
コード例 #10
0
ファイル: utils.py プロジェクト: thrix/gluetool
def check_for_commands(cmds):
    """ Checks if all commands in list cmds are valid """
    for cmd in cmds:
        try:
            run_command(['/bin/bash', '-c', 'command -v {}'.format(cmd)], stdout=DEVNULL)

        except GlueError:
            raise GlueError("Command '{}' not found on the system".format(cmd))
コード例 #11
0
ファイル: utils.py プロジェクト: jpopelka/gluetool
def render_template(template, logger=None, **kwargs):
    """
    Render Jinja2 template. Logs errors, and raises an exception when it's not possible
    to correctly render the template.

    :param template: Template to render. It can be either :py:class:`jinja2.environment.Template` instance,
        or a string.
    :param dict kwargs: Keyword arguments passed to render process.
    :rtype: str
    :returns: Rendered template.
    :raises gluetool.glue.GlueError: when the rednering failed.
    """

    logger = logger or Logging.get_logger()

    try:

        def _render(template):
            log_blob(logger.debug, 'rendering template', template.source)
            log_dict(logger.debug, 'context', kwargs)

            return str(template.render(**kwargs).strip())

        if isinstance(template, (str, unicode)):
            source, template = template, jinja2.Template(template)
            template.source = source

            return _render(template)

        if isinstance(template, jinja2.environment.Template):
            if template.filename != '<template>':
                with open(template.filename, 'r') as f:
                    template.source = f.read()

            else:
                template.source = '<unknown template source>'

            return _render(template)

        raise GlueError('Unhandled template type {}'.format(type(template)))

    except Exception as exc:
        raise GlueError('Cannot render template: {}'.format(exc))
コード例 #12
0
ファイル: utils.py プロジェクト: thrix/gluetool
            def _replace(pattern, target):
                """
                Use `repl` to construct image from `target`, honoring all backreferences made by `pattern`.
                """

                self.debug("pattern '{}', repl '{}', target '{}'".format(pattern.pattern, repl, target))

                try:
                    return pattern.sub(repl, target)

                except re.error as e:
                    raise GlueError("Cannot transform pattern '{}' with target '{}', repl '{}': {}".format(
                        pattern.pattern, target, repl, str(e)))
コード例 #13
0
    def _deduce_pipeline_desc(self, argv, modules):
        # type: (List[Any], List[str]) -> List[gluetool.glue.PipelineStep]

        # pylint: disable=no-self-use
        """
        Split command-line arguments, left by ``gluetool``, into a pipeline description, splitting them
        by modules and their options.

        :param list argv: Remainder of :py:data:`sys.argv` after removing ``gluetool``'s own options.
        :param list(str) modules: List of known module names.
        :returns: Pipeline description in a form of a list of :py:class:`gluetool.glue.PipelineStep` instances.
        """

        alias_pattern = re.compile(r'^([a-z\-]*):([a-z\-]*)$', re.I)

        pipeline_desc = []
        step = None

        while argv:
            arg = argv.pop(0)

            # is the "arg" a module name? If so, add new step to the pipeline
            if arg in modules:
                step = PipelineStep(arg)
                pipeline_desc.append(step)
                continue

            # is the "arg" a module with an alias? If so, add a new step to the pipeline, and note the alias
            match = alias_pattern.match(arg)
            if match is not None:
                module, actual_module = match.groups()

                step = PipelineStep(module, actual_module=actual_module)
                pipeline_desc.append(step)
                continue

            if step is None:
                raise GlueError(
                    "Cannot parse module argument: '{}'".format(arg))

            step.argv.append(arg)

        return pipeline_desc
コード例 #14
0
def test_caused_by_detect():
    mock_exc = ValueError('dummy error')

    with pytest.raises(GlueError, match=r'dummy message') as excinfo:
        try:
            raise mock_exc

        except ValueError:
            raise GlueError('dummy message')

    exc = excinfo.value

    assert isinstance(exc.caused_by, tuple)
    assert len(exc.caused_by) == 3

    klass, error, tb = exc.caused_by

    assert klass is mock_exc.__class__
    assert error is mock_exc
    assert isinstance(tb, types.TracebackType)
コード例 #15
0
ファイル: utils.py プロジェクト: thrix/gluetool
    def match(self, s):
        """
        Try to match ``s`` by the map. If the match is found - the first one wins - then its
        transformation is applied to the ``s``.

        :rtype: str
        :returns: if matched, output of the corresponding transformation.
        """

        self.debug("trying to match string '{}' with patterns in the map".format(s))

        for pattern, result in self._compiled_map:
            self.debug("testing pattern '{}'".format(pattern.pattern))

            match = pattern.match(s)
            if match is None:
                continue

            self.debug('  matched!')
            return result

        raise GlueError("Could not match string '{}' with any pattern".format(s))
コード例 #16
0
def test_sentry_fingerprint(current):
    assert GlueError('').sentry_fingerprint(current) == current
コード例 #17
0
def test_caused_by_detect_empty():
    with pytest.raises(GlueError) as excinfo:
        raise GlueError('')

    assert excinfo.value.caused_by is None
コード例 #18
0
def test_caused_by_explicit(caused_by):
    expected = None if caused_by == (None, None, None) else caused_by

    assert GlueError('dummy message',
                     caused_by=caused_by).caused_by == expected
コード例 #19
0
def test_message(message):
    with pytest.raises(GlueError) as excinfo:
        raise GlueError(message)

    assert excinfo.value.message == message
コード例 #20
0
        def _sigusr1_handler(signum, frame):
            # type: (int, FrameType) -> None

            # pylint: disable=unused-argument

            raise GlueError('Pipeline timeout expired.')
コード例 #21
0
ファイル: utils.py プロジェクト: jpopelka/gluetool
    def run(self, inspect=False, inspect_callback=None, **kwargs):
        """
        Run the command, wait for it to finish and return the output.

        :param bool inspect: If set, ``inspect_callback`` will receive the output of command in "real-time".
        :param callable inspect_callback: callable that will receive command output. If not set, default
            "write to ``sys.stdout``" is used.
        :rtype: gluetool.utils.ProcessOutput instance
        :returns: :py:class:`gluetool.utils.ProcessOutput` instance whose attributes contain data returned
            by the child process.
        :raises gluetool.glue.GlueError: When somethign went wrong.
        :raises gluetool.glue.GlueCommandError: When command exited with non-zero exit code.
        """

        # pylint: disable=too-many-branches

        def _check_types(items):
            if not isinstance(items, list):
                raise GlueError('Only list of strings is accepted')

            if not all((isinstance(s, str) for s in items)):
                raise GlueError(
                    'Only list of strings is accepted, {} found'.format(
                        [type(s) for s in items]))

        _check_types(self.executable)
        _check_types(self.options)

        self._command = self._apply_quotes()

        if self.use_shell is True:
            self._command = [' '.join(self._command)]

        # Set default stdout/stderr, unless told otherwise
        if 'stdout' not in kwargs:
            kwargs['stdout'] = subprocess.PIPE

        if 'stderr' not in kwargs:
            kwargs['stderr'] = subprocess.PIPE

        if self.use_shell:
            kwargs['shell'] = True

        self._popen_kwargs = kwargs

        def _format_stream(stream):
            if stream == subprocess.PIPE:
                return 'PIPE'
            if stream == DEVNULL:
                return 'DEVNULL'
            if stream == subprocess.STDOUT:
                return 'STDOUT'
            return stream

        printable_kwargs = kwargs.copy()
        for stream in ('stdout', 'stderr'):
            if stream in printable_kwargs:
                printable_kwargs[stream] = _format_stream(
                    printable_kwargs[stream])

        log_dict(self.debug, 'command', self._command)
        log_dict(self.debug, 'kwargs', printable_kwargs)
        log_blob(self.debug, 'runnable (copy & paste)',
                 format_command_line([self._command]))

        try:
            self._process = subprocess.Popen(self._command,
                                             **self._popen_kwargs)

            if inspect is True:
                self._communicate_inspect(inspect_callback)

            else:
                self._communicate_batch()

        except OSError as e:
            if e.errno == errno.ENOENT:
                raise GlueError("Command '{}' not found".format(
                    self._command[0]))

            raise e

        self._exit_code = self._process.poll()

        output = self._construct_output()

        if self._exit_code != 0:
            raise GlueCommandError(self._command, output)

        return output
コード例 #22
0
def test_sentry_tags(current):
    assert GlueError('').sentry_tags(current) == current
コード例 #23
0
ファイル: utils.py プロジェクト: thrix/gluetool
def run_command(cmd, logger=None, inspect=False, inspect_callback=None, **kwargs):
    """
    Run external command, and return it's exit code and output.

    This is a very thin and simple wrapper above :py:class:`subprocess.Popen`,
    and its main purpose is to log everything that happens before and after
    execution. All additional arguments are passed directly to `Popen` constructor.

    If ``stdout`` or ``stderr`` keyword arguments are not specified, function
    will set them to :py:const:`subprocess.PIPE`, to capture both output streams
    in separate strings.

    By default, output of the process is captured for both ``stdout`` and ``stderr``,
    and returned back to the caller. Under some conditions, caller might want to see
    the output in "real-time". For that purpose, it can pass callable via ``inspect_callback``
    parameter - such callable will be called for every received bit of input on both
    ``stdout`` and ``stderr``. E.g.

    .. code-block:: python

       def foo(stream, s, flush=False):
         if s is not None and 'a' in s:
           print s

       run_command(['/bin/foo'], inspect=foo)

    This example will print all substrings containing letter `a`. Strings passed to ``foo``
    may be of arbitrary lengths, and may change between subsequent calls of ``run_command``.

    :param list cmd: command to execute.
    :param gluetool.log.ContextAdapter logger: parent logger whose methods will be used for logging.
    :param bool inspect: if set, ``inspect_callback`` will receive the output of command in "real-time".
    :param callable inspect_callback: callable that will receive command output. If not set,
        default "write to ``sys.stdout``" is used.
    :rtype: gluetool.utils.ProcessOutput instance
    :returns: :py:class:`gluetool.utils.ProcessOutput` instance whose attributes contain
        data returned by the process.
    :raises gluetool.glue.GlueError: when command was not found.
    :raises gluetool.glue.GlueCommandError: when command exited with non-zero exit code.
    :raises Exception: when anything else breaks.
    """

    assert isinstance(cmd, list), 'Only list of strings accepted as a command'

    if not all((isinstance(s, str) for s in cmd)):
        raise GlueError('Only list of strings accepted as a command, {} found'.format([type(s) for s in cmd]))

    logger = logger or Logging.get_logger()

    stdout, stderr = None, None

    # Set default stdout/stderr, unless told otherwise
    if 'stdout' not in kwargs:
        kwargs['stdout'] = subprocess.PIPE

    if 'stderr' not in kwargs:
        kwargs['stderr'] = subprocess.PIPE

    def _format_stream(stream):
        if stream == subprocess.PIPE:
            return 'PIPE'
        if stream == DEVNULL:
            return 'DEVNULL'
        if stream == subprocess.STDOUT:
            return 'STDOUT'
        return stream

    printable_kwargs = kwargs.copy()
    for stream in ('stdout', 'stderr'):
        if stream in printable_kwargs:
            printable_kwargs[stream] = _format_stream(printable_kwargs[stream])

    # Make tests happy by sorting kwargs - it's a dictionary, therefore
    # unpredictable from the observer's point of view. Can print its entries
    # in different order with different Pythons, making tests a mess.
    # sorted_kwargs = ', '.join(["'%s': '%s'" % (k, printable_kwargs[k]) for k in sorted(printable_kwargs.iterkeys())])

    log_dict(logger.debug, 'command', cmd)
    log_dict(logger.debug, 'kwargs', printable_kwargs)
    log_blob(logger.debug, 'runnable (copy & paste)', format_command_line([cmd]))

    try:
        p = subprocess.Popen(cmd, **kwargs)

        if inspect is True:
            # let's capture *both* streams - capturing just a single one leads to so many ifs
            # and elses and messy code
            p_stdout = StreamReader(p.stdout, name='<stdout>')
            p_stderr = StreamReader(p.stderr, name='<stderr>')

            if inspect_callback is None:
                def stdout_write(stream, data, flush=False):
                    # pylint: disable=unused-argument

                    if data is None:
                        return

                    # Not suitable for multiple simultaneous commands. Shuffled output will
                    # ruin your day. And night. And few following weeks, full of debugging, as well.
                    sys.stdout.write(data)
                    sys.stdout.flush()

                inspect_callback = stdout_write

            inputs = (p_stdout, p_stderr)

            with BlobLogger('Output of command: {}'.format(format_command_line([cmd])), outro='End of command output',
                            writer=logger.info):
                logger.debug("output of command is inspected by the caller")
                logger.debug('following blob-like header and footer are expected to be empty')
                logger.debug('the captured output will follow them')

                # As long as process runs, keep calling callbacks with incoming data
                while True:
                    for stream in inputs:
                        inspect_callback(stream, stream.read())

                    if p.poll() is not None:
                        break

                    # give up OS' attention and let others run
                    # time.sleep(0) is a Python synonym for "thread yields the rest of its quantum"
                    time.sleep(0.1)

                # OK, process finished but we have to wait for our readers to finish as well
                p_stdout.wait()
                p_stderr.wait()

                for stream in inputs:
                    while True:
                        data = stream.read()

                        if data in ('', None):
                            break

                        inspect_callback(stream, data)

                    inspect_callback(stream, None, flush=True)

            stdout, stderr = p_stdout.content, p_stderr.content

        else:
            stdout, stderr = p.communicate()

    except OSError as e:
        if e.errno == errno.ENOENT:
            raise GlueError("Command '{}' not found".format(cmd[0]))

        raise e

    exit_code = p.poll()

    output = ProcessOutput(cmd, exit_code, stdout, stderr, kwargs)

    output.log(logger.debug)

    if exit_code != 0:
        raise GlueCommandError(cmd, output)

    return output
コード例 #24
0
def test_sentry_tags(current, explicit):
    expected = current.copy()
    if explicit:
        expected.update(explicit)

    assert GlueError('', sentry_tags=explicit).sentry_tags(current) == expected
コード例 #25
0
ファイル: utils.py プロジェクト: jpopelka/gluetool
    def __init__(self, filepath, spices=None, logger=None):
        self.logger = logger or Logging.get_logger()
        logger.connect(self)

        spices = spices or {}

        pattern_map = load_yaml(filepath, logger=self.logger)

        if pattern_map is None:
            raise GlueError(
                "pattern map '{}' does not contain any patterns".format(
                    filepath))

        def _create_simple_repl(repl):
            def _replace(pattern, target):
                """
                Use `repl` to construct image from `target`, honoring all backreferences made by `pattern`.
                """

                self.debug("pattern '{}', repl '{}', target '{}'".format(
                    pattern.pattern, repl, target))

                try:
                    return pattern.sub(repl, target)

                except re.error as e:
                    raise GlueError(
                        "Cannot transform pattern '{}' with target '{}', repl '{}': {}"
                        .format(pattern.pattern, target, repl, str(e)))

            return _replace

        self._compiled_map = []

        for pattern_dict in pattern_map:
            log_dict(logger.debug, 'pattern dict', pattern_dict)

            if not isinstance(pattern_dict, dict):
                raise GlueError(
                    "Invalid format: '- <pattern>: <transform>' expected, '{}' found"
                    .format(pattern_dict))

            pattern = pattern_dict.keys()[0]
            converter_chains = pattern_dict[pattern]

            if isinstance(converter_chains, str):
                converter_chains = [converter_chains]

            try:
                pattern = re.compile(pattern)

            except re.error as e:
                raise GlueError("Pattern '{}' is not valid: {}".format(
                    pattern, str(e)))

            compiled_chains = []

            for chain in converter_chains:
                converters = [s.strip() for s in chain.split(',')]

                # first item in `converters` is always a simple string used by `pattern.sub()` call
                converter = _create_simple_repl(converters.pop(0))

                # if there any any items left, they name "spices" to apply, one by one,
                # on the result of the first operation
                for spice in converters:
                    if spice not in spices:
                        raise GlueError(
                            "Unknown 'spice' function '{}'".format(spice))

                    converter = spices[spice](converter)

                compiled_chains.append(converter)

            self._compiled_map.append((pattern, compiled_chains))