Ejemplo n.º 1
0
class Hg(Service):
    """
    Mercurial repository.
    """
    __metaclass__ = HgBase

    repository = required_property()
    repository_location = required_property()
    default_changeset = 'default'

    commands = {}  # Extra hg commands. Map function name to hg command.

    @dont_isolate_yet
    def checkout(self, changeset=None):
        if not changeset:
            commit = input('Hg changeset', default=self.default_changeset)
            if not commit: raise Exception('No changeset given')

        self._checkout(changeset)

    def _checkout(self, changeset):
        # Clone the fist time
        existed = self.host.exists(self.repository_location)
        if not existed:
            self.host.run(
                "hg clone '%s' '%s'" %
                (esc1(self.repository), esc1(self.repository_location)))

        # Checkout
        with self.host.cd(self.repository_location):
            self.host.run("hg checkout '%s'" % esc1(changeset))
Ejemplo n.º 2
0
class MySQLClient(Service):
    """
    For simple Mysql operations, on a remote host.
    """
    hostname = required_property()
    username = required_property()
    password = required_property()
    database = required_property()

    @isolate_one_only
    def restore_backup_from_url(self):
        backup_url = input('Enter the URL of the backup location (an .sql.gz file)')
        self.hosts.run("curl '%s' | gunzip | /usr/bin/mysql --user '%s' --password='******' --host '%s' '%s' " %
                    (esc1(backup_url), esc1(self.username), esc1(self.password), esc1(self.hostname), esc1(self.database)))

    @isolate_one_only
    def shell(self):
        self.hosts.run("/usr/bin/mysql --user '%s' --password='******' --host '%s' '%s' " %
                    (esc1(self.username), esc1(self.password), esc1(self.hostname), esc1(self.database)))
Ejemplo n.º 3
0
class User(Service):
    """
    Unix/Linux user management.
    """
    username = required_property()
    groupname = Q.username
    has_home_directory = True
    home_directory_base = None
    shell = '/bin/bash'

    def create(self):
        """
        Create this user and home directory.
        (Does not fail when the user or directory already exists.)
        """
        if self.exists():
            return

        useradd_args = []
        useradd_args.append("'%s'" % esc1(self.username))
        useradd_args.append("-s '%s'" % self.shell)
        if self.has_home_directory:
            useradd_args.append('-m')
            if self.home_directory_base:
                useradd_args.append("-b '%s'" % self.home_directory_base)
        else:
            useradd_args.append('-M')

        # Group
        if self.username == self.groupname:
            useradd_args.append('-U')
        else:
            if self.groupname:
                self.host.sudo(
                    "grep '%s' /etc/group || groupadd '%s'" %
                    esc1(self.groupname), esc1(self.groupname))
                useradd_args.append("-g '%s'" % esc1(self.groupname))

        self.host.sudo("useradd " + " ".join(useradd_args))

    def exists(self):
        """
        Return true when this user account was already created.
        """
        try:
            self.host.sudo("grep '%s' /etc/passwd" % self.username)
            return True
        except:
            return False
Ejemplo n.º 4
0
class SysVInitService(Service):
    slug = required_property()
    no_pty = False

    def _make_command(command):
        def run(self):
            self.hosts.sudo("service '%s' %s" % (esc1(self.slug), command),
                            interactive=not self.no_pty)

        return run

    stop = _make_command('stop')
    start = _make_command('start')
    status = _make_command('status')
    restart = _make_command('restart')
    reload = _make_command('reload')

    def install(self, runlevels='defaults', priority='20'):
        self.hosts.sudo("update-rc.d '%s' %s %s" %
                        (esc1(self.slug), runlevels, priority))

    def uninstall(self):
        self.hosts.sudo("update-rc.d '%s' remove" % esc1(self.slug))
Ejemplo n.º 5
0
class Redis(Service):
    """
    Key/Value storage server
    """
    # Port and slug should be unique between all redis installations on one
    # host.
    port = 6379
    database = 0
    password = None
    slug = required_property()

    timeout = 0

    # username for the server process
    username = required_property()

    # Bind to interface, e.g. '127.0.0.1'
    bind = None

    # Download URL
    redis_download_url = 'http://redis.googlecode.com/files/redis-2.6.13.tar.gz'

    # directory for the database file, or None for the home directory
    @property
    def directory(self):
        # Fallback to home directory
        return self.host.get_home_directory()

    # Make persisntent True, when you want to save this database to the disk.
    persistent = False

    @property
    def database_file(self):
        return 'redis-db-%s.rdb' % self.slug

    @property
    def config_file(self):
        return '/etc/redis-%s.conf' % self.slug

    @property
    def logfile(self):
        return '/var/log/redis-%s.log' % self.slug

    class packages(AptGet):
        # Packages required for building redis
        packages = ('make', 'gcc', 'telnet', 'build-essential')
        # Depending on the system (x86 or 64bit), some packages are not available.
        packages_if_available = ('libc6-dev', 'libc6-dev-amd64',
                                 'libc6-dev-i386', 'libjemalloc-dev')

    class upstart_service(UpstartService):
        """
        Redis upstart service.
        """
        slug = Q('redis-%s') % Q.parent.slug
        chdir = '/'
        user = Q.parent.username
        command = Q('/usr/local/bin/redis-server %s') % Q.parent.config_file

    def setup(self):
        # Also make sure that redis was not yet installed
        if self.is_already_installed:
            print 'Warning: Redis is already installed'
            if not confirm('Redis is already installed. Reinstall?'):
                return

        # Install dependencies
        self.packages.install()

        # Download, compile and install redis
        # If not yet installed
        if not self.host.has_command('redis-server'):
            # Download redis
            self.host.run(wget(self.redis_download_url, 'redis.tgz'))
            self.host.run('tar xvzf redis.tgz')

            # Unset ARCH variable, otherwise redis doesn't compile.
            # http://comments.gmane.org/gmane.linux.slackware.slackbuilds.user/6686
            with self.host.env('ARCH', ''):
                # Make and install
                with self.host.cd('redis-2.*'):
                    if self.host.is_64_bit:
                        self.host.run('make ARCH="-m64"')
                    else:
                        self.host.run('make 32bit')
                    self.host.sudo('make install')

        self.config.setup()

        # Install upstart config, and run
        self.upstart_service.setup()
        self.upstart_service.start()

        print 'Redis setup successfully on host'

    class config(Config):
        remote_path = Q.parent.config_file
        lexer = IniLexer

        @property
        def content(self):
            self = self.parent
            return config_template % {
                'database_file':
                self.database_file,
                'directory':
                self.directory,
                'password':
                ('requirepass %s' % self.password if self.password else ''),
                'port':
                self.port,
                'auto_save':
                'save 60 1' if self.persistent else '',
                'bind': ('bind %s' % self.bind if self.bind else ''),
                'timeout':
                str(int(self.timeout)),
                'logfile':
                self.logfile,
            }

        def setup(self):
            Config.setup(self)
            self.host.sudo("chown '%s' '%s' " %
                           (self.parent.username, self.remote_path))

    def touch_logfile(self):
        # Touch and chown logfile.
        self.host.sudo("touch '%s'" % esc1(self.logfile))
        self.host.sudo("chown '%s' '%s'" %
                       (esc1(self.username), esc1(self.logfile)))

    def tail_logfile(self):
        self.host.sudo("tail -n 20 -f '%s'" % esc1(self.logfile))

    @property
    def is_already_installed(self):
        """
        Returns true when redis was already installed on all hosts
        """
        return self.host.exists(
            self.config_file) and self.upstart_service.is_already_installed()

    def shell(self):
        print 'Opening telnet connection to Redis... Press Ctrl-C to exit.'
        print
        self.host.run('redis-cli -h localhost -a "%s" -p %s' %
                      (self.password or '', self.port))

    def monitor(self):
        """
        Monitor all commands that are currently executed on this redis database.
        """
        self.host.run('echo "MONITOR" | redis-cli -h localhost -a "%s" -p %s' %
                      (self.password or '', self.port))

    def dbsize(self):
        """
        Return the number of keys in the selected database.
        """
        self.host.run('echo "DBSIZE" | redis-cli -h localhost -a "%s" -p %s' %
                      (self.password or '', self.port))

    def info(self):
        """
        Get information and statistics about the server
        """
        self.host.run('echo "INFO" | redis-cli -h localhost -a "%s" -p %s' %
                      (self.password or '', self.port))
Ejemplo n.º 6
0
class Config(Service):
    """
    Base class for all configuration files.
    """
    # Full path of the location where this config should be stored. (Start with slash)
    remote_path = required_property()

    # The textual content that should be saved in this place.
    content = required_property()

    # Pygments Lexer
    lexer = TextLexer

    use_sudo = True
    make_executable = False
    always_backup_existing_config = False

    # TODO: maybe we should make this True by default,
    #       but don't backup when the 'diff' is empty.

    def show_new_config(self):
        """
        Show the new configuration file. (What will be installed on 'setup')
        """
        print highlight(self.content, self.lexer(), Formatter())

    def show(self):
        """
        Show the currently installed configuration file.
        """
        print highlight(self.current_content, self.lexer(), Formatter())

    @property
    def current_content(self):
        """
        Return the content which currently exists in this file.
        """
        return self.host.open(self.remote_path, 'rb', use_sudo=True).read()

    @supress_action_result
    def diff(self):
        """
        Show changes to be written to the file. (diff between the current and
        the new config.)
        """
        # Split new and existing content in lines
        current_content = self.current_content.splitlines(1)
        new_content = self.content.splitlines(1)

        # Call difflib
        diff = ''.join(difflib.unified_diff(current_content, new_content))
        print highlight(diff, DiffLexer(), Formatter())

        return diff

    @supress_action_result
    def exists(self):
        """
        True when this config exists.
        """
        if self.host.exists(self.remote_path):
            print 'Yes, config exists already.'
            return True
        else:
            print 'Config doesn\'t exist yet'
            return False

    def changed(self):
        """
        Return True when there are configuration changes.
        (Or when the file does not yet exist)
        """
        if self.exists():
            return self.current_content != self.content
        else:
            return True

    def setup(self):
        """
        Install config on remote machines.
        """
        # Backup existing configuration
        if self.always_backup_existing_config:
            self.backup()

        self.host.open(self.remote_path, 'wb',
                       use_sudo=self.use_sudo).write(self.content)

        if self.make_executable:
            self.host.sudo("chmod a+x '%s'" %
                           esc1(self.host.expand_path(self.remote_path)))

    def backup(self):
        """
        Create a backup of this configuration file on the same host, in the same directory.
        """
        import datetime
        suffix = datetime.datetime.now().strftime('%Y-%m-%d--%H-%M-%S')
        self.host.sudo("test -f '%s' && cp --archive '%s' '%s.%s'" %
                       (esc1(self.remote_path), esc1(self.remote_path),
                        esc1(self.remote_path), esc1(suffix)))

    def edit_in_vim(self):
        """
        Edit this configuration manually in Vim.
        """
        self.host.sudo("vim '%s'" % esc1(self.remote_path))
Ejemplo n.º 7
0
class Cron(Service):
    # ===============[ Cron config ]================

    interval = '20 * * * *'  # Every hour by default

    # Username of the user as which the cron should be executed.  e.g.: 'username'
    username = required_property()

    # The command that this cron has to execute.  e.g.: 'echo "dummy command"'
    command = required_property()

    # Should be unique between all crons.
    slug = required_property()

    @property
    def doc(self):
        return self.slug

    # ===============[ Tasks ]================

    def activate_all(self, host=None):
        hosts = [host] if host else self.hosts
        for host in hosts:
            home = host.get_home_directory(self.username)
            host.sudo('cat %s/.deployer-crons/* | crontab' % home,
                      user=self.username)

    # Deprecated (confusing naming)
    def install(self):
        self.add(skip_activate=False)

    def add(self, skip_activate=False):
        """
        Install cronjob
        (This will leave the other cronjobs, created by this service intact.)
        """
        for host in self.hosts:
            # Get home directory for this user
            home = host.get_home_directory(self.username)

            # Create a subdirectory .deployer-crons if this does not yet exist
            host.sudo('mkdir -p %s/.deployer-crons' % home)
            host.sudo('chown %s %s/.deployer-crons' % (self.username, home))

            # Write this cronjob into deployer-crons/slug
            host.open('%s/.deployer-crons/%s' % (home, self.slug),
                      'wb',
                      use_sudo=True).write(self.cron_line)
            host.sudo('chown %s %s/.deployer-crons/%s' %
                      (self.username, home, self.slug))

            if not skip_activate:
                self.activate_all(host)

    # Deprecated (confusing naming)
    def uninstall(self):
        self.remove(skip_activate=False)

    def remove(self, skip_activate=False):
        """
        Uninstall cronjob
        """
        for host in self.hosts:
            # Get home directory for this user
            home = host.get_home_directory(self.username)

            # Remove this cronjob
            path = '%s/.deployer-crons/%s' % (home, self.slug)
            if host.exists(path):
                host.sudo("rm '%s' " % path)

            if not skip_activate:
                self.activate_all(host)

    @property
    def cron_line(self):
        cron_line = '%s %s\n' % (self.interval, self.command)
        if self.doc:
            cron_line = "\n".join(["# %s" % l for l in self.doc.split('\n')] +
                                  [cron_line])
        return cron_line

    def show_new_line(self):
        print self.cron_line

    def run_now(self):
        self.hosts.sudo(self.command, user=self.username)

    def list_all_crons(self):
        for host in self.hosts:
            # Get home directory for this user
            home = host.get_home_directory(self.username)

            # Print crontabs
            host.sudo('cat %s/.deployer-crons/* ' % home, user=self.username)
Ejemplo n.º 8
0
class Git(Service):
    """
    Manage the git checkout of a project
    """
    __metaclass__ = GitBase

    repository = required_property()
    repository_location = required_property()
    default_revision = 'master'

    commands = { } # Extra git commands. Map function name to git command.

    @dont_isolate_yet
    def checkout(self, commit=None):
        # NOTE: this public 'checkout'-method uses @dont_isolate_yet, so that
        # in case of a parrallel checkout, we only ask once for the commit
        # name, and fork only to several threads after calling '_checkout'.

        # If no commit was given, ask for commit.
        if not commit:
            commit = input('Git commit', default=self.default_revision)
            if not commit: raise Exception('No commit given')

        self._checkout(commit)

    def _checkout(self, commit):
        """
        This will either clone or checkout the given commit. Changes in the
        repository are always stashed before checking out, and stash-popped
        afterwards.
        """
        # Checkout on every host.
        for host in self.hosts:
            existed = host.exists(self.repository_location)

            if not existed:
                # Do a new checkout
                host.run('git clone --recursive %s %s' % (self.repository, self.repository_location))

            with host.cd(self.repository_location):
                host.run('git fetch --all --prune')

                # Stash
                if existed:
                    host.run('git stash')

                # Checkout
                try:
                    host.run("git checkout '%s'" % esc1(commit))
                    host.run("git submodule update --init") # Also load submodules.
                finally:
                    # Pop stash
                    try:
                        if existed:
                            host.run('git stash pop 2>&1', interactive=False) # will fail when checkout had no local changes
                    except ExecCommandFailed, e:
                        result = e.result
                        if result.strip() not in ('Nothing to apply', 'No stash found.'):
                            print result
                            if not confirm('Should we continue?'):
                                raise Exception('Problem with popping your stash, please check logs and try again.')
Ejemplo n.º 9
0
class Uwsgi(Service):
    virtual_env_location = required_property()
    slug = required_property()
    uwsgi_socket = 'localhost:3032'  # Can be either a tcp socket or unix file socket
    wsgi_app_location = required_property()
    run_from_directory = required_property()
    uwsgi_threads = 10
    uwsgi_workers = 2
    username = required_property()

    uwsgi_download_url = 'http://projects.unbit.it/downloads/uwsgi-1.4.8.tar.gz'

    # HTTP
    use_http = False
    http_port = 80

    @map_roles.just_one
    class _packages(AptGet):
        # libxml2-dev is required for compiling uwsgi
        packages = ('libxml2-dev', )

    @map_roles.just_one
    class virtual_env(VirtualEnv):
        @property
        def requirements(self):
            return (
                self.parent.uwsgi_download_url,

                # For monitoring uwsgi.
                'uwsgitop',
            )

        virtual_env_location = Q.parent.virtual_env_location

    def setup(self):
        self._packages.install()
        self.virtual_env.upgrade_requirements()
        self.init_d_file.setup()

    @property
    def pidfile(self):
        return '/tmp/django-uwsgi-%s.pid' % self.slug

    @property
    def logfile(self):
        return '/tmp/uwsgi-log-%s' % self.slug

    @property
    def stats_socket(self):
        return '/tmp/uwsgi-stats-%s' % self.slug

    def _get_start_command(self, daemonize=True):
        """
        UWSGI startup command
        Because of --daemonize, we don't need upstart anymore.
        """
        if self.use_http:
            socket = '--http 127.0.0.1:%s' % self.http_port
        else:
            socket = '-s %s' % self.uwsgi_socket

        return '%(virtual_env)s/bin/uwsgi -H %(virtual_env)s %(uwsgi_socket)s --threads %(threads)i --workers %(workers)i --stats %(stats)s ' \
                '%(pidfile)s %(daemonize)s %(log)s --logfile-chown %(username)s -M %(wsgi_app)s --uid %(username)s --chmod-socket %(chmod_socket)s' % {
                    'virtual_env': self.virtual_env_location,
                    'uwsgi_socket': socket,
                    'pidfile': ('--pidfile=%s' % self.pidfile if daemonize else ''),
                    'daemonize': '--daemonize' if daemonize else '',
                    'log': self.logfile,
                    'wsgi_app': self.wsgi_app_location,
                    'threads': self.uwsgi_threads,
                    'workers': self.uwsgi_workers,
                    'stats': self.stats_socket,
                    'username': self.username,
                    'chmod_socket': 666,
                    }

    @property
    def reload_command(self):
        return "kill -SIGHUP ` cat '%s' ` " % esc1(self.pidfile)

    @property
    def stop_command(self):
        return "kill -SIGQUIT ` cat '%s' ` && rm '%s' " % (esc1(
            self.pidfile), esc1(self.pidfile))

    def monitor_log(self):
        """
        Show uwsgi tail.
        """
        self.hosts.sudo("tail -f '%s' " % esc1(self.logfile),
                        ignore_exit_status=True)

    def top(self):
        """
        Run uwsgi top
        """
        with self.hosts.prefix(self.virtual_env.activate_cmd):
            self.hosts.sudo("uwsgitop '%s'" % self.stats_socket)

    def start(self, daemonize=True):
        """
        Start uWSGI stack
        """
        for h in self.hosts:
            if not h.exists(self.pidfile):
                with h.cd(self.run_from_directory):
                    h.sudo(self._get_start_command(daemonize))
            else:
                print 'Pidfile %s already exists' % self.pidfile

    def run_in_shell(self):
        self.start(daemonize=False)

    def stop(self):
        """
        Kill all the uWSGI stack.
        """
        self.hosts.sudo(self.stop_command)

    def status(self):
        """
        True when this uwsgi process is running
        """
        for h in self.hosts:
            if h.exists(self.pidfile):
                print 'Running'

    def reload(self):
        """
        Reload (gracefully) all the workers and the master process.
        """
        self.hosts.sudo(self.reload_command)

    def rmpidfile(self):
        """
        Remove pidfile, sometimes it can happen that the pidfile was created, and the
        server crached due to a bad configuration, without removing the pidfile.
        """
        if input('Remove pidfile', answers=['y', 'n']) == 'y':
            self.hosts.sudo("kill -SIGQUIT ` cat '%s' ` || rm '%s' " %
                            (esc1(self.pidfile), esc1(self.pidfile)))

    class init_d_file(Config):
        @property
        def slug(self):
            return 'uwsgi-%s' % self.parent.slug

        @property
        def remote_path(self):
            return '/etc/init.d/%s' % self.slug

        lexer = BashLexer
        use_sudo = True
        make_executable = True

        @property
        def content(self):
            self = self.parent

            return init_d_template % {
                'slug': self.slug,
                'run_from_directory': self.run_from_directory,
                'pidfile': self.pidfile,
                'start_command': self._get_start_command(True),
                'reload_command': self.reload_command,
                'stop_command': self.stop_command,
            }

        def setup(self):
            Config.setup(self)

            # Automatically start on system boot.
            self.hosts.sudo("update-rc.d '%s' defaults" % self.slug)
Ejemplo n.º 10
0
class Django(Service):
    __metaclass__ = DjangoBase

    # Location of the virtual env
    virtual_env_location = ''

    # Django project location. This is the directory which contains the
    # manage.py file.
    django_project = ''

    # Django commands (mapping from command name, to ./manage.py parameter)
    commands = { }

    # User for the upstart service
    username = required_property()
    slug = 'default-django-app'
    uwsgi_socket = 'localhost:3032' # Can be either a tcp socket or unix file socket
    uwsgi_threads = 10
    uwsgi_workers = 2
    uwsgi_use_http = False # When true, we will use the same port as runserver.
                           # this has the advantage that Django's runserver and
                           # uwsgi can be used interchangable.

    # HTTP Server
    http_port = 8000


    uwsgi_auto_reload = False

    def _get_management_command(self, command):
        """
        Create the call for a management command.
        (For use in cronjobs, etc...)
        NOTE: The command itself is not shell-escaped, be sure to use proper quoting
              if necessary!
        """
        parent_directory, dir2 = self.django_project.rsplit('/', 1)
        return "cd '%s'; '%s/bin/python' '%s/manage.py' %s" % (
                    parent_directory, self.virtual_env_location, dir2, command)

    def _run_management_command(self, command):
        self.hosts.run(self._get_management_command(command))

    @isolate_one_only
    @alias('manage.py')
    def _manage_py(self, command=None):
        command = command or input('python manage.py (...)')
        self._run_management_command(command)

    @property
    def settings_module(self):
        return self.django_project.rstrip('/').rsplit('/', 1)[-1] + '.settings'

    @property
    def wsgi_app_location(self):
        return '/etc/wsgi-apps/%s.py' % self.slug


    # ===========[ WSGI setup ]============

    @map_roles.just_one
    class uwsgi(Uwsgi):
        uwsgi_socket = Q.parent.uwsgi_socket
        slug = Q.parent.slug
        wsgi_app_location = Q.parent.wsgi_app_location
        uwsgi_threads = Q.parent.uwsgi_threads
        uwsgi_workers = Q.parent.uwsgi_workers
        virtual_env_location = Q.parent.virtual_env_location
        username = Q.parent.username
        use_http = Q.parent.uwsgi_use_http

        @property
        def run_from_directory(self):
            return self.parent.django_project + '/..'

        def setup(self):
            Uwsgi.setup(self)
            self.parent.wsgi_app.setup()

    class wsgi_app(Config):
        remote_path = Q.parent.wsgi_app_location

        @property
        def content(self):
            self = self.parent
            return wsgi_app_template % {
                'auto_reload': repr(self.uwsgi_auto_reload),
                'settings_module': repr(self.settings_module),
            }

        def setup(self):
            self.host.sudo("mkdir -p $(dirname '%s')" % self.remote_path)
            Config.setup(self)
            self.host.sudo("chown %s '%s'" % (self.parent.username, self.remote_path))
Ejemplo n.º 11
0
class Variants(Config):
    """
    Simple server-side persistent key-value list of attributes.

    Mostly useful for an installation of a service on a server: what version is
    installed with what options? Do we need to install it again or extend it
    with new options for this extra service?

    Similar to variants for MacPorts. Variants are conditional modifications
    of installations. They are flags, saved on the target system. If we detect
    that the variants don't match with those that we expect, then we know that
    we have to reinstall the service.
    This is useful, for when some services are installed system-wide from
    several set-ups. Each set-up can add their own variants. If some variants
    are already in place, and our service adds another variant, then we should
    probably reinstall the service, combining all these variants.

    Variants can be specified as a list/set or as a dict, but the helper
    attributes convert them to dicts. This allows you to use keys and values,
    which can be compared (like version numbers). It is possible that we will
    drop list support in the future.

    Example:

    variants = ('version:1.2', 'plugin_foo')

    Equivalent:

    variants = {'version': '1.2', 'plugin_foo': True}

    If the server would already contain ('version:1.1', 'plugin_bar'), you know
    you will have to install Version 1.2 with plugins foo and bar to satisfy
    your own service and other services on the same host that depend on it.
    """
    # Override
    variants = set()
    slug = required_property()

    @property
    def remote_path(self):
        return '/etc/variants/%s' % self.slug

    @property
    def content(self):
        final_variants = self._as_list(self.variants_final)

        # Return result
        return ' '.join(final_variants)

    def _as_dict(self, var_list):
        if isinstance(var_list, dict):
            return var_list
        var_dict = {}
        for var in var_list:
            var_parts = var.split(':')
            if len(var_parts) == 1:
                var_dict[var_parts[0]] = True
            elif len(var_parts) > 2:
                raise Exception('Variant %s contains more than one colon' %
                                var)
            else:
                var_dict[var_parts[0]] = var_parts[1]
        return var_dict

    def _as_list(self, var_dict):
        if isinstance(var_dict, list):
            return var_dict
        var_list = []
        for v, s in var_dict.iteritems():
            if not isinstance(s, bool):
                var_list.append('%s:%s' % (v, s))
            else:
                var_list.append(v)
        var_list.sort()
        return var_list

    @property
    def variants_installed(self):
        """
        The currently installed variants.
        """
        if self.host.exists(self.remote_path):
            return self.current_content.split()
        return {}

    @property
    def variants_final(self):
        """
        Merge variants installed with requested.

        If you (re-)install a service, use this as the guide of what to install.
        """
        vars_installed = self._as_dict(self.variants_installed)
        vars_installed.update(self.variants_to_update)
        return vars_installed

    @property
    def variants_to_update(self):
        """
        Compare the installed variants with the variants requested by this service.

        This will return the variants that need to change, with their final
        version. You can check this property to determine whether you need to
        reinstall the service. If you decide to reinstall, use variants_final
        as a guide of what to install, because variants_to_update will not
        include variants that have not changed.
        """
        vars_installed = self._as_dict(self.variants_installed)
        vars_requested = self._as_dict(self.variants)
        vars_to_update = {}
        for var_req, var_spec_req in vars_requested.iteritems():
            if var_req not in vars_installed:
                # The variant is not yet installed
                # Install the requested version
                vars_to_update[var_req] = var_spec_req
            else:
                # The variant is already installed
                if isinstance(var_spec_req, bool):
                    # We do not request a specific version
                    # No need to update
                    pass
                else:
                    var_spec_cur = vars_installed[var_req]
                    # We request a specific version
                    if isinstance(var_spec_cur, bool):
                        # We currently have an unspecified version
                        # Go to the specific version
                        vars_to_update[var_req] = var_spec_req
                    else:
                        # Comparison time!
                        from distutils.version import LooseVersion
                        if LooseVersion(var_spec_cur) < LooseVersion(
                                var_spec_req):
                            # We currently have a lower version, install ours
                            vars_to_update[var_req] = var_spec_req
        return vars_to_update

    @property
    def clear(self):
        """
        Clear (reset) the variants file.
        """
        self.host.sudo("rm '/etc/variants/%s'" % esc1(self.remote_path))

    def setup(self):
        self.host.sudo('mkdir -p /etc/variants')
        Config.setup(self)
Ejemplo n.º 12
0
class UpstartService(Service):
    chdir = '/'
    user = '******'
    author = '(author)'
    command = required_property()  # e.g. '/bin/sleep 1000'
    pre_start_script = ''
    post_start_script = ''
    pre_stop_script = ''
    post_stop_script = ''
    extra = ''

    slug = required_property()  # A /etc/init/(slug).conf file will be created

    @property
    def description(self):
        # Can be more verbose than the slug, e.g. 'Upstart Service'
        return self.slug

    @property
    def config_file(self):
        return '/etc/init/%s.conf' % self.slug

    @property
    def full_command(self):
        if self.user and self.user != 'root':
            return "su -c '%s' '%s' " % (esc1(self.command), esc1(self.user))
        else:
            return self.command

    @map_roles.just_one  # The parent, UpstartService already has host isolation.
    class config(Config):
        remote_path = Q.parent.config_file
        use_sudo = True
        lexer = BashLexer  # No UpstartLexer available yet?

        @property
        def content(self):
            self = self.parent

            extra_scripts = ''
            for s in ('start', 'stop'):
                for p in ('pre', 'post'):
                    script = getattr(self, '%s_%s_script' % (p, s), '')
                    if script:
                        extra_scripts += """
%s-%s script
%s
end script
""" % (p, s, indent(script))

            return upstart_template % {
                'description': esc1(self.description),
                'author': esc1(self.author),
                'chdir': esc1(self.chdir),
                'command': self.full_command,
                'user': esc1(self.user),
                'extra': self.extra,
                'extra_scripts': extra_scripts,
            }

    def setup(self):
        """
        Install upstart configuration
        """
        self.config.setup()

    def start(self):
        self.hosts.sudo('start "%s" || true' % self.slug)

    def stop(self):
        self.hosts.sudo('stop "%s" || true' % self.slug)

    def restart(self):
        self.hosts.sudo('restart "%s" || true' % self.slug)

    def status(self):
        self.hosts.sudo('status "%s"' % self.slug)

    def run_in_shell(self):
        with self.hosts.cd(self.chdir):
            self.hosts.sudo(self.full_command)

    def is_already_installed(self):
        """
        True when this service is installed.
        """
        # Note: thanks to @isolate_host, there can only be one host in
        # self.hosts.filter('host')
        return self.hosts.filter('host')[0].exists(self.config_file)