A => .gitignore +2 -0
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')