Commits

Rod Morison  committed 3aba406 Draft

fabfile enhancements, too many to list

  • Participants
  • Parent commits 9623579

Comments (0)

Files changed (7)

File deploy/gunicorn.conf.py

 import os
 
-bind = "127.0.0.1:%(gunicorn_port)s"
-workers = (os.sysconf("SC_NPROCESSORS_ONLN") * 2) + 1
+bind = "%(wsgi_listen)s"
+workers = %(wsgi_workers)s
 loglevel = "error"
 proc_name = "%(proj_name)s"

File deploy/htpasswd

+%(site_name)s:%(hashed_htpasswd)s

File deploy/live_settings.py

 }
 
 SESSION_ENGINE = "django.contrib.sessions.backends.cache"
+
+GOOGLE_ANALYTICS_ID = "%(gaid)s"

File deploy/nginx.conf

 
 upstream %(proj_name)s {
-    server 127.0.0.1:%(gunicorn_port)s;
+    server %(wsgi_listen)s;
 }
 
 server {
 
-    listen 80;
-    listen 443;
-    server_name %(live_host)s;
+    listen               80;
+    server_name          %(site_domain)s;
     client_max_body_size 10M;
     keepalive_timeout    15;
 
-    ssl                  on;
-    ssl_certificate      conf/%(proj_name)s.crt;
-    ssl_certificate_key  conf/%(proj_name)s.key;
-    ssl_session_cache    shared:SSL:10m;
-    ssl_session_timeout  10m;
+    listen 443 ssl;
+    ssl_certificate /etc/ssl/certs/rodmtech.net.pem;
+    ssl_certificate_key /etc/ssl/private/rodmtech.net.key;
+
+    ssl_session_timeout 5m;
+
+    ssl_protocols SSLv3 TLSv1;
+    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
+     
+    if ($scheme = http) {
+        rewrite ^/admin https://$host$request_uri permanent;
+        rewrite ^/cyrup https://$host$request_uri permanent;
+        rewrite ^/webmail https://$host$request_uri permanent;
+    }
+
+    # php sites supported in certain subdirs
+    index index.html index.htm index.php;
+    location /cyrup/ {
+        root            /usr/share/nginx/www;
+        access_log      off;
+        log_not_found   off;
+    }
+    location ~ /cyrup/.*\.php {
+    	fastcgi_split_path_info ^(.+\.php)(/.+)$;
+    	fastcgi_pass 127.0.0.1:9000;
+        root /usr/share/nginx/www/;
+    	fastcgi_index index.php;
+    	include fastcgi_params;
+    }
+
+    location /webmail/ {
+        root            /usr/share/nginx/www;
+        access_log      off;
+        log_not_found   off;
+    }
+    location ~ /webmail/.*\.php {
+    	fastcgi_split_path_info ^(.+\.php)(/.+)$;
+    	fastcgi_pass 127.0.0.1:9000;
+        root /usr/share/nginx/www/;
+    	fastcgi_index index.php;
+    	include fastcgi_params;
+    }
 
     location / {
         proxy_redirect      off;
         proxy_set_header    X-Real-IP               $remote_addr;
         proxy_set_header    X-Forwarded-For         $proxy_add_x_forwarded_for;
         proxy_set_header    X-Forwarded-Protocol    $scheme;
-        proxy_pass          http://%(proj_name)s;
+        %(proxy_pass)s
+        %(httpauth)s
     }
 
     location /static/ {
-        root            %(proj_path)s;
+        root            %(repo_path)s;
         access_log      off;
         log_not_found   off;
     }
 
     location /robots.txt {
-        root            %(proj_path)s/static;
+        root            %(repo_path)s/static;
         access_log      off;
         log_not_found   off;
     }
 
     location /favicon.ico {
-        root            %(proj_path)s/static/img;
+        root            %(repo_path)s/static/img;
         access_log      off;
         log_not_found   off;
     }

File deploy/supervisor.conf

 [group:%(proj_name)s]
-programs=gunicorn_%(proj_name)s
+programs=%(wsgi_server)s_%(proj_name)s
 
-[program:gunicorn_%(proj_name)s]
-command=%(venv_path)s/bin/gunicorn_django -c gunicorn.conf.py -p gunicorn.pid
-directory=%(proj_path)s
+[program:%(wsgi_server)s_%(proj_name)s]
+command=%(wsgi_command)s
+directory=%(repo_path)s
 user=%(user)s
 autostart=true
 autorestart=true
 redirect_stderr=true
-environment=LANG="%(locale)s",LC_ALL="%(locale)s",LC_LANG="%(locale)s"
+environment=LANG="%(locale)s",LC_ALL="%(locale)s",LC_LANG="%(locale)s",PATH=$PATH:%(project_path)s/bin
+stopsignal=QUIT

File deploy/uwsgi.ini

+[uwsgi]
+chdir = %(repo_path)s
+module = wsgi:application
+env = DJANGO_SETTINGS_MODULE=settings
+master = True
+pidfile = %(repo_path)s/uwsgi.pid
+socket = %(wsgi_listen)s
+protocol = uwsgi
+vacuum = True
+virtualenv = %(project_path)s
+workers = %(wsgi_workers)s
 
 import os
-import re
 import sys
+import random
+from importlib import import_module
 from functools import wraps
 from getpass import getpass, getuser
 from glob import glob
 from contextlib import contextmanager
 
-from fabric.api import env, cd, prefix, sudo as _sudo, run as _run, hide, task
+from fabric.api import env, cd, prefix, task, settings, put, hide, require, local, \
+     sudo as _sudo, run as _run, show as _show
 from fabric.contrib.files import exists, upload_template
 from fabric.colors import yellow, green, blue, red
 
 
+####################
+# htpasswd supprot #
+####################
+
+def salt():
+    """Returns a string of 2 randome letters"""
+    letters = 'abcdefghijklmnopqrstuvwxyz' \
+              'ABCDEFGHIJKLMNOPQRSTUVWXYZ' \
+              '0123456789/.'
+    return random.choice(letters) + random.choice(letters)
+
+
+def hash_htpasswd(password):
+    try:
+        import crypt
+    except ImportError:
+        try:
+            import fcrypt as crypt
+        except ImportError:
+            sys.stderr.write("Cannot find a crypt module.  "
+                             "Possibly http://carey.geek.nz/code/python-fcrypt/\n")
+            sys.exit(1)
+
+    return crypt.crypt(password, salt())
+
 ################
 # Config setup #
 ################
 
-conf = {}
-if sys.argv[0].split(os.sep)[-1] == "fab":
-    # Ensure we import settings from the current dir
-    try:
-        conf = __import__("settings", globals(), locals(), [], 0).FABRIC
-        try:
-            conf["HOSTS"][0]
-        except (KeyError, ValueError):
-            raise ImportError
-    except (ImportError, AttributeError):
-        print "Aborting, no hosts defined."
-        exit()
+def load_env(conf):
+    # whether to use the usual ~/.ssh/config settings
+    env.use_ssh_config = conf.get("USE_SSH_CONFIG", False)
+    # password for the initial django admin user (/admin login)
+    env.admin_pass = conf.get("ADMIN_PASS", None)
+    # postgres site db login: site_name/db_pass
+    env.db_pass = conf.get("PROJECT_DB_PASS", None)
+    # if not None then setup httpauth to cloak site: site_name/http_pass
+    env.http_pass = conf.get("HTTP_PASS", None)
+    # username used by SSH when connecting to remote hosts
+    env.user = conf.get("SSH_USER", "") or getuser()
+    # password used by SSH when connecting to remote hosts, and/or answering sudo prompts
+    env.password = conf.get("SSH_PASS", None)
+    # paths to SSH key files to try when connecting
+    env.key_filename = conf.get("SSH_KEY_PATH", None)
 
-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", os.getcwd().split(os.sep)[-1])
-env.venv_home = conf.get("VIRTUALENV_HOME", "/home/%s" % env.user)
-env.venv_path = "%s/%s" % (env.venv_home, env.proj_name)
-env.proj_dirname = "project"
-env.proj_path = "%s/%s" % (env.venv_path, env.proj_dirname)
-env.manage = "%s/bin/python %s/project/manage.py" % (env.venv_path,
-                                                     env.venv_path)
-env.live_host = conf.get("LIVE_HOSTNAME", env.hosts[0] if env.hosts else None)
-env.repo_url = conf.get("REPO_URL", None)
-env.reqs_path = conf.get("REQUIREMENTS_PATH", None)
-env.gunicorn_port = conf.get("GUNICORN_PORT", 8000)
-env.locale = conf.get("LOCALE", "en_US.UTF-8")
+    # primary name for the project, top of code tree, config file
+    env.proj_name = conf.get("PROJECT_NAME", os.getcwd().split(os.sep)[-1].split('_')[0])
+    # root of virtualenvs, can be set by virtualenvwrapper WORKON_HOME env
+    env.workon_home = conf.get("WORKON_HOME", _run("echo \$WORKON_HOME", False))
+    if not os.path.isabs(env.workon_home):
+        home = run("pwd", False)
+        env.workon_home = os.path.join(home, env.workon_home)
+    # full path to project tree
+    env.project_path = "%s/%s" % (env.workon_home, env.proj_name)
+    # name of repo dir under project_path
+    env.repo_name = conf.get("REPO_NAME", os.getcwd().split(os.sep)[-1])
+    # full path to repo dir
+    env.repo_path = "%s/%s" % (env.project_path, env.repo_name)
+    # run project's manage.py with venv's python
+    env.manage = "%s/bin/python %s/manage.py" % (env.project_path, env.repo_path)
+    # The domain name associated with the Web site (see django sites franework)
+    env.site_domain = conf.get("SITE_DOMAIN", env.hosts[0] if env.hosts else None)
+    # A human-readable "verbose" name for the Web site (see django sites franework)
+    env.site_name = conf.get("SITE_NAME", env.hosts[0] if env.hosts else None)
+    # path to upstream/central code repo
+    env.repo_url = conf.get("REPO_URL", None)
+    # path under repo_path to requirements.txt
+    env.reqs_path = conf.get("REQUIREMENTS_PATH", None)
+    # wsgi server type: gunicorn or uwsgi
+    env.wsgi_server = conf.get("WSGI_SERVER", "uwsgi")
+    # host:port wsgi server is on
+    env.wsgi_listen = conf.get("WSGI_LISTEN", "localhost:8888")
+    # number of wsgi worker processes to run
+    env.wsgi_workers = conf.get("WSGI_WORKERS", nprocessors()*2 + 1)
+    # locale for database create
+    env.locale = conf.get("LOCALE", "en_US.UTF-8")
+    # run manage.py compress on deploy
+    env.compress_offline = conf.get("COMPRESS_OFFLINE", False)
+    # for GOOGLE_ANALYTICS_ID in local_settings.py
+    env.gaid = conf.get("GOOGLE_ANALYTICS_ID", "")
 
 
 ##################
 # contents has changed, in which case, the reload command is
 # also run.
 
-templates = {
-    "nginx": {
-        "local_path": "deploy/nginx.conf",
-        "remote_path": "/etc/nginx/sites-enabled/%(proj_name)s.conf",
-        "reload_command": "service nginx restart",
-    },
-    "supervisor": {
-        "local_path": "deploy/supervisor.conf",
-        "remote_path": "/etc/supervisor/conf.d/%(proj_name)s.conf",
-        "reload_command": "supervisorctl reload",
-    },
-    "cron": {
-        "local_path": "deploy/crontab",
-        "remote_path": "/etc/cron.d/%(proj_name)s",
-        "owner": "root",
-        "mode": "600",
-    },
-    "gunicorn": {
-        "local_path": "deploy/gunicorn.conf.py",
-        "remote_path": "%(proj_path)s/gunicorn.conf.py",
-    },
-    "settings": {
-        "local_path": "deploy/live_settings.py",
-        "remote_path": "%(proj_path)s/local_settings.py",
-    },
-}
+def load_templates():
+    env.templates = {
+        "nginx": {
+            "local_path": "deploy/nginx.conf",
+            "remote_path": "/etc/nginx/sites-enabled/%(proj_name)s",
+            "reload_command": "service nginx restart",
+            "owner": "root",
+        },
+        "supervisor": {
+            "local_path": "deploy/supervisor.conf",
+            "remote_path": "/etc/supervisor/conf.d/%(proj_name)s.conf",
+            "reload_command": "supervisorctl reload",
+        },
+        "cron": {
+            "local_path": "deploy/crontab",
+            "remote_path": "/etc/cron.d/%(proj_name)s",
+            "owner": "root",
+            "mode": "600",
+        },
+        "settings": {
+            "local_path": "deploy/live_settings.py",
+            "remote_path": "%(repo_path)s/local_settings.py",
+        },
+    }
+    if env.wsgi_server == "gunicorn":
+        env.templates["gunicorn"] = {
+            "local_path": "deploy/gunicorn.conf.py",
+            "remote_path": "%(repo_path)s/gunicorn.conf.py",
+        }
+        env.wsgi_command = "%(project_path)s/bin/gunicorn_django -c gunicorn.conf.py -p gunicorn.pid" % env
+        env.proxy_pass = "        proxy_pass          http://%(proj_name)s;" % env
+    elif env.wsgi_server == "uwsgi":
+        env.templates["uwsgi"] = {
+            "local_path": "deploy/uwsgi.ini",
+            "remote_path": "%(repo_path)s/uwsgi.ini",
+        }
+        env.wsgi_command = "%(project_path)s/bin/uwsgi --ini uwsgi.ini" % env
+        env.proxy_pass = "include             uwsgi_params;\n" \
+                         "        uwsgi_pass          %(proj_name)s;" % env
+
+    if env.http_pass:
+        # goes in nginx-site.conf
+        env.httpauth = """
+            auth_basic            "Restricted";
+            auth_basic_user_file  htpasswd-%(site_name)s;""" % env
+        # goes in htpasswd
+        env.hashed_htpasswd = hash_htpasswd(env.http_pass)
+        env.templates["htpasswd"] = {
+            "local_path": "deploy/htpasswd",
+            "remote_path": "/etc/nginx/htpasswd-%(site_name)s" % env,
+        }
+    else:
+        env.httpauth = ""
+        env.hashed_htpasswd = ""
+
+
+# handle fabsettings task outside of fabric task, env.hosts must be set
+# before invoking the first fabric task
+try:
+    env.fabsettings = filter(lambda x: x.startswith('fabsettings:'), sys.argv)[0].split(':')[1]
+except:
+    pass
+else:
+    try:
+        # can't assume fabfile's dir is on path in a task
+        sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+        conf = import_module(env.fabsettings).FABRIC
+        try:
+            conf["HOSTS"][0]
+        except (KeyError, ValueError):
+            print "Aborting, cannot find HOSTS in FABRIC dict in %s" % via
+            exit()
+    except (ImportError, AttributeError):
+        print "Aborting, cannot import %(fabsettings)s" % env
+        exit()
+    # global host list used when composing per-task host lists
+    env.hosts = conf["HOSTS"]
+
+@task
+def fabsettings(via):
+    """Choose a FABRIC dict settings file"""
+    if not env.fabsettings:
+        print "Invalid or missing fabsettings:fabmodule, aborting"
+        exit()
+    load_env(conf)
+    load_templates()
 
 
 ######################################
     """
     Runs commands within the project's virtualenv.
     """
-    with cd(env.venv_path):
-        with prefix("source %s/bin/activate" % env.venv_path):
+    with cd(env.project_path):
+        with prefix("source %s/bin/activate" % env.project_path):
             yield
 
 
     Runs commands within the project's directory.
     """
     with virtualenv():
-        with cd(env.proj_dirname):
+        with cd(env.repo_name):
             yield
 
 
     Checks for changes in the requirements file across an update,
     and gets new requirements if changes have occurred.
     """
-    reqs_path = os.path.join(env.proj_path, env.reqs_path)
+    reqs_path = os.path.join(env.repo_path, env.reqs_path)
     get_reqs = lambda: run("cat %s" % reqs_path, show=False)
     old_reqs = get_reqs() if env.reqs_path else ""
     yield
             else:
                 # All requirements are pinned.
                 return
-        pip("-r %s/%s" % (env.proj_path, env.reqs_path))
+        pip("-r %s/%s" % (env.repo_path, env.reqs_path))
+        apply_patches()
 
 
 ###########################################
 
 
 @task
-def sudo(command, show=True):
+def sudo(command, user=None, show=True):
     """
     Runs a command as sudo.
     """
     if show:
         print_command(command)
     with hide("running"):
-        return _sudo(command)
+        return _sudo(command, user=user)
 
 
 def log_call(func):
     Returns each of the templates with env vars injected.
     """
     injected = {}
-    for name, data in templates.items():
+    for name, data in env.templates.items():
         injected[name] = dict([(k, v % env) for k, v in data.items()])
     return injected
 
 
+@task
 def upload_template_and_reload(name):
     """
     Uploads a template only if it has changed, and if so, reload a
     related service.
     """
+    require("templates", provided_by=fabsettings)
     template = get_templates()[name]
     local_path = template["local_path"]
     remote_path = template["remote_path"]
             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
     Prompts for the database password if unknown.
     """
     if not env.db_pass:
-        env.db_pass = getpass("Enter the database password: ")
+        env.db_pass = getpass("Enter the project database password: ")
     return env.db_pass
 
-
 @task
 def apt(packages):
     """
     Installs one or more Python packages within the virtual environment.
     """
     with virtualenv():
-        return sudo("pip install %s" % packages)
+        return run("pip install %s" % packages)
 
 
 def postgres(command):
     Runs the given command as the postgres user.
     """
     show = not command.startswith("psql")
-    return run("sudo -u root sudo -u postgres %s" % command, show=show)
+    # must cd away because postgres user may not have r access to cwd
+    with cd("/tmp"):
+        return sudo("%s" % command, show=show, user="postgres")
 
 
 @task
     """
     Runs SQL against the project's database.
     """
-    out = postgres('psql -c "%s"' % sql)
     if show:
         print_command(sql)
+    out = postgres('psql -c "%s"' % sql)
     return out
 
 
 @task
+def patch(base_dir, patchfile, strip=0, use_sudo=False):
+    """
+    runs patch on a file
+    """
+    command = "patch -r - -N -p%d <%s" % (strip, patchfile)
+    cmd = sudo if use_sudo else run
+    with cd(base_dir), settings(warn_only=True):
+        return cmd(command)
+
+
+@task
 def backup(filename):
     """
     Backs up the database.
     """
+    require("proj_name", provided_by=fabsettings)
     return postgres("pg_dump -Fc %s > %s" % (env.proj_name, filename))
 
 
     """
     Restores the database.
     """
+    require("proj_name", provided_by=fabsettings)
     return postgres("pg_restore -c -d %s %s" % (env.proj_name, filename))
 
 
     Runs Python code in the project's virtual environment, with Django loaded.
     """
     setup = "import os; os.environ[\'DJANGO_SETTINGS_MODULE\']=\'settings\';"
-    full_code = 'python -c "%s%s"' % (setup, code.replace("`", "\\\`"))
     with project():
-        result = run(full_code, show=False)
         if show:
             print_command(code)
-    return result
+        return run('python -c "%s%s"' % (setup, code), show=False)
+
+@task
+def nprocessors(show=False):
+    """Return # of processers on a machine"""
+    with hide("stdout"):
+        nprocs = int(python('print os.sysconf(\'SC_NPROCESSORS_ONLN\')', show=False))
+    if show:
+        _print("nprocessors = %d" % nprocs)
+    return nprocs
 
 
 def static():
     Returns the live STATIC_ROOT directory.
     """
     return python("from django.conf import settings;"
-                  "print settings.STATIC_ROOT").split("\n")[-1]
+                  "print settings.STATIC_ROOT")
 
 
 @task
     """
     Runs a Django management command.
     """
+    require("manage", provided_by=fabsettings)
     return run("%s %s" % (env.manage, command))
 
 
             run("exit")
     sudo("apt-get update -y -q")
     apt("nginx libjpeg-dev python-dev python-setuptools git-core "
-        "postgresql libpq-dev memcached supervisor")
+        "postgresql libpq-dev memcached supervisor "
+        "libjpeg-dev zlib1g-dev libfreetype6-dev")
     sudo("easy_install pip")
     sudo("pip install virtualenv mercurial")
 
     live host.
     """
 
+    require("proj_name", provided_by=fabsettings)
     # Create virtualenv
-    with cd(env.venv_home):
+    if not exists(env.workon_home):
+        run("mkdir -p %s" % env.workon_home)
+    with cd(env.workon_home):
         if exists(env.proj_name):
             prompt = raw_input("\nVirtualenv exists: %s\nWould you like "
                                "to replace it? (yes/no) " % env.proj_name)
             if prompt.lower() != "yes":
                 print "\nAborting!"
                 return False
-            remove()
+            remove_project()
         run("virtualenv %s --distribute" % env.proj_name)
         vcs = "git" if env.repo_url.startswith("git") else "hg"
-        run("%s clone %s %s" % (vcs, env.repo_url, env.proj_path))
+        run("%s clone %s %s" % (vcs, env.repo_url, env.repo_path))
 
     # 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))
+    remove_db()
+    create_db()
 
     # Set up SSL certificate.
     conf_path = "/etc/nginx/conf"
                 crt_local, = glob(os.path.join("deploy", "*.crt"))
                 key_local, = glob(os.path.join("deploy", "*.key"))
             except ValueError:
-                parts = (crt_file, key_file, env.live_host)
+                parts = (crt_file, key_file, env.site_domain)
                 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)
+                upload_template(crt_file, crt_local, use_sudo=True)
+                upload_template(key_file, key_local, use_sudo=True)
 
     # Set up project.
     upload_template_and_reload("settings")
     with project():
         if env.reqs_path:
-            pip("-r %s/%s" % (env.proj_path, env.reqs_path))
-        pip("gunicorn setproctitle south psycopg2 "
-            "django-compressor python-memcached")
-        manage("createdb --noinput --nodata")
-        python("from django.conf import settings;"
-               "from django.contrib.sites.models import Site;"
-               "site, _ = Site.objects.get_or_create(id=settings.SITE_ID);"
-               "site.domain = '" + env.live_host + "';"
-               "site.save();")
-        if env.admin_pass:
-            pw = env.admin_pass
-            user_py = ("from django.contrib.auth.models import User;"
-                       "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))
+            pip("-r %s/%s" % (env.repo_path, env.reqs_path))
+            apply_patches()
+        pip("%(wsgi_server)s setproctitle south psycopg2 "
+            "django-compressor python-memcached" % env)
+        init_db()
 
     return True
 
-
 @task
 @log_call
-def remove():
+def remove_project():
     """
     Blow away the current project.
     """
-    if exists(env.venv_path):
-        sudo("rm -rf %s" % env.venv_path)
+    require("proj_name", provided_by=fabsettings)
+    if exists(env.project_path):
+        sudo("rm -rf %s" % env.project_path)
     for template in get_templates().values():
         remote_path = template["remote_path"]
         if exists(remote_path):
             sudo("rm %s" % remote_path)
-    psql("DROP DATABASE %s;" % env.proj_name)
-    psql("DROP USER %s;" % env.proj_name)
+
+
+def remote_find(path, glob):
+    with cd(path):
+        # make sure we use abs path
+        return run("find `pwd` -name %s" % glob).split()
+
+
+@task
+def init_db():
+    """Initialize db schema: sync & migrate"""
+    require("proj_name", provided_by=fabsettings)
+    manage("createdb --nodata --noinput")
+    python("from django.conf import settings;"
+           "from django.contrib.sites.models import Site;"
+           "site, _ = Site.objects.get_or_create(id=settings.SITE_ID);"
+           "site.domain = '" + env.site_domain + "';"
+           "site.name = '" + env.site_name + "';"
+           "site.save();")
+    if env.admin_pass:
+        pw = env.admin_pass
+        user_py = ("from django.contrib.auth.models import User;"
+                   "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))
+
+
+@task
+@log_call
+def create_db():
+    """Create DB and DB user."""
+    require("proj_name", provided_by=fabsettings)
+    pw = db_pass()
+    user_sql_args = (env.proj_name, pw.replace("'", "\'"))
+    user_sql = "CREATE USER %s WITH ENCRYPTED PASSWORD '%s';\n" % user_sql_args
+    user_sql += "ALTER USER %s CREATEDB;" % env.proj_name
+    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))
+
+
+@task
+@log_call
+def remove_db():
+    """Blow away the current db."""
+    require("proj_name", provided_by=fabsettings)
+    psql("DROP DATABASE IF EXISTS %s;" % env.proj_name)
+    psql("DROP DATABASE IF EXISTS test_%s;" % env.proj_name)
+    psql("DROP USER IF EXISTS %s;" % env.proj_name)
+
+@task
+def remove():
+    """
+    Blow away the current project and db.
+    """
+    remove_project()
+    remove_db()
 
 
 ##############
 @log_call
 def restart():
     """
-    Restart gunicorn worker processes for the project.
+    Restart wsgi server worker processes for the project.
     """
-    pid_path = "%s/gunicorn.pid" % env.proj_path
+    require("repo_path", "wsgi_server", provided_by=fabsettings)
+    pid_path = "%(repo_path)s/%(wsgi_server)s.pid" % env
     if exists(pid_path):
         sudo("kill -HUP `cat %s`" % pid_path)
     else:
-        start_args = (env.proj_name, env.proj_name)
-        sudo("supervisorctl start %s:gunicorn_%s" % start_args)
+        start_args = (env.proj_name, env.wsgi_server, env.proj_name)
+        sudo("supervisorctl start %s:%s_%s" % start_args)
+
+
+@task
+def apply_patches(path=None):
+    """Apply patches found in requirements/patches"""
+    require("proj_name", provided_by=fabsettings)
+    path = path or env.project_path
+    with project():
+        patchdir = os.path.join(os.path.dirname(env.reqs_path), "patches")
+        for patchfile in remote_find(patchdir, "*.patch"):
+            patch(path, patchfile)
 
 
 @task
     collect any new static assets, and restart gunicorn's work
     processes for the project.
     """
-    if not exists(env.venv_path):
+    require("proj_name", provided_by=fabsettings)
+    if not exists(env.project_path):
         prompt = raw_input("\nVirtualenv doesn't exist: %s\nWould you like "
                            "to create it? (yes/no) " % env.proj_name)
         if prompt.lower() != "yes":
         create()
     for name in get_templates():
         upload_template_and_reload(name)
+    backup("last.db")
     with project():
-        backup("last.db")
         run("tar -cf last.tar %s" % static())
         git = env.repo_url.startswith("git")
         run("%s > last.commit" % "git rev-parse HEAD" if git else "hg id -i")
         with update_changed_requirements():
             run("git pull origin master -f" if git else "hg pull && hg up -C")
+        apply_patches()
         manage("collectstatic -v 0 --noinput")
+        if env.compress_offline:
+            manage("compress")
         manage("syncdb --noinput")
         manage("migrate --noinput")
     restart()
     and all static files. Calling rollback will revert all of these to
     their state prior to the last deploy.
     """
+    require("proj_name", provided_by=fabsettings)
     with project():
         with update_changed_requirements():
             git = env.repo_url.startswith("git")
             update = "git checkout" if git else "hg up -C"
             run("%s `cat last.commit`" % update)
         with cd(os.path.join(static(), "..")):
-            run("tar -xf %s" % os.path.join(env.proj_path, "last.tar"))
-        restore("last.db")
+            run("tar -xf %s" % os.path.join(env.repo_path, "last.tar"))
+    restore("last.db")
     restart()