# HG changeset patch # User Peter Sanchez # Date 1289437279 28800 # Wed Nov 10 17:01:19 2010 -0800 # Node ID 4221f80fe7ee8674938bc5dd0d9461276a471620 # Parent 083cd77724a7a812446b27e007879979d773bf2d Initial files... diff --git a/BSD-LICENSE b/BSD-LICENSE new file mode 100644 --- /dev/null +++ b/BSD-LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 2010, Peter Sanchez +All rights reserved. + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the +following conditions are met: + + * Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + + * 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. + + * Neither the name of Peter Sanchez 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. diff --git a/README.txt b/README.txt new file mode 100644 --- /dev/null +++ b/README.txt @@ -0,0 +1,142 @@ +fabric-deploy-django +------------------ + +Easily reusable fabfile that can be used with the fabric application +to quickly deploy a Django project using Apache, mod_wsgi, virtualenv +and pip. + + +Dependencies +------------ + +You will need to have fabric installed to use this fabfile. Install it +it like so: + +$ sudo pip install fabric + +It was written for Python 2.6+ + +If you use a Python version less than 2.6 then please refer to this +URL: + +http://docs.fabfile.org/0.9.2/#documentation + +You will need to add the following to your fabfile.py: + +from __future__ import with_statement + + +Install +------- + +Simply copy the fabfile.py to the root of your project directory and +edit the fabfile.py to fit your project. + +Project structure should look like so: + +myproject/ + apache/ + wsgi_handler.py + myproject.conf + requirements.txt + fabfile.py + myproject/ + __init__.py + urls.py + settings.py + + +Configure +--------- + +Edit the following globals: + + +env.project_name = 'PROJECT_NAME' + +This is the name the project (ie, "myproject" above in the structure example) + + +env.run_south_migrate = True + +Run 'python manage.py migrate' after syncdb is run. + + +env.copy_base_settings_local = False + +If true, the script will look for a file named myproject/base_settings_local.py +and copy it to myproject/settings_local.py + + +env.num_releases = 7 + +The number of old releases to leave on the deploy destination. + + +Edit the 3 deploy defining functions in the fabfile (local, staging, prod): + + +env.hosts = ['localhost'] + +Host(s) that the commands should be run on + + +env.path = '/path/to/envs/myproject' + +Path for this project to be deployed to. + + +env.user = 'yourusername' + +The username to run the commands as + + +env.run_type = 'local' + +If this is set to 'local' then the internal function _is_local() will evaluate +to True. This means that the deploy is actually being run on the LOCAL machine. +This is mostly used for testing the deploy process. Anything other than +'local' will mean that the deploy will be run on whatever hosts are set in +"env.hosts" + + +env.repo_path = '/path/to/local/repo/myproject' + +Path (local FS or URL) to the repository to clone (or pull from) during the +deploy. + + +env.repo_rev = 'tip' + +The revision to use when pulling/cloning from the repository +(set in env.repo_path above) + + +Use +--- + +Here is a basic deploy to the LOCAL machine: + +$ fab local setup + +Or say to the production environment: + +$ fab prod setup + + +TODO +---- +Create a more through install and use docs. + +Auto import __future__ if using Python < 2.6 + +Make various functions more loosely coupled to the tools currently. +(ie, easy to switch from deploying with hg to git or svn, etc.) + + +Copyright & Warranty +-------------------- +All documentation, libraries, and sample code are +Copyright 2010 Peter Sanchez . The library and +sample code are made available to you under the terms of the BSD license +which is contained in the included file, BSD-LICENSE. diff --git a/fabfile.py b/fabfile.py new file mode 100644 --- /dev/null +++ b/fabfile.py @@ -0,0 +1,296 @@ +''' +Original idea from: + +http://gist.github.com/571155 + +Hacked it to basically be completely original. + +''' +import os +from fabric.api import * + + +# globals +env.project_name = 'PROJECT_NAME' +env.run_south_migrate = True +env.copy_base_settings_local = False +env.num_releases = 7 + + +# Environments +def _is_local(): + require('run_type', provided_by=[local, staging, prod]) + return env.run_type == 'local' + + +def _get_callable(default=run): + 'Return local or default (run) based on hosts variable' + require('run_type', provided_by=[local, staging, prod]) + def _local(cmd, *args, **kwargs): + return local(cmd) + return _local if _is_local() else default + + +def _get_cmds(): + 'Return run, cd and sudo commands.' + return ( + _get_callable(), + _get_callable(default=sudo), + ) + + +def localdev(): + 'Use the local machine' + env.hosts = ['localhost'] + env.path = '/path/to/environments/PROJECT_NAME' + env.user = 'username' + env.run_type = 'local' + env.repo_path = '/path/to/local/repository' + env.repo_rev = 'tip' + env.copy_base_settings_local = True + + +def staging(): + 'Use the staging environment' + pass + + +def prod(): + 'Use the production environment' + env.hosts = ['yourproject.yourdomain.com'] + env.path = '/path/to/environments/PROJECT_NAME' + env.user = 'username' + env.run_type = 'prod' + env.repo_path = 'http://bitbucket.org/someuser/yourproject' + env.repo_rev = 'tip' + + +def set_repo_revision(rev='tip'): + env.repo_rev = rev + + +def set_num_releases(amount=7): + env.num_releases = amount + + +# Tasks +def test(): + 'Run the test suite and bail out if it fails' + _run, _sudo = _get_cmds() + + with cd(env.project_name): + _run('python manage.py test') + + +def setup(): + ''' + Setup a fresh virtualenv as well as a few useful directories, then run + a full deployment + ''' + require('hosts', provided_by=[localdev, staging, prod]) + require('path') + _run, _sudo = _get_cmds() + + import time + env.release = time.strftime('%Y%m%d%H%M%S') + + lpath = '%(path)s/releases/%(release)s' % env + _run('mkdir -p %s' % lpath) + with cd(lpath): + _run('virtualenv --no-site-packages .') + _run('mkdir -p shared packages') + deploy() + + +def soft_update(): + 'Just update codebase. Do not do a full deploy' + require('hosts', provided_by=[localdev, staging, prod]) + require('path') + + checkout_code_repo(update=True) + touch_wsgi_handler() + + +def deploy(): + ''' + Deploy the latest version of the site to the servers, install any + required third party modules, install the virtual host and + then restart the webserver + ''' + require('hosts', provided_by=[localdev, staging, prod]) + require('path') + + checkout_code_repo() + install_requirements() + migrate() + install_site() + symlink_current_release() + restart_webserver() + remove_oldest_release() + + +def show_versions(): + 'List all deployed versions' + require('path') + _run, _sudo = _get_cmds() + + with cd('%(path)s/releases' % env): + print _run('ls -xt') + + +def rollback_version(version): + 'Specify a specific version to be made live' + require('hosts', provided_by=[localdev, staging, prod]) + require('path') + _run, _sudo = _get_cmds() + + env.version = version + with cd('%(path)s' % env): + _run('ln -nfs releases/%(version)s current' % env) + restart_webserver() + + +def rollback(): + 'Simple GENERIC rollback. Symlink to the second most recent release' + require('hosts', provided_by=[localdev, staging, prod]) + require('path') + _run, _sudo = _get_cmds() + + releases = _get_releases_list() + release = releases[-2] + _run('ln -nfs releases/%s current' % release) + restart_webserver() + + +def fail_cleanup(): + ''' + If your VERY LAST deploy failed, call this to clean up the + mess left behind. It will only remove the most recent + release directory. BE CAREFUL USING THIS. + ''' + require('hosts', provided_by=[localdev, staging, prod]) + require('path') + _run, _sudo = _get_cmds() + + releases = _get_releases_list() + release = releases[-1] + with cd('%(path)s/releases' % env): + _run('rm -rf %s' % release) + + +# Helpers. These are called by other functions rather than directly +def _get_releases_list(): + 'Returns sorted list of all current releases' + require('hosts', provided_by=[localdev, staging, prod]) + require('path') + _run, _sudo = _get_cmds() + + with cd('%(path)s/releases' % env): + releases = _run('ls -xt') + releases = [x.replace('/', '') for x in releases.split()] + return sorted(releases) + + +def checkout_code_repo(update=False): + 'Create an archive from the current Git master branch and upload it' + require('repo_path', provided_by=[localdev, staging, prod]) + require('repo_rev', provided_by=[localdev, staging, prod]) + _run, _sudo = _get_cmds() + + if update: + lpath = '%(path)s/current/%(project_name)s' % env + with cd(lpath): + _run('hg pull -r %(repo_rev)s -u' % env) + else: + require('release', provided_by=[deploy, setup]) + lpath = '%(path)s/releases/%(release)s' % env + with cd(lpath): + cmd = 'hg clone -r ' + cmd += '%(repo_rev)s %(repo_path)s %(project_name)s' % env + _run(cmd) + + if env.copy_base_settings_local: + lpath = os.path.join(lpath, '%(project_name)s/%(project_name)s' % env) + with cd(lpath): + with settings(warn_only=True): + _run('cp base_settings_local.py settings_local.py') + + +def install_site(): + 'Add the virtualhost file to apache' + require('release', provided_by=[deploy, setup]) + _run, _sudo = _get_cmds() + + if not _is_local(): + with cd('%(path)s/releases/%(release)s' % env): + cmd = 'cp %(project_name)s/apache/%(project_name)s.conf ' % env + \ + '/etc/apache2/conf.d/' + _sudo(cmd) + + +def install_requirements(): + 'Install the required packages from the requirements file using pip' + require('release', provided_by=[deploy, setup]) + _run, _sudo = _get_cmds() + + with cd('%(path)s/releases/%(release)s' % env): + cmd = './bin/pip install -E . ' + cmd += '-r ./%(project_name)s/requirements.txt' % env + _run(cmd) + + +def symlink_current_release(): + 'Symlink our current release' + require('release', provided_by=[deploy, setup]) + _run, _sudo = _get_cmds() + + with cd('%(path)s' % env): + _run('ln -nfs releases/%(release)s current' % env) + + +def migrate(): + 'Update the database' + require('project_name') + require('release', provided_by=[deploy, setup]) + _run, _sudo = _get_cmds() + + p = '%(path)s/releases/%(release)s/%(project_name)s/%(project_name)s' % env + with cd(p): + _run('../../bin/python manage.py syncdb --noinput') + if env.run_south_migrate: + _run('../../bin/python manage.py migrate') + + +def restart_webserver(): + 'Restart the web server' + _run, _sudo = _get_cmds() + + if not _is_local(): + _sudo('service apache2 restart') + + +def touch_wsgi_handler(): + 'Touch the mod_wsgi wsgi_handler.py file' + require('project_name') + require('hosts', provided_by=[localdev, staging, prod]) + _run, _sudo = _get_cmds() + + with cd('%(path)s/current/%(project_name)s/apache' % env): + _run('touch wsgi_handler.py') + + +def remove_oldest_release(): + ''' + Remove oldest releases past the ammount passed + in with the "allow" variable. Default 7. + ''' + require('hosts', provided_by=[localdev, staging, prod]) + require('num_releases') + require('path') + _run, _sudo = _get_cmds() + + releases = _get_releases_list() + with cd('%(path)s/releases' % env): + while len(releases) > env.num_releases: + release = releases.pop(0) + _run('rm -rf %s' % release)