e5cf30b36381d400118ca59dd2a3f73ac069e960
[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
14 """
15 from fabric.api import *
16 from os.path import abspath, dirname, exists, join
17 from fabric.contrib import files
18 from fabric.tasks import Task, execute
19
20 env.virtualenv = '/usr/bin/virtualenv'
21 env.services = None
22
23
24 @task
25 def setup():
26     """
27     Setup a fresh virtualenv as well as a few useful directories.
28     virtualenv should be already installed.
29     """
30     require('hosts', 'app_path', 'virtualenv')
31
32     run('mkdir -p %(app_path)s' % env, pty=True)
33     run('%(virtualenv)s %(app_path)s/ve' % env, pty=True)
34     run('mkdir -p %(app_path)s/releases %(app_path)s/packages' % env, pty=True)
35     run('cd %(app_path)s/releases; ln -sfT . current; ln -sfT . previous' % env, pty=True)
36     upload_samples()
37     print "Fill out db details in localsettings.py and run deploy."
38
39
40 @task(default=True)
41 def deploy():
42     """
43     Deploy the latest version of the site to the servers,
44     install any required third party modules,
45     install the virtual host and then restart the webserver
46     """
47     require('hosts', 'app_path')
48
49     import time
50     env.release = time.strftime('%Y-%m-%dT%H%M')
51
52     check_setup()
53     upload_tar_from_git()
54     install_requirements()
55     copy_localsettings()
56     symlink_current_release()
57     migrate()
58     collectstatic()
59     restart()
60
61 @task
62 def rollback():
63     """
64     Limited rollback capability. Simple loads the previously current
65     version of the code. Rolling back again will swap between the two.
66     Warning: this will almost certainly go wrong, it there were any migrations
67     in the meantime!
68     """
69     require('hosts', 'app_path')
70     with cd(env.path):
71         run('mv releases/current releases/_previous;', pty=True)
72         run('mv releases/previous releases/current;', pty=True)
73         run('mv releases/_previous releases/previous;', pty=True)
74     collectstatic()
75     restart()
76
77 @task
78 def deploy_version(version):
79     """
80     Loads the specified version.
81     Warning: this will almost certainly go wrong, it there were any migrations
82     in the meantime!
83     """
84     "Specify a specific version to be made live"
85     require('hosts', 'app_path')
86     env.version = version
87     with cd(env.path):
88         run('rm releases/previous; mv releases/current releases/previous;', pty=True)
89         run('ln -s %(version)s releases/current' % env, pty=True)
90     collectstatic()
91     restart()
92
93 @task
94 def restart():
95     require('services')
96     for service in env.services:
97         execute(service)
98
99
100 # =====================================================================
101 # = Helpers. These are called by other functions rather than directly =
102 # =====================================================================
103 class DebianGunicorn(Task):
104     def __init__(self, name):
105         super(Task, self).__init__()
106         self.name = name
107
108     def run(self):
109         print '>>> restart webserver using gunicorn-debian'
110         sudo('gunicorn-debian restart %s' % self.name, shell=False)
111
112 class Apache(Task):
113     def run(self):
114         print '>>> restart webserver by touching WSGI'
115         with path('/sbin'):
116             run('touch %(app_path)s/%(project_name)s/wsgi.py' % env)
117
118 class Supervisord(Task):
119     def __init__(self, name):
120         super(Task, self).__init__()
121         self.name = name
122
123     def run(self):
124         print '>>> supervisord: restart %s' % self.name
125         sudo('supervisorctl restart %s' % self.name, shell=False)
126
127 def check_setup():
128     require('app_path')
129     try:
130         run('[ -e %(app_path)s/ve ]' % env)
131     except SystemExit:
132         print "Environment isn't ready. Run `fab setup` first."
133         raise
134
135 def upload_samples():
136     upload_localsettings_sample()
137     upload_nginx_sample()
138     upload_gunicorn_sample()
139
140 def upload_localsettings_sample():
141     "Fill out localsettings template and upload as a sample."
142     print '>>> upload localsettings template'
143     require('app_path', 'project_name')
144     template = '%(project_name)s/localsettings.py.template'
145     if not exists(template):
146         template = join(dirname(abspath(__file__)), 'templates/localsettings.py.template')
147     env.secret_key = '' # sth random
148     files.upload_template(template, '%(app_path)s/localsettings.py.sample' % env, env)
149
150 def upload_nginx_sample():
151     "Fill out nginx conf template and upload as a sample."
152     print '>>> upload nginx template'
153     require('app_path', 'project_name')
154     template = '%(project_name)s/nginx.template'
155     if not exists(template):
156         template = join(dirname(abspath(__file__)), 'templates/nginx.template')
157     files.upload_template(template, '%(app_path)s/nginx.sample' % env, env)
158
159 def upload_gunicorn_sample():
160     "Fill out gunicorn conf template and upload as a sample."
161     print '>>> upload gunicorn template'
162     require('app_path', 'project_name')
163     template = '%(project_name)s/gunicorn.template'
164     if not exists(template):
165         template = join(dirname(abspath(__file__)), 'templates/gunicorn.template')
166     files.upload_template(template, '%(app_path)s/gunicorn.sample' % env, env)
167
168 def upload_tar_from_git():
169     "Create an archive from the current Git branch and upload it"
170     print '>>> upload tar from git'
171     require('release', provided_by=[deploy])
172     require('app_path')
173     local('git-archive-all.sh --format tar %(release)s.tar' % env)
174     local('gzip %(release)s.tar' % env)
175     run('mkdir -p %(app_path)s/releases/%(release)s' % env, pty=True)
176     run('mkdir -p %(app_path)s/packages' % env, pty=True)
177     put('%(release)s.tar.gz' % env, '%(app_path)s/packages/' % env)
178     run('cd %(app_path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty=True)
179     local('rm %(release)s.tar.gz' % env)
180
181 def install_requirements():
182     "Install the required packages from the requirements file using pip"
183     print '>>> install requirements'
184     require('release', provided_by=[deploy])
185     require('app_path')
186     run('cd %(app_path)s; ve/bin/pip install -r %(app_path)s/releases/%(release)s/requirements.txt' % env, pty=True)
187
188 def copy_localsettings():
189     "Copy localsettings.py from root directory to release directory (if this file exists)"
190     print ">>> copy localsettings"
191     require('release', provided_by=[deploy])
192     require('app_path', 'project_name')
193
194     with settings(warn_only=True):
195         run('cp %(app_path)s/localsettings.py %(app_path)s/releases/%(release)s/%(project_name)s' % env)
196
197 def symlink_current_release():
198     "Symlink our current release"
199     print '>>> symlink current release'
200     require('release', provided_by=[deploy])
201     require('app_path')
202     with cd(env.path):
203         run('rm releases/previous; mv releases/current releases/previous')
204         run('ln -s %(release)s releases/current' % env)
205
206 def migrate():
207     "Update the database"
208     print '>>> migrate'
209     require('app_path', 'project_name')
210     with cd('%(app_path)s/releases/current' % env):
211         run('%(app_path)s/ve/bin/python manage.py syncdb --noinput' % env, pty=True)
212         run('%(app_path)s/ve/bin/python manage.py migrate' % env, pty=True)
213
214 def collectstatic():
215     """Collect static files"""
216     print '>>> collectstatic'
217     require('app_path', 'project_name')
218     with cd('%(app_path)s/releases/current' % env):
219         run('%(app_path)s/ve/bin/python manage.py collectstatic --noinput' % env, pty=True)