aboutsummaryrefslogtreecommitdiffstats
path: root/django/fabfile.py
diff options
context:
space:
mode:
Diffstat (limited to 'django/fabfile.py')
-rwxr-xr-xdjango/fabfile.py721
1 files changed, 721 insertions, 0 deletions
diff --git a/django/fabfile.py b/django/fabfile.py
new file mode 100755
index 0000000..8dd73c8
--- /dev/null
+++ b/django/fabfile.py
@@ -0,0 +1,721 @@
+# ============LICENSE_START==========================================
+# org.onap.vvp/cms
+# ===================================================================
+# Copyright © 2017 AT&T Intellectual Property. All rights reserved.
+# ===================================================================
+#
+# Unless otherwise specified, all software contained herein is licensed
+# under the Apache License, Version 2.0 (the “License”);
+# you may not use this software except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+#
+# Unless otherwise specified, all documentation contained herein is licensed
+# under the Creative Commons License, Attribution 4.0 Intl. (the “License”);
+# you may not use this documentation except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://creativecommons.org/licenses/by/4.0/
+#
+# Unless required by applicable law or agreed to in writing, documentation
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# ============LICENSE_END============================================
+#
+# ECOMP is a trademark and service mark of AT&T Intellectual Property.
+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 #
+################
+
+env.proj_app = real_project_name("cms")
+
+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", "")
+
+# 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()