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', 'samples':
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)
155 def __init__(self, commands, working_dir):
156 if not hasattr(commands, '__iter__'):
157 commands = [commands]
158 self.name = 'Command: %s @ %s' % (commands, working_dir)
159 self.commands = commands
160 self.working_dir = working_dir
164 with cd(join('%(app_path)s/releases/current' % env, self.working_dir)):
165 for command in self.commands:
168 def upload_samples():
169 upload_localsettings_sample()
170 upload_nginx_sample()
171 for service in env.services:
172 service.upload_sample()
174 def upload_sample(name):
175 require('app_path', 'project_name')
176 upload_path = '%(app_path)s/samples/' % env + name + '.sample'
177 if files.exists(upload_path):
179 print '>>> upload %s template' % name
180 template = '%(project_name)s/' % env + name + '.template'
181 if not exists(template):
182 template = join(dirname(abspath(__file__)), 'templates/' + name + '.template')
183 files.upload_template(template, upload_path, env)
185 def upload_localsettings_sample():
186 "Fill out localsettings template and upload as a sample."
187 env.secret_key = get_random_string(50)
188 upload_sample('localsettings.py')
190 upload_nginx_sample = lambda: upload_sample('nginx')
192 def upload_tar_from_git():
193 "Create an archive from the current Git branch and upload it"
194 print '>>> upload tar from git'
195 require('release', provided_by=[deploy])
197 local('git-archive-all.sh --format tar %(release)s.tar' % env)
198 local('gzip %(release)s.tar' % env)
199 run('mkdir -p %(app_path)s/releases/%(release)s' % env, pty=True)
200 run('mkdir -p %(app_path)s/packages' % env, pty=True)
201 put('%(release)s.tar.gz' % env, '%(app_path)s/packages/' % env)
202 run('cd %(app_path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty=True)
203 local('rm %(release)s.tar.gz' % env)
205 def install_requirements():
206 "Install the required packages from the requirements file using pip"
207 print '>>> install requirements'
208 require('release', provided_by=[deploy])
210 if not files.exists('%(app_path)s/ve' % env):
211 require('virtualenv')
212 run('%(virtualenv)s %(app_path)s/ve' % env, pty=True)
213 with cd('%(app_path)s/releases/%(release)s' % env):
214 run('%(app_path)s/ve/bin/pip install -r requirements.txt' % env, pty=True)
215 with cd(get_django_root_path(env['release'])):
216 # Install DB requirement
218 'django.db.backends.postgresql_psycopg2': 'psycopg2',
219 'django.db.backends.mysql': 'MySQL-python',
221 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)
222 for database in databases.split():
223 if database in database_reqs:
224 # TODO: set pip default pypi
225 run('%(app_path)s/ve/bin/pip install ' % env + database_reqs[database])
228 def copy_localsettings():
229 "Copy localsettings.py from root directory to release directory (if this file exists)"
230 print ">>> copy localsettings"
231 require('release', provided_by=[deploy])
232 require('app_path', 'project_name')
234 with settings(warn_only=True):
235 copy_to = join(get_django_root_path(env['release']), env.get('localsettings_dst_path', env['project_name']))
236 run('cp %(app_path)s/localsettings.py ' % env + copy_to)
238 def symlink_current_release():
239 "Symlink our current release"
240 print '>>> symlink current release'
241 require('release', provided_by=[deploy])
243 with cd(env.app_path):
244 run('rm releases/previous; mv releases/current releases/previous')
245 run('ln -s %(release)s releases/current' % env)
248 "Update the database"
250 require('app_path', 'project_name')
251 with cd(get_django_root_path('current')):
252 run('%(app_path)s/ve/bin/python manage.py syncdb --noinput' % env, pty=True)
253 run('%(app_path)s/ve/bin/python manage.py migrate' % env, pty=True)
255 def pre_collectstatic():
256 print '>>> pre_collectstatic'
257 for task in env.get('pre_collectstatic', []):
261 """Collect static files"""
262 print '>>> collectstatic'
263 require('app_path', 'project_name')
264 with cd(get_django_root_path('current')):
265 run('%(app_path)s/ve/bin/python manage.py collectstatic --noinput' % env, pty=True)
268 def get_django_root_path(release):
270 path = '%(app_path)s/releases/%(release)s' % dict(app_path = env['app_path'], release = release)
271 if 'django_root_path' in env:
272 path = join(path, env['django_root_path'])