def install_file_legacy(path, sudo=False, from_path=None, **substitutions):
    '''Install file with path on the host target.

    The from file is the first of this list which exists:
     * custom file
     * custom file.template
     * common file
     * common file.template
    '''
    # source paths 'from_custom' and 'from_common'
    from_path = from_path or path
    # remove beginning '/' (if any), eg '/foo/bar' -> 'foo/bar'
    from_tail = join('files', from_path.lstrip(os.sep))
    if from_path.startswith('~/'):
        from_tail = join('files', 'home', 'USERNAME',
                         from_path[2:])  # without beginning '~/'
    from_common = join(FABFILE_DATA_DIR, from_tail)
    from_custom = join(FABSETUP_CUSTOM_DIR, from_tail)

    # target path 'to_' (path or tempfile)
    for subst in ['SITENAME', 'USER', 'ADDON', 'TASK']:
        sitename = substitutions.get(subst, False)
        if sitename:
            path = path.replace(subst, sitename)
    to_ = path
    if sudo:
        to_ = join(os.sep, 'tmp', 'fabsetup_' + os.path.basename(path))
    path_dir = dirname(path)

    # copy file
    if isfile(from_custom):
        run(flo('mkdir -p  {path_dir}'))
        put(from_custom, to_)
    elif isfile(from_custom + '.template'):
        _install_file_from_template_legacy(from_custom + '.template', to_=to_,
                                    **substitutions)
    elif isfile(from_common):
        run(flo('mkdir -p  {path_dir}'))
        put(from_common, to_)
    else:
        _install_file_from_template_legacy(from_common + '.template', to_=to_,
                                    **substitutions)
    if sudo:
        run(flo('sudo mv --force  {to_}  {path}')) def needs_packages(*packages):
    '''Decorator: ensure that packages are installed on host given by fabric
    argument `-H` (local or remote).
    '''
    def real_decorator(func):

        @wraps(func)
        def wrapper(*args, **kwargs):
            non_installed = _non_installed(packages)
            if non_installed:
                what_for = 'in order to run this task'
                install_packages(non_installed, what_for=what_for)
            return func(*args, **kwargs)

        return wrapper

    return real_decorator def run(*args, **kwargs):
    func, kwargs = _func(kwargs)
    return func(*args, **kwargs) def subtask(*args, **kwargs):
    '''Decorator which prints out the name of the decorated function on
    execution.
    '''
    depth = kwargs.get('depth', 2)
    prefix = kwargs.get('prefix', '\n' + '#' * depth + ' ')
    tail = kwargs.get('tail', '\n')
    doc1 = kwargs.get('doc1', False)
    color = kwargs.get('color', cyan)

    def real_decorator(func):
        if doc1:
            return print_full_name(color=color, prefix=prefix,
                                   tail=tail)(print_doc1(func))
        return print_full_name(color=color, prefix=prefix, tail=tail)(func)

    invoked = bool(not args or kwargs)
    if not invoked:
        # invoke decorator function which returns the wrapper function
        return real_decorator(func=args[0])
    return real_decorator def subsubtask(*args, **kwargs):

    def real_decorator(func):
        return subtask(depth=3, color=default_color, *args, **kwargs)(func)

    invoked = bool(not args or kwargs)
    if not invoked:
        # invoke decorator function which returns the wrapper function
        return real_decorator(func=args[0])
    return real_decorator def dn_cn_of_certificate_with_san(domain):
    '''Return the Common Name (cn) from the Distinguished Name (dn) of the
    certificate which contains the `domain` in its Subject Alternativ Name (san)
    list.

    Needs repo ~/.fabsetup-custom.

    Return None if no certificate is configured with `domain` in SAN.
    '''
    cn_dn = None
    from config import domain_groups
    cns = [domains[0]
           for domains
           in domain_groups
           if domain in domains]
    if cns:
        if len(cns) > 1:
            print_msg(yellow(flo('Several certificates are configured to '
                                 'contain {domain} '
                                 '(You should clean-up your config.py)\n')))
        cn_dn = cns[0]
    return cn_dn def task(func, *args, **kwargs):
    '''Composition of decorator functions for inherent self-documentation on
    task execution.

    On execution, each task prints out its name and its first docstring line.
    '''
    prefix = '\n# '
    tail = '\n'
    return fabric.api.task(
        print_full_name(color=magenta,
                        prefix=prefix,
                        tail=tail)(print_doc1(func)),
        *args,
        **kwargs) def put(*args, **kwargs):
    func, kwargs = _func(kwargs, func_remote=fabric.operations.put)
    if func == fabric.api.local:
        from_, to = [os.path.expanduser(arg) for arg in args]
        args = [flo('cp  {from_}  {to}')]
        kwargs = {}
    return func(*args, **kwargs) def exists(*args, **kwargs):
    func, kwargs = _func(kwargs, func_local=os.path.exists,
                         func_remote=fabric.contrib.files.exists)
    if func == os.path.exists:
        args_list = list(args)
        args_list[0] = os.path.expanduser(args[0])
        args = tuple(args_list)
    return func(*args, **kwargs) def print_msg(msg):
    if msg is not None:
        print(cyan(flo('{msg}'))) def needs_repo_fabsetup_custom(func):
    '''Decorator, ensures that fabsetup-custom exists and it is a git repo.'''
    from fabric.api import local

    @wraps(func)
    def wrapper(*args, **kwargs):
        if not os.path.exists(FABSETUP_CUSTOM_DIR):
            msg = '''\
Git repository ~/.fabsetup-custom with configurations does not exist.
This configs are required to use fabsetup.

Clone it if you already have your own fabsetup-custom repository:

    git clone <user>@<hostname>:/path/to/fabsetup-custom.git  ~/.fabetup-custom

Else, initialize a new repository.

Init a new repository `~/.fabsetup-custom`?'''
            if not query_yes_no(msg, default='yes'):
                sys.exit('abort')
            custom_dir = FABSETUP_CUSTOM_DIR
            presetting_dir = join(FABFILE_DATA_DIR,
                                  'presetting-fabsetup-custom')
            if not isdir(custom_dir):
                print(yellow('\n** **     Init ') +
                      yellow('~/.fabsetup-custom', bold=True) +
                      yellow('      ** **\n'))
                print(yellow(flo('** Create files in dir {custom_dir} **')))
                local(flo('mkdir -p {custom_dir}'))
                local(flo('cp -r --no-clobber {presetting_dir}/. {custom_dir}'))
                import_fabsetup_custom(globals())
            else:
                with quiet():
                    local(flo(
                        'cp -r --no-clobber {presetting_dir}/. {custom_dir}'))

            if not isdir(join(custom_dir, '.git')):
                print(yellow(
                    '\n** Git repo ~/.fabsetup-custom: '
                    'init and first commit **'))
                local(flo('cd {custom_dir} && git init'))
                local(flo('cd {custom_dir} && git add .'))
                local(flo('cd {custom_dir} && git commit -am "Initial commit"'))
                print(yellow("** Done. Don't forget to create a backup of your "
                             '~/.fabsetup-custom repo **\n'))
                print(yellow("** But do not make it public, it's custom **\n",
                             bold=True))
            else:
                with quiet():
                    cmd = flo('cd {custom_dir} && git status --porcelain')
                    res = local(cmd, capture=True)
                    if res:
                        print(yellow('\n** git repo  ') +
                              magenta('~/.fabsetup-custom  ') +
                              yellow('has uncommitted changes: **'))
                        print(cmd)
                        print(yellow(res, bold=True))
                        print(yellow(
                            "** Don't forget to commit them and make a "
                            "backup of your repo **\n"))
        return func(*args, **kwargs)
    return wrapper FABFILE_DATA_DIR = join(FABSETUP_DIR, 'fabfile-data') def flo(string):
    '''Return the string given by param formatted with the callers locals.'''
    callers_locals = {}
    frame = inspect.currentframe()
    try:
        outerframe = frame.f_back
        callers_locals = outerframe.f_locals
    finally:
        del frame
    return string.format(**callers_locals) def query_input(question, default=None, color=default_color):
    """Ask a question for input via raw_input() and return their answer.

    "question" is a string that is presented to the user.
    "default" is the presumed answer if the user just hits <Enter>.

    The "answer" return value is a str.
    """
    if default is None or default == '':
        prompt = ' '
    elif type(default) == str:
        prompt = flo(' [{default}] ')
    else:
        raise ValueError("invalid default answer: '%s'" % default)

    while True:
        sys.stdout.write(color(question + prompt))
        choice = raw_input()
        if default is not None and choice == '':
            return default
        if choice != '':
            return choice def query_yes_no(question, default="yes"):
    """Ask a yes/no question via raw_input() and return their answer.

    "question" is a string that is presented to the user.
    "default" is the presumed answer if the user just hits <Enter>.
    It must be "yes" (the default), "no", or None (which means an answer
    of the user is required).

    The "answer" return value is True for "yes" or False for "no".
    """
    valid = {"yes": True, "y": True, "ye": True, '1': True,
             "no": False, "n": False, '0': False, }
    if default is None:
        prompt = " [y/n] "
    elif default == "yes":
        prompt = " [Y/n] "
    elif default == "no":
        prompt = " [y/N] "
    else:
        raise ValueError("invalid default answer: '%s'" % default)

    while True:
        sys.stdout.write(question + prompt)
        choice = raw_input().lower()
        if default is not None and choice == '':
            return valid[default]
        elif choice in valid:
            return valid[choice]
        else:
            sys.stdout.write("Please respond with 'yes' or 'no' "
                             "(or 'y' or 'n').\n") def filled_out_template(filename, **substitutions):
    '''Return content of file filename with applied substitutions.'''
    res = None
    with open(filename, 'r') as fp:
        template = fp.read()
        res = filled_out_template_str(template, **substitutions)
    return res def namedtuple(typename, field_names, **kwargs):
    if isinstance(field_names, str):
        field_names = field_names.replace(',', ' ').split()
    field_names = list(map(str, field_names))
    field_names_without_defaults = []
    defaults = []
    for name in field_names:
        list_ = name.split('=', 1)
        if len(list_) > 1:
            name, default = list_
            defaults.append(eval(default))
        elif len(defaults) != 0:
            raise ValueError('non-keyword arg after keyword arg')
        field_names_without_defaults.append(name)
    result = collections.namedtuple(typename, field_names_without_defaults,
                                    **kwargs)
    result.__new__.__defaults__ = tuple(defaults)
    return result
import re
import tempfile
from fabric.api import env
from fabric.context_managers import warn_only
from fabsetup.fabutils import install_file_legacy, needs_packages, run, subtask
from fabsetup.fabutils import subsubtask, dn_cn_of_certificate_with_san
from fabsetup.fabutils import task, put, exists, print_msg
from fabsetup.fabutils import needs_repo_fabsetup_custom
from fabsetup.fabutils import FABFILE_DATA_DIR
from fabsetup.utils import flo, query_input, query_yes_no
from fabsetup.utils import filled_out_template
from fabsetup.utils import namedtuple
        from config import trac_plugins_by_sitename
# -*- coding:utf-8 -*-





# FIXME test-run as comment: "usefull commands for debugging: ..."
#    run_tracd(site_dir, bin_dir)
#    run_gunicorn(site_dir)
#    run_wsgi(site_dir)


@task
@needs_packages('nginx', 'python-pip', 'python-pygments', 'git', 'subversion')
@needs_repo_fabsetup_custom  # import of trac_plugins_by_sitename from config.py
def trac():
    '''Set up or update a trac project.

    This trac installation uses python2, git, sqlite (trac-default), gunicorn,
    and nginx.

    The connection is https-only and secured by a letsencrypt certificate.  This
    certificate must be created separately with task setup.server_letsencrypt.

    This task installes or updates to the latest trac version hosted at
    https://pypi.python.org/pypi/Trac

    Created and modified files and dirs of this task:

        ```
        ~/sites/<sitename>      <--- example: sitename = trac.example.com
        │
        ├── backup.sh           <--- create a local backup (deletes dir backup/
        ├── backup                                     |    before it creates
        │   └── <sitename>_tracenv_hotcopy.tar.gz  <--´     the tarball)
        ├── run
        │   └── trac.sock       <--- file-socket for binding to nginx
        ├── scripts
        │   └── tracwsgi.py
        ├── tracenv
        │   ├── conf
        │   │   ├── trac.htpasswd   <--- trac user password hashes
        │   │   └── trac.ini        <--- trac config file
        │   ├── db
        │   │   └── trac.db         <--- sqlite database
        │   ├── files
        │   ├── git
        │   ├── htdocs
        │   ├── log
        │   ├── plugins
        │   ├── README
        │   ├── templates
        │   └── VERSION
        └── virtualenv
            ├── bin
            ├── include
            ├── lib
            ├── local
            └── pip-selfcheck.json
        ```

    How to create a backup tarball "manually":
    `~/sites/<sitename>/backup/tracenv_hotcopy_<yyyy-mm-dd>.tar.gz`:

        ```
        cd ~/sites/<sitename>  &&  rm -rf ./backup
        ./virtualenv/bin/trac-admin ./tracenv  hotcopy ./backup/tracenv_hotcopy
        mkdir -p ./backup  &&  cd ./backup
        tar czf <sitename>_tracenv_hotcopy_$(date +%F).tar.gz  tracenv_hotcopy/
        rm -rf tracenv_hotcopy; ls -hl
        ```

    More infos:
      https://trac.edgewall.org/wiki/TracInstall
      https://trac.edgewall.org/wiki/TracFastCgi#NginxConfiguration
      https://trac.edgewall.org/wiki/TracNginxRecipe
      https://trac.edgewall.org/wiki/Gunicorn
      http://www.obeythetestinggoat.com/book/chapter_08.html#_getting_to_a_production_ready_deployment
      Setting REMOTE_USER for Trac in Gunicorn behind Nginx:
        http://serverfault.com/a/392096
      https://trac.edgewall.org/wiki/TracBackup
    '''
    hostname = re.sub(r'^[^@]+@', '', env.host)  # without username if any
    sitename = query_input(
                   question='\nEnter site-name of Your trac web service',
                   default=flo('trac.{hostname}'))
    username = env.user

    site_dir = flo('/home/{username}/sites/{sitename}')
    bin_dir = flo('{site_dir}/virtualenv/bin')

    # provisioning steps
    install_or_upgrade_virtualenv_pip_package()
    create_directory_structure(site_dir)
    update_virtualenv(site_dir, sitename)
    set_up_trac_plugins(sitename, site_dir, bin_dir)
    set_up_gunicorn(site_dir, sitename)
    configure_nginx(username, sitename, hostname)


    if query_yes_no('\nRestore trac environment from backup tarball?',