-
Notifications
You must be signed in to change notification settings - Fork 1
/
fabfile.py
387 lines (306 loc) · 13.8 KB
/
fabfile.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
from __future__ import with_statement
from fabric.api import *
import os, datetime
# I'm a little weary of this being here by default, since I don't want to accidently
# do something on production (with the exception of pulling data down for testing)
prod_host = '[email protected]'
#env.hosts = [prod_host]
env.local_prj_dir = '/home/webuser/prj'
env.local_drupal_dir = env.local_prj_dir+'/bbcom/drupal'
env.local_python_env_dir = env.local_prj_dir+'/python-env'
env.local_drush_alias = '@en.master.bibliobird.vm'
env.remote_prj_dir = '/home/webuser/prj';
env.remote_drupal_dir = env.remote_prj_dir+'/bbcom/drupal'
env.remote_python_env_dir = env.remote_prj_dir+'/python-env'
env.remote_drush_alias = '@en.bibliobird.com'
env.repos = ['bbcom','lingwo','lingwo-old']
class _Drush(object):
def __init__(self, remote=False):
self.alias = env.remote_drush_alias if remote else env.local_drush_alias
self.func = run if remote else local
self.remote = remote
def run(self, *args, **kw):
# quote the arguments
def quote(s):
return '"'+s.replace('"', '\\"')+'"'
args = [quote(x) for x in args]
cmd = " ".join(["drush",self.alias]+args)
res = None
if not self.remote and kw.get('capture', True) is False:
local(cmd, capture=False)
else:
res = self.func(cmd)
return res
def vset(self, **kw):
for k, v in kw.items():
self.run('vset', '--yes', k, v)
def vdel(self, *args):
for v in args:
self.run('vdel', '--yes', v)
def sql_query(self, s):
self.run('sql-query', s)
def cc(self, spec='all'):
self.run('cc', spec)
def en(self, *args):
for mod in args:
self.run('en', mod, '--yes')
def dis(self, *args):
for mod in args:
self.run('dis', mod, '--yes')
def _python_env_requirements(limit_to=None):
f = open('python-env-requirements.txt', 'rt')
versions = []
for line in f.readlines():
if line.startswith('#'):
continue
if line == '\n':
continue
line = line[:-1]
if limit_to is not None and line.split('==')[0] not in limit_to:
continue
versions.append(line)
return versions
# TODO: these should be put into some kind of object so it can go both local and remote
def _pip(*args):
local(os.path.join(env.local_python_env_dir, 'bin', 'pip')+' '+' '.join(args), capture=False)
def _python(*args):
local(os.path.join(env.local_python_env_dir, 'bin', 'python')+' '+' '.join(args), capture=False)
@hosts(prod_host)
def pull_live_db():
"""Pull data from the live database and set it up here for testing"""
drush = _Drush(remote=True)
backup = 'bibliobird-backup.mysql.gz'
with cd(env.remote_drupal_dir):
with hide('stdout','stderr'):
drush.run('bam-backup', 'db', 'manual', '60a4968e1a793e5a8a20fa52644244e2')
#run("drush bam-backup db manual 60a4968e1a793e5a8a20fa52644244e2".format(env.remote_drush_alias))
get("{0}/lingwo_backup/manual/{1}".format(env.remote_prj_dir,backup), "/tmp/")
with cd(env.local_drupal_dir):
#local("drush sqlq 'DROP DATABASE bibliobird; CREATE DATABASE bibliobird;'", capture=False)
local("zcat /tmp/{0} | drush {1} sql-cli".format(backup, env.local_drush_alias))
local("rm -f /tmp/{0}".format(backup))
make_testing_safe()
def make_testing_safe():
"""Configures drupal such that it is safe to develop with it."""
drush = _Drush(remote=False)
# this seems to correct any problems we encounter after putting a foreign db in place
drush.run('updatedb', '--yes', capture=False)
with settings(warn_only=True):
drush.cc()
drush.sql_query("UPDATE languages SET domain = 'http://en.master.bibliobird.vm', prefix = '' WHERE language = 'en'")
drush.sql_query("UPDATE languages SET domain = 'http://pl.master.bibliobird.vm', prefix = '' WHERE language = 'pl'")
drush.vdel('language_default')
drush.vset(
language_negotiation="3",
preprocess_css="0",
preprocess_js="0",
site_name="TESTING",
bbcom_news_mailchimp_integration="0",
bbcom_metrics_mixpanel_token="ec4433cb33d091816ce93058686b0ae8",
bbcom_metrics_mixpanel_key="d151eb6fe740fe1c3d6392c78df25113",
bbcom_metrics_mixpanel_secret="a59ee2473dee43341d97a0b1bc63bd96"
)
# disable notifications, so we can still test it, but we won't hit real users with e-mails
drush.sql_query("DELETE FROM notifications; DELETE FROM notifications_fields; DELETE FROM notifications_queue;")
drush.en('messaging_simple')
# disable dangerous modules
drush.dis('backup_migrate', 'mollom', 'googleanalytics', 'spambot')
# enable the development module
drush.sql_query("DROP TABLE devel_queries; DROP TABLE devel_times")
drush.en('devel')
drush.vset(smtp_library="sites/all/modules/devel/devel.module")
def branch(source=None, target=None):
"""Branch all the repos to create a new family of branches."""
# We do some argument shuffling magic, so that we can specify one argument
# to branch from 'master', specify both in a sane order... Not very pythonic
# but it stops me from losing my mind with how this is supposed to work!
if source is None and target is None:
raise TypeError('Must pass atleast one argument')
if target is None:
target = source
source = 'master'
if source is None:
source = 'master'
for repo in env.repos:
with cd(os.path.join(env.local_prj_dir, 'bibliobird', repo)):
local('git checkout -b {0} {1}'.format(target, source), capture=False)
local('git push origin {0}'.format(target), capture=False)
# TODO: can we simply use the --set-upstream argument of 'git push'
# setup configuration for this branch
fd = open(os.path.join(env.cwd, '.git', 'config'), 'at')
fd.write('[branch "{0}"]\n'.format(target))
fd.write('\tremote = origin\n')
fd.write('\tmerge = refs/heads/{0}\n'.format(target))
fd.close()
def checkout(branch):
"""Switch all our symlinks to point to the given family of branches."""
for repo in env.repos:
with cd(os.path.join(env.local_prj_dir, 'bibliobird', repo)):
local('git checkout {0}'.format(branch))
def merge(source, target='master', message=None):
"""Merge a family of branches into another family (by default, "master")."""
base_message = 'Merge branch \'{0}\''.format(source)
if message is None:
message = base_message
else:
message = base_message + ' ' + message
# TODO: we need to check for uncommited changes!
for repo in env.repos:
with cd(os.path.join(env.local_prj_dir, 'bibliobird', repo)):
local('git checkout {0}'.format(target), capture=False)
#output = local('git merge --no-commit --squash {0}'.format(source), capture=True)
output = local('git merge --no-commit {0}'.format(source), capture=True)
if 'Already up-to-date' not in output:
with settings(warn_only=True):
local('git commit -m "{0}"'.format(message.replace('"', '\\"')), capture=False)
local('git push --all', capture=False)
def pull(branch=None):
"""Pull all the code on a given branch."""
cmd = 'git pull'
if branch is None:
cmd += ' --all'
for repo in env.repos:
with cd(os.path.join(env.local_prj_dir, 'bibliobird', repo)):
if branch is not None:
local('git checkout {0}'.format(branch), capture=False)
local(cmd, capture=False)
def push(branch=None):
"""Push all the code on a given branch."""
cmd = 'git push'
if branch is None:
cmd += ' --all'
for repo in env.repos:
with cd(os.path.join(env.local_prj_dir, 'bibliobird', repo)):
if branch is not None:
local('git checkout {0}'.format(branch), capture=False)
local(cmd, capture=False)
def tag_release(name):
"""Tags production for release."""
# tag production with release name
for repo in env.repos:
with cd(os.path.join(env.local_prj_dir, 'bibliobird', repo)):
local('git checkout production', capture=False)
local('git tag release--{0}'.format(name), capture=False)
local('git push --tags', capture=False)
def make_release(name):
"""Merges master into production and then tags for release."""
# merge mainline into production
merge('master', 'production', 'Creating release {0}'.format(name))
tag_release(name)
@hosts(prod_host)
def backup_live_db():
drush = _Drush(remote=True)
drush.run('bam-backup', 'db', 'manual', 'default')
@hosts(prod_host)
def backup_live_code():
today = datetime.date.today()
with cd(env.remote_prj_dir):
# TODO: we should specifically target certain directories! We don't want to backup
# "python-env" along with the other code. It should be:
#
# env.repos + ['anki_files']
# (remember to swap bbcom in if it hasn't been yet)
#
run('tar -cjvpf bibliobird-{0}.tar.bz2 bibliobird'.format(today.strftime('%Y-%m-%d')))
@hosts(prod_host)
def unsafe_deploy(release_name):
"""UNSAFE: Does deployment without first backing things up!"""
# check out the new code
for repo in env.repos:
with cd(os.path.join(env.remote_prj_dir, 'bibliobird', repo)):
run('git checkout tags/release--{0}'.format(release_name))
# update the database (should also force new dependencies to be enabled)
drush = _Drush(remote=True)
drush.run('updatedb')
@hosts(prod_host)
def deploy(release_name):
"""Backs up db/code and the deploys a release."""
backup_live_db()
backup_live_code()
unsafe_deploy(release_name)
def create_bootstrap():
"""Creates the boostrap.py for bootstrapping our development environment."""
# TODO: The bootstrap should also add this to bin/activate:
#
# # DRS: make our fabfile always get called
# alias fab='fab -f /home/dsnopek/prj/bibliobird/bbcom/fabfile.py'
#
# TODO: The bootstrap should initialize the top project directory
# as a Bazaar repository, so that revisiions are shared between branches.
#
import virtualenv, textwrap
deps = ['pip', 'virtualenv', 'Fabric', 'pycrypto', 'paramiko']
deps = ', '.join(["'{0}'".format(x) for x in _python_env_requirements(deps)])
output = virtualenv.create_bootstrap_script(textwrap.dedent("""
import os, sys, subprocess
def extend_parser(parser):
for opt in ['clear','no-site-packages','unzip-setuptools','relocatable','distribute','setuptools']:
if parser.has_option('--'+opt):
parser.remove_option('--'+opt)
parser.add_option('--bbcom-branch', dest='bbcom_branch', metavar='BRANCH',
help='Location of the bbcom_branch',
default='bzr+ssh://code.hackyourlife.org/home/dsnopek/bzr-lingwo/bbcom/mainline')
parser.add_option('--only-virtualenv', dest='only_virtualenv', action='store_true',
help='Only create the virtualenv, not the entire BiblioBird project directory',
default=False)
def adjust_options(options, args):
if len(args) > 0:
if os.path.exists(args[0]):
print >> sys.stderr, "ERROR: %s already exists!" % args[0]
sys.exit(1)
if not options.only_virtualenv:
os.mkdir(args[0])
options.bbcom_project_dir = args[0]
args[0] = join(args[0], 'python-env')
options.no_site_packages = True
options.prompt = 'bibliobird'
def after_install(options, home_dir):
subprocess.call([join(home_dir, 'bin', 'pip'),
'install', '-U', {deps}])
if not options.only_virtualenv:
subprocess.call([join(home_dir, 'bin', 'bzr'),
'co', options.bbcom_branch,
join(options.bbcom_project_dir, 'bbcom')])
subprocess.call([join(home_dir, 'bin', 'fab'),
'-f', join(options.bbcom_project_dir, 'bbcom', 'fabfile.py'), 'bootstrap'],
cwd=join(options.bbcom_project_dir, 'bbcom'))
""".format(deps=deps)))
f = open('bootstrap.py', 'w')
f.write(output)
f.close()
def setup_python_env():
"""Sets up our python environment."""
# TODO: if the python-env doesn't exist, we should create it with the bootstrap script
tarball_installs = {
'nltk==2.0b9': 'http://nltk.googlecode.com/files/nltk-2.0b9.zip#egg=nltk',
'html5lib==0.90': 'http://html5lib.googlecode.com/files/html5lib-0.90.zip#egg=html5lib',
}
anki_tarball = 'http://anki.googlecode.com/files/anki-1.2.8.tgz'
requirements = _python_env_requirements()
for i, r in enumerate(requirements[:]):
if tarball_installs.has_key(r):
requirements[i] = tarball_installs[r]
elif r.split('==')[0] == 'anki':
# we handle Anki special at the end
del requirements[i]
# we have to install PyYAML first due to a weird bug in nltk
_pip('install', '-U', *_python_env_requirements(['PyYAML']))
# install (almost) all the requirements
_pip('install', '-U', *requirements)
# install libanki special
from tempfile import mkdtemp
from shutil import rmtree
tempdir = mkdtemp()
try:
fn = os.path.basename(anki_tarball)
dn = os.path.splitext(fn)[0]
with cd(tempdir):
local('wget '+anki_tarball, capture=False)
local('tar -xzvf '+fn, capture=False)
with cd(os.path.join(tempdir, dn, 'libanki')):
_python('setup.py', 'install')
finally:
rmtree(tempdir)
def bootstrap():
"""Takes an empty project directory, populates and sets everything up."""
setup_python_env()