3fce28615916e314815580fb0bd99febb9381096
[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     skip_collect_static (optional): if True, Django collectstatic command is not called
20 """
21 from os.path import abspath, dirname, exists, join
22 from django.utils.crypto import get_random_string
23 from fabric.api import *
24 from fabric.contrib import files
25 from fabric.tasks import Task, execute
26
27 env.virtualenv = '/usr/bin/virtualenv'
28 env.services = None
29
30
31 @task
32 def setup():
33     """
34     Setup all needed directories.
35     """
36     require('hosts', 'app_path')
37
38     if not files.exists(env.app_path):
39         run('mkdir -p %(app_path)s' % env, pty=True)
40     with cd(env.app_path):
41         for subdir in 'releases', 'packages', 'log', 'samples':
42             if not files.exists(subdir):
43                 run('mkdir -p %s' % subdir, pty=True)
44     with cd('%(app_path)s/releases' % env):
45         if not files.exists('current'):
46             run('ln -sfT . current', pty=True)
47         if not files.exists('previous'):
48             run('ln -sfT . previous', pty=True)
49     
50     upload_samples()
51
52
53 def check_localsettings():
54     return files.exists('%(app_path)s/localsettings.py' % env)
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     if not check_localsettings():
71         abort('Setup is complete, but\n    %(app_path)s/localsettings.py\n'
72               'is needed for actual deployment.' % env)
73     upload_tar_from_git()
74     copy_localsettings()
75     install_requirements()
76     symlink_current_release()
77     migrate()
78     pre_collectstatic()
79     collectstatic()
80     restart()
81
82 @task
83 def rollback():
84     """
85     Limited rollback capability. Simple loads the previously current
86     version of the code. Rolling back again will swap between the two.
87     Warning: this will almost certainly go wrong, it there were any migrations
88     in the meantime!
89     """
90     require('hosts', 'app_path')
91     with cd(env.path):
92         run('mv releases/current releases/_previous;', pty=True)
93         run('mv releases/previous releases/current;', pty=True)
94         run('mv releases/_previous releases/previous;', pty=True)
95     collectstatic()
96     restart()
97
98 @task
99 def deploy_version(version):
100     """
101     Loads the specified version.
102     Warning: this will almost certainly go wrong, it there were any migrations
103     in the meantime!
104     """
105     "Specify a specific version to be made live"
106     require('hosts', 'app_path')
107     env.version = version
108     with cd(env.path):
109         run('rm releases/previous; mv releases/current releases/previous;', pty=True)
110         run('ln -s %(version)s releases/current' % env, pty=True)
111     collectstatic()
112     restart()
113
114 @task
115 def restart():
116     for service in env.services or ():
117         execute(service)
118
119
120 # =====================================================================
121 # = Helpers. These are called by other functions rather than directly =
122 # =====================================================================
123
124 class Service(Task):
125     def upload_sample(self):
126         pass
127
128 class DebianGunicorn(Service):
129     def __init__(self, name):
130         super(Task, self).__init__()
131         self.name = name
132
133     def run(self):
134         print '>>> restart webserver using gunicorn-debian'
135         sudo('gunicorn-debian restart %s' % self.name, shell=False)
136
137     def upload_sample(self):
138         upload_sample('gunicorn')
139
140 class Apache(Service):
141     def run(self):
142         print '>>> restart webserver by touching WSGI'
143         with path('/sbin'):
144             run('touch %(app_path)s/%(project_name)s/wsgi.py' % env)
145
146 class Supervisord(Service):
147     def __init__(self, name):
148         super(Task, self).__init__()
149         self.name = name
150
151     def run(self):
152         print '>>> supervisord: restart %s' % self.name
153         sudo('supervisorctl restart %s' % self.name, shell=False)
154
155 class Command(Task):
156     def __init__(self, commands, working_dir):
157         if not hasattr(commands, '__iter__'):
158             commands = [commands]
159         self.name = 'Command: %s @ %s' % (commands, working_dir)
160         self.commands = commands
161         self.working_dir = working_dir
162
163     def run(self):
164         require('app_path')
165         with cd(join('%(app_path)s/releases/current' % env, self.working_dir)):
166             for command in self.commands:
167                 run(command)
168
169 def upload_samples():
170     upload_localsettings_sample()
171     upload_nginx_sample()
172     for service in env.services:
173         service.upload_sample()
174
175 def upload_sample(name, where="samples/"):
176     require('app_path', 'project_name')
177     upload_path = '%s/%s%s.sample' % (env['app_path'], where, name)
178     if files.exists(upload_path):
179         return
180     print '>>> upload %s template' % name
181     template = '%(project_name)s/' % env + name + '.template'
182     if not exists(template):
183         template = join(dirname(abspath(__file__)), 'templates/' + name + '.template')
184     files.upload_template(template, upload_path, env)
185
186 def upload_localsettings_sample():
187     "Fill out localsettings template and upload as a sample."
188     env.secret_key = get_random_string(50)
189     upload_sample('localsettings.py', where="")
190
191 upload_nginx_sample = lambda: upload_sample('nginx')
192
193 def upload_tar_from_git():
194     "Create an archive from the current Git branch and upload it"
195     print '>>> upload tar from git'
196     require('release', provided_by=[deploy])
197     require('app_path')
198     local('git-archive-all.sh --format tar %(release)s.tar' % env)
199     local('gzip %(release)s.tar' % env)
200     run('mkdir -p %(app_path)s/releases/%(release)s' % env, pty=True)
201     run('mkdir -p %(app_path)s/packages' % env, pty=True)
202     put('%(release)s.tar.gz' % env, '%(app_path)s/packages/' % env)
203     run('cd %(app_path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty=True)
204     local('rm %(release)s.tar.gz' % env)
205
206 def install_requirements():
207     "Install the required packages from the requirements file using pip"
208     print '>>> install requirements'
209     require('release', provided_by=[deploy])
210     require('app_path')
211     if not files.exists('%(app_path)s/ve' % env):
212         require('virtualenv')
213         run('%(virtualenv)s %(app_path)s/ve' % env, pty=True)
214     with cd('%(app_path)s/releases/%(release)s' % env):
215         run('%(app_path)s/ve/bin/pip install -r requirements.txt' % env, pty=True)
216     with cd(get_django_root_path(env['release'])):
217         # Install DB requirement
218         database_reqs = {
219             'django.db.backends.postgresql_psycopg2': 'psycopg2',
220             'django.db.backends.mysql': 'MySQL-python',
221         }
222         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)
223         for database in databases.split():
224             if database in database_reqs:
225                 # TODO: set pip default pypi
226                 run('%(app_path)s/ve/bin/pip install ' % env + database_reqs[database])
227
228
229 def copy_localsettings():
230     "Copy localsettings.py from root directory to release directory (if this file exists)"
231     print ">>> copy localsettings"
232     require('release', provided_by=[deploy])
233     require('app_path', 'project_name')
234
235     with settings(warn_only=True):
236         copy_to = join(get_django_root_path(env['release']), env.get('localsettings_dst_path', env['project_name']))
237         run('cp %(app_path)s/localsettings.py ' % env + copy_to)
238
239 def symlink_current_release():
240     "Symlink our current release"
241     print '>>> symlink current release'
242     require('release', provided_by=[deploy])
243     require('app_path')
244     with cd(env.app_path):
245         run('rm releases/previous; mv releases/current releases/previous')
246         run('ln -s %(release)s releases/current' % env)
247
248 def migrate():
249     "Update the database"
250     print '>>> migrate'
251     require('app_path', 'project_name')
252     with cd(get_django_root_path('current')):
253         run('%(app_path)s/ve/bin/python manage.py syncdb --noinput' % env, pty=True)
254         run('%(app_path)s/ve/bin/python manage.py migrate' % env, pty=True)
255
256 def pre_collectstatic():
257     print '>>> pre_collectstatic'
258     for task in env.get('pre_collectstatic', []):
259         execute(task)
260
261 def collectstatic():
262     """Collect static files"""
263     print '>>> collectstatic'
264     if env.get('skip_collect_static', False):
265         print '... skipped'
266         return
267     require('app_path', 'project_name')
268     with cd(get_django_root_path('current')):
269         run('%(app_path)s/ve/bin/python manage.py collectstatic --noinput' % env, pty=True)
270
271
272 def get_django_root_path(release):
273     require('app_path')
274     path = '%(app_path)s/releases/%(release)s' % dict(app_path = env['app_path'], release = release)
275     if 'django_root_path' in env:
276         path = join(path, env['django_root_path'])
277     return path