#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2015 gimoh
#
# This file is part of devops-utils.
#
# devops-utils is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# devops-utils 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with devops-utils. If not, see <http://www.gnu.org/licenses/>.
"""Implements installation process for the devops-utils image.
The main function here is :py:func:`install` which implements the
installation process.
The :py:class:`Replacer`, used from within :py:func:`install`,
implements a lightweight preprocessing/templating process, thanks to
which the external runner can be used as-is when installed as well as
directly from source.
"""
from __future__ import print_function
import argparse
import re
import os
import shutil
import sys
import textwrap
from pkgutil import find_loader
from subprocess import check_call, CalledProcessError
from devops_utils import PROGS, plugin
string_types = str
if sys.version_info[0] == 2:
string_types = basestring
__all__ = ('install', 'Replacer')
class InvalidOperator(Exception):
"""Invalid operation passed through Replacer."""
[docs]class Replacer(object):
"""Used to insert/replace chunks of code in a stream of lines.
This is used to embed some values into the runner script (as it
doesn't have access to them from outside a container) when
installing it on the host system.
Iterating through the object will yield lines from the input with
lines containing a special marker replaced. The marker format is
``##INIT:OPERATOR[:PARAM]##`` and should be followed by a newline.
The ``OPERATOR`` can be:
- ``MODULE``: then ``PARAM`` specifies a python module whose
contents should be inserted instead of the original line
- ``PLUGINS``: then ``PARAM`` specifies type of plugins to be
included instead of the original line
- ``SUPPRESS``: supresses the line from output (no parameter)
- ``VAR``: then ``PARAM`` specifies name of variable to look up
and place it's definition in output instead of the original
line
Typical usage::
src = StringIO('FOO = 1 ##INIT:VAR:FOO##\\n')
for line in Replacer(src, {'FOO': 2}):
assert line == 'FOO = 2\\n'
:param iter input: iterator for input lines
:param dict context: look up variables to be replaced in this dict
"""
RE_MARKER = re.compile('##INIT:([^#]+)##$')
"Regex for matching the marker."
def __init__(self, input, context):
self.input = input
self.context = context
[docs] def handle_module(self, mod):
l = find_loader(mod)
n = getattr(l, 'fullname', getattr(l, 'name', None))
return l.get_source(n)
[docs] def handle_plugins(self, type_):
for fn in plugin.get_plugins('runner'):
with open(fn) as fobj:
for line in fobj:
yield line
else:
yield ''
[docs] def handle_suppress(self):
return ''
[docs] def handle_var(self, var):
return '{} = {!r}\n'.format(var, self.context[var])
def __iter__(self):
for line in self.input:
match = self.RE_MARKER.search(line.rstrip())
if match:
op, _, param = match.group(1).partition(':')
try:
func = getattr(self, 'handle_{}'.format(op.lower()))
except AttributeError:
raise InvalidOperator(op)
param = (param,) if param else () # for parameterless handlers
line = func(*param)
if isinstance(line, string_types):
yield line
else:
for generated in line:
yield generated
[docs]def install(args):
"""Install a runner and shortcuts to all supported programs.
The runner will be installed as ``devops-utils`` script in a
directory from host mounted at ``/target``. The links to all
included programs will be created in the same directory, pointing
to ``devops-utils``.
The runner will execute the command it's run as (or passed as
first parameter if executed as ``devops-utils``) via docker run.
:param list args: command line arguments
"""
parser = argparse.ArgumentParser(prog='install',
description=install.__doc__)
parser.add_argument(
'--image-name', help=('name of docker image to use when running '
'commands via runner (def: %(default)s)'))
parser.add_argument('--no-links', action='store_true',
help='only install the runner, skip the symlinks')
parser.add_argument(
'--runner-name', '-n',
help='override the target runner name (def: %(default)s)')
parser.set_defaults(image_name='gimoh/devops-utils',
runner_name='devops-utils')
args = parser.parse_args(args)
try:
check_call(('mountpoint', '-q', '/target'))
except CalledProcessError:
print(textwrap.dedent('''\
/target is not a mountpoint
Re-run this image with -v $HOME/.local/bin:/target
'''))
return 2
print("installing runner as `{}'".format(args.runner_name))
replacements = {
'DOCKER_IMAGE': args.image_name,
'PROGS': PROGS,
'RUNNER_NAME': args.runner_name,
}
source = '/opt/devops-utils/external_runner.py'
target = '/target/{}'.format(args.runner_name)
with open(source, 'r') as sfobj,\
open(target, 'w') as dfobj:
for line in Replacer(sfobj, replacements):
dfobj.write(line)
shutil.copystat(source, target)
if args.no_links:
print('skipping links')
return
print('installing links ... ', end='')
for prog in PROGS:
link = os.path.join('/target', prog)
print(' {}'.format(prog), end='')
if os.path.exists(link):
print(' (skip)', end='')
else:
os.symlink(args.runner_name, link)
print('')