9c7d762202e4429220005c47f5b60c015bf26b8c
[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 -s . current; ln -s . 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         with path('/sbin'):
111             sudo('gunicorn-debian restart %s' % self.site_name, shell=False)
112
113 class Apache(Task):
114     def run(self):
115         print '>>> restart webserver by touching WSGI'
116         with path('/sbin'):
117             run('touch %(app_path)s/%(project_name)s/wsgi.py' % env)
118
119 class Supervisord(Task):
120     def __init__(self, name):
121         super(Task, self).__init__()
122         self.name = name
123
124     def run(self):
125         print '>>> supervisord: restart %s' % self.name
126         with path('/sbin'):
127             sudo('supervisorctl restart %s' % self.name, shell=False)
128
129 def check_setup():
130     require('app_path')
131     try:
132         run('[ -e %(app_path)s/ve ]' % env)
133     except SystemExit:
134         print "Environment isn't ready. Run fab deploy.setup first."
135         raise
136
137 def upload_samples():
138     upload_localsettings_sample()
139     upload_nginx_sample()
140     upload_gunicorn_sample()
141
142 def upload_localsettings_sample():
143     "Fill out localsettings template and upload as a sample."
144     print '>>> upload localsettings template'
145     require('app_path', 'project_name')
146     template = '%(project_name)s/localsettings.py.template'
147     if not exists(template):
148         template = join(dirname(abspath(__file__)), 'localsettings.py.template')
149     env.secret_key = '' # sth random
150     files.upload_template(template, '%(app_path)s/localsettings.py.sample' % env, env)
151
152 def upload_nginx_sample():
153     "Fill out nginx conf template and upload as a sample."
154     print '>>> upload nginx template'
155     require('app_path', 'project_name')
156     template = '%(project_name)s/nginx.template'
157     if not exists(template):
158         template = join(dirname(abspath(__file__)), 'nginx.template')
159     files.upload_template(template, '%(app_path)s/nginx.sample' % env, env)
160
161 def upload_gunicorn_sample():
162     "Fill out gunicorn conf template and upload as a sample."
163     print '>>> upload gunicorn template'
164     require('app_path', 'project_name')
165     template = '%(project_name)s/gunicorn.template'
166     if not exists(template):
167         template = join(dirname(abspath(__file__)), 'gunicorn.template')
168     files.upload_template(template, '%(app_path)s/gunicorn.sample' % env, env)
169
170 def upload_tar_from_git():
171     "Create an archive from the current Git branch and upload it"
172     print '>>> upload tar from git'
173     require('release', provided_by=[deploy])
174     require('app_path')
175     local('git-archive-all.sh --format tar %(release)s.tar' % env)
176     local('gzip %(release)s.tar' % env)
177     run('mkdir -p %(app_path)s/releases/%(release)s' % env, pty=True)
178     run('mkdir -p %(app_path)s/packages' % env, pty=True)
179     put('%(release)s.tar.gz' % env, '%(app_path)s/packages/' % env)
180     run('cd %(app_path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty=True)
181     local('rm %(release)s.tar.gz' % env)
182
183 def install_requirements():
184     "Install the required packages from the requirements file using pip"
185     print '>>> install requirements'
186     require('release', provided_by=[deploy])
187     require('app_path')
188     run('cd %(app_path)s; ve/bin/pip install -r %(app_path)s/releases/%(release)s/requirements.txt' % env, pty=True)
189
190 def copy_localsettings():
191     "Copy localsettings.py from root directory to release directory (if this file exists)"
192     print ">>> copy localsettings"
193     require('release', provided_by=[deploy])
194     require('app_path', 'project_name')
195
196     with settings(warn_only=True):
197         run('cp %(app_path)s/localsettings.py %(app_path)s/releases/%(release)s/%(project_name)s' % env)
198
199 def symlink_current_release():
200     "Symlink our current release"
201     print '>>> symlink current release'
202     require('release', provided_by=[deploy])
203     require('app_path')
204     with cd(env.path):
205         run('rm releases/previous; mv releases/current releases/previous')
206         run('ln -s %(release)s releases/current' % env)
207
208 def migrate():
209     "Update the database"
210     print '>>> migrate'
211     require('app_path', 'project_name')
212     with cd('%(app_path)s/releases/current/%(project_name)s' % env):
213         run('%(app_path)s/ve/bin/python manage.py syncdb --noinput' % env, pty=True)
214         run('%(app_path)s/ve/bin/python manage.py migrate' % env, pty=True)
215
216 def collectstatic():
217     """Collect static files"""
218     print '>>> collectstatic'
219     require('app_path', 'project_name')
220     with cd('%(app_path)s/releases/current/%(project_name)s' % env):
221         run('%(app_path)s/ve/bin/python manage.py collectstatic --noinput' % env, pty=True)