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