Configurable path to the localsettings.py file
[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     collectstatic()
77     restart()
78
79 @task
80 def rollback():
81     """
82     Limited rollback capability. Simple loads the previously current
83     version of the code. Rolling back again will swap between the two.
84     Warning: this will almost certainly go wrong, it there were any migrations
85     in the meantime!
86     """
87     require('hosts', 'app_path')
88     with cd(env.path):
89         run('mv releases/current releases/_previous;', pty=True)
90         run('mv releases/previous releases/current;', pty=True)
91         run('mv releases/_previous releases/previous;', pty=True)
92     collectstatic()
93     restart()
94
95 @task
96 def deploy_version(version):
97     """
98     Loads the specified version.
99     Warning: this will almost certainly go wrong, it there were any migrations
100     in the meantime!
101     """
102     "Specify a specific version to be made live"
103     require('hosts', 'app_path')
104     env.version = version
105     with cd(env.path):
106         run('rm releases/previous; mv releases/current releases/previous;', pty=True)
107         run('ln -s %(version)s releases/current' % env, pty=True)
108     collectstatic()
109     restart()
110
111 @task
112 def restart():
113     require('services')
114     for service in env.services:
115         execute(service)
116
117
118 # =====================================================================
119 # = Helpers. These are called by other functions rather than directly =
120 # =====================================================================
121
122 class Service(Task):
123     def upload_sample(self):
124         pass
125
126 class DebianGunicorn(Service):
127     def __init__(self, name):
128         super(Task, self).__init__()
129         self.name = name
130
131     def run(self):
132         print '>>> restart webserver using gunicorn-debian'
133         sudo('gunicorn-debian restart %s' % self.name, shell=False)
134
135     def upload_sample(self):
136         upload_sample('gunicorn')
137
138 class Apache(Service):
139     def run(self):
140         print '>>> restart webserver by touching WSGI'
141         with path('/sbin'):
142             run('touch %(app_path)s/%(project_name)s/wsgi.py' % env)
143
144 class Supervisord(Service):
145     def __init__(self, name):
146         super(Task, self).__init__()
147         self.name = name
148
149     def run(self):
150         print '>>> supervisord: restart %s' % self.name
151         sudo('supervisorctl restart %s' % self.name, shell=False)
152
153 def upload_samples():
154     upload_localsettings_sample()
155     upload_nginx_sample()
156     for service in env.services:
157         service.upload_sample()
158
159 def upload_sample(name):
160     require('app_path', 'project_name')
161     upload_path = '%(app_path)s/' % env + name + '.sample'
162     if files.exists(upload_path):
163         return
164     print '>>> upload %s template' % name
165     template = '%(project_name)s/' % env + name + '.template'
166     if not exists(template):
167         template = join(dirname(abspath(__file__)), 'templates/' + name + '.template')
168     files.upload_template(template, upload_path, env)
169
170 def upload_localsettings_sample():
171     "Fill out localsettings template and upload as a sample."
172     env.secret_key = get_random_string(50)
173     upload_sample('localsettings.py')
174
175 upload_nginx_sample = lambda: upload_sample('nginx')
176
177 def upload_tar_from_git():
178     "Create an archive from the current Git branch and upload it"
179     print '>>> upload tar from git'
180     require('release', provided_by=[deploy])
181     require('app_path')
182     local('git-archive-all.sh --format tar %(release)s.tar' % env)
183     local('gzip %(release)s.tar' % env)
184     run('mkdir -p %(app_path)s/releases/%(release)s' % env, pty=True)
185     run('mkdir -p %(app_path)s/packages' % env, pty=True)
186     put('%(release)s.tar.gz' % env, '%(app_path)s/packages/' % env)
187     run('cd %(app_path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty=True)
188     local('rm %(release)s.tar.gz' % env)
189
190 def install_requirements():
191     "Install the required packages from the requirements file using pip"
192     print '>>> install requirements'
193     require('release', provided_by=[deploy])
194     require('app_path')
195     if not files.exists('%(app_path)s/ve' % env):
196         require('virtualenv')
197         run('%(virtualenv)s %(app_path)s/ve' % env, pty=True)
198     with cd('%(app_path)s/releases/%(release)s' % env):
199         run('%(app_path)s/ve/bin/pip install -r requirements.txt' % env, pty=True)
200     with cd(get_django_root_path(env['release'])):
201         # Install DB requirement
202         database_reqs = {
203             'django.db.backends.postgresql_psycopg2': 'psycopg2',
204             'django.db.backends.mysql': 'MySQL-python',
205         }
206         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)
207         for database in databases.split():
208             if database in database_reqs:
209                 # TODO: set pip default pypi
210                 run('%(app_path)s/ve/bin/pip install ' % env + database_reqs[database])
211
212
213 def copy_localsettings():
214     "Copy localsettings.py from root directory to release directory (if this file exists)"
215     print ">>> copy localsettings"
216     require('release', provided_by=[deploy])
217     require('app_path', 'project_name')
218
219     with settings(warn_only=True):
220         copy_to = join(get_django_root_path(env['release']), env.get('localsettings_dst_path', env['project_name']))
221         run('cp %(app_path)s/localsettings.py ' % env + copy_to)
222
223 def symlink_current_release():
224     "Symlink our current release"
225     print '>>> symlink current release'
226     require('release', provided_by=[deploy])
227     require('app_path')
228     with cd(env.app_path):
229         run('rm releases/previous; mv releases/current releases/previous')
230         run('ln -s %(release)s releases/current' % env)
231
232 def migrate():
233     "Update the database"
234     print '>>> migrate'
235     require('app_path', 'project_name')
236     with cd(get_django_root_path('current')):
237         run('%(app_path)s/ve/bin/python manage.py syncdb --noinput' % env, pty=True)
238         run('%(app_path)s/ve/bin/python manage.py migrate' % env, pty=True)
239
240 def collectstatic():
241     """Collect static files"""
242     print '>>> collectstatic'
243     require('app_path', 'project_name')
244     with cd(get_django_root_path('current')):
245         run('%(app_path)s/ve/bin/python manage.py collectstatic --noinput' % env, pty=True)
246
247
248 def get_django_root_path(release):
249     require('app_path')
250     path = '%(app_path)s/releases/%(release)s' % dict(app_path = env['app_path'], release = release)
251     if 'django_root_path' in env:
252         path = join(path, env['django_root_path'])
253     return path