~kp/dj-fabric-tasks

f8f3682402510930e4e6df548d448286ee627ddf — Konstantinos Pachnis 9 years ago master
Initial import
13 files changed, 508 insertions(+), 0 deletions(-)

A .gitignore
A LICENSE
A README.md
A __init__.py
A code.py
A db.py
A deploy.py
A django.py
A env.py
A newrelic.py
A requirements.py
A service.py
A translations.py
A  => .gitignore +2 -0
@@ 1,2 @@
*.py[co]


A  => LICENSE +27 -0
@@ 1,27 @@
Copyright (c) 2012, Konstantinos Pachnis
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors
   may be used to endorse or promote products derived from this software without
   specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.

A  => README.md +3 -0
@@ 1,3 @@
# Fabric tasks

Fabric tasks used in Django projects 

A  => __init__.py +11 -0
@@ 1,11 @@
import code
import django
import deploy
import db
import requirements
import newrelic
import requirements
import service
import translations

from env import *

A  => code.py +35 -0
@@ 1,35 @@
# -*- coding: utf-8 -*-
from compileall import compile_file
from fnmatch import fnmatch
import os

from fabric.api import task


@task
def clean(directory):
    """
    Clean project python compiled files
    """

    for root, paths, files in os.walk(directory):
        for file in files:
            if fnmatch(file, '*.pyc'):
                try:
                    print "Removing file %s" % os.path.join(root, file)
                    os.remove(os.path.join(root, file))
                except OSError as e:
                    print e
                    exit(1)


@task
def compile(directory):
    """
    Compile project files
    """

    for root, paths, files in os.walk(directory):
        for file in files:
            if fnmatch(file, '*.py'):
                compile_file(os.path.join(root, file))

A  => db.py +34 -0
@@ 1,34 @@
from fabric.api import cd, env, hide, roles, run, settings, task
from fabric.colors import green


@roles('db')
@task
def init():
    """
    Populate the application database and assign the database owner
    """

    print(green('Creating MySQL database for {0}'.format(env.app)))
    with hide('running'):
        run('mysql -u {0} -p{1} -e "CREATE DATABASE {2} CHARACTER SET utf8 COLLATE utf8_general_ci;"'.format(env.dba['user'], env.dba['password'], env.db['name']))
        run('mysql -u {0} -p{1} -e "CREATE USER \'{2}\'@\'localhost\' IDENTIFIED BY \'{3}\'"'.format(env.dba['user'], env.dba['password'], env.db['user'], env.db['password']))
        run('mysql -u {0} -p{1} -e "GRANT ALL PRIVILEGES ON {2}.* TO \'{3}\'@\'localhost\'"'.format(env.dba['user'], env.dba['password'], env.db['name'], env.db['user']))

    print(green('Initializing database for the first time'))
    with settings(user=env.deployer['account']):
        with cd(env.app_path):
            run('./env/bin/python manage.py syncdb --noinput --migrate')


@roles('db')
@task
def migrate():
    """
    Run database migrations
    """

    print(green('Applying database migrations'))
    with settings(user=env.deployer['account']):
        with cd(env.app_path):
            run('./env/bin/python manage.py migrate --delete-ghost-migrations')

A  => deploy.py +180 -0
@@ 1,180 @@
import datetime

from fabric.api import (
    cd,
    env,
    execute,
    local,
    roles,
    run,
    settings,
    sudo,
    task,
)

from fabric.colors import green
from fabric.contrib.files import append
from fabric.contrib.files import upload_template

import db
import requirements
import service
import translations


@roles('app')
@task
def setup():
    """
    Prepare production server(s)
    """

    execute(create_accounts)
    execute(initialize_application)
    execute(config)
    execute(requirements.install)
    execute(db.init)

    print(green('Updating nginx configuration'))
    with cd('/etc/nginx/sites-enabled'):
        sudo('ln -s {0}/etc/nginx/{1}.conf .'.format(env.app_path, env.app))

    print(green('Updating supervisord configuration'))
    with cd('/etc/supervisor/conf.d'):
        sudo('ln -s {0}/etc/supervisor/conf.d/{1}.conf .'.format(env.app_path, env.app))

    print(green('Creating required dirs'))
    with cd('/var'):
        sudo('mkdir log/{0}'.format(env.app))
        sudo('chown {0}:{1} log/{2}'.format(env.service_account, env.service_account, env.app))
        sudo('mkdir run/{0}'.format(env.app))
        sudo('chown {0}:{1} run/{2}'.format(env.service_account, env.service_account, env.app))


@roles('app')
@task
def release(release_no=datetime.datetime.now().strftime('%d%m%Y%H%M%S')):
    """
    Deploy a new release
    """

    local('git tag -a {0} -m "Production release {1}"'.format(release_no, release_no))
    local('git push origin {0}'.format(release_no))
    update_code(release_no)
    execute(requirements.upgrade)
    execute(translations.compile)
#    execute(collectstatic)
    execute(db.migrate)
    execute(service.restart)

    print(green('Optimizing images'))
    with settings(user=env.deployer['account']):
        with cd('{}/src/mstr/static/images'.format(env.app_path)):
            run('find . -name \*.png -exec optipng -quiet "{}" \;')
            run('find . -name \*.jpg -exec jpegoptim --quiet --strip-all "{}" \;')


@roles('app')
@task
def update_code(release_no):
    """
    Update project codebase
    """

    print(green('Updating {0} code'.format(env.app)))
    with settings(user=env.deployer['account']):
        with cd(env.app_path):
            run('git pull --tags --quiet origin {0}'.format(env.git['branch']))
            run('git checkout -b production_{0} {1}'.format(release_no, release_no))


@roles('app')
@task
def collectstatic():
    """
    Run django collectstatic command
    """

    print(green('Building assets'))
    with settings(user=env.deployer['account']):
        with cd(env.app_path):
            run('./env/bin/python manage.py collectstatic -v 0 --noinput -c ')


@roles('app')
@task
def config():
    """
    Create project local configuration
    """

    context = {
        'secret_key': env.secret_key,
        'db': {
            'name': env.db['name'],
            'user': env.db['user'],
            'password': env.db['password'],
            'host': env.db['host'],
            'port': env.db['port'],
        },
        'recaptcha': {
            'public_key': env.recaptcha['public_key'],
            'private_key': env.recaptcha['private_key'],
        },
        'email': {
            'host': env.email['host'],
            'user': env.email['user'],
            'password': env.email['password'],
            'port': env.email['port'],
            'ssl': env.email['ssl'],
        }
    }

    print('Creating project local configuration')
    with settings(user=env.deployer['account']):
        upload_template('local.py.jinja',
                        '{0}/src/mstr/conf/local.py'.format(env.app_path),
                        context=context, use_jinja=True, template_dir='etc/conf')


def create_accounts():
    """
    Setup the account used to deploy the application.
    """

    print(green('Creating deployer account'))
    sudo('useradd -c "{0} application deployer" -p "{1}" -m -s /bin/bash {2}'.format(env.app, env.deployer['password'], env.deployer['account']))

    print(green('Creating {0} application service account'.format(env.app)))
    sudo('useradd -c "{0} service" --system {1}'.format(env.app, env.app))

    print(green('Uploading SSH keys for deployer account:'))
    with cd('/home/{0}'.format(env.deployer['account'])):
        sudo('mkdir .ssh; chmod 700 .ssh; chown {0}:{1} .ssh'.format(env.deployer['account'], env.deployer['account']))

    with cd('/home/%s/.ssh' % env.deployer['account']):
        sudo('touch authorized_keys; chmod 600 authorized_keys; chown {0}:{1} authorized_keys'.format(env.deployer['account'], env.deployer['account']))
    append('/home/{0}/.ssh/authorized_keys'.format(env.deployer['account']), env.deployer['key'], use_sudo=True)


def initialize_application():
    """
    Create the application path, set the correct permissions and clone the
    application from the repote repository.
    """

    print(green('Creating application path: {0}'.format(env.app_path)))
    sudo('mkdir -p {0}'.format(env.app_path))

    print(green('Updating {0} permissions for account {1}'.format(env.app_path, env.deployer['account'])))
    sudo('chown {0}:{1} {2}'.format(env.deployer['account'], env.deployer['account'], env.app_path))

    print(green('Cloning {0} repo'.format(env.app)))
    with settings(user=env.deployer['account']):
        with cd(env.app_path):
            run('git clone --quiet {0} .'.format(env.git['url']))

    print(green('Creating python virtualenv for app {0}'.format(env.app)))
    with settings(user=env.deployer['account']):
        with cd(env.app_path):
            run('virtualenv --no-site-packages --distribute --prompt="({0}) " env'.format(env.app))

A  => django.py +37 -0
@@ 1,37 @@
# -*- coding: utf-8 -*-
import os
from random import choice
from string import letters, digits
from ConfigParser import RawConfigParser

from fabric.api import task
from fabric.colors import red, green
from fabric.contrib import console


@task
def generate_key(length=80, project='project'):
    """
    Generate an N length string in the .secret_key.cfg file under the project directory.
    """

    secret_key = ''.join([choice(letters + digits + '!@#$%^&*()-_=+') for i in xrange(int(length))])
    secret_key_file = os.path.join(os.getcwd(), project, '.secret_key.cfg')

    if os.path.exists(secret_key_file):
        if console.confirm("File exists. Overwrite?"):
            pass
        else:
            print "Quitting..."
            exit(0)

    config = RawConfigParser()
    config.add_section(project)
    config.set(project, 'SECRET_KEY', secret_key)

    try:
        with open(secret_key_file, 'wb') as config_file:
            config.write(config_file)
            print(green("Key writter in %s") % secret_key_file)
    except IOError:
        print(red("Cannot write to file %s") % secret_key_file)

A  => env.py +67 -0
@@ 1,67 @@
import crypt
import os
from random import choice
from string import digits, letters

from fabric.api import env


PUNCTUATION = '!@#$%^&*(-_=+)'

env.roledefs = {
    'web': ['<hostname>'],
    'app': ['<hostname>'],
    'db': ['<hostname>']
}

env.forward_agent = True

env.password = '<password>'

env.app = '<app_name>'
env.app_path = '/u/apps/{}'.format(env.app)
env.service_account = env.app

env.deployer = {
    'account': '<username>',
    'password': crypt.crypt('test', 'Afe'),
    'key': open(os.path.abspath(os.path.join(os.getenv('HOME'), '.ssh', '<filename>.pub'))).read()
}

env.git = {
    'url': '<user>@<hostname>:<repo>.git',
    'branch': 'master'
}

env.db = {
    'name': '<db_name>',
    'user': '<db_user>',
    'password': '<db_password>',
    'host': '<db_host>',
    'port': None,
}

env.dba = {
    'user': '<dba_user>',
    'password': '<dba_password>'
}

env.sentry_dsn = '<get_sentry_url>'

env.secret_key = ''.join([choice(digits + letters + PUNCTUATION) for i in xrange(80)])

env.recaptcha = {
    'public_key': '<public_key>',
    'private_key': '<private_key>',
}

env.email = {
    'host': '<email_host>',
    'user': '<email_user>',
    'password': '<email_password>',
    'port': 587,
    'ssl': True,
}

env.newrelic_key = '<newrelic_key>'


A  => newrelic.py +23 -0
@@ 1,23 @@
from fabric.api import env, roles, sudo, task
from fabric.colors import green
from fabric.contrib.files import upload_template


@roles('app')
@task
def config():
    """
    Generate NewRelic configuration
    """

    print(green('Creating NewRelic configuration.'))
    sudo('mkdir -p /etc/%s' % env.app)

    context = {
        'app_name': env.app,
        'newrelic_key': env.newrelic_key,
    }

    upload_template('etc/conf/newrelic.ini.jinja',
                    '/etc/%s/newrelic.ini' % env.app,
                    context=context, use_jinja=True, use_sudo=True)

A  => requirements.py +28 -0
@@ 1,28 @@
from fabric.api import cd, env, roles, run, settings, task
from fabric.colors import green


@roles('app')
@task
def install():
    """
    Install application requirements
    """

    print(green('Installing application requirements'))
    with settings(user=env.deployer['account']):
        with cd(env.app_path):
            run('./env/bin/pip -q install -r requirements/common.pip')


@roles('app')
@task
def upgrade():
    """
    Upgrade application requirements
    """

    print(green('Upgrading application requirements'))
    with settings(user=env.deployer['account']):
        with cd(env.app_path):
            run('./env/bin/pip -q install -U -r requirements/common.pip')

A  => service.py +46 -0
@@ 1,46 @@
from fabric.api import env, roles, sudo, task
from fabric.colors import green


@roles('app')
@task
def start():
    """
    Start application
    """

    print(green('Starting app {0}'.format(env.app)))
    sudo('supervisorctl start {0}'.format(env.app))


@roles('app')
@task
def stop():
    """
    Stop application
    """

    print(green('Stopping app {0}'.format(env.app)))
    sudo('supervisorctl stop {0}'.format(env.app))


@roles('app')
@task
def restart():
    """
    Restart application
    """

    print(green('Restarting app {0}'.format(env.app)))
    sudo('supervisorctl restart {0}'.format(env.app))


@roles('app')
@task
def status():
    """
    Show application status
    """

    print(green('Getting app {0} status'.format(env.app)))
    sudo('supervisorctl status {0}'.format(env.app))

A  => translations.py +15 -0
@@ 1,15 @@
from fabric.api import cd, env, roles, run, settings, task
from fabric.colors import green


@roles('app')
@task
def compile():
    """
    Compile gettext translations
    """

    print(green('Compiling gettext translations'))
    with settings(user=env.deployer['account']):
        with cd(env.app_path):
            run('./env/bin/python manage.py compilemessages')