Source

scripts / backup_sites.py

#!/usr/bin/env python
"""
Sites Backup
~~~~~~~~~~~~

В файле `~/.config/backup_sites.yaml` прописываем::

    - backups: ~/backups/share
    - configs:
      my_site: ~/src/my_site_com/backup.yaml
      another: ~/src/another_site_org/backup.yaml

Каждый конфиг проекта примерно таков::

    host: my.site.com
    user: andy
    path: /var/lib/django/my_site_com/
    dirs: [media, db_dump]
    remote_command: ./db_dump.sh && ./fix_permissions.sh && echo "OK"

Инструмент запускается так::

    $ backup-sites --list
    $ backup-sites [project, [project ...]]

Процесс и результат:

* сборка конфигов проектов из настроек программы
* для каждого конфига (или для явно выбранного, если найден):

  * собрать SSH-пути вида ``{user}@{host}/{path}/{dir}``
  * выполнить `remote_command`::

        ssh {user}@{host} "cd {path}; {remote_command}"

    конечно, это не очень безопасно, но ведь всё берется из самого
проекта.

  * выполнить rsync для каждой `dir` в ``dirs``::

        rsync --recursive --times --progress {user}@{host}:{path}/{dir} {backups}/{project}

"""
import argh
import os
import subprocess
import yaml
import xdg.BaseDirectory


APP_NAME = 'backup_sites'


def get_app_conf():
    defaults = {
        'backups': '~/backups',
        'configs': {},
    }

    filename = APP_NAME + '.yaml'
    path = os.path.join(xdg.BaseDirectory.xdg_config_home, filename)
    if os.path.exists(path):
        with open(path) as f:
            loaded = yaml.load(f)
    else:
        loaded = {}

    conf = dict((k, loaded.get(k,v)) for k,v in defaults.items())

    conf['backups'] = os.path.expanduser(conf['backups'])
    for label, path_ in conf['configs'].items():
        conf['configs'][label] = os.path.expanduser(path_)

    return conf


def backup_site(label, conf_path, local_dir, verbose=False):
    yield '#'
    yield '#  {0}'.format(label)
    yield '#'

    with open(conf_path) as f:
        conf = yaml.load(f)

    assert all(x in conf for x in ('host', 'user', 'path', 'dirs'))

    ssh_string = '{user}@{host}'.format(**conf)

    if conf.get('remote_command'):
        cmd = 'cd {path} && {remote_command}'.format(**conf)
        if verbose:
            yield 'REMOTE: ssh {ssh_string} "{cmd}"'.format(**locals())
        ret = subprocess.Popen(['ssh', ssh_string, cmd]).wait()

    # actually these can be dirs and files, not just dirs
    dirs = conf.get('dirs') or []    # config may contain None
    for directory in dirs:
        yield 'Copying {directory}...'.format(directory=directory)
        if not os.path.exists(local_dir):
            os.mkdir(local_dir)
        remote_path = os.path.join(conf.get('path'), directory)
        remote = '{ssh_string}:{remote_path}'.format(ssh_string=ssh_string,
                                                     remote_path=remote_path)
        if verbose:
            yield 'RSYNC: {remote}'.format(**locals())
        rsync_args = ['--recursive', '--times'] #, '--progress']
        ret = subprocess.Popen(['rsync'] + rsync_args +
                               [remote, local_dir]).wait()


def main(yes : 'do not ask confirmation' = False, verbose=False, *sites):
    conf = get_app_conf()

    sites = sites or conf['configs'].keys()

    yield 'Will now backup sites: {0}.'.format(', '.join(sites))
    if not yes and not argh.confirm('==> Proceed'):
        yield 'Cancelled by user.'
        return

    for site in sites:
        site_conf = conf['configs'][site]
        local_dir = os.path.join(conf['backups'], site)
        for line in backup_site(site, site_conf, local_dir, verbose=verbose):
            yield line


if __name__ == '__main__':
    argh.dispatch_command(main)