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