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