pre_collectstatic hook
[fnpdjango.git] / fnpdjango / deploy / __init__.py
1 """
2 Generic fabric deployment script.
3 Create a fabfile.py in the project and start it with:
4
5     from fnpdjango.deploy import *
6
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 """
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
25
26 env.virtualenv = '/usr/bin/virtualenv'
27 env.services = None
28
29
30 @task
31 def setup():
32     """
33     Setup all needed directories.
34     """
35     require('hosts', 'app_path')
36
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)
48     
49     upload_samples()
50
51
52 def check_localsettings():
53     if not files.exists('%(app_path)s/localsettings.py' % env):
54         abort('localsettings.py file missing.')
55
56
57 @task(default=True)
58 def deploy():
59     """
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
63     """
64     require('hosts', 'app_path')
65
66     import time
67     env.release = time.strftime('%Y-%m-%dT%H%M')
68
69     setup()
70     check_localsettings()
71     upload_tar_from_git()
72     copy_localsettings()
73     install_requirements()
74     symlink_current_release()
75     migrate()
76     pre_collectstatic()
77     collectstatic()
78     restart()
79
80 @task
81 def rollback():
82     """
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
86     in the meantime!
87     """
88     require('hosts', 'app_path')
89     with cd(env.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)
93     collectstatic()
94     restart()
95
96 @task
97 def deploy_version(version):
98     """
99     Loads the specified version.
100     Warning: this will almost certainly go wrong, it there were any migrations
101     in the meantime!
102     """
103     "Specify a specific version to be made live"
104     require('hosts', 'app_path')
105     env.version = version
106     with cd(env.path):
107         run('rm releases/previous; mv releases/current releases/previous;', pty=True)
108         run('ln -s %(version)s releases/current' % env, pty=True)
109     collectstatic()
110     restart()
111
112 @task
113 def restart():
114     require('services')
115     for service in env.services:
116         execute(service)
117
118
119 # =====================================================================
120 # = Helpers. These are called by other functions rather than directly =
121 # =====================================================================
122
123 class Service(Task):
124     def upload_sample(self):
125         pass
126
127 class DebianGunicorn(Service):
128     def __init__(self, name):
129         super(Task, self).__init__()
130         self.name = name
131
132     def run(self):
133         print '>>> restart webserver using gunicorn-debian'
134         sudo('gunicorn-debian restart %s' % self.name, shell=False)
135
136     def upload_sample(self):
137         upload_sample('gunicorn')
138
139 class Apache(Service):
140     def run(self):
141         print '>>> restart webserver by touching WSGI'
142         with path('/sbin'):
143             run('touch %(app_path)s/%(project_name)s/wsgi.py' % env)
144
145 class Supervisord(Service):
146     def __init__(self, name):
147         super(Task, self).__init__()
148         self.name = name
149
150     def run(self):
151         print '>>> supervisord: restart %s' % self.name
152         sudo('supervisorctl restart %s' % self.name, shell=False)
153
154 def upload_samples():
155     upload_localsettings_sample()
156     upload_nginx_sample()
157     for service in env.services:
158         service.upload_sample()
159
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):
164         return
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)
170
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')
175
176 upload_nginx_sample = lambda: upload_sample('nginx')
177
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])
182     require('app_path')
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)
190
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])
195     require('app_path')
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
203         database_reqs = {
204             'django.db.backends.postgresql_psycopg2': 'psycopg2',
205             'django.db.backends.mysql': 'MySQL-python',
206         }
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])
212
213
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')
219
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)
223
224 def symlink_current_release():
225     "Symlink our current release"
226     print '>>> symlink current release'
227     require('release', provided_by=[deploy])
228     require('app_path')
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)
232
233 def migrate():
234     "Update the database"
235     print '>>> migrate'
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)
240
241 def pre_collectstatic():
242     print '>>> pre_collectstatic'
243     for task in env.get('pre_collectstatic', []):
244         execute(task)
245
246 def 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)
252
253
254 def get_django_root_path(release):
255     require('app_path')
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'])
259     return path