2 Generic fabric deployment script.
3 Create a fabfile.py in the project and start it with:
5 from fnpdjango.deploy import *
7 Then set up some env properties:
8 project_name: slug-like project name
9 hosts: list of target host names
10 user: remote user name
11 app_path: where does the app go
12 services: list of tasks to run after deployment
13 django_root_path (optional): path to the directory
14 containing django project, relative to the
15 root of the repository (defaults to '.')
16 localsettings_dst_path (optional): path indicating
17 where to copy the localsettings file, relative
18 to django_root_path (defaults to project_name/localsettings.py)
20 from os.path import abspath, dirname, exists, join
21 from django.utils.crypto import get_random_string
22 from fabric.api import *
23 from fabric.contrib import files
24 from fabric.tasks import Task, execute
26 env.virtualenv = '/usr/bin/virtualenv'
33 Setup all needed directories.
35 require('hosts', 'app_path')
37 if not files.exists(env.app_path):
38 run('mkdir -p %(app_path)s' % env, pty=True)
39 with cd(env.app_path):
40 for subdir in 'releases', 'packages', 'log':
41 if not files.exists(subdir):
42 run('mkdir -p %s' % subdir, pty=True)
43 with cd('%(app_path)s/releases' % env):
44 if not files.exists('current'):
45 run('ln -sfT . current', pty=True)
46 if not files.exists('previous'):
47 run('ln -sfT . previous', pty=True)
52 def check_localsettings():
53 if not files.exists('%(app_path)s/localsettings.py' % env):
54 abort('localsettings.py file missing.')
60 Deploy the latest version of the site to the servers,
61 install any required third party modules,
62 install the virtual host and then restart the webserver
64 require('hosts', 'app_path')
67 env.release = time.strftime('%Y-%m-%dT%H%M')
73 install_requirements()
74 symlink_current_release()
83 Limited rollback capability. Simple loads the previously current
84 version of the code. Rolling back again will swap between the two.
85 Warning: this will almost certainly go wrong, it there were any migrations
88 require('hosts', 'app_path')
90 run('mv releases/current releases/_previous;', pty=True)
91 run('mv releases/previous releases/current;', pty=True)
92 run('mv releases/_previous releases/previous;', pty=True)
97 def deploy_version(version):
99 Loads the specified version.
100 Warning: this will almost certainly go wrong, it there were any migrations
103 "Specify a specific version to be made live"
104 require('hosts', 'app_path')
105 env.version = version
107 run('rm releases/previous; mv releases/current releases/previous;', pty=True)
108 run('ln -s %(version)s releases/current' % env, pty=True)
115 for service in env.services:
119 # =====================================================================
120 # = Helpers. These are called by other functions rather than directly =
121 # =====================================================================
124 def upload_sample(self):
127 class DebianGunicorn(Service):
128 def __init__(self, name):
129 super(Task, self).__init__()
133 print '>>> restart webserver using gunicorn-debian'
134 sudo('gunicorn-debian restart %s' % self.name, shell=False)
136 def upload_sample(self):
137 upload_sample('gunicorn')
139 class Apache(Service):
141 print '>>> restart webserver by touching WSGI'
143 run('touch %(app_path)s/%(project_name)s/wsgi.py' % env)
145 class Supervisord(Service):
146 def __init__(self, name):
147 super(Task, self).__init__()
151 print '>>> supervisord: restart %s' % self.name
152 sudo('supervisorctl restart %s' % self.name, shell=False)
154 def upload_samples():
155 upload_localsettings_sample()
156 upload_nginx_sample()
157 for service in env.services:
158 service.upload_sample()
160 def upload_sample(name):
161 require('app_path', 'project_name')
162 upload_path = '%(app_path)s/' % env + name + '.sample'
163 if files.exists(upload_path):
165 print '>>> upload %s template' % name
166 template = '%(project_name)s/' % env + name + '.template'
167 if not exists(template):
168 template = join(dirname(abspath(__file__)), 'templates/' + name + '.template')
169 files.upload_template(template, upload_path, env)
171 def upload_localsettings_sample():
172 "Fill out localsettings template and upload as a sample."
173 env.secret_key = get_random_string(50)
174 upload_sample('localsettings.py')
176 upload_nginx_sample = lambda: upload_sample('nginx')
178 def upload_tar_from_git():
179 "Create an archive from the current Git branch and upload it"
180 print '>>> upload tar from git'
181 require('release', provided_by=[deploy])
183 local('git-archive-all.sh --format tar %(release)s.tar' % env)
184 local('gzip %(release)s.tar' % env)
185 run('mkdir -p %(app_path)s/releases/%(release)s' % env, pty=True)
186 run('mkdir -p %(app_path)s/packages' % env, pty=True)
187 put('%(release)s.tar.gz' % env, '%(app_path)s/packages/' % env)
188 run('cd %(app_path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty=True)
189 local('rm %(release)s.tar.gz' % env)
191 def install_requirements():
192 "Install the required packages from the requirements file using pip"
193 print '>>> install requirements'
194 require('release', provided_by=[deploy])
196 if not files.exists('%(app_path)s/ve' % env):
197 require('virtualenv')
198 run('%(virtualenv)s %(app_path)s/ve' % env, pty=True)
199 with cd('%(app_path)s/releases/%(release)s' % env):
200 run('%(app_path)s/ve/bin/pip install -r requirements.txt' % env, pty=True)
201 with cd(get_django_root_path(env['release'])):
202 # Install DB requirement
204 'django.db.backends.postgresql_psycopg2': 'psycopg2',
205 'django.db.backends.mysql': 'MySQL-python',
207 databases = run('''DJANGO_SETTINGS_MODULE=%(project_name)s.settings %(app_path)s/ve/bin/python -c 'from django.conf import settings; print " ".join(set([d["ENGINE"] for d in settings.DATABASES.values()]))' ''' % env)
208 for database in databases.split():
209 if database in database_reqs:
210 # TODO: set pip default pypi
211 run('%(app_path)s/ve/bin/pip install ' % env + database_reqs[database])
214 def copy_localsettings():
215 "Copy localsettings.py from root directory to release directory (if this file exists)"
216 print ">>> copy localsettings"
217 require('release', provided_by=[deploy])
218 require('app_path', 'project_name')
220 with settings(warn_only=True):
221 copy_to = join(get_django_root_path(env['release']), env.get('localsettings_dst_path', env['project_name']))
222 run('cp %(app_path)s/localsettings.py ' % env + copy_to)
224 def symlink_current_release():
225 "Symlink our current release"
226 print '>>> symlink current release'
227 require('release', provided_by=[deploy])
229 with cd(env.app_path):
230 run('rm releases/previous; mv releases/current releases/previous')
231 run('ln -s %(release)s releases/current' % env)
234 "Update the database"
236 require('app_path', 'project_name')
237 with cd(get_django_root_path('current')):
238 run('%(app_path)s/ve/bin/python manage.py syncdb --noinput' % env, pty=True)
239 run('%(app_path)s/ve/bin/python manage.py migrate' % env, pty=True)
241 def pre_collectstatic():
242 print '>>> pre_collectstatic'
243 for task in env.get('pre_collectstatic', []):
247 """Collect static files"""
248 print '>>> collectstatic'
249 require('app_path', 'project_name')
250 with cd(get_django_root_path('current')):
251 run('%(app_path)s/ve/bin/python manage.py collectstatic --noinput' % env, pty=True)
254 def get_django_root_path(release):
256 path = '%(app_path)s/releases/%(release)s' % dict(app_path = env['app_path'], release = release)
257 if 'django_root_path' in env:
258 path = join(path, env['django_root_path'])