skippy-ec2onrails 0.9.10 → 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/CHANGELOG +21 -0
  2. data/Manifest +7 -1
  3. data/README.textile +10 -13
  4. data/Rakefile +4 -3
  5. data/TODO +10 -8
  6. data/ec2onrails.gemspec +17 -15
  7. data/examples/deploy.rb +7 -1
  8. data/lib/ec2onrails/recipes.rb +11 -714
  9. data/lib/ec2onrails/recipes/db.rb +377 -0
  10. data/lib/ec2onrails/recipes/deploy.rb +30 -0
  11. data/lib/ec2onrails/recipes/server.rb +489 -0
  12. data/lib/ec2onrails/version.rb +1 -1
  13. data/server/files/etc/apache2/sites-available/app.common +6 -1
  14. data/server/files/etc/cron.d/{backup_app_db_to_s3 → ec2onrails} +8 -0
  15. data/server/files/etc/cron.daily/app +17 -2
  16. data/server/files/etc/cron.hourly/app +16 -2
  17. data/server/files/etc/cron.monthly/app +16 -2
  18. data/server/files/etc/cron.weekly/app +16 -2
  19. data/server/files/etc/ec2onrails/README +1 -1
  20. data/server/files/etc/god/app.god +7 -2
  21. data/server/files/etc/god/dkim_filter.god +20 -0
  22. data/server/files/etc/god/system.god +1 -1
  23. data/server/files/etc/god/web.god +6 -4
  24. data/server/files/etc/mysql/my.cnf +3 -0
  25. data/server/files/etc/nginx/nginx.conf +11 -2
  26. data/server/files/etc/rcS.d/S91ec2-first-startup +36 -1
  27. data/server/files/etc/rcS.d/S92ec2-every-startup +29 -1
  28. data/server/files/etc/rcS.d/S99set_roles +3 -1
  29. data/server/files/etc/sudoers +26 -1
  30. data/server/files/usr/bin/god +0 -0
  31. data/server/files/usr/local/ec2onrails/bin/backup_app_db.rb +3 -2
  32. data/server/files/usr/local/ec2onrails/bin/backup_dir.rb +89 -0
  33. data/server/files/usr/local/ec2onrails/bin/exec_runner +9 -6
  34. data/server/files/usr/local/ec2onrails/bin/init_services.rb +7 -0
  35. data/server/files/usr/local/ec2onrails/bin/rails_env +1 -2
  36. data/server/files/usr/local/ec2onrails/bin/setup_web_proxy.rb +32 -28
  37. data/server/files/usr/local/ec2onrails/bin/update_hostname +40 -0
  38. data/server/files/usr/local/ec2onrails/lib/mysql_helper.rb +1 -1
  39. data/server/files/usr/local/ec2onrails/lib/s3_helper.rb +22 -0
  40. data/server/files/usr/local/ec2onrails/startup-scripts/every-startup/get-hostname.sh +1 -3
  41. data/server/rakefile.rb +12 -5
  42. data/test/test_app/config/deploy.rb +1 -1
  43. metadata +16 -12
@@ -0,0 +1,377 @@
1
+ Capistrano::Configuration.instance.load do
2
+ cfg = ec2onrails_config
3
+
4
+ namespace :ec2onrails do
5
+ desc <<-DESC
6
+ Deploy and restore database from S3
7
+ DESC
8
+ task :restore_db_and_deploy do
9
+ db.recreate
10
+ deploy.update_code
11
+ deploy.symlink
12
+ db.restore
13
+ deploy.migrations
14
+ end
15
+
16
+ namespace :db do
17
+ desc <<-DESC
18
+ [internal] Load configuration info for the database from
19
+ config/database.yml, and start mysql (it must be running
20
+ in order to interact with it).
21
+ DESC
22
+ task :load_config do
23
+ unless hostnames_for_role(:db, :primary => true).empty?
24
+ db_config = YAML::load(ERB.new(File.read("config/database.yml")).result)[rails_env.to_s] || {}
25
+ cfg[:db_name] ||= db_config['database']
26
+ cfg[:db_user] ||= db_config['username'] || db_config['user']
27
+ cfg[:db_password] ||= db_config['password']
28
+ cfg[:db_host] ||= db_config['host']
29
+ cfg[:db_port] ||= db_config['port']
30
+ cfg[:db_socket] ||= db_config['socket']
31
+
32
+ if (cfg[:db_host].nil? || cfg[:db_host].empty?) && (cfg[:db_socket].nil? || cfg[:db_socket].empty?)
33
+ raise "ERROR: missing database config. Make sure database.yml contains a '#{rails_env}' section with either 'host: hostname' or 'socket: /var/run/mysqld/mysqld.sock'."
34
+ end
35
+
36
+ [cfg[:db_name], cfg[:db_user], cfg[:db_password]].each do |s|
37
+ if s.nil? || s.empty?
38
+ raise "ERROR: missing database config. Make sure database.yml contains a '#{rails_env}' section with a database name, user, and password."
39
+ elsif s.match(/['"]/)
40
+ raise "ERROR: database config string '#{s}' contains quotes."
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ desc <<-DESC
47
+ Create the MySQL database. Assumes there is no MySQL root \
48
+ password. To create a MySQL root password create a task that's run \
49
+ after this task using an after hook.
50
+ DESC
51
+ task :create, :roles => :db do
52
+ on_rollback { drop }
53
+ load_config
54
+ start
55
+ sleep(5) #make sure the db has some time to start up!
56
+
57
+ #NOTE: if these commands fail, it is most likely because the command has already been run....
58
+
59
+ cmds = []
60
+ #sometimes there is a test database that comes with the default installation of MySQL...get rid of it!
61
+
62
+ cmds << %{mysql -u root -e "drop database if exists test; flush privileges;"}
63
+ # removing anonymous mysql accounts
64
+ cmds << %{mysql -u root -D mysql -e "delete from db where User = ''; flush privileges;"}
65
+ cmds << %{mysql -u root -D mysql -e "delete from user where User = ''; flush privileges;"}
66
+
67
+ # qoting of database names allows special characters eg (the-database-name)
68
+ # the quotes need to be double escaped. Once for capistrano and once for the host shell
69
+ cmds << %{mysql -u root -e "create database if not exists \\`#{cfg[:db_name]}\\`;"}
70
+ cmds << %{mysql -u root -e "grant all on \\`#{cfg[:db_name]}\\`.* to '#{cfg[:db_user]}'@'%' identified by '#{cfg[:db_password]}';"}
71
+ cmds << %{mysql -u root -e "grant reload on *.* to '#{cfg[:db_user]}'@'%' identified by '#{cfg[:db_password]}';"}
72
+ cmds << %{mysql -u root -e "grant super on *.* to '#{cfg[:db_user]}'@'%' identified by '#{cfg[:db_password]}';"}
73
+
74
+ cmds.each do |cmd|
75
+ begin
76
+ run cmd
77
+ rescue Exception => e
78
+ puts " CONTINUING: this command was previously run, so continuing"
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ desc <<-DESC
85
+ Move the MySQL database to Amazon's Elastic Block Store (EBS), \
86
+ which is a persistant data store for the cloud.
87
+ OPTIONAL PARAMETERS:
88
+ * SIZE: Pass in a number representing the GB's to hold, like 10. \
89
+ It will default to 10 gigs.
90
+ * VOLUME_ID: The volume_id to use for the mysql database
91
+ NOTE: keep track of the volume ID, as you'll want to keep this for your \
92
+ records and probably add it to the :db role in your deploy.rb file \
93
+ (see the ec2onrails sample deploy.rb file for additional information)
94
+ DESC
95
+ task :enable_ebs, :roles => :db, :only => { :primary => true } do
96
+ # based off of Eric's work:
97
+ # http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1663&categoryID=100
98
+ #
99
+ # EXPLAINATION:
100
+ # There is a lot going on here! At the end, the setup should be:
101
+ # * create EBS volume if run outside of the ec2onrails:setup and
102
+ # VOLUME_ID is not passed in when the cap task is called
103
+ # * EBS volume attached to /dev/sdh
104
+ # * format to xfs if new or do a xfs_check if previously existed
105
+ # * mounted on /var/local and update /etc/fstab
106
+ # * move /mnt/mysql_data -> /var/local/mysql_data
107
+ # * move /mnt/log/mysql -> /var/local/log/mysql
108
+ # * change mysql configs by writing /etc/mysql/conf.d/mysql-ec2-ebs.cnf
109
+ # * keep a copy of the mysql configs with the EBS volume, and if that volume is hooked into
110
+ # another instance, make sure the mysql configs that go with that volume are symlinked to /etc/mysql
111
+ # * update the file locations of the mysql binary logs in /mnt/log/mysql/mysql-bin.index
112
+ # * symlink the moved folders to their old position... makes the move to EBS transparent
113
+ # * Amazon doesn't contain EBS information in the meta-data API (yet). So write
114
+ # /etc/ec2onrails/ebs_info.yml
115
+ # to contain the meta-data information that we need
116
+ #
117
+ # DESIGN CONSIDERATIONS
118
+ # * only moving mysql data to EBS. seems the most obvious, and if we move over other components
119
+ # we will have to share that bandwidth (1 Gbps pipe to SAN). So limiting to what we really need
120
+ # * not moving all mysql logic over (tmp scratch space stays local). Again, this is to limit
121
+ # unnecessary bandwidth usage, PLUS, we are charged per million IO to EBS
122
+ #
123
+ # TODO:
124
+ # * make sure if we have a predefined ebs_vol_id, that we error out with a nice msg IF the zones do not match
125
+ # * can we move more of the mysql cache files back to the local disk and off of EBS, like the innodb table caches?
126
+ # * right now we force this task to only be run on one server; that works for db :primary => true
127
+ # But what is the best way to make this work if it needs to setup multiple servers (like db slaves)?
128
+ # I need to figure out how to do a direct mapping from a server definition to a ebs_vol_id
129
+ # * when we enable slaves and we setup ebs volumes on them, make it transparent to the user.
130
+ # have the slave create a snapshot of the db.master volume, and then use that to mount from
131
+ # * need to do a rollback that if the volume is created but something fails, lets uncreate it?
132
+ # carefull though! If it fails towards the end when information is copied over, it could cause information
133
+ # to be lost!
134
+ #
135
+
136
+ mysql_dir_root = '/var/local'
137
+ block_mnt = '/dev/sdh'
138
+ servers = find_servers_for_task(current_task)
139
+
140
+ if servers.empty?
141
+ raise Capistrano::NoMatchingServersError, "`#{task.fully_qualified_name}' is only run for servers matching #{task.options.inspect}, but no servers matched"
142
+ elsif servers.size > 1
143
+ raise Capistrano::Error, "`#{task.fully_qualified_name}' is can only be run on one server, not #{server.size}"
144
+ end
145
+
146
+ vol_id = ENV['VOLUME_ID'] || servers.first.options[:ebs_vol_id]
147
+
148
+ #HACK! capistrano doesn't allow arguments to be passed in if we call this task as a method, like 'db.enable_ebs'
149
+ # the places where we do call it like that, we don't want to force a move to ebs, so....
150
+ # if the call frame is > 1 (ie, another task called it), do NOT force the ebs move
151
+ no_force = task_call_frames.size > 1
152
+ prev_created = !(vol_id.nil? || vol_id.empty?)
153
+ #no vol_id was passed in, but perhaps it is already mounted...?
154
+ prev_created = true if !quiet_capture("mount | grep -inr '#{mysql_dir_root}' || echo ''").empty?
155
+
156
+ unless no_force && (vol_id.nil? || vol_id.empty?)
157
+ zone = quiet_capture("/usr/local/ec2onrails/bin/ec2_meta_data.rb -key 'placement/availability-zone'")
158
+ instance_id = quiet_capture("/usr/local/ec2onrails/bin/ec2_meta_data.rb -key 'instance-id'")
159
+
160
+ unless prev_created
161
+ puts "creating new ebs volume...."
162
+ size = ENV["SIZE"] || "10"
163
+ cmd = "ec2-create-volume -s #{size} -z #{zone} 2>&1"
164
+ puts "running: #{cmd}"
165
+ output = `#{cmd}`
166
+ puts output
167
+ vol_id = (output =~ /^VOLUME\t(.+?)\t/ && $1)
168
+ puts "NOTE: remember that vol_id"
169
+ sleep(2)
170
+ end
171
+ vol_id.strip! if vol_id
172
+ if quiet_capture("mount | grep -inr '#{block_mnt}' || echo ''").empty?
173
+ cmd = "ec2-attach-volume -d #{block_mnt} -i #{instance_id} #{vol_id} 2>&1"
174
+ puts "running: #{cmd}"
175
+ output = `#{cmd}`
176
+ puts output
177
+ if output =~ /Client.InvalidVolume.ZoneMismatch/i
178
+ raise Exception, "The volume you are trying to attach does not reside in the zone of your instance. Stopping!"
179
+ end
180
+ while !system( "ec2-describe-volumes | grep #{vol_id} | grep attached" )
181
+ puts "Waiting for #{vol_id} to be attached..."
182
+ sleep 1
183
+ end
184
+ end
185
+
186
+ ec2onrails.server.allow_sudo do
187
+ # try to format the volume... if it is already formatted, lets run a check on
188
+ # it to make sure it is ok, and then continue on
189
+ # if errors, the device is busy...something else is going on here and it is already mounted... skip!
190
+ if prev_created
191
+ # Stop the db (mysql server) for cases where this is being run after the original run
192
+ # If EBS partiion is already mounted and being used by mysql, it will fail when umount is run
193
+ god_status = quiet_capture("sudo god status")
194
+ god_status = god_status.empty? ? {} : YAML::load(god_status)
195
+ start_stop_db = false
196
+ start_stop_db = god_status['db']['mysql'] == 'up'
197
+ if start_stop_db
198
+ stop
199
+ puts "Waiting for mysql to stop"
200
+ sleep(10)
201
+ end
202
+ quiet_capture("sudo umount #{mysql_dir_root}") #unmount if need to
203
+ puts "Checking if the filesystem needs to be created (if you created #{vol_id} yourself)"
204
+ existing = quiet_capture( "mkfs.xfs /dev/sdh", :via => 'sudo' ).match( /existing filesystem/ )
205
+ sudo "xfs_check #{block_mnt}"
206
+ # Restart the db if it
207
+ start if start_stop_db && existing
208
+ else
209
+ sudo "mkfs.xfs #{block_mnt}"
210
+ end
211
+
212
+ # if not added to /etc/fstab, lets do so
213
+ sudo "sh -c \"grep -iqn '#{mysql_dir_root}' /etc/fstab || echo '#{block_mnt} #{mysql_dir_root} xfs noatime 0 0' >> /etc/fstab\""
214
+ sudo "mkdir -p #{mysql_dir_root}"
215
+ #if not already mounted, lets mount it
216
+ sudo "sh -c \"mount | grep -iqn '#{mysql_dir_root}' || mount '#{mysql_dir_root}'\""
217
+
218
+ #ok, now lets move the mysql stuff off of /mnt -> mysql_dir_root
219
+ stop rescue nil #already stopped
220
+ sudo "mkdir -p #{mysql_dir_root}/log"
221
+ #move the data over, but keep a symlink to the new location for backwards compatibility
222
+ #and do not do it if /mnt/mysql_data has already been moved
223
+ quiet_capture("sudo sh -c 'test ! -d #{mysql_dir_root}/mysql_data && mv /mnt/mysql_data #{mysql_dir_root}/'")
224
+ sudo "mv /mnt/mysql_data /mnt/mysql_data_old 2>/dev/null || echo"
225
+ sudo "ln -fs #{mysql_dir_root}/mysql_data /mnt/mysql_data"
226
+
227
+ #but keep the tmpdir on mnt
228
+ sudo "sh -c 'mkdir -p /mnt/tmp/mysql && chown mysql:mysql /mnt/tmp/mysql'"
229
+ #move the logs over, but keep a symlink to the new location for backwards compatibility
230
+ #and do not do it if the logs have already been moved
231
+ quiet_capture("sudo sh -c 'test ! -d #{mysql_dir_root}/log/mysql_data && mv /mnt/log/mysql #{mysql_dir_root}/log/'")
232
+ sudo "ln -fs #{mysql_dir_root}/log/mysql /mnt/log/mysql"
233
+ quiet_capture("sudo sh -c \"test -f #{mysql_dir_root}/log/mysql/mysql-bin.index && \
234
+ perl -pi -e 's%/mnt/log/%#{mysql_dir_root}/log/%' #{mysql_dir_root}/log/mysql/mysql-bin.index\"") rescue false
235
+
236
+ if quiet_capture("test -d /var/local/etc/mysql && echo 'yes'").empty?
237
+ txt = <<-FILE
238
+ [mysqld]
239
+ datadir = #{mysql_dir_root}/mysql_data
240
+ tmpdir = /mnt/tmp/mysql
241
+ log_bin = #{mysql_dir_root}/log/mysql/mysql-bin.log
242
+ log_slow_queries = #{mysql_dir_root}/log/mysql/mysql-slow.log
243
+ FILE
244
+ put txt, '/tmp/mysql-ec2-ebs.cnf'
245
+ sudo 'mv /tmp/mysql-ec2-ebs.cnf /etc/mysql/conf.d/mysql-ec2-ebs.cnf'
246
+
247
+ #keep a copy
248
+ sudo "rsync -aR /etc/mysql #{mysql_dir_root}/"
249
+ end
250
+ # lets use the mysql configs on the EBS volume
251
+ sudo "mv /etc/mysql /etc/mysql.orig 2>/dev/null"
252
+ sudo "ln -sf #{mysql_dir_root}/etc/mysql /etc/mysql"
253
+
254
+ #just put a README on the drive so we know what this volume is for!
255
+ txt = <<-FILE
256
+ This volume is setup to be used by Ec2onRails in conjunction with Amazon's EBS, for primary MySql database persistence.
257
+ RAILS_ENV: #{fetch(:rails_env, 'undefined')}
258
+ DOMAIN: #{fetch(:domain, 'undefined')}
259
+
260
+ Modify this volume at your own risk
261
+ FILE
262
+
263
+ put txt, "/tmp/VOLUME-README"
264
+ sudo "mv /tmp/VOLUME-README #{mysql_dir_root}/VOLUME-README"
265
+ sudo "touch /etc/ec2onrails/ebs_info.yml"
266
+ ebs_info = quiet_capture("cat /etc/ec2onrails/ebs_info.yml")
267
+
268
+ ebs_info = ebs_info.empty? ? {} : YAML::load(ebs_info)
269
+ ebs_info[mysql_dir_root] = {'block_loc' => block_mnt, 'volume_id' => vol_id}
270
+ put(ebs_info.to_yaml, "/tmp/ebs_info.yml")
271
+ sudo "mv /tmp/ebs_info.yml /etc/ec2onrails/ebs_info.yml"
272
+ #lets start it back up
273
+ start
274
+ end #end of sudo
275
+ end
276
+ end
277
+
278
+
279
+ desc <<-DESC
280
+ [internal] Make sure the MySQL server has been started, just in case the db role
281
+ hasn't been set, e.g. when called from ec2onrails:setup.
282
+ (But don't enable monitoring on it.)
283
+ DESC
284
+ task :start, :roles => :db do
285
+ sudo "god start db"
286
+ end
287
+
288
+ task :stop, :roles => :db do
289
+ sudo "god stop db"
290
+ end
291
+
292
+
293
+ desc <<-DESC
294
+ Drop the MySQL database. Assumes there is no MySQL root \
295
+ password. If there is a MySQL root password, create a task that removes \
296
+ it and run that task before this one using a before hook.
297
+ DESC
298
+ task :drop, :roles => :db do
299
+ load_config
300
+ run %{mysql -u root -e "drop database if exists \\`#{cfg[:db_name]}\\`;"}
301
+ end
302
+
303
+ desc <<-DESC
304
+ db:drop and db:create.
305
+ DESC
306
+ task :recreate, :roles => :db do
307
+ drop
308
+ create
309
+ end
310
+
311
+ desc <<-DESC
312
+ Set a root password for MySQL, using the variable mysql_root_password \
313
+ if it is set. If this is done db:drop won't work.
314
+ DESC
315
+ task :set_root_password, :roles => :db do
316
+ if cfg[:mysql_root_password]
317
+ begin
318
+ run %{mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('#{cfg[:mysql_root_password]}') WHERE User='root'; FLUSH PRIVILEGES;"}
319
+ rescue Exception => e
320
+ #most likely because the password was already set
321
+ #in that case this is fine to swallow the error because the task is 'set' db password, not reset it.... we would have to know
322
+ #what the old root password was
323
+ end
324
+ end
325
+ end
326
+
327
+ desc <<-DESC
328
+ Dump the MySQL database to ebs (if enabled) or the S3 bucket specified by \
329
+ ec2onrails_config[:archive_to_bucket]. The filename will be \
330
+ "database-archive/<timestamp>/dump.sql.gz".
331
+ DESC
332
+ task :archive, :roles => :db do
333
+ run "/usr/local/ec2onrails/bin/backup_app_db.rb --bucket #{cfg[:archive_to_bucket]} --dir #{cfg[:archive_to_bucket_subdir]}"
334
+ end
335
+
336
+ desc <<-DESC
337
+ Restore the MySQL database from the S3 bucket specified by \
338
+ ec2onrails_config[:restore_from_bucket]. The archive filename is \
339
+ expected to be the default, "mysqldump.sql.gz".
340
+ DESC
341
+ task :restore, :roles => :db do
342
+ run "/usr/local/ec2onrails/bin/restore_app_db.rb --bucket #{cfg[:restore_from_bucket]} --dir #{cfg[:restore_from_bucket_subdir]}"
343
+ end
344
+
345
+ desc <<-DESC
346
+ [internal] Initialize the default backup folder on S3 (i.e. do a full
347
+ backup of the newly-created db so the automatic incremental backups
348
+ make sense). NOTE: Only of use if you do not have ebs enabled
349
+ DESC
350
+ task :init_backup, :roles => :db do
351
+ server.allow_sudo do
352
+ sudo "/usr/local/ec2onrails/bin/backup_app_db.rb --reset"
353
+ end
354
+ end
355
+
356
+ # do NOT run if the flag does not exist. This is placed by a startup script
357
+ # and it is only run on the first-startup. This means after the db has been
358
+ # optimized, this task will not work again.
359
+ #
360
+ # Of course you can overload it or call the file directly
361
+ task :optimize, :roles => :db do
362
+ if !quiet_capture("test -e /tmp/optimize_db_flag && echo 'file exists'").empty?
363
+ begin
364
+ sudo "/usr/local/ec2onrails/bin/optimize_mysql.rb"
365
+ ensure
366
+ sudo "rm -rf /tmp/optimize_db_flag" #remove so we cannot run again
367
+ end
368
+ else
369
+ puts "skipping as it looks like this task has already been run"
370
+ end
371
+ end
372
+
373
+ end
374
+
375
+ end
376
+ end
377
+
@@ -0,0 +1,30 @@
1
+ Capistrano::Configuration.instance(:must_exist).load do
2
+ cfg = ec2onrails_config
3
+
4
+ # override default start/stop/restart tasks to use god
5
+ namespace :deploy do
6
+ desc <<-DESC
7
+ Overrides the default Capistrano deploy:start, uses \
8
+ 'god start app'
9
+ DESC
10
+ task :start, :roles => :app do
11
+ sudo "god start app"
12
+ end
13
+
14
+ desc <<-DESC
15
+ Overrides the default Capistrano deploy:stop, uses \
16
+ 'god stop app'
17
+ DESC
18
+ task :stop, :roles => :app do
19
+ sudo "god stop app"
20
+ end
21
+
22
+ desc <<-DESC
23
+ Overrides the default Capistrano deploy:restart, uses \
24
+ 'god restart app'
25
+ DESC
26
+ task :restart, :roles => :app do
27
+ sudo "god restart app"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,489 @@
1
+ Capistrano::Configuration.instance(:must_exist).load do
2
+ cfg = ec2onrails_config
3
+
4
+ namespace :ec2onrails do
5
+ namespace :server do
6
+ desc <<-DESC
7
+ Tell the servers what roles they are in. This configures them with \
8
+ the appropriate settings for each role, and starts and/or stops the \
9
+ relevant services.
10
+ DESC
11
+ task :set_roles do
12
+ # TODO generate this based on the roles that actually exist so arbitrary new ones can be added
13
+ # user_defined_roles = roles
14
+ # roles.each do |k,v|
15
+ # puts "#{k}, #{v.servers.first.options.inspect}"
16
+ # {:primary=>true}
17
+ #
18
+ # end
19
+ #
20
+ roles = {
21
+ :web => hostnames_for_role(:web),
22
+ :app => hostnames_for_role(:app),
23
+ :db_primary => hostnames_for_role(:db, :primary => true),
24
+ # doing th ebelow can cause errors elsewhere unless :db is populated.
25
+ # :db => hostnames_for_role(:db),
26
+ :memcache => hostnames_for_role(:memcache)
27
+ }
28
+ roles_yml = YAML::dump(roles)
29
+ put roles_yml, "/tmp/roles.yml"
30
+ server.allow_sudo do
31
+ sudo "cp /tmp/roles.yml /etc/ec2onrails"
32
+ #we want everyone to be able to read to it
33
+ sudo "chmod a+r /etc/ec2onrails/roles.yml"
34
+ sudo "/usr/local/ec2onrails/bin/set_roles.rb"
35
+ end
36
+ end
37
+
38
+ task :init_services do
39
+ server.allow_sudo do
40
+ #lets pick up the new configuration files
41
+ sudo "/usr/local/ec2onrails/bin/init_services.rb"
42
+ end
43
+ end
44
+
45
+ task :setup_web_proxy, :roles => :web do
46
+ sudo "/usr/local/ec2onrails/bin/setup_web_proxy.rb --mode #{cfg[:web_proxy_server].to_s}"
47
+ end
48
+
49
+ task :setup_elastic_ip, :roles => :web do
50
+ #TODO: for elastic IP
51
+ # * make sure the hostname is reset on the web server
52
+ # * make sure the roles.yml file is updated for ALL servers....
53
+ vol_id = ENV['ELASTIC_IP'] || servers.first.options[:elastic_ip]
54
+ ec2onrails.server.allow_sudo do
55
+ server.set_roles
56
+ end
57
+ end
58
+
59
+ desc <<-DESC
60
+ Change the default value of RAILS_ENV on the server. Technically
61
+ this changes the server's mongrel config to use a different value
62
+ for "environment". The value is specified in :rails_env.
63
+ Be sure to do deploy:restart after this.
64
+ DESC
65
+ task :set_rails_env do
66
+ rails_env = fetch(:rails_env, "production")
67
+ sudo "/usr/local/ec2onrails/bin/set_rails_env #{rails_env}"
68
+ end
69
+
70
+ desc <<-DESC
71
+ Upgrade to the newest versions of all Ubuntu packages.
72
+ DESC
73
+ task :upgrade_packages do
74
+ sudo "aptitude -q update"
75
+ sudo "sh -c 'export DEBIAN_FRONTEND=noninteractive; aptitude -q -y safe-upgrade'"
76
+ end
77
+
78
+ desc <<-DESC
79
+ Upgrade to the newest versions of all rubygems.
80
+ DESC
81
+ task :upgrade_gems do
82
+ sudo "gem update --system --no-rdoc --no-ri"
83
+ sudo "gem update --no-rdoc --no-ri" do |ch, str, data|
84
+ ch[:data] ||= ""
85
+ ch[:data] << data
86
+ if data =~ />\s*$/
87
+ puts data
88
+ choice = Capistrano::CLI.ui.ask("The gem command is asking for a number:")
89
+ ch.send_data("#{choice}\n")
90
+ else
91
+ puts data
92
+ end
93
+ end
94
+ end
95
+
96
+ desc <<-DESC
97
+ Install extra Ubuntu packages. Set ec2onrails_config[:packages], it \
98
+ should be an array of strings.
99
+ NOTE: the package installation will be non-interactive, if the packages \
100
+ require configuration either set ec2onrails_config[:interactive_packages] \
101
+ like you would for ec2onrails_config[:packages] (we'll flood the server \
102
+ with 'Y' inputs), or log in as 'root' and run \
103
+ 'dpkg-reconfigure packagename' or replace the package's config files \
104
+ using the 'ec2onrails:server:deploy_files' task.
105
+ DESC
106
+ task :install_packages do
107
+ ec2onrails.server.allow_sudo do
108
+ sudo "aptitude -q update"
109
+ if cfg[:packages] && cfg[:packages].any?
110
+ sudo "sh -c 'export DEBIAN_FRONTEND=noninteractive; aptitude -q -y install #{cfg[:packages].join(' ')}'"
111
+ end
112
+ if cfg[:interactive_packages] && cfg[:interactive_packages].any?
113
+ # sudo "aptitude install #{cfg[:interactive_packages].join(' ')}", {:env => {'DEBIAN_FRONTEND' => 'readline'} }
114
+ #trying to pick WHEN to send a Y is a bit tricky...it totally depends on the
115
+ #interactive package you want to install. FLOODING it with 'Y'... but not sure how
116
+ #'correct' or robust this is
117
+ cmd = "sudo sh -c 'export DEBIAN_FRONTEND=readline; aptitude -y -q install #{cfg[:interactive_packages].join(' ')}'"
118
+ run(cmd) do |channel, stream, data|
119
+ channel.send_data "Y\n"
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ task :configure_firewall do
126
+ # TODO
127
+ end
128
+
129
+
130
+ desc <<-DESC
131
+ Provide extra security measures. Set ec2onrails_config[:harden_server] = true \
132
+ to allow the hardening of the server.
133
+ These security measures are those which can make initial setup and playing around
134
+ with Ec2onRails tricky. For example, you can be logged out of your server forever
135
+ DESC
136
+ task :harden_server do
137
+ #NOTES: for those security features that will get in the way of ease-of-use
138
+ # hook them in here
139
+ # Like encrypting the mnt directory
140
+ # http://groups.google.com/group/ec2ubuntu/web/encrypting-mnt-using-cryptsetup-on-ubuntu-7-10-gutsy-on-amazon-ec2
141
+ if cfg[:harden_server]
142
+ #lets install some extra packages:
143
+ # denyhosts: sshd security tool. config file is already installed...
144
+ #
145
+ security_pkgs = %w{denyhosts}
146
+ sudo "sh -c 'export DEBIAN_FRONTEND=noninteractive; aptitude -q -y install #{security_pkgs.join(' ')}'"
147
+
148
+ #lets setup dkim
149
+ setup_email_signing
150
+ end
151
+ end
152
+
153
+ #based on the recipe here (but which is missing a few key steps!)
154
+ #http://www.howtoforge.com/quick-and-easy-setup-for-domainkeys-using-ubuntu-postfix-and-dkim-filter
155
+ desc <<-DESC
156
+ enables dkim signing of outgoing msgs. This helps with fightint spam.
157
+ You'll have to update your dns records to take advantage of this, but we'll
158
+ help you out with that
159
+ NOTE: set ec2onrails_config[:service_domain] = 'yourdomain.com' before running this task
160
+ DESC
161
+ task :setup_email_signing, :roles => :app do
162
+ ec2onrails.server.allow_sudo do
163
+ if cfg[:service_domain].nil? || cfg[:service_domain].empty?
164
+ raise "ERROR: missing the :service_domain key. Please set that in your deploy script if you would like to use this task."
165
+ end
166
+
167
+ domain = cfg[:service_domain]
168
+ postmaster_email = "postmaster@#{domain}"
169
+
170
+ #make the selector something that will help us roll over and expire the old key next year
171
+ selector = "mail#{Time.now.year.to_s[-2..-1]}" #ie, mail09
172
+
173
+ sudo "sh -c 'export DEBIAN_FRONTEND=noninteractive; aptitude -q -y install postfix dkim-filter'"
174
+ #do NOT change the size of the key; making it longer can cause problems with some of the dkim implementations
175
+
176
+ keys_exist = File.exist?("config/mail/dkim/dkim_#{selector}.private.key") && File.exist?("config/mail/dkim/dkim_#{selector}.public.key")
177
+
178
+ unless keys_exist
179
+ #lets make them!
180
+ cmds = <<-CMDS
181
+ mkdir -p config/mail/dkim;
182
+ cd config/mail/dkim;
183
+ openssl genrsa -out dkim_#{selector}.private.key 1024;
184
+ openssl rsa -in dkim_#{selector}.private.key -out dkim_#{selector}.public.key -pubout -outform PEM
185
+ CMDS
186
+ system cmds
187
+ end
188
+
189
+ pub_key = File.read("config/mail/dkim/dkim_#{selector}.public.key")
190
+ pub_key = pub_key.split("\n")[1..-2].join('')
191
+
192
+ #lets get the private and public keys up to the server
193
+ put File.read("config/mail/dkim/dkim_#{selector}.private.key"), "/tmp/dkim_#{selector}.private.key"
194
+ put File.read("config/mail/dkim/dkim_#{selector}.public.key"), "/tmp/dkim_#{selector}.public.key"
195
+ sudo "mkdir -p /var/dkim-filter"
196
+ sudo "mv /tmp/dkim_#{selector}.p*.key /var/dkim-filter/."
197
+
198
+ #saw a note that Canonicalization relaxed was helpful for rails applications...
199
+ #haven't tested that yet
200
+ dkim_filter_conf = <<-SCRIPT
201
+ # Log to syslog
202
+ Syslog yes
203
+
204
+ # Sign for example.com with key in /etc/mail/dkim.key using
205
+ Domain #{domain}
206
+ KeyFile /var/dkim-filter/dkim_#{selector}.private.key
207
+ Selector #{selector}
208
+
209
+ # Common settings. See dkim-filter.conf(5) for more information.
210
+ AutoRestart no
211
+ Background yes
212
+ SubDomains no
213
+ Canonicalization relaxed
214
+ SCRIPT
215
+
216
+ put dkim_filter_conf, "/tmp/dkim-filter.conf.tmp"
217
+ sudo "mv /etc/dkim-filter.conf /etc/dkim-filter.conf.orig"
218
+ sudo "mv /tmp/dkim-filter.conf.tmp /etc/dkim-filter.conf"
219
+ cmds = <<-CMDS
220
+ sudo postconf -e 'myhostname = #{domain}';
221
+ sudo postconf -e 'mydomain = #{domain}';
222
+ sudo postconf -e 'myorigin = $mydomain';
223
+ sudo postconf -e 'mynetworks_style=subnet';
224
+ sudo postconf -e 'biff = no';
225
+ sudo postconf -e 'alias_maps = hash:/etc/aliases';
226
+ sudo postconf -e 'alias_database = hash:/etc/aliases';
227
+ sudo postconf -e 'mydestination = localdomain, localhost, localhost.localdomain, localhost';
228
+ sudo postconf -e 'relay_domains=$mydestination';
229
+ sudo postconf -e 'mynetworks = 127.0.0.0/8';
230
+ sudo postconf -e 'smtpd_milters = inet:localhost:8891';
231
+ sudo postconf -e 'non_smtpd_milters = inet:localhost:8891';
232
+ sudo postconf -e 'milter_protocol = 2';
233
+ sudo postconf -e 'milter_default_action = accept'
234
+ CMDS
235
+ sudo cmds
236
+
237
+ #lets lock it down
238
+ sudo "chown -R dkim-filter:dkim-filter /var/dkim-filter"
239
+ sudo "chmod 600 /var/dkim-filter/*"
240
+
241
+ puts "*" * 80
242
+ puts "NOTE: you need to do a few things"
243
+ puts " * created public and private DKIM keys to config/mail/dkim_#{selector}.*.key" unless keys_exist
244
+ puts "\n"
245
+ msg = <<-MSG
246
+ * Enter these *TWO* records into your DNS record:
247
+ #{selector}._domainkey.#{domain} IN TXT 'k=rsa; t=y; p=#{pub_key}'
248
+ _domainkey.#{domain} IN TXT 't=y; o=~; r=#{postmaster_email}'
249
+
250
+ I would recommend signing into your ec2 instance and running some test emails. Gmail is very fast in updating their records, but yahoo (as of this writing) is slow and inconsistent. But you can run a command like this to various email address to see how it works:
251
+
252
+ echo 'something searchable so you can find it in your spam filter! did dkim work?' | mail -s "my dkim email; lets see how it went" adam@someservice.com
253
+
254
+
255
+ NOTE: in the near future, when things are looking good, if you take away the 't=y; ' from the above two records, it tells the email services that you are no longer testing the service and to treat your signings with tough love.
256
+
257
+
258
+ MSG
259
+ puts msg
260
+
261
+ #sometimes the dkim-filter restart fails; it seems to be a race condition with some of the postfix changes going in...
262
+ #but a sleep here seems to do the trick.
263
+ sleep(10)
264
+ output = quiet_capture "sudo /etc/init.d/dkim-filter restart"
265
+ if output =~ /smfi_opensocket\(\) failed/
266
+ #ah, if we didn't sleep enough above, lets try it one more time; but this time it will fail if we still get
267
+ #the smfi_opensocket error
268
+ sleep(5)
269
+ sudo "/etc/init.d/dkim-filter restart 2>&1"
270
+ end
271
+ sleep(2)
272
+ sudo "/etc/init.d/postfix restart 2>&1"
273
+ end
274
+
275
+ end
276
+
277
+
278
+ desc <<-DESC
279
+ Install extra rubygems. Set ec2onrails_config[:rubygems], it should \
280
+ be with an array of strings.
281
+ DESC
282
+ task :install_gems do
283
+ if cfg[:rubygems]
284
+ cfg[:rubygems].each do |gem|
285
+ sudo "gem install #{gem} --no-rdoc --no-ri" do |ch, str, data|
286
+ ch[:data] ||= ""
287
+ ch[:data] << data
288
+ if data =~ />\s*$/
289
+ puts data
290
+ choice = Capistrano::CLI.ui.ask("The gem command is asking for a number:")
291
+ ch.send_data("#{choice}\n")
292
+ else
293
+ puts data
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ task :run_rails_rake_gems_install do
301
+ #if running under Rails 2.1, lets trigger 'rake gems:install', but in such a way
302
+ #so it fails gracefully if running rails < 2.1
303
+ # ALSO, this might be the first time rake is run, and running it as sudo means that
304
+ # if any plugins are loaded and create directories... like what image_science does for
305
+ # ruby_inline, then the dirs will be created as root. so trigger the rails loading
306
+ # very quickly before the sudo is called
307
+ # run "cd #{release_path} && rake RAILS_ENV=#{rails_env} -T 1>/dev/null && sudo rake RAILS_ENV=#{rails_env} gems:install"
308
+ ec2onrails.server.allow_sudo do
309
+ output = quiet_capture "cd #{release_path} && rake RAILS_ENV=#{rails_env} db:version > /dev/null 2>&1 || sudo rake RAILS_ENV=#{rails_env} gems:install"
310
+ puts output
311
+ end
312
+ end
313
+
314
+ desc <<-DESC
315
+ Add extra gem sources to rubygems (to able to fetch gems from for example gems.github.com).
316
+ Set ec2onrails_config[:rubygems_sources], it should be with an array of strings.
317
+ DESC
318
+ task :add_gem_sources do
319
+ if cfg[:rubygems_sources]
320
+ cfg[:rubygems_sources].each do |gem_source|
321
+ sudo "gem sources -a #{gem_source}"
322
+ end
323
+ end
324
+ end
325
+
326
+ desc <<-DESC
327
+ A convenience task to upgrade existing packages and gems and install \
328
+ specified new ones.
329
+ DESC
330
+ task :upgrade_and_install_all do
331
+ upgrade_packages
332
+ upgrade_gems
333
+ install_packages
334
+ install_gems
335
+ end
336
+
337
+ desc <<-DESC
338
+ Set the timezone using the value of the variable named timezone. \
339
+ Valid options for timezone can be determined by the contents of \
340
+ /usr/share/zoneinfo, which can be seen here: \
341
+ http://packages.ubuntu.com/cgi-bin/search_contents.pl?searchmode=filelist&word=tzdata&version=gutsy&arch=all&page=1&number=all \
342
+ Remove 'usr/share/zoneinfo/' from the filename, and use the last \
343
+ directory and file as the value. For example 'Africa/Abidjan' or \
344
+ 'posix/GMT' or 'Canada/Eastern'.
345
+ DESC
346
+ task :set_timezone do
347
+ if cfg[:timezone]
348
+ ec2onrails.server.allow_sudo do
349
+ sudo "bash -c 'echo #{cfg[:timezone]} > /etc/timezone'"
350
+ sudo "cp /usr/share/zoneinfo/#{cfg[:timezone]} /etc/localtime"
351
+ end
352
+ end
353
+ end
354
+
355
+ desc <<-DESC
356
+ Deploy a set of config files to the server, the files will be owned by \
357
+ root. This doesn't delete any files from the server. This is intended
358
+ mainly for customized config files for new packages installed via the \
359
+ ec2onrails:server:install_packages task. Subdirectories and files \
360
+ inside here will be placed within the same directory structure \
361
+ relative to the root of the server's filesystem.
362
+ DESC
363
+ task :deploy_files do
364
+ if cfg[:server_config_files_root]
365
+ begin
366
+ filename = "config_files.tar"
367
+ local_file = "#{Dir.tmpdir}/#{filename}"
368
+ remote_file = "/tmp/#{filename}"
369
+ FileUtils.cd(cfg[:server_config_files_root]) do
370
+ File.open(local_file, 'wb') { |tar| Minitar.pack(".", tar) }
371
+ end
372
+ put File.read(local_file), remote_file
373
+ sudo "tar xvf #{remote_file} -o -C /"
374
+ ensure
375
+ rm_rf local_file
376
+ sudo "rm -f #{remote_file}"
377
+ end
378
+ end
379
+ end
380
+
381
+ desc <<-DESC
382
+ Restart a set of services. Set ec2onrails_config[:services_to_restart] \
383
+ to an array of strings. It's assumed that each service has a script \
384
+ in /etc/init.d
385
+ DESC
386
+ task :restart_services do
387
+ if cfg[:services_to_restart] && cfg[:services_to_restart].any?
388
+ cfg[:services_to_restart].each do |service|
389
+ run_init_script(service, "restart")
390
+ end
391
+ end
392
+ end
393
+
394
+ desc <<-DESC
395
+ Set the email address that mail to the app user forwards to.
396
+ DESC
397
+ task :set_mail_forward_address do
398
+ run "echo '#{cfg[:mail_forward_address]}' >> /home/app/.forward" if cfg[:mail_forward_address]
399
+ # put cfg[:admin_mail_forward_address], "/home/admin/.forward" if cfg[:admin_mail_forward_address]
400
+ end
401
+
402
+ desc <<-DESC
403
+ Enable ssl for the web server. The SSL cert file should be in
404
+ /etc/ssl/certs/default.pem and the SSL key file should be in
405
+ /etc/ssl/private/default.key (use the deploy_files task).
406
+ DESC
407
+ task :enable_ssl, :roles => :web do
408
+ #TODO: enable for nginx
409
+ sudo "a2enmod ssl"
410
+ sudo "a2enmod headers" # the headers module is necessary to forward a header so that rails can detect it is handling an SSL connection. NPG 7/11/08
411
+ sudo "a2ensite default-ssl"
412
+ run_init_script("web_proxy", "restart")
413
+ end
414
+
415
+ desc <<-DESC
416
+ Restrict the main user's sudo access.
417
+ Defaults the user to only be able to \
418
+ sudo to god
419
+ DESC
420
+ task :restrict_sudo_access do
421
+ old_user = fetch(:user)
422
+ begin
423
+ set :user, 'root'
424
+ sessions.clear #clear out sessions cache..... this way the ssh connections are reinitialized
425
+
426
+ run "test ! -L /etc/sudoers || ( echo 'removing symlink /etc/sudoers' ; unlink /etc/sudoers )"
427
+ run "cp -f /etc/sudoers.restricted_access /etc/sudoers && chmod 440 /etc/sudoers"
428
+ # this doesn't work; sudo needs the file to not be a symlink
429
+ # run "ln -sf /etc/sudoers.restricted_access /etc/sudoers"
430
+
431
+ ensure
432
+ set :user, old_user
433
+ sessions.clear
434
+ end
435
+ end
436
+
437
+ desc <<-DESC
438
+ Grant *FULL* sudo access to the main user.
439
+ DESC
440
+ task :grant_sudo_access do
441
+ allow_sudo
442
+ end
443
+
444
+ @within_sudo = 0
445
+ def allow_sudo
446
+ begin
447
+ @within_sudo += 1
448
+ old_user = fetch(:user)
449
+ if @within_sudo > 1
450
+ yield if block_given?
451
+ true
452
+ elsif capture("ls -l /etc/sudoers /etc/sudoers.full_access | awk '{print $5}'").split.uniq.size == 1
453
+ yield if block_given?
454
+ false
455
+ else
456
+ begin
457
+ # need to cheet and temporarily set the user to ROOT so we
458
+ # can (re)grant full sudo access.
459
+ # we can do this because the root and app user have the same
460
+ # ssh login preferences....
461
+ #
462
+ # TODO:
463
+ # do not escalate priv. to root...use another user like 'admin' that has full sudo access
464
+ set :user, 'root'
465
+ sessions.clear #clear out sessions cache..... this way the ssh connections are reinitialized
466
+
467
+
468
+ # note, this approach prevents end users from effectively editing the sudoers file directly :(
469
+ sudo "test ! -L /etc/sudoers || ( echo 'removing symlink /etc/sudoers' ; unlink /etc/sudoers )"
470
+ run "cp /etc/sudoers.full_access /etc/sudoers && chmod 440 /etc/sudoers"
471
+ set :user, old_user
472
+ sessions.clear
473
+ yield if block_given?
474
+ ensure
475
+ server.restrict_sudo_access if block_given?
476
+ set :user, old_user
477
+ sessions.clear
478
+ true
479
+ end
480
+ end
481
+ ensure
482
+ @within_sudo -= 1
483
+ end
484
+ end
485
+ end
486
+
487
+ end
488
+
489
+ end