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)
19 skip_collect_static (optional): if True, Django collectstatic command is not called
21 from os.path import abspath, dirname, exists, join
22 from django.utils.crypto import get_random_string
23 from fabric.api import *
24 from fabric.contrib import files
25 from fabric.tasks import Task, execute
27 env.virtualenv = '/usr/bin/virtualenv'
34 Setup all needed directories.
36 require('hosts', 'app_path')
38 if not files.exists(env.app_path):
39 run('mkdir -p %(app_path)s' % env, pty=True)
40 with cd(env.app_path):
41 for subdir in 'releases', 'packages', 'log', 'samples':
42 if not files.exists(subdir):
43 run('mkdir -p %s' % subdir, pty=True)
44 with cd('%(app_path)s/releases' % env):
45 if not files.exists('current'):
46 run('ln -sfT . current', pty=True)
47 if not files.exists('previous'):
48 run('ln -sfT . previous', pty=True)
53 def check_localsettings():
54 if not files.exists('%(app_path)s/localsettings.py' % env):
55 abort('localsettings.py file missing.')
61 Deploy the latest version of the site to the servers,
62 install any required third party modules,
63 install the virtual host and then restart the webserver
65 require('hosts', 'app_path')
68 env.release = time.strftime('%Y-%m-%dT%H%M')
74 install_requirements()
75 symlink_current_release()
84 Limited rollback capability. Simple loads the previously current
85 version of the code. Rolling back again will swap between the two.
86 Warning: this will almost certainly go wrong, it there were any migrations
89 require('hosts', 'app_path')
91 run('mv releases/current releases/_previous;', pty=True)
92 run('mv releases/previous releases/current;', pty=True)
93 run('mv releases/_previous releases/previous;', pty=True)
98 def deploy_version(version):
100 Loads the specified version.
101 Warning: this will almost certainly go wrong, it there were any migrations
104 "Specify a specific version to be made live"
105 require('hosts', 'app_path')
106 env.version = version
108 run('rm releases/previous; mv releases/current releases/previous;', pty=True)
109 run('ln -s %(version)s releases/current' % env, pty=True)
116 for service in env.services:
120 # =====================================================================
121 # = Helpers. These are called by other functions rather than directly =
122 # =====================================================================
125 def upload_sample(self):
128 class DebianGunicorn(Service):
129 def __init__(self, name):
130 super(Task, self).__init__()
134 print '>>> restart webserver using gunicorn-debian'
135 sudo('gunicorn-debian restart %s' % self.name, shell=False)
137 def upload_sample(self):
138 upload_sample('gunicorn')
140 class Apache(Service):
142 print '>>> restart webserver by touching WSGI'
144 run('touch %(app_path)s/%(project_name)s/wsgi.py' % env)
146 class Supervisord(Service):
147 def __init__(self, name):
148 super(Task, self).__init__()
152 print '>>> supervisord: restart %s' % self.name
153 sudo('supervisorctl restart %s' % self.name, shell=False)
156 def __init__(self, commands, working_dir):
157 if not hasattr(commands, '__iter__'):
158 commands = [commands]
159 self.name = 'Command: %s @ %s' % (commands, working_dir)
160 self.commands = commands
161 self.working_dir = working_dir
165 with cd(join('%(app_path)s/releases/current' % env, self.working_dir)):
166 for command in self.commands:
169 def upload_samples():
170 upload_localsettings_sample()
171 upload_nginx_sample()
172 for service in env.services:
173 service.upload_sample()
175 def upload_sample(name):
176 require('app_path', 'project_name')
177 upload_path = '%(app_path)s/samples/' % env + name + '.sample'
178 if files.exists(upload_path):
180 print '>>> upload %s template' % name
181 template = '%(project_name)s/' % env + name + '.template'
182 if not exists(template):
183 template = join(dirname(abspath(__file__)), 'templates/' + name + '.template')
184 files.upload_template(template, upload_path, env)
186 def upload_localsettings_sample():
187 "Fill out localsettings template and upload as a sample."
188 env.secret_key = get_random_string(50)
189 upload_sample('localsettings.py')
191 upload_nginx_sample = lambda: upload_sample('nginx')
193 def upload_tar_from_git():
194 "Create an archive from the current Git branch and upload it"
195 print '>>> upload tar from git'
196 require('release', provided_by=[deploy])
198 local('git-archive-all.sh --format tar %(release)s.tar' % env)
199 local('gzip %(release)s.tar' % env)
200 run('mkdir -p %(app_path)s/releases/%(release)s' % env, pty=True)
201 run('mkdir -p %(app_path)s/packages' % env, pty=True)
202 put('%(release)s.tar.gz' % env, '%(app_path)s/packages/' % env)
203 run('cd %(app_path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty=True)
204 local('rm %(release)s.tar.gz' % env)
206 def install_requirements():
207 "Install the required packages from the requirements file using pip"
208 print '>>> install requirements'
209 require('release', provided_by=[deploy])
211 if not files.exists('%(app_path)s/ve' % env):
212 require('virtualenv')
213 run('%(virtualenv)s %(app_path)s/ve' % env, pty=True)
214 with cd('%(app_path)s/releases/%(release)s' % env):
215 run('%(app_path)s/ve/bin/pip install -r requirements.txt' % env, pty=True)
216 with cd(get_django_root_path(env['release'])):
217 # Install DB requirement
219 'django.db.backends.postgresql_psycopg2': 'psycopg2',
220 'django.db.backends.mysql': 'MySQL-python',
222 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)
223 for database in databases.split():
224 if database in database_reqs:
225 # TODO: set pip default pypi
226 run('%(app_path)s/ve/bin/pip install ' % env + database_reqs[database])
229 def copy_localsettings():
230 "Copy localsettings.py from root directory to release directory (if this file exists)"
231 print ">>> copy localsettings"
232 require('release', provided_by=[deploy])
233 require('app_path', 'project_name')
235 with settings(warn_only=True):
236 copy_to = join(get_django_root_path(env['release']), env.get('localsettings_dst_path', env['project_name']))
237 run('cp %(app_path)s/localsettings.py ' % env + copy_to)
239 def symlink_current_release():
240 "Symlink our current release"
241 print '>>> symlink current release'
242 require('release', provided_by=[deploy])
244 with cd(env.app_path):
245 run('rm releases/previous; mv releases/current releases/previous')
246 run('ln -s %(release)s releases/current' % env)
249 "Update the database"
251 require('app_path', 'project_name')
252 with cd(get_django_root_path('current')):
253 run('%(app_path)s/ve/bin/python manage.py syncdb --noinput' % env, pty=True)
254 run('%(app_path)s/ve/bin/python manage.py migrate' % env, pty=True)
256 def pre_collectstatic():
257 print '>>> pre_collectstatic'
258 for task in env.get('pre_collectstatic', []):
262 """Collect static files"""
263 print '>>> collectstatic'
264 if env.get('skip_collect_static', False):
267 require('app_path', 'project_name')
268 with cd(get_django_root_path('current')):
269 run('%(app_path)s/ve/bin/python manage.py collectstatic --noinput' % env, pty=True)
272 def get_django_root_path(release):
274 path = '%(app_path)s/releases/%(release)s' % dict(app_path = env['app_path'], release = release)
275 if 'django_root_path' in env:
276 path = join(path, env['django_root_path'])