123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692 |
- from __future__ import print_function, unicode_literals
- from future.builtins import open
- import os
- import re
- import sys
- from contextlib import contextmanager
- from functools import wraps
- from getpass import getpass, getuser
- from glob import glob
- from importlib import import_module
- from posixpath import join
- from mezzanine.utils.conf import real_project_name
- from fabric.api import abort, env, cd, prefix, sudo as _sudo, run as _run, \
- hide, task, local
- from fabric.context_managers import settings as fab_settings
- from fabric.contrib.console import confirm
- from fabric.contrib.files import exists, upload_template
- from fabric.contrib.project import rsync_project
- from fabric.colors import yellow, green, blue, red
- from fabric.decorators import hosts
- ################
- # Config setup #
- ################
- if not hasattr(env, "proj_app"):
- env.proj_app = real_project_name("acreditacion")
- conf = {}
- if sys.argv[0].split(os.sep)[-1] in ("fab", "fab-script.py"):
- # Ensure we import settings from the current dir
- try:
- conf = import_module("%s.settings" % env.proj_app).FABRIC
- try:
- conf["HOSTS"][0]
- except (KeyError, ValueError):
- raise ImportError
- except (ImportError, AttributeError):
- print("Aborting, no hosts defined.")
- exit()
- env.db_pass = conf.get("DB_PASS", None)
- env.admin_pass = conf.get("ADMIN_PASS", None)
- env.user = conf.get("SSH_USER", getuser())
- env.password = conf.get("SSH_PASS", None)
- env.key_filename = conf.get("SSH_KEY_PATH", None)
- env.hosts = conf.get("HOSTS", [""])
- env.proj_name = conf.get("PROJECT_NAME", env.proj_app)
- env.venv_home = conf.get("VIRTUALENV_HOME", "/home/%s/.virtualenvs" % env.user)
- env.venv_path = join(env.venv_home, env.proj_name)
- env.proj_path = "/home/%s/mezzanine/%s" % (env.user, env.proj_name)
- env.manage = "%s/bin/python %s/manage.py" % (env.venv_path, env.proj_path)
- env.domains = conf.get("DOMAINS", [conf.get("LIVE_HOSTNAME", env.hosts[0])])
- env.domains_nginx = " ".join(env.domains)
- env.domains_regex = "|".join(env.domains)
- env.domains_python = ", ".join(["'%s'" % s for s in env.domains])
- env.ssl_disabled = "#" if len(env.domains) > 1 else ""
- env.vcs_tools = ["git", "hg"]
- env.deploy_tool = conf.get("DEPLOY_TOOL", "rsync")
- env.reqs_path = conf.get("REQUIREMENTS_PATH", None)
- env.locale = conf.get("LOCALE", "en_US.UTF-8")
- env.num_workers = conf.get("NUM_WORKERS",
- "multiprocessing.cpu_count() * 2 + 1")
- env.secret_key = conf.get("SECRET_KEY", "")
- env.nevercache_key = conf.get("NEVERCACHE_KEY", "")
- if not env.secret_key:
- print("Aborting, no SECRET_KEY setting defined.")
- exit()
- # Remote git repos need to be "bare" and reside separated from the project
- if env.deploy_tool == "git":
- env.repo_path = "/home/%s/git/%s.git" % (env.user, env.proj_name)
- else:
- env.repo_path = env.proj_path
- ##################
- # Template setup #
- ##################
- # Each template gets uploaded at deploy time, only if their
- # contents has changed, in which case, the reload command is
- # also run.
- templates = {
- "nginx": {
- "local_path": "deploy/nginx.conf.template",
- "remote_path": "/etc/nginx/sites-enabled/%(proj_name)s.conf",
- "reload_command": "service nginx restart",
- },
- "supervisor": {
- "local_path": "deploy/supervisor.conf.template",
- "remote_path": "/etc/supervisor/conf.d/%(proj_name)s.conf",
- "reload_command": "supervisorctl update gunicorn_%(proj_name)s",
- },
- "cron": {
- "local_path": "deploy/crontab.template",
- "remote_path": "/etc/cron.d/%(proj_name)s",
- "owner": "root",
- "mode": "600",
- },
- "gunicorn": {
- "local_path": "deploy/gunicorn.conf.py.template",
- "remote_path": "%(proj_path)s/gunicorn.conf.py",
- },
- "settings": {
- "local_path": "deploy/local_settings.py.template",
- "remote_path": "%(proj_path)s/%(proj_app)s/local_settings.py",
- },
- }
- ######################################
- # Context for virtualenv and project #
- ######################################
- @contextmanager
- def virtualenv():
- """
- Runs commands within the project's virtualenv.
- """
- with cd(env.venv_path):
- with prefix("source %s/bin/activate" % env.venv_path):
- yield
- @contextmanager
- def project():
- """
- Runs commands within the project's directory.
- """
- with virtualenv():
- with cd(env.proj_path):
- yield
- @contextmanager
- def update_changed_requirements():
- """
- Checks for changes in the requirements file across an update,
- and gets new requirements if changes have occurred.
- """
- reqs_path = join(env.proj_path, env.reqs_path)
- get_reqs = lambda: run("cat %s" % reqs_path, show=False)
- old_reqs = get_reqs() if env.reqs_path else ""
- yield
- if old_reqs:
- new_reqs = get_reqs()
- if old_reqs == new_reqs:
- # Unpinned requirements should always be checked.
- for req in new_reqs.split("\n"):
- if req.startswith("-e"):
- if "@" not in req:
- # Editable requirement without pinned commit.
- break
- elif req.strip() and not req.startswith("#"):
- if not set(">=<") & set(req):
- # PyPI requirement without version.
- break
- else:
- # All requirements are pinned.
- return
- pip("-r %s/%s" % (env.proj_path, env.reqs_path))
- ###########################################
- # Utils and wrappers for various commands #
- ###########################################
- def _print(output):
- print()
- print(output)
- print()
- def print_command(command):
- _print(blue("$ ", bold=True) +
- yellow(command, bold=True) +
- red(" ->", bold=True))
- @task
- def run(command, show=True, *args, **kwargs):
- """
- Runs a shell comand on the remote server.
- """
- if show:
- print_command(command)
- with hide("running"):
- return _run(command, *args, **kwargs)
- @task
- def sudo(command, show=True, *args, **kwargs):
- """
- Runs a command as sudo on the remote server.
- """
- if show:
- print_command(command)
- with hide("running"):
- return _sudo(command, *args, **kwargs)
- def log_call(func):
- @wraps(func)
- def logged(*args, **kawrgs):
- header = "-" * len(func.__name__)
- _print(green("\n".join([header, func.__name__, header]), bold=True))
- return func(*args, **kawrgs)
- return logged
- def get_templates():
- """
- Returns each of the templates with env vars injected.
- """
- injected = {}
- for name, data in templates.items():
- injected[name] = dict([(k, v % env) for k, v in data.items()])
- return injected
- def upload_template_and_reload(name):
- """
- Uploads a template only if it has changed, and if so, reload the
- related service.
- """
- template = get_templates()[name]
- local_path = template["local_path"]
- if not os.path.exists(local_path):
- project_root = os.path.dirname(os.path.abspath(__file__))
- local_path = os.path.join(project_root, local_path)
- remote_path = template["remote_path"]
- reload_command = template.get("reload_command")
- owner = template.get("owner")
- mode = template.get("mode")
- remote_data = ""
- if exists(remote_path):
- with hide("stdout"):
- remote_data = sudo("cat %s" % remote_path, show=False)
- with open(local_path, "r") as f:
- local_data = f.read()
- # Escape all non-string-formatting-placeholder occurrences of '%':
- local_data = re.sub(r"%(?!\(\w+\)s)", "%%", local_data)
- if "%(db_pass)s" in local_data:
- env.db_pass = db_pass()
- local_data %= env
- clean = lambda s: s.replace("\n", "").replace("\r", "").strip()
- if clean(remote_data) == clean(local_data):
- return
- upload_template(local_path, remote_path, env, use_sudo=True, backup=False)
- if owner:
- sudo("chown %s %s" % (owner, remote_path))
- if mode:
- sudo("chmod %s %s" % (mode, remote_path))
- if reload_command:
- sudo(reload_command)
- def rsync_upload():
- """
- Uploads the project with rsync excluding some files and folders.
- """
- excludes = ["*.pyc", "*.pyo", "*.db", ".DS_Store", ".coverage",
- "local_settings.py", "/static", "/.git", "/.hg"]
- local_dir = os.getcwd() + os.sep
- return rsync_project(remote_dir=env.proj_path, local_dir=local_dir,
- exclude=excludes)
- def vcs_upload():
- """
- Uploads the project with the selected VCS tool.
- """
- if env.deploy_tool == "git":
- remote_path = "ssh://%s@%s%s" % (env.user, env.host_string,
- env.repo_path)
- if not exists(env.repo_path):
- run("mkdir -p %s" % env.repo_path)
- with cd(env.repo_path):
- run("git init --bare")
- local("git push -f %s master" % remote_path)
- with cd(env.repo_path):
- run("GIT_WORK_TREE=%s git checkout -f master" % env.proj_path)
- run("GIT_WORK_TREE=%s git reset --hard" % env.proj_path)
- elif env.deploy_tool == "hg":
- remote_path = "ssh://%s@%s/%s" % (env.user, env.host_string,
- env.repo_path)
- with cd(env.repo_path):
- if not exists("%s/.hg" % env.repo_path):
- run("hg init")
- print(env.repo_path)
- with fab_settings(warn_only=True):
- push = local("hg push -f %s" % remote_path)
- if push.return_code == 255:
- abort()
- run("hg update")
- def db_pass():
- """
- Prompts for the database password if unknown.
- """
- if not env.db_pass:
- env.db_pass = getpass("Enter the database password: ")
- return env.db_pass
- @task
- def apt(packages):
- """
- Installs one or more system packages via apt.
- """
- return sudo("apt-get install -y -q " + packages)
- @task
- def pip(packages):
- """
- Installs one or more Python packages within the virtual environment.
- """
- with virtualenv():
- return run("pip install %s" % packages)
- def postgres(command):
- """
- Runs the given command as the postgres user.
- """
- show = not command.startswith("psql")
- return sudo(command, show=show, user="postgres")
- @task
- def psql(sql, show=True):
- """
- Runs SQL against the project's database.
- """
- out = postgres('psql -c "%s"' % sql)
- if show:
- print_command(sql)
- return out
- @task
- def backup(filename):
- """
- Backs up the project database.
- """
- tmp_file = "/tmp/%s" % filename
- # We dump to /tmp because user "postgres" can't write to other user folders
- # We cd to / because user "postgres" might not have read permissions
- # elsewhere.
- with cd("/"):
- postgres("pg_dump -Fc %s > %s" % (env.proj_name, tmp_file))
- run("cp %s ." % tmp_file)
- sudo("rm -f %s" % tmp_file)
- @task
- def restore(filename):
- """
- Restores the project database from a previous backup.
- """
- return postgres("pg_restore -c -d %s %s" % (env.proj_name, filename))
- @task
- def python(code, show=True):
- """
- Runs Python code in the project's virtual environment, with Django loaded.
- """
- setup = "import os;" \
- "os.environ[\'DJANGO_SETTINGS_MODULE\']=\'%s.settings\';" \
- "import django;" \
- "django.setup();" % env.proj_app
- full_code = 'python -c "%s%s"' % (setup, code.replace("`", "\\\`"))
- with project():
- if show:
- print_command(code)
- result = run(full_code, show=False)
- return result
- def static():
- """
- Returns the live STATIC_ROOT directory.
- """
- return python("from django.conf import settings;"
- "print(settings.STATIC_ROOT)", show=False).split("\n")[-1]
- @task
- def manage(command):
- """
- Runs a Django management command.
- """
- return run("%s %s" % (env.manage, command))
- ###########################
- # Security best practices #
- ###########################
- @task
- @log_call
- @hosts(["root@%s" % host for host in env.hosts])
- def secure(new_user=env.user):
- """
- Minimal security steps for brand new servers.
- Installs system updates, creates new user (with sudo privileges) for future
- usage, and disables root login via SSH.
- """
- run("apt-get update -q")
- run("apt-get upgrade -y -q")
- run("adduser --gecos '' %s" % new_user)
- run("usermod -G sudo %s" % new_user)
- run("sed -i 's:RootLogin yes:RootLogin no:' /etc/ssh/sshd_config")
- run("service ssh restart")
- print(green("Security steps completed. Log in to the server as '%s' from "
- "now on." % new_user, bold=True))
- #########################
- # Install and configure #
- #########################
- @task
- @log_call
- def install():
- """
- Installs the base system and Python requirements for the entire server.
- """
- # Install system requirements
- sudo("apt-get update -y -q")
- apt("nginx libjpeg-dev python-dev python-setuptools git-core "
- "postgresql libpq-dev memcached supervisor python-pip")
- run("mkdir -p /home/%s/logs" % env.user)
- # Install Python requirements
- sudo("pip install -U pip virtualenv virtualenvwrapper mercurial")
- # Set up virtualenv
- run("mkdir -p %s" % env.venv_home)
- run("echo 'export WORKON_HOME=%s' >> /home/%s/.bashrc" % (env.venv_home,
- env.user))
- run("echo 'source /usr/local/bin/virtualenvwrapper.sh' >> "
- "/home/%s/.bashrc" % env.user)
- print(green("Successfully set up git, mercurial, pip, virtualenv, "
- "supervisor, memcached.", bold=True))
- @task
- @log_call
- def create():
- """
- Creates the environment needed to host the project.
- The environment consists of: system locales, virtualenv, database, project
- files, SSL certificate, and project-specific Python requirements.
- """
- # Generate project locale
- locale = env.locale.replace("UTF-8", "utf8")
- with hide("stdout"):
- if locale not in run("locale -a"):
- sudo("locale-gen %s" % env.locale)
- sudo("update-locale %s" % env.locale)
- sudo("service postgresql restart")
- run("exit")
- # Create project path
- run("mkdir -p %s" % env.proj_path)
- # Set up virtual env
- run("mkdir -p %s" % env.venv_home)
- with cd(env.venv_home):
- if exists(env.proj_name):
- if confirm("Virtualenv already exists in host server: %s"
- "\nWould you like to replace it?" % env.proj_name):
- run("rm -rf %s" % env.proj_name)
- else:
- abort()
- run("virtualenv %s" % env.proj_name)
- # Upload project files
- if env.deploy_tool in env.vcs_tools:
- vcs_upload()
- else:
- rsync_upload()
- # Create DB and DB user
- pw = db_pass()
- user_sql_args = (env.proj_name, pw.replace("'", "\'"))
- user_sql = "CREATE USER %s WITH ENCRYPTED PASSWORD '%s';" % user_sql_args
- psql(user_sql, show=False)
- shadowed = "*" * len(pw)
- print_command(user_sql.replace("'%s'" % pw, "'%s'" % shadowed))
- psql("CREATE DATABASE %s WITH OWNER %s ENCODING = 'UTF8' "
- "LC_CTYPE = '%s' LC_COLLATE = '%s' TEMPLATE template0;" %
- (env.proj_name, env.proj_name, env.locale, env.locale))
- # Set up SSL certificate
- if not env.ssl_disabled:
- conf_path = "/etc/nginx/conf"
- if not exists(conf_path):
- sudo("mkdir %s" % conf_path)
- with cd(conf_path):
- crt_file = env.proj_name + ".crt"
- key_file = env.proj_name + ".key"
- if not exists(crt_file) and not exists(key_file):
- try:
- crt_local, = glob(join("deploy", "*.crt"))
- key_local, = glob(join("deploy", "*.key"))
- except ValueError:
- parts = (crt_file, key_file, env.domains[0])
- sudo("openssl req -new -x509 -nodes -out %s -keyout %s "
- "-subj '/CN=%s' -days 3650" % parts)
- else:
- upload_template(crt_local, crt_file, use_sudo=True)
- upload_template(key_local, key_file, use_sudo=True)
- # Install project-specific requirements
- upload_template_and_reload("settings")
- with project():
- if env.reqs_path:
- pip("-r %s/%s" % (env.proj_path, env.reqs_path))
- pip("gunicorn setproctitle psycopg2 "
- "django-compressor python-memcached")
- # Bootstrap the DB
- manage("createdb --noinput --nodata")
- python("from django.conf import settings;"
- "from django.contrib.sites.models import Site;"
- "Site.objects.filter(id=settings.SITE_ID).update(domain='%s');"
- % env.domains[0])
- for domain in env.domains:
- python("from django.contrib.sites.models import Site;"
- "Site.objects.get_or_create(domain='%s');" % domain)
- if env.admin_pass:
- pw = env.admin_pass
- user_py = ("from django.contrib.auth import get_user_model;"
- "User = get_user_model();"
- "u, _ = User.objects.get_or_create(username='admin');"
- "u.is_staff = u.is_superuser = True;"
- "u.set_password('%s');"
- "u.save();" % pw)
- python(user_py, show=False)
- shadowed = "*" * len(pw)
- print_command(user_py.replace("'%s'" % pw, "'%s'" % shadowed))
- return True
- @task
- @log_call
- def remove():
- """
- Blow away the current project.
- """
- if exists(env.venv_path):
- run("rm -rf %s" % env.venv_path)
- if exists(env.proj_path):
- run("rm -rf %s" % env.proj_path)
- for template in get_templates().values():
- remote_path = template["remote_path"]
- if exists(remote_path):
- sudo("rm %s" % remote_path)
- if exists(env.repo_path):
- run("rm -rf %s" % env.repo_path)
- sudo("supervisorctl update")
- psql("DROP DATABASE IF EXISTS %s;" % env.proj_name)
- psql("DROP USER IF EXISTS %s;" % env.proj_name)
- ##############
- # Deployment #
- ##############
- @task
- @log_call
- def restart():
- """
- Restart gunicorn worker processes for the project.
- If the processes are not running, they will be started.
- """
- pid_path = "%s/gunicorn.pid" % env.proj_path
- if exists(pid_path):
- run("kill -HUP `cat %s`" % pid_path)
- else:
- sudo("supervisorctl update")
- @task
- @log_call
- def deploy():
- """
- Deploy latest version of the project.
- Backup current version of the project, push latest version of the project
- via version control or rsync, install new requirements, sync and migrate
- the database, collect any new static assets, and restart gunicorn's worker
- processes for the project.
- """
- if not exists(env.proj_path):
- if confirm("Project does not exist in host server: %s"
- "\nWould you like to create it?" % env.proj_name):
- create()
- else:
- abort()
- # Backup current version of the project
- with cd(env.proj_path):
- backup("last.db")
- if env.deploy_tool in env.vcs_tools:
- with cd(env.repo_path):
- if env.deploy_tool == "git":
- run("git rev-parse HEAD > %s/last.commit" % env.proj_path)
- elif env.deploy_tool == "hg":
- run("hg id -i > last.commit")
- with project():
- static_dir = static()
- if exists(static_dir):
- run("tar -cf static.tar --exclude='*.thumbnails' %s" %
- static_dir)
- else:
- with cd(join(env.proj_path, "..")):
- excludes = ["*.pyc", "*.pio", "*.thumbnails"]
- exclude_arg = " ".join("--exclude='%s'" % e for e in excludes)
- run("tar -cf {0}.tar {1} {0}".format(env.proj_name, exclude_arg))
- # Deploy latest version of the project
- with update_changed_requirements():
- if env.deploy_tool in env.vcs_tools:
- vcs_upload()
- else:
- rsync_upload()
- with project():
- manage("collectstatic -v 0 --noinput")
- manage("migrate --noinput")
- for name in get_templates():
- upload_template_and_reload(name)
- restart()
- return True
- @task
- @log_call
- def rollback():
- """
- Reverts project state to the last deploy.
- When a deploy is performed, the current state of the project is
- backed up. This includes the project files, the database, and all static
- files. Calling rollback will revert all of these to their state prior to
- the last deploy.
- """
- with update_changed_requirements():
- if env.deploy_tool in env.vcs_tools:
- with cd(env.repo_path):
- if env.deploy_tool == "git":
- run("GIT_WORK_TREE={0} git checkout -f "
- "`cat {0}/last.commit`".format(env.proj_path))
- elif env.deploy_tool == "hg":
- run("hg update -C `cat last.commit`")
- with project():
- with cd(join(static(), "..")):
- run("tar -xf %s/static.tar" % env.proj_path)
- else:
- with cd(env.proj_path.rsplit("/", 1)[0]):
- run("rm -rf %s" % env.proj_name)
- run("tar -xf %s.tar" % env.proj_name)
- with cd(env.proj_path):
- restore("last.db")
- restart()
- @task
- @log_call
- def all():
- """
- Installs everything required on a new system and deploy.
- From the base software, up to the deployed project.
- """
- install()
- if create():
- deploy()
|