-
Notifications
You must be signed in to change notification settings - Fork 27
/
Copy pathfabfile.py
1115 lines (946 loc) · 44.1 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
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
:copyright: (c) 2011 Local Projects, all rights reserved
:license: Affero GNU GPL v3, see LICENSE for more details.
"""
from fabric.api import *
from fabric.contrib import files as files
import os
import time
import re
from boto.ec2.connection import EC2Connection
from boto.ec2.autoscale import AutoScaleConnection
from boto.ec2.autoscale import LaunchConfiguration
from boto.ec2.autoscale import AutoScalingGroup
# from boto.ec2.autoscale import Trigger
"""
------------------------------
INSTRUCTIONS:
------------------------------
This fabfile requires a set of parameters to be set in an rcfile, for fabric to load.
A minimum of fabric 1.2.0 is required
Install fabric:
pip install fabric
CAVEATS AND NOTES:
* Do NOT user short if[] syntax, meaning [ -e "foobar.txt' ] && {do_something}, in fabric scripts.
Go for explicit if [ -e "foobar.txt" ];then do_something; fi
The short syntax does not return the correct value for success, and this causes scripts to fail
* ROLES (and other decorators) MUST be on the parent function.
Running a child function through a decorator just won't work due to some fabric stupidity!
TODO:
* Easy way to install pip/pear/cpan packages when RHEL does not have them:
* easy_install-2.6 install pip
* pip-2.6 install pyyaml
------------------------------
COOKBOOK:
------------------------------
The general format of a command is:
fab --config=<rcfile> <environment> <tasks ...>
environment : the environment to launch. This is mandatory.
value is one of dev, prod, demo
Setup the initial system with prerequisites
fab --config=rcfile.dev dev setup_system
A quick way to get the initial system setup
fab --config=rcfile.demo demo setup_application deploy_configurations bundle_code deploy
Setup a set of web servers
fab --config=rcfile.dev dev setup_application
Create new configuration file:
fab --config=rcfile.name dev create_config_yaml
Create stage-specific lighttpd.conf file into the etc/ folder
fab --config=rcfile.name dev create_lighttpd_conf
Create (via interpolation) and upload configurations to remote host(s):
fab --config=rcfile.dev dev deploy_configurations
Create a new bundle and deploy
fab --config=rcfile.name dev bundle_code deploy
Create a new bundle and deploy at a different time
fab --config=rcfile.name dev bundle_code
Save the release bundle name (see the ATTENTION line in the output)
fab --config=rcfile.name dev deploy
# Webserver related tasks
Start / stop / restart the webserver
fab --config=rcfile.environment environment stop_webserver
# Deploy cron tasks
fab --config=rcfile.dev dev deploy_cron
# Set up and deploy the database backup script(s)
fab --config=rcfile.dev dev setup_db_backup
"""
# GLOBALS
DEBUG = True
# WHether to clean the build_path folder before the build. This is set
# using the clean_build option
env.clean_build = False
# SSH Key configuration
env.key_filename = os.path.expanduser(env.key_filename)
while not os.path.exists(env.key_filename):
env.key_filename = prompt("Keffile %s not found. Provide valid SSH keyfile (eg. ~/.ssh/ChangeByUs.pem: " % env.key_filename)
env.key_filename = os.path.expanduser(env.key_filename)
env.ssh_port = 48022
# We need to make the hosts into a list
env.hosts = env.hosts.split(',')
# Interpolate the build_path if necessary
env.build_path = env.build_path % env
# if len(env.build_path) == 0 or not os.path.exists(env.build_path):
# raise Exception("Option build_path (%(build_path)s is incorrect, or the target folder does not exist. Please correct it before proceeding" % env)
if not env.get('home_path'):
env.home_path = "/home"
# Roles for different targets clusters
env.roledefs = {
'web': env.hosts,
}
# Miscellaneous options
env.vars = {}
# Params for config_files are:
# filename : post-interpolated filename on the remote host
# templatename : name of the template file that interpolation is performed on
# path: : path on the remote host to store filename to
# CAVEAT: Don't link anything into the web/ path unless you want it visible to the public!
# [ {'filename':'config.yaml', 'templatename':'config.yaml.sample', 'path':'etc'} ]
env.config_files = [ {'filename':'config.yaml', 'path': '', 'templatename': 'config.yaml.tmpl'} ]
# Packages required to run the application. Used by system_setup
# The specific package key (os-name) is determined on system setup
env.packages = {'rhel5': {
'required' :
[ 'mysql55', 'mysql55-libs', 'php53', 'php53-cli', 'php53-cgi', 'php53-gd', 'php-pear-Net-Curl',
'php53-common', 'php53-mysql', 'php53-xmlrpc', 'php53-pecl-memcache','php53-mysql',
'python26', 'python26-setuptools', 'python26-imaging', 'python26-mysqldb', 'python26-simplejson',
'elinks', 'httpd', 'apachetop', 'sendmail', 'exim', 's3cmd' ],
'optional' : [] },
# Ubuntu 10.04 has python2.6.5 by default
'ubuntu10': {
'required':
[ 'mysql-client', 'php5', 'php5-cli', 'php5-cgi', 'php5-gd',
'php5-common', 'php5-mysql', 'php5-xmlrpc', 'php5-memcache', 'php5-curl',
'python-setuptools' , 'python-imaging', 'python-mysqldb', 'python-simplejson',
'elinks', 'apache2', 'apachetop', 'apache2-utils', 'libapache2-mod-php5', 'sendmail', 'exim4',
's3cmd' ],
'optional': [],
'additional_commands':['a2enmod php5', 'a2enmod headers'] }
}
# Protected folders are those under .htaccess control, and are always relative to the document root
env.protected_folders = []
# Template paths will be interpolated. This is any folder that contains a
# template file, but in addition to the etc_path folder. SO DO NOT ADD
# etc_path to this list!
env.template_paths = []
# Should we minify?
env.run_minifier = True
env.minifier_cmd = 'python %(build_path)s/scripts/minifier/minifier.py -v -c %(build_path)s/scripts/minifier.conf --force'
#------------------------------------------------
# AWS related tasks require some configurations
#------------------------------------------------
# env.ec2Conn = EC2Connection(env.aws_access_key_id, env.aws_secret_access_key)
# env.asConn = AutoScaleConnection(env.aws_access_key_id, env.aws_secret_access_key)
# Production Alias is the alias/tag that all production instances use
env.production_alias = '911memorial_names_'
# Security groups that are given access to newly generated production AMIs
env.aws = { 'security_groups': ['ns11_multiplatform_prod'],
'key_pair': 'ns11_0630', 'as_group': 'n11asgroup',
'balancers': ['n11loadbalancer'], 'instance_type':'m1.large',
'availability_zones': ['us-east-1a', 'us-east-1b', 'us-east-1c', 'us-east-1d'] }
# 'min_size': 3, 'max_size': 12 }
#----- Decorator(s) -----
def common_config(func):
"""
Common are the environment variables that need to be evaluated after the others are loaded
due to dependencies. There has to be a way to inherist this stuff though!!!
"""
def wrapper():
if env.rcfile is None:
env.rcfile = 'rcfile.%s' % env.settings
# execute the caller to load that set of configurations
func()
get_remote_host_info()
# Now load all the configurations that were dependent on the caller's var-space
if env.get('deploy_to') is None:
env.deploy_to = '%(home_path)s/%(user)s' % env # The base path should always be the logged-in user
env.deploy_to = env.deploy_to % env
# System paths are under app_path, so provide the relative paths that we need
# for system services (like lighttpd, apache, etc.)
# Thes COULD be specific for each environment/stage, but it's best to have them common
env.system_paths = ['var/log', 'etc/%(webserver)s' % env, 'run', 'releases', 'shared']
# The scratch/work space for putting temporary stuff while we deploy from local dev
env.app_path = '%(deploy_to)s/%(application)s' % env
env.current_path = "%(app_path)s/current" % env
env.releases_path = "%(app_path)s/releases" % env
env.shared_path = "%(app_path)s/shared" % env
env.run_path = "%(app_path)s/run" % env
env.previous_path = "%(app_path)s/previous" % env
env.next_path = "%(app_path)s/next" % env
# Configuration Template Files and config files
env.etc_path = '%(app_path)s/etc' % env
env.log_path = env.log_path or '%(app_path)s/var/log' % env
env.local_etc_path = '%(build_path)s/etc' % env
# Todo: these might need to be moved to a common location
for conf in env.config_files:
conf['local_config_template'] = '%s/%s' % (env.local_etc_path, conf.get('templatename'))
conf['local_config_file'] = '%s/%s/%s' % (env.build_path, conf.get('path'), conf.get('filename'))
# Each webservec can have its own set of configurations
# Eg. apache, lighttpd, etc.
env.webserver_template = '%(local_etc_path)s/%(webserver)s/%(application)s.conf.sample' % env
env.webserver_file = '%(local_etc_path)s/%(webserver)s/%(application)s.conf' % env
# Some vars may need to be interpolated after we have all the data:
# TODO: Wonder if this can be done globally (ie on all vars)
for item in env:
if type(env[item]) == str:
# We may have to recursively keep interpolating
while '%(' in env[item]:
env[item] = env[item] % env
debug("Interpolated %s to %s" % (item, env[item]))
# for item in ['deploy_to', 'build_path', 'script_working_path', 'media_store_path', 'webserver_docroot', 'media_url',
# 'app_path', 'current_path', 'shared_path', 'run_path', 'log_path', 'local_etc_path', ]:
# # We may have to recursively keep interpolating
# while '%(' in env[item]:
# env[item] = env[item] % env
# print "Interpolated %s to %s" % (item, env[item])
# Template paths are the folders to be interpolated in addition to etc
env.template_paths = [x % env for x in env.template_paths]
return wrapper
def get_remote_host_info():
"""
Determine the remote host's OS, and based on this set environment options
"""
# We do this as sudo_as() since it's part of initial setup and the
# app user may not yet exist
uname = sudo_as('uname -a')
debug("Uname is %s" % uname)
# TODO; add support for getting the exact target version
# ver = run('more /etc/issue')
# .. then parse the resp and convert into ver
if re.search('el5|fc8', uname):
env.os_name = 'rhel5'
env.webuser = 'apache'
elif re.search('Ubuntu', uname):
env.os_name = 'ubuntu10'
env.webuser = 'www-data'
else:
raise Exception("Cannot proceed with platform %s. This platform is currently not supported" % uname)
debug("Remote host is %s" % env.os_name)
#----- /decorator(s) -----
#----- Utility Functions -----
def sudo_as(cmd, **kwargs):
"""
Perform sudo as a higher-rights user
"""
temp_user = env.user
env.user = env.sudo_as
debug("sudoing command %s as user %s" % (cmd, env.sudo_as))
resp = sudo(cmd, **kwargs)
env.user = temp_user
return resp
#----- /utility funcs -----
# Environments
# Each of these is a simple way of configuring the entire environment with
# any customizations.
@common_config
def live():
"""
Work on live environment
"""
if env.rcfile is None:
env.rcfile = 'rcfile.%s' % env.settings
debug("Using default rcfile since one was not provided with --config option:" % env.rcfile)
@common_config
def demo():
"""
Work on demo environment
"""
if env.rcfile is None:
env.rcfile = 'rcfile.%s' % env.settings
debug("Using default rcfile since one was not provided with --config option:" % env.rcfile)
@common_config
def dev():
"""
Work on dev environment
"""
if env.rcfile is None:
env.rcfile = 'rcfile.%s' % env.settings
debug("Using default rcfile since one was not provided with --config option:" % env.rcfile)
def clean_build():
"""
Force clean out the build_path folder prior to building
"""
env.clean_build = True
def dump_env():
"""
Test function for dumping all current environment variables
"""
for key in env.keys():
debug("%s => %s" % (key, env.get(key)))
#------------------------------------------------
# Create Configuration files from rcfile
#------------------------------------------------
def create_config_files():
"""
Create configuration files from templates, as defined in the env.config_files option
"""
if not os.path.exists(env.rcfile):
raise Exception("%(rcfile)s does not exist. See rcfile.sample and run fab --config=rcfile.name <commands>!" % env)
# Make sure that the code is the latest in the build_path
with lcd(env.build_path):
local('/usr/local/bin/git fetch && /usr/local/bin/git checkout %(branch)s' % env)
for item in env.config_files:
if not os.path.exists(item.get('local_config_template')):
raise Exception("Unable to find configuration template file (%s) to create config from" % item.get('local_config_template'))
infilename = item.get('local_config_template')
outfilename = item.get('local_config_file')
_interpolate_file(infilename, outfilename)
def _interpolate_file(infilename=None, outfilename=None):
infile = open(infilename, 'r')
outfile = open(outfilename, 'w')
for line in infile:
if line.strip().startswith('#'):
continue
else:
try:
outfile.write(line % env)
except:
print "Unable to write line %s" % line
raise
outfile.close()
infile.close()
def upload_config_files():
" Upload the interpolated config.yaml to the target servers"
for item in env.config_files:
put(item.get('local_config_file'), env.shared_path)
@roles('web')
def deploy_app_configurations():
"""
Interpolate templates into final config files and deploy to targets, but do NOT restart webserver
"""
create_config_files()
upload_config_files()
_upload_interpolated_files(_interpolate_templates())
# Load the cronfile for the current user
if files.exists('%(shared_path)s/etc/cron/cron_table' % env):
run('crontab %(shared_path)s/etc/cron/cron_table' % env)
@roles('web')
def deploy_configurations():
"""
Perform deploy_app_configurations, create the media-link for the web apps, and restart webserver
"""
deploy_app_configurations()
# Serve the media folder if necessary
# _create_media_link()
restart_webserver()
#---- /create-config-files ----------------------
#----- CONFIGURATION related tasks -----
def create_local_configs():
"""
Create all configuration files in the local environment. Useful only for development.
"""
print "Local configuration files will be generated in %(build_path)s" % env
create_config_files()
_interpolate_templates()
def _interpolate_templates():
"""
Translate template files to replace all python string-substitution points
Should never be called directly, since the purpose of this function is
to perform the first step of config-file deployment
"""
if not os.path.exists(env.rcfile):
raise Exception("%(rcfile)s does not exist. See rcfile.sample and run fab --config=rcfile.name <commands>!" % env)
interpolated_files = []
# Get a list of all template files in /etc/ that we need to interpolate
template_paths = []
template_paths.extend(env.template_paths)
template_paths.append(env.local_etc_path)
for template_path in template_paths:
for root, dirs, files in os.walk(template_path):
for name in files:
infilename = os.path.join(root, name)
if re.search('.tmpl$', infilename):
debug("Processing template file %s" % infilename)
outfilename = os.path.splitext(infilename)[0]
_interpolate_file(infilename, outfilename)
# infile = open(infilename, 'r')
# outfile = open(outfilename, 'w')
# try:
# outfile.write(infile.read() % env)
# except TypeError, e:
# if re.search("not enough arguments for format string", e[0]):
# # We can safely ignore this since it means that there's nothing to interpolate
# print e[0]
# print "Continuing by using the template file (%s) as the target (ie no interpolation)" % infilename
# # Remember that we have to go back to the top due to read() being at eof
# infile.seek(0)
# outfile.write(infile.read())
# else:
# raise
#
# outfile.close()
# infile.close()
interpolated_files.append(outfilename)
return interpolated_files
def _upload_interpolated_files(files):
"""
Uploads template files after interpolation
Expects to have interpolation run initially, so this function should
not be called directly.
"""
# Get a list of all template files in /etc/ that we need to interpolate
template_paths = []
template_paths.extend(env.template_paths)
template_paths.append(env.local_etc_path)
for filename in files:
env.temp = None
if not re.search('.tmpl$', filename):
# Get the relative path to the interpolated file. The template
# is either at etc/... or current/...
for tmplpath in template_paths:
if re.search(tmplpath, filename):
if re.match(env.local_etc_path, tmplpath):
env.temp = filename.split(env.local_etc_path)[1]
elif re.match(env.build_path, tmplpath):
# We want everything except the build_path
env.temp = filename.split(env.build_path)[1]
else:
raise Exception("Unknown root path - not etc or build_path. Please check your configs!")
# Now that we found something, we can move on
break
base = [x for x in env.temp.split('/') if x is not None and x != '']
temp_path = list(base) # redundant, but helps
# Template file are either in the etc/ or under the current/
if re.match(env.local_etc_path, filename):
temp_path.insert(0, env.etc_path)
elif re.match(env.build_path, filename):
temp_path.insert(0, env.current_path)
remote_path = os.path.join(*temp_path[:-1])
remote_file = os.path.join(*temp_path)
# debug("remote_path: %s; remote_file: %s" % (remote_path, remote_file))
run('mkdir -p %s' % remote_path)
put(filename, remote_file)
# sudo_as('chgrp %s %s' % (env.webserver_group, remote_file))
if re.match('/cron', env.temp) or re.match('/logrotate.d', env.temp):
# Set the cron script to be executable
run('chmod +x %s' % remote_file)
# # Symlink the cron job to the correct place
# temp_path = list(base)
# temp_path.insert(0, '/etc')
# abs_etc_path = os.path.join(*temp_path[:-1]) # assuming the last index is the file
# abs_etc_file = os.path.join(*temp_path)
# # We want the target path to exist, but not the target file
# sudo_as('if [ -d %s ];then if [ ! -e %s ];then sudo ln -snf %s %s; else echo "Target file already exists! Will not overwrite"; fi; else echo "Target path is incorrect"; fi' % (abs_etc_path, abs_etc_file, remote_file, abs_etc_file))
def _create_media_link():
"""
Create symlink for local media path as a URL in web docroot
"""
if env.get('serve_media') and env.serve_media == "True":
src = env.media_store_path % env
if re.match('s3://', env.media_store_path):
raise Exception("Cannot create a media link to an S3 resource. Terminating due to risk of other misconfigurations.")
link = "%(webserver_docroot)s/_store" % env
with settings(warn_only=True):
if files.exists(src):
run('ln -s %s %s' % (src, link))
else:
print "This environment does not need media to be served"
#----- /configuration related tasks -----
"""
Branches
"""
def stable():
"""
Work on stable branch.
"""
branch('stable')
def master():
"""
Work on development branch.
"""
branch('master')
def branch(branch_name):
"""
Work on any specified branch.
"""
env.branch = branch_name
print "Branch has been manually overridden to %(branch)s" % env
"""
SETUP AND INITIALIZATION TASKS
"""
@roles('web')
def setup_system():
"""
Set up a new system with all pre-requisites and a target user context
"""
# Install pre-requisites with package manager
install_requirements()
# Create user and target paths as necessary
create_app_context()
# Validate that requisites are in place
check_system()
@roles('web')
def setup_application():
"""
Set up the application path and all the application specific things
"""
require('branch')
setup_directories()
deploy_configurations()
def setup_directories():
"""
Create directories necessary for deployment.
"""
# First set up the system paths for the server/services
run('mkdir -p %(app_path)s' % env)
#----
# Server paths (for web/other servers)
#----
for path in env.system_paths:
env.temp_var = path
run('mkdir -p %(app_path)s/%(temp_var)s' % env)
# Change ownership of paths.
sudo_as('chgrp -R %(webserver_group)s %(app_path)s/%(temp_var)s; chmod -R g+w %(app_path)s/%(temp_var)s;' % env)
"""
SCM and Code related functions
"""
def bundle_code():
"""
Pull the latest code from the SCM and bundle for deployment
"""
# env.release = time.strftime('%Y%m%d%H%M%S')
# We want the parent paths to the temporary location ...
if not env.get('build_path'):
raise "Need a build_path to build code to"
if not os.path.exists(env.build_path):
os.makedirs(env.build_path)
# And if the code path already exists, let's update it with the latest code
# Unless we've been asked to do a clean-build in which case remove all old content
if env.clean_build:
local('rm -rf %(build_path)s' % env)
if env.scm == 'git':
if not os.path.exists(env.build_path):
"Create an archive from the current Git master branch and upload it"
local('/usr/local/bin/git clone --depth 0 %(repository)s %(build_path)s' % env)
else:
with lcd(env.build_path):
local('if [ $(/usr/local/bin/git config --get remote.origin.url) != "%(repository)s" ];then echo "Existing repository is not the one requested. Deleting build path."; rm -rf %(build_path)s; fi' % env)
try:
with lcd(env.build_path):
local('/usr/local/bin/git clean -d -x -f' % env)
except:
local('rm -rf %(build_path)s' % env)
"Create an archive from the current Git master branch and upload it"
local('/usr/local/bin/git clone --depth 0 %(repository)s %(build_path)s' % env)
with lcd(env.build_path):
local('/usr/local/bin/git clean -d -x -f && /usr/local/bin/git fetch && /usr/local/bin/git checkout %(branch)s' % env)
local('/usr/local/bin/git submodule init && /usr/local/bin/git submodule update' % env)
env.release = local('/usr/local/bin/git rev-parse %(branch)s | cut -c 1-9' % env, capture=True)
# Save the revision information to a file for post-deployment info
local('/usr/local/bin/git rev-parse %(branch)s > REVISION.txt' % env)
# To be safe, remove any newline characters
env.release = re.sub('[\r\n]', '', env.release)
# Archive the bundle for upload
local('/usr/local/bin/git archive --format=tar %(branch)s > %(build_path)s/%(release)s.tar' % env)
if env.run_minifier == True and env.get('minifier_cmd') is not None:
with settings(warn_only=True):
# If minification fails, we only want to warn about it, not crash
local(env.minifier_cmd) # 'python %(build_path)s/scripts/minifier/minifier.py -v -c %(build_path)s/scripts/minifier.conf --force' % env)
local('/usr/local/bin/git status -s | grep -i "^ M" | tr -s " " | cut -d\ -f 3 | xargs tar -v --append --file=%(release)s.tar ' % env)
# Add any updated files
local('tar --append --file=%(release)s.tar REVISION.txt' % env)
elif env.scm == "git-svn":
# Get repo information and store it to REVISION.txt
local('cd %(repository)s && /usr/local/bin/git svn info > %(build_path)s/REVISION.txt' % env)
local('cd %(build_path)s && tar --append --file=%(release)s.tar REVISION.txt' % env)
elif env.scm == 'svn':
local('svn export %(repository)s/%(branch)s %(build_path)s' % env)
local('svn info %(repository)s/%(branch)s > %(build_path)s/REVISION.txt' % env)
local('cd %(build_path)s && tar -cf %(release)s.tar .' % env)
else:
raise "Unknown SCM type %s" % env.scm
# Keep in mind that the .gitattributes file might ignore certain files from the archive
local('cd %(build_path)s && gzip %(release)s.tar' % env)
print "Bundled code is at %(build_path)s/%(release)s.tar.gz" % env
print "----- ATTENTION -----"
print "If you plan to run the deployer at a later time, execute this first .."
print " echo 'release = %(release)s' >> %(rcfile)s" % env
print "----- ATTENTION -----"
# Don't delete the local copy in case we need to debug - that will be done on the next cycle
def upload_and_explode_code_bundle():
"""
Upload the local tarball of latest code to the target host
"""
put('%(build_path)s/%(release)s.tar.gz' % env, '%(releases_path)s' % env)
with settings(warn_only=True):
result = run('cd %(releases_path)s/ && mkdir -p %(release)s && tar -xf %(release)s.tar.gz -C %(release)s' % env)
if result.failed:
print "Ignoring as though everything is good"
run('cd %(releases_path)s/ && if [ -e %(release)s.tar.gz ];then rm -rf %(release)s.tar.gz; fi' % env)
sudo_as('chgrp -R %(webuser)s %(releases_path)s/%(release)s' % env)
def symlink_current_release():
"""
Symlink the current release, and also symlink all shared configuration files (from env.config_files)
"""
require('release', provided_by=[deploy_webapp, setup_application])
# if exists('%(app_path)s/previous' % env):
run('if [ -e %(app_path)s/previous ];then rm %(app_path)s/previous; fi; if [ -e %(app_path)s/current ];then mv %(app_path)s/current %(app_path)s/previous; fi' % env)
# Link the shared config file into the current configuration
for item in env.config_files:
run('rm -f %s/%s/%s/%s' % (env.releases_path, env.release, item.get('path'), item.get('filename')))
run('ln -nsf %s %s' % (os.path.join(env.etc_path, item.get('filename')), os.path.join(env.releases_path, env.release, item.get('path'), item.get('filename'))))
run('ln -s %(releases_path)s/%(release)s %(app_path)s/current' % env)
def install_requirements():
"""
Figure out what the target platform is to set the package manager
"""
if env.os_name == 'rhel5':
install_rhel5_packages()
elif env.os_name == 'ubuntu10':
install_ubuntu10_packages()
else:
raise "Unable to proceed - don't know os_name = %s" % env.os_name
# Set up python's default encoding as utf-8
sudo_as('echo -e "import sys\nsys.setdefaultencoding(\'utf-8\')\n" > /usr/lib/python2.6/site-packages/sitecustomize.py')
def install_rhel5_packages():
"""
Install the required packages using yum.
We can assume that security and other "core" system updates have already been applied. So no need to run "yum update"
"""
# First install the IUS Community repo to get RHEL5 up to date with the newer requirements
sudo_as('cd /tmp && rm -f *.rpm*') # initial cleanup
print "Checking if wget is installed. Ignore any errors here ..."
with settings(warn_only=True):
result = sudo_as('wget')
if result.failed:
sudo_as("yum -y install wget")
sudo_as('cd /tmp && wget http://dl.iuscommunity.org/pub/ius/stable/Redhat/5/x86_64/ius-release-1.0-8.ius.el5.noarch.rpm')
sudo_as('cd /tmp && wget http://dl.iuscommunity.org/pub/ius/archive/Redhat/5/x86_64/epel-release-1-1.ius.el5.noarch.rpm')
with settings(warn_only=True):
sudo_as('cd /tmp && rpm -Uhv epel-release-1-1.ius.el5.noarch.rpm && rpm -Uhv ius-release-1.0-8.ius.el5.noarch.rpm')
if result.failed:
print "IUS community may already have been installed. Ignoring and continuing"
# Now that IUS Community in installed let's clean things up and continue
sudo_as('yum clean all && yum -y upgrade')
# Remove existing mysql if it exists, so that there's no conflict
sudo_as('yum -y remove mysql mysql-libs')
# Python dependencies - these don't seem to want to work without a good repo :(
if len(env.packages.get('rhel5').get('required')) > 0:
try:
sudo_as('yum -y --skip-broken install %s' % ' '.join(env.packages.get('rhel5').get('required')))
except Exception, e:
print "Required packages installation process failed. Cannot proceed!"
raise
# Python dependencies - these don't seem to want to work without a good repo :(
if len(env.packages.get('rhel5').get('optional')) > 0:
try:
sudo_as('yum -y --skip-broken install %s' % ' '.join(env.packages.get('rhel5').get('optional')))
except Exception, e:
print "Optional packages installation process failed. But proceeding nevertheless. Assuming it'll be fixed manually!"
print "Error was %s", e
# Disable firewall on RHEL5 since EC2 configuration should be managed by security groups
print "Please ensure that you've disabled SELinux and any Firewalls via system-config-securitylevel-tui"
def install_ubuntu10_packages():
"""
Install the defined packages on Ubuntu
"""
sudo_as('aptitude clean && aptitude update')
try:
sudo_as('aptitude -y install %s' % ' '.join(env.packages.get('ubuntu10').get('required')))
except Exception, e:
print "Required packages installation process failed. Cannot proceed!"
raise
try:
sudo_as('aptitude -y install %s' % ' '.join(env.packages.get('ubuntu10').get('optional')))
except Exception, e:
print "Optional packages installation process failed. But proceeding nevertheless. Assuming it'll be fixed manually!"
print "Error was %s", e
for action in env.packages.get('ubuntu10').get('additional_commands'):
try:
sudo_as(action)
except Exception, e:
print "Additional_command %s failed. Continuing anyway!" % action
print "Error was %s", e
continue
def create_app_context():
"""
Create the user context under which the application will run
"""
env.tmpuser = env.user
try:
sudo_as('useradd -d %(home_path)s/%(tmpuser)s -m %(tmpuser)s' % env)
except SystemExit:
print "WARNING: Failed to create app context since it probably already exists. Continuing."
try:
sudo_as('mkdir %(home_path)s/%(tmpuser)s/.ssh/ && cp ~/.ssh/authorized_keys %(home_path)s/%(tmpuser)s/.ssh/ && chown -R %(tmpuser)s %(home_path)s/%(tmpuser)s/.ssh' % env)
sudo_as('chmod 755 %(home_path)s/%(tmpuser)s && chgrp -R %(webuser)s %(home_path)s/%(tmpuser)s' % env)
except:
print "WARNING: something failed in copying SSH keys into user %(tmpuser)s context" % env
return True
def check_system():
"""
Ensure that the prerequisites and system locations exist
"""
sudo_as('php --version | grep -i "php 5"')
sudo_as('python2.6 --version')
# sudo_as('mysql --version | grep -i "distrib 5"')
if env.os_name == 'rhel5':
sudo_as('apachectl -v | grep -i "apache\/2"')
elif env.os_name == 'ubuntu10':
sudo_as('apache2ctl -v | grep -i "apache\/2"')
"""
Deployment Related Tasks
"""
def deploy_assets_to_s3():
"""
Deploy the latest assets and JS to S3 bucket
"""
# run('s3cmd del --recursive s3://%(s3_bucket)s/%(application)s/%(admin_media_prefix)s/' % env)
# run('s3cmd -P --guess-mime-type sync %(venv_path)s/src/django/django/contrib/admin/media/ s3://%(s3_bucket)s/%(application)s/%(site_media_prefix)s/' % env)
# run('s3cmd del --recursive s3://%(s3_bucket)s/%(application)s/%(newsapps_media_prefix)s/' % env)
# run('s3cmd -P --guess-mime-type sync %(venv_path)s/src/newsapps/newsapps/na_media/ s3://%(s3_bucket)s/%(application)s/%(newsapps_media_prefix)s/' % env)
pass
@roles('web')
def deploy_webapp():
"""
Deploy the latest version of the site to the server and restart Apache2.
"""
# require('settings', provided_by=[production, staging])
# require('branch', provided_by=[stable, master, branch])
# with settings(warn_only=True):
# maintenance_up()
upload_and_explode_code_bundle()
# Apply requirements.txt, if it exists
# _install_pip_requirements()
# Restart the web server with the latest code
stop_webserver()
symlink_current_release()
# maintenance_down()
start_webserver()
def _install_pip_requirements():
'''
If there is a pip requirement file, apply it under a root context
TODO: This should be moved to a virtualenv context
'''
if files.exists('%(releases_path)s/%(release)s/requirements.txt' % env):
sudo_as('pip install -r %(releases_path)s/%(release)s/requirements.txt' % env)
def _db_migrations():
'''
Apply database migrations as necessary
'''
pass
@roles('web')
def deploy_webapp_and_configs():
"""
Convenience: deploy_webapp and deploy_configurations rolled into one
"""
deploy_webapp()
deploy_configurations()
@roles('web')
def deploy_app():
"""
Deploy the latest application bundle, and symlink current. But do NOT restart the web server
NOTE: The reason this is not called by deploy_webapp is due to lack of decorator
inheritance is Fabric, so roles will not pass down
"""
upload_and_explode_code_bundle()
symlink_current_release()
@roles('web')
def deploy_app_and_configs():
"""
Convenience: deploy_app and deploy_app_configurations rolled into one
"""
deploy_app()
deploy_app_configurations()
"""
WebServer related tasks
"""
def stop_webserver():
""" Stop the webserver.
Note that some web servers allow for command-line definition of the configuration file to use. Others don't
"""
_webserver_do('stop')
def start_webserver():
""" Start the webserver """
_webserver_do('start')
def restart_webserver():
"""Restart the webserver """
_webserver_do('restart')
def _webserver_do(action=''):
"""
Helper function to perform an action on the webserver (start, stop, restart, etc).
"""
params = {}
params['webserver'] = env.webserver
params['action'] = action
params['app_path'] = env.app_path
with settings(warn_only=True):
# If we get an exception here it's probably not catastrophic
if env.webserver == 'lighttpd':
# Keep in mind that this has to be configured for the new lighttpd init script
sudo_as('/etc/init.d/%(webserver)s %(action)s %(app_path)s' % params, shell=False, pty=False)
elif env.webserver == 'apache':
if env.os_name == 'rhel5':
sudo_as('/usr/sbin/apachectl %(action)s' % params)
elif env.os_name == 'ubuntu10':
sudo_as('/usr/sbin/apache2ctl %(action)s' % params)
elif env.webserver == 'nginx':
sudo_as('/etc/init.d/%(webserver)s %(action)s' % params, shell=False, pty=False)
params['application'] = env.application
sudo_as('supervisorctl %(action)s %(application)s' % params, shell=False, pty=False)
def secure_website():
"""
Apply htaccess (basicAuth) to folders defined by env.protected_folders
"""
if env.webserver == 'apache':
# Symlink the .htaccess file to the webserver root
run ('if [ ! -e %(etc_path)s/htpasswd ];then touch %(etc_path)s/htpasswd; fi && htpasswd -b %(etc_path)s/htpasswd %(webserver_auth_user)s %(webserver_auth_password)s ' % env)
for path in env.get('protected_folders'):
env.temp = path
run('ln -snf %(etc_path)s/%(webserver)s/htaccess %(webserver_docroot)s/%(temp)s/.htaccess' % env)
else:
raise "Cannot set security parameters for webserver %(webserver)s yet. Please contact the developer." % env
"""
Rollback deployed code tasks
"""
def rollback(commit_id=None):
"""
Rolls back to specified git/svn commit hash or tag or timestamp.
Deployments should be to timestamp-commitTag.
If commit_id is not provided, move current to next and previous to current
Obviously there is NO guarantee we have deployed this commit-hash!
"""
if commit_id is not None:
raise Exception('Rolling back to a specific commit-id is not yet supported')
run('if [ [ -e %(previous_path)s ] && [ -e %(current_path)s ] ];then mv %(current_path)s %(next_path)s && mv %(previous_path)s %(current_path)s; fi' % env)
stop_webserver()
start_webserver()
"""
Database Related Tasks
"""
def setup_db_backup():
""" """
pass
def create_mycnf():
"""
Create the remote ~/.my.cnf file with appropriate content and permissions
"""
# First escape any weird characters in the password
env.temp = re.sub('\$', '\\$', env.database_password)
# env.temp = env.database_password
run('echo "[client]\nuser=%(database_user)s\npassword=%(temp)s\n" > $HOME/.my.cnf && chmod 600 $HOME/.my.cnf' % env)
"""
Miscellaneaus Tasks
"""
def echo_host():
"""
Echo the current host to the command line.
"""
run('echo %(settings)s; echo %(hosts)s' % env)
@roles('web')
def test():
"""
Test and dump the rcfile
"""
print "Testing the rcfile"
for item in env.keys():
print "%s => %s" % (item, env.get(item))
@roles('web')
def disable_cron():
"""
Remove all cron tasks from the current user's context
"""
run('crontab -r')