rudy 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,138 +8,229 @@ module Rudy
8
8
  class NoCred < RuntimeError; end;
9
9
 
10
10
  class Base < Drydock::Command
11
-
12
- attr_accessor :access_key
13
- attr_accessor :secret_key
14
- attr_accessor :accnt_num
15
-
16
- attr_accessor :ec2_cert
17
- attr_accessor :ec2_private_key
18
- attr_accessor :keypairs
19
-
20
- attr_reader :user
21
- attr_reader :environment
22
- attr_reader :role
23
- attr_reader :position
24
-
25
- attr_reader :zone
26
- attr_reader :region
27
-
11
+
28
12
  attr_reader :scm
29
-
13
+
30
14
  attr_reader :rscripts
31
15
  attr_reader :domains
32
16
  attr_reader :machine_images
33
17
 
34
18
  attr_reader :config
35
19
 
20
+
36
21
  def init
37
- @access_key = ENV['AWS_ACCESS_KEY'] unless @access_key
38
- @secret_key = ENV['AWS_SECRET_KEY'] unless @secret_key
39
- @account_num = ENV['AWS_ACCOUNT_NUMBER'] unless @account_num
40
22
 
41
- @ec2_cert = ENV['EC2_CERT'] unless @ec2_cert
42
- @ec2_private_key = ENV['EC2_PRIVATE_KEY'] unless @ec2_private_key
43
23
 
44
- if ENV['RUDY_SVN_BASE']
45
- @scm = Rudy::SCM::SVN.new(ENV['RUDY_SVN_BASE'])
46
- end
47
-
48
- @config = File.exists?(RUDY_CONFIG) ? Rudy::Config.from_file(RUDY_CONFIG) : Rudy::Config.new
49
-
50
- @region ||= DEFAULT_REGION
51
- @zone ||= DEFAULT_ZONE
52
- @environment ||= DEFAULT_ENVIRONMENT
53
- @role ||= DEFAULT_ROLE
54
- @position ||= DEFAULT_POSITION
55
-
56
- @user ||= DEFAULT_USER
57
-
58
- @keypairs = {}
59
- ENV.keys.select { |key| key.match /EC2_KEYPAIR/i }.each do |key|
60
- ec2, keypair, env, role, user = key.split '_' # EC2_KEYPAIR_STAGE_APP_RUDY
61
- raise "#{key} is malformed." unless env && role && user
62
- new_key = "#{env}-#{role}-#{user}".downcase
63
- @keypairs[new_key] = ENV[key]
64
- end
24
+ raise "PRODUCTION ACCESS IS DISABLED IN DEBUG MODE" if @global.environment == "prod" && Drydock.debug?
65
25
 
66
- @rscripts = {}
67
- ENV.keys.select { |key| key.match /RUDY_RSCRIPT/i }.each do |key|
68
- rudy, rscript, env, role, user = key.split '_' # RUDY_RSCRIPT_STAGE_APP_ROOT
69
- raise "#{key} is malformed." unless env && role && user
70
- new_key = "#{env}-#{role}-#{user}".downcase
71
- @rscripts[new_key] = ENV[key]
72
- end
26
+ @global.config ||= RUDY_CONFIG_FILE
73
27
 
74
- @machine_images = {}
75
- ENV.keys.select { |key| key.match /EC2_AMI_/i }.each do |key|
76
- ec2, ami, env, role = key.split '_' # RUDY_RSCRIPT_STAGE_APP_ROOT
77
- raise "#{key} is malformed." unless env && role
78
- new_key = "#{env}-#{role}".downcase
79
- @machine_images[new_key] = ENV[key]
28
+ unless File.exists?(@global.config)
29
+ init_config_dir
80
30
  end
81
31
 
82
- if @verbose > 1 && Drydock.debug?
83
- instance_variables.each do |var|
84
- puts "#{var}: #{instance_variable_get(var)}"
32
+ @config = Rudy::Config.new(@global.config, {:verbose => (@global.verbose > 0)} )
33
+ @config.look_and_load
34
+
35
+ raise "There is no machine group configured" if @config.machines.nil?
36
+ raise "There is no AWS info configured" if @config.awsinfo.nil?
37
+
38
+
39
+ @global.accesskey ||= @config.awsinfo.accesskey || ENV['AWS_ACCESS_KEY']
40
+ @global.secretkey ||= @config.awsinfo.secretkey || ENV['AWS_SECRET_KEY'] || ENV['AWS_SECRET_ACCESS_KEY']
41
+ @global.account ||= @config.awsinfo.account || ENV['AWS_ACCOUNT_NUMBER']
42
+
43
+ @global.cert ||= @config.awsinfo.cert || ENV['EC2_CERT']
44
+ @global.privatekey ||= @config.awsinfo.privatekey || ENV['EC2_PRIVATE_KEY']
45
+
46
+ @global.cert = File.expand_path(@global.cert || '')
47
+ @global.privatekey = File.expand_path(@global.privatekey || '')
48
+
49
+ @global.region ||= @config.defaults.region || DEFAULT_REGION
50
+ @global.zone ||= @config.defaults.zone || DEFAULT_ZONE
51
+ @global.environment ||= @config.defaults.environment || DEFAULT_ENVIRONMENT
52
+ @global.role ||= @config.defaults.role || DEFAULT_ROLE
53
+ @global.position ||= @config.defaults.position || DEFAULT_POSITION
54
+ @global.user ||= @config.defaults.user || DEFAULT_USER
55
+
56
+ @global.local_user = ENV['USER'] || :user
57
+ @global.local_hostname = Socket.gethostname || :host
58
+
59
+ check_keys
60
+
61
+ if @global.verbose > 1
62
+ puts "GLOBALS:"
63
+ @global.marshal_dump.each_pair do |n,v|
64
+ puts "#{n}: #{v}"
65
+ end
66
+ ["machines", "routines"].each do |type|
67
+ puts "#{$/*2}#{type.upcase}:"
68
+ val = @config.send(type).find_deferred(@global.environment, @global.role)
69
+ puts val.to_hash.to_yaml
85
70
  end
71
+ puts
86
72
  end
87
73
 
88
- if @access_key && @secret_key
89
- @ec2 = Rudy::AWS::EC2.new(@access_key, @secret_key)
90
- # RightAws::SdbInterface is on thin ice. No select support! Or sort!
91
- @sdb = Rudy::AWS::SimpleDB.new(@access_key, @secret_key)
92
- #@s3 = Rudy::AWS::SimpleDB.new(@access_key, @secret_key)
74
+
75
+
76
+ # TODO: enforce home directory permissions
77
+ #if File.exists?(RUDY_CONFIG_DIR)
78
+ # puts "Checking #{check_environment} permissions..."
79
+ #end
80
+
81
+ if has_keys?
82
+ @ec2 = Rudy::AWS::EC2.new(@global.accesskey, @global.secretkey)
83
+ @sdb = Rudy::AWS::SimpleDB.new(@global.accesskey, @global.secretkey)
84
+ #@s3 = Rudy::AWS::SimpleDB.new(@global.accesskey, @global.secretkey)
93
85
  end
94
86
  end
87
+ protected :init
88
+
89
+ def machine_data
90
+ machine_data = {
91
+ # Give the machine an identity
92
+ :zone => @global.zone,
93
+ :environment => @global.environment,
94
+ :role => @global.role,
95
+ :position => @global.position,
96
+
97
+ # Add hosts to the /etc/hosts file
98
+ :hosts => {
99
+ :dbmaster => "127.0.0.1",
100
+ }
101
+ }
102
+
103
+ machine_data.to_hash
104
+ end
95
105
 
96
106
 
97
107
  # Raises exceptions if the requested user does
98
108
  # not have a valid keypair configured. (See: EC2_KEYPAIR_*)
99
109
  def check_keys
100
- raise "No SSH key provided for #{keypairname}!" unless has_keypair?(keypairname)
101
- raise "SSH key provided but cannot be found! (#{keypairname}: #{keypairpath})" unless File.exists?(keypairpath)
110
+ raise "No SSH key provided for #{@global.user}! (check #{RUDY_CONFIG_FILE})" unless has_keypair?
111
+ raise "SSH key provided but cannot be found! (check #{RUDY_CONFIG_FILE})" unless File.exists?(keypairpath)
102
112
  end
103
113
 
104
114
  def has_pem_keys?
105
- (@ec2_cert && File.exists?(@ec2_cert) &&
106
- @ec2_private_key && File.exists?(@ec2_private_key))
115
+ (@global.cert && File.exists?(@global.cert) &&
116
+ @global.privatekey && File.exists?(@global.privatekey))
107
117
  end
108
118
 
109
119
  def has_keys?
110
- (@access_key && @secret_key)
120
+ (@global.accesskey && !@global.accesskey.empty? && @global.secretkey && !@global.secretkey.empty?)
111
121
  end
112
122
 
113
- def has_keypair?(name)
114
- (has_keypairs? && @keypairs.has_key?(name))
123
+ def keypairpath(name=nil)
124
+ name ||= @global.user
125
+ raise "No default user configured" unless name
126
+ kp = @config.machines.find(@global.environment, @global.role, :users, name, :keypair2)
127
+ kp ||= @config.machines.find(@global.environment, :users, name, :keypair)
128
+ kp ||= @config.machines.find(:users, name, :keypair)
129
+ kp &&= File.expand_path(kp)
130
+ kp
131
+ end
132
+ def has_keypair?(name=nil)
133
+ kp = keypairpath(name)
134
+ (!kp.nil? && File.exists?(kp))
115
135
  end
116
136
 
117
- def has_keypairs?
118
- (!@keypairs.empty?)
137
+ # Opens an SSH session.
138
+ # <li>+host+ the hostname to connect to. Defaults to the machine specified
139
+ # by @global.environment, @global.role, @global.position.</li>
140
+ # <li>+b+ a block to execute on the host. Receives |session|</li>
141
+ #
142
+ # ssh do |session|
143
+ # session.exec(cmd)
144
+ # end
145
+ #
146
+ # See Net::SSH
147
+ #
148
+ def ssh(host=nil, &b)
149
+ host ||= machine_hostname
150
+ raise "No host provided for SSH" unless host
151
+ raise "No block provided for SSH" unless b
152
+
153
+ Net::SSH.start(host, @global.user, :keys => [keypairpath]) do |session|
154
+ b.call(session)
155
+ end
119
156
  end
120
157
 
121
- def machine_group
122
- [@environment, @role].join(RUDY_DELIM)
158
+ # Secure copy.
159
+ #
160
+ # scp do |scp|
161
+ # # upload a file to a remote server
162
+ # scp.upload! "/local/path", "/remote/path"
163
+ #
164
+ # # upload from an in-memory buffer
165
+ # scp.upload! StringIO.new("some data to upload"), "/remote/path"
166
+ #
167
+ # # run multiple downloads in parallel
168
+ # d1 = scp.download("/remote/path", "/local/path")
169
+ # d2 = scp.download("/remote/path2", "/local/path2")
170
+ # [d1, d2].each { |d| d.wait }
171
+ # end
172
+ #
173
+ def scp(host=nil, &b)
174
+ host ||= machine_hostname
175
+ raise "No host provided for scp" unless host
176
+ raise "No block provided for scp" unless b
177
+
178
+ Net::SCP.start(host, @global.user, :keys => [keypairpath]) do |scp|
179
+ b.call(scp)
180
+ end
123
181
  end
124
182
 
125
- def machine_image
126
- @machine_images[machine_group]
183
+ # +name+ the name of the remote user to use for the remainder of the command
184
+ # (or until switched again). If no name is provided, the user will be revert
185
+ # to whatever it was before the previous switch.
186
+ def switch_user(name=nil)
187
+ if name == nil && @switch_user_previous
188
+ @global.user = @switch_user_previous
189
+ elsif @global.user != name
190
+ puts "Remote commands will be run as #{name} user"
191
+ @switch_user_previous = @global.user
192
+ @global.user = name
193
+ end
127
194
  end
128
195
 
129
- # TODO: fix machine_group to include zone
130
- def machine_name
131
- [@zone, machine_group, @position].join(RUDY_DELIM)
196
+ # Returns a hash of info for the requested machine. If the requested machine
197
+ # is not running, it will raise an exception.
198
+ def find_current_machine
199
+ find_machine(machine_group)
200
+ end
201
+
202
+ def find_machine(group)
203
+ machine_list = @ec2.instances.list(group)
204
+ machine = machine_list.values.first # NOTE: Only one machine per group, for now...
205
+ raise "There's no machine running in #{group}" unless machine
206
+ raise "The primary machine in #{group} is not in a running state" unless machine[:aws_state] == 'running'
207
+ machine
208
+ end
209
+
210
+ def machine_hostname(group=nil)
211
+ group ||= machine_group
212
+ find_machine(group)[:dns_name]
132
213
  end
133
214
 
134
- def keypairname
135
- [machine_group, user].join(RUDY_DELIM)
215
+ def machine_group
216
+ [@global.environment, @global.role].join(RUDY_DELIM)
136
217
  end
137
218
 
138
- def keypairpath
139
- return unless has_keypair?(keypairname)
140
- @keypairs[keypairname]
219
+ def machine_image
220
+ ami = @config.machines.find_deferred(@global.environment, @global.role, :ami)
221
+ raise "There is no AMI configured for #{machine_group}" unless ami
222
+ ami
141
223
  end
142
224
 
225
+ def machine_address
226
+ @config.machines.find_deferred(@global.environment, @global.role, :address)
227
+ end
228
+
229
+ # TODO: fix machine_group to include zone
230
+ def machine_name
231
+ [@global.zone, machine_group, @global.position].join(RUDY_DELIM)
232
+ end
233
+
143
234
  def instance_id?(id=nil)
144
235
  (id && id[0,2] == "i-")
145
236
  end
@@ -157,77 +248,459 @@ module Rudy
157
248
  end
158
249
 
159
250
 
160
- def do_dirty_disk_volume_deeds(disk, machine)
161
- puts "-"*30
162
- puts "Disk: #{disk.name} (path: #{disk.path}, device: #{disk.device})"
163
- puts
251
+ def wait_for_machine(id)
252
+
253
+ print "Waiting for #{id} to become available"
254
+ STDOUT.flush
164
255
 
165
- if @ec2.instances.attached_volume?(machine[:aws_instance_id], disk.device)
166
- raise "#{disk.device} is already in use on #{machine[:aws_instance_id]}! (Try umounting it)"
256
+ while @ec2.instances.pending?(id)
257
+ sleep 2
258
+ print '.'
259
+ STDOUT.flush
167
260
  end
168
261
 
169
- if !disk.awsid || (disk.awsid && !@ec2.volumes.exists?(disk.awsid))
170
- disk = Rudy::MetaData::Disk.update_volume(@sdb, @ec2, disk, machine)
262
+ machine = @ec2.instances.get(id)
263
+
264
+ puts " It's up!\a\a" # with bells
265
+ print "Waiting for SSH daemon at #{machine[:dns_name]}"
266
+ STDOUT.flush
267
+
268
+ while !Rudy::Utils.service_available?(machine[:dns_name], 22)
269
+ print '.'
270
+ STDOUT.flush
171
271
  end
272
+ puts " It's up!\a\a\a"
273
+
274
+ end
275
+
276
+
277
+ def device_to_path(machine, device)
278
+ # /dev/sdr 10321208 154232 9642688 2% /rilli/app
279
+ dfoutput = ssh_command(machine[:dns_name], keypairpath, @global.user, "df #{device} | tail -1").chomp
280
+ dfvals = dfoutput.scan(/(#{device}).+\s(.+?)$/).flatten # ["/dev/sdr", "/rilli/app"]
281
+ dfvals.last
282
+ end
283
+
284
+ # +action+ is one of: :shutdown, :start, :deploy
285
+ # +machine+ is a right_aws machine instance hash
286
+ def execute_disk_routines(machines, action)
287
+ machines = [machines] unless machines.is_a?( Array)
172
288
 
173
- raise "Unknown error creating volume! #{disk.awsid}" unless disk && disk.awsid
289
+ puts "Running #{action.to_s.capitalize} DISK routines".att(:bright)
174
290
 
175
- puts "Attaching #{disk.awsid} to #{machine[:aws_instance_id]}"
176
- @ec2.volumes.attach(machine[:aws_instance_id], disk.awsid, disk.device)
177
- sleep 2
291
+ disks = @config.machines.find_deferred(@global.environment, @global.role, :disks)
292
+ routines = @config.routines.find(@global.environment, @global.role, action, :disks)
178
293
 
179
- @user = "root"
294
+ unless routines
295
+ puts "No #{action} disk routines for #{machine_group}"
296
+ return
297
+ end
180
298
 
181
- if disk.raw_volume
182
- puts "Creating the filesystem (mkfs.ext3 -F #{disk.device})"
183
- capture(:stdout) do
184
- ssh machine[:dns_name], keypairpath, user, "mkfs.ext3 -F #{disk.device}"
299
+ switch_user("root")
300
+
301
+ machines.each do |machine|
302
+
303
+ unless machine[:aws_instance_id]
304
+ puts "Machine given has no instance ID. Skipping disks."
305
+ return
306
+ end
307
+
308
+ unless machine[:dns_name]
309
+ puts "Machine given has no DNS name. Skipping disks."
310
+ return
185
311
  end
186
312
 
187
- puts "Saving disk metadata"
188
- disk.raw_volume = false
189
- Rudy::MetaData::Disk.save(@sdb, disk)
313
+ if routines.destroy
314
+ disk_paths = routines.destroy.keys
315
+ vols = @ec2.instances.volumes(machine[:aws_instance_id]) || []
316
+ puts "No volumes to destroy for (#{machine[:aws_instance_id]})" if vols.empty?
317
+ vols.each do |vol|
318
+ disk = Rudy::MetaData::Disk.find_from_volume(@sdb, vol[:aws_id])
319
+ if disk
320
+ this_path = disk.path
321
+ else
322
+ puts "No disk metadata for volume #{vol[:aws_id]}. Going old school..."
323
+ this_path = device_to_path(machine, vol[:aws_device])
324
+ end
325
+
326
+ dconf = disks[this_path]
327
+
328
+ unless dconf
329
+ puts "#{this_path} is not defined for this machine. Check your machines config."
330
+ next
331
+ end
332
+
333
+ if disk_paths.member?(this_path)
334
+
335
+ unless disks.has_key?(this_path)
336
+ puts "#{this_path} is not defined as a machine disk. Skipping..."
337
+ next
338
+ end
339
+
340
+ begin
341
+ puts "Unmounting #{this_path}..."
342
+ ssh_command machine[:dns_name], keypairpath, @global.user, "umount #{this_path}"
343
+ sleep 3
344
+ rescue => ex
345
+ puts "Error while unmounting #{this_path}: #{ex.message}"
346
+ puts ex.backtrace if Drydock.debug?
347
+ puts "We'll keep going..."
348
+ end
349
+
350
+ begin
351
+
352
+ if @ec2.volumes.attached?(disk.awsid)
353
+ puts "Detaching #{vol[:aws_id]}"
354
+ @ec2.volumes.detach(vol[:aws_id])
355
+ sleep 3 # TODO: replace with something like wait_for_machine
356
+ end
357
+
358
+ puts "Destroying #{this_path} (#{vol[:aws_id]})"
359
+ if @ec2.volumes.available?(disk.awsid)
360
+ @ec2.volumes.destroy(vol[:aws_id])
361
+ else
362
+ puts "Volume is still attached (maybe a web server of database is running?)"
363
+ end
364
+
365
+ if disk
366
+ puts "Deleteing metadata for #{disk.name}"
367
+ Rudy::MetaData::Disk.destroy(@sdb, disk)
368
+ end
369
+
370
+ rescue => ex
371
+ puts "Error while detaching volume #{vol[:aws_id]}: #{ex.message}"
372
+ puts ex.backtrace if Drydock.debug?
373
+ puts "Continuing..."
374
+ end
375
+
376
+ end
377
+ puts
378
+
379
+ end
190
380
 
191
- sleep 2
192
- end
381
+ end
382
+
383
+
384
+ if routines.mount
385
+ disk_paths = routines.mount.keys
386
+ vols = @ec2.instances.volumes(machine[:aws_instance_id]) || []
387
+ puts "No volumes to mount for (#{machine[:aws_instance_id]})" if vols.empty?
388
+ vols.each do |vol|
389
+ disk = Rudy::MetaData::Disk.find_from_volume(@sdb, vol[:aws_id])
390
+ if disk
391
+ this_path = disk.path
392
+ else
393
+ puts "No disk metadata for volume #{vol[:aws_id]}. Going old school..."
394
+ this_path = device_to_path(machine, vol[:aws_device])
395
+ end
396
+
397
+ next unless disk_paths.member?(this_path)
398
+
399
+ dconf = disks[this_path]
400
+
401
+ unless dconf
402
+ puts "#{this_path} is not defined for this machine. Check your machines config."
403
+ next
404
+ end
405
+
406
+
407
+ begin
408
+ unless @ec2.instances.attached_volume?(machine[:aws_instance_id], vol[:aws_device])
409
+ puts "Attaching #{vol[:aws_id]} to #{machine[:aws_instance_id]}".att(:bright)
410
+ @ec2.volumes.attach(machine[:aws_instance_id], vol[:aws_id],vol[:aws_device])
411
+ sleep 3
412
+ end
413
+
414
+ puts "Mounting #{this_path} to #{vol[:aws_device]}".att(:bright)
415
+ ssh_command machine[:dns_name], keypairpath, @global.user, "mkdir -p #{this_path} && mount -t ext3 #{vol[:aws_device]} #{this_path}"
416
+
417
+ sleep 1
418
+ rescue => ex
419
+ puts "There was an error mounting #{this_path}: #{ex.message}"
420
+ puts ex.backtrace if Drydock.debug?
421
+ end
422
+ puts
423
+ end
424
+ end
193
425
 
194
- puts "Mounting #{disk.device} to #{disk.path}"
195
- capture(:stdout) do
196
- ssh machine[:dns_name], keypairpath, user, "mkdir -p #{disk.path} && mount -t ext3 #{disk.device} #{disk.path}"
197
- end
198
- sleep 1
199
426
 
427
+
428
+ if routines.restore
429
+
430
+ routines.restore.each_pair do |path,props|
431
+ from = props[:from] || "unknown"
432
+ unless from.to_s == "backup"
433
+ puts "Sorry! You can currently only restore from backup. Check your routines config."
434
+ next
435
+ end
436
+
437
+ begin
438
+ puts "Restoring disk for #{path}"
439
+
440
+ dconf = disks[path]
441
+
442
+ unless dconf
443
+ puts "#{path} is not defined for this machine. Check your machines config."
444
+ next
445
+ end
446
+
447
+ zon = props[:zone] || @global.zone
448
+ env = props[:environment] || @global.environment
449
+ rol = props[:role] || @global.role
450
+ pos = props[:position] || @global.position
451
+ puts "Looking for backup from #{zon}-#{env}-#{rol}-#{pos}"
452
+ backup = find_most_recent_backup(zon, env, rol, pos, path)
453
+
454
+ unless backup
455
+ puts "No backups found"
456
+ next
457
+ end
458
+
459
+ puts "Found: #{backup.name}".att(:bright)
460
+
461
+ disk = Rudy::MetaData::Disk.new
462
+ disk.path = path
463
+ [:region, :zone, :environment, :role, :position].each do |n|
464
+ disk.send("#{n}=", @global.send(n)) if @global.send(n)
465
+ end
466
+
467
+ disk.device = dconf[:device]
468
+ size = (backup.size.to_i > dconf[:size].to_i) ? backup.size : dconf[:size]
469
+ disk.size = size.to_i
470
+
471
+
472
+ if Rudy::MetaData::Disk.is_defined?(@sdb, disk)
473
+ puts "The disk #{disk.name} already exists."
474
+ puts "You probably need to define when to destroy the disk."
475
+ puts "Skipping..."
476
+ next
477
+ end
478
+
479
+ if @ec2.instances.attached_volume?(machine[:aws_instance_id], disk.device)
480
+ puts "Skipping disk for #{disk.path} (device #{disk.device} is in use)"
481
+ next
482
+ end
483
+
484
+ # NOTE: It's important to use Caesars' hash syntax b/c the disk property
485
+ # "size" conflicts with Hash#size which is what we'll get if there's no
486
+ # size defined.
487
+ unless disk.size.kind_of?(Integer)
488
+ puts "Skipping disk for #{disk.path} (size not defined)"
489
+ next
490
+ end
491
+
492
+ if disk.path.nil?
493
+ puts "Skipping disk for #{disk.path} (no path defined)"
494
+ next
495
+ end
496
+
497
+ unless disk.valid?
498
+ puts "Skipping #{disk.name} (not enough info)"
499
+ next
500
+ end
501
+
502
+ puts "Creating volume... (from #{backup.awsid})".att(:bright)
503
+ volume = @ec2.volumes.create(@global.zone, disk.size, backup.awsid)
504
+
505
+ puts "Attaching #{volume[:aws_id]} to #{machine[:aws_instance_id]}".att(:bright)
506
+ @ec2.volumes.attach(machine[:aws_instance_id], volume[:aws_id], disk.device)
507
+ sleep 3
508
+
509
+ puts "Mounting #{disk.device} to #{disk.path}".att(:bright)
510
+ ssh_command machine[:dns_name], keypairpath, @global.user, "mkdir -p #{disk.path} && mount -t ext3 #{disk.device} #{disk.path}"
511
+
512
+ puts "Creating disk metadata for #{disk.name}"
513
+ disk.awsid = volume[:aws_id]
514
+ Rudy::MetaData::Disk.save(@sdb, disk)
515
+
516
+ sleep 1
517
+ rescue => ex
518
+ puts "There was an error creating #{path}: #{ex.message}"
519
+ puts ex.backtrace if Drydock.debug?
520
+ if disk
521
+ puts "Removing metadata for #{disk.name}"
522
+ Rudy::MetaData::Disk.destroy(@sdb, disk)
523
+ end
524
+ end
525
+ puts
526
+ end
527
+ end
528
+
529
+
530
+
531
+ if routines.create
532
+ routines.create.each_pair do |path,props|
533
+
534
+ begin
535
+ puts "Creating disk for #{path}"
536
+
537
+ dconf = disks[path]
538
+
539
+ unless dconf
540
+ puts "#{path} is not defined for this machine. Check your machines config."
541
+ next
542
+ end
543
+
544
+ disk = Rudy::MetaData::Disk.new
545
+ disk.path = path
546
+ [:region, :zone, :environment, :role, :position].each do |n|
547
+ disk.send("#{n}=", @global.send(n)) if @global.send(n)
548
+ end
549
+ [:device, :size].each do |n|
550
+ disk.send("#{n}=", dconf[n]) if dconf.has_key?(n)
551
+ end
552
+
553
+ if Rudy::MetaData::Disk.is_defined?(@sdb, disk)
554
+ puts "The disk #{disk.name} already exists."
555
+ puts "You probably need to define when to destroy the disk."
556
+ puts "Skipping..."
557
+ next
558
+ end
559
+
560
+ if @ec2.instances.attached_volume?(machine[:aws_instance_id], disk.device)
561
+ puts "Skipping disk for #{disk.path} (device #{disk.device} is in use)"
562
+ next
563
+ end
564
+
565
+ # NOTE: It's important to use Caesars' hash syntax b/c the disk property
566
+ # "size" conflicts with Hash#size which is what we'll get if there's no
567
+ # size defined.
568
+ unless disk.size.kind_of?(Integer)
569
+ puts "Skipping disk for #{disk.path} (size not defined)"
570
+ next
571
+ end
572
+
573
+ if disk.path.nil?
574
+ puts "Skipping disk for #{disk.path} (no path defined)"
575
+ next
576
+ end
577
+
578
+ unless disk.valid?
579
+ puts "Skipping #{disk.name} (not enough info)"
580
+ next
581
+ end
582
+
583
+ puts "Creating volume... (#{disk.size}GB in #{@global.zone})".att(:bright)
584
+ volume = @ec2.volumes.create(@global.zone, disk.size)
585
+
586
+ puts "Attaching #{volume[:aws_id]} to #{machine[:aws_instance_id]}".att(:bright)
587
+ @ec2.volumes.attach(machine[:aws_instance_id], volume[:aws_id], disk.device)
588
+ sleep 6
589
+
590
+ puts "Creating the filesystem (mkfs.ext3 -F #{disk.device})".att(:bright)
591
+ ssh_command machine[:dns_name], keypairpath, @global.user, "mkfs.ext3 -F #{disk.device}"
592
+ sleep 3
593
+
594
+ puts "Mounting #{disk.device} to #{disk.path}".att(:bright)
595
+ ssh_command machine[:dns_name], keypairpath, @global.user, "mkdir -p #{disk.path} && mount -t ext3 #{disk.device} #{disk.path}"
596
+
597
+ puts "Creating disk metadata for #{disk.name}"
598
+ disk.awsid = volume[:aws_id]
599
+ Rudy::MetaData::Disk.save(@sdb, disk)
600
+
601
+ sleep 1
602
+ rescue => ex
603
+ puts "There was an error creating #{path}: #{ex.message}"
604
+ if disk
605
+ puts "Removing metadata for #{disk.name}"
606
+ Rudy::MetaData::Disk.destroy(@sdb, disk)
607
+ end
608
+ end
609
+ puts
610
+ end
611
+ end
612
+ end
200
613
  end
201
614
 
615
+ def find_most_recent_backup(zon, env, rol, pos, path)
616
+ criteria = [zon, env, rol, pos, path]
617
+ (Rudy::MetaData::Backup.list(@sdb, *criteria) || []).first
618
+ end
202
619
 
620
+ def execute_routines(machines, action, before_or_after)
621
+ machines = [machines] unless machines.is_a?( Array)
622
+ config = @config.routines.find_deferred(@global.environment, @global.role, :config) || {}
623
+ config[:global] = @global.marshal_dump
624
+ config[:global].reject! { |n,v| n == :cert || n == :privatekey }
625
+
626
+ # The config file contains settings from ~/.rudy/config
627
+ #
628
+ # routines do
629
+ # config do
630
+ # end
631
+ # end
632
+ #
633
+ config_file = "#{action}-config.yaml"
634
+ tf = Tempfile.new(config_file)
635
+ write_to_file(tf.path, config.to_hash.to_yaml, 'w')
636
+ puts "Running #{action.to_s.capitalize} #{before_or_after.to_s.upcase} routines".att(:bright)
637
+ machines.each do |machine|
638
+ puts "Machine Group: #{machine_group}"
639
+ puts "Hostname: #{machine[:dns_name]}"
640
+
641
+ rscripts = @config.routines.find_deferred(@global.environment, @global.role, action, before_or_after) || []
642
+ rscripts = [rscripts] unless rscripts.is_a?(Array)
643
+
644
+ puts "No scripts defined." if !rscripts || rscripts.empty?
645
+
646
+ rscripts.each do |rscript|
647
+ user, script = rscript.shift
648
+
649
+ switch_user(user) # scp and ssh will run as this user
650
+
651
+ puts "Transfering #{config_file}..."
652
+ scp do |scp|
653
+ scp.upload!(tf.path, "~/#{config_file}") do |ch, name, sent, total|
654
+ "#{name}: #{sent}/#{total}"
655
+ end
656
+ end
657
+ ssh do |session|
658
+ puts "Running #{script}...".att(:bright)
659
+ session.exec!("chmod 700 ~/#{config_file}")
660
+ session.exec!("chmod 700 #{script}")
661
+ puts session.exec!("#{script}")
662
+
663
+ puts "Removing remote copy of #{config_file}..."
664
+ session.exec!("rm ~/#{config_file}")
665
+ end
666
+ puts $/
667
+ end
668
+ end
669
+
670
+ tf.delete # remove local copy of config_file
671
+ #switch_user # return to the requested user
672
+ end
203
673
 
204
674
  # Print a default header to the screen for every command.
205
675
  # +cmd+ is the name of the command current running.
206
676
  def print_header(cmd=nil)
207
- title = "RUDY v#{Rudy::VERSION}"
208
- now_utc = Time.now.utc
677
+ title = "RUDY v#{Rudy::VERSION}" unless @global.quiet
678
+ now_utc = Time.now.utc.strftime("%Y-%m-%d %H:%M:%S")
209
679
  criteria = []
210
680
  [:zone, :environment, :role, :position].each do |n|
211
- val = instance_variable_get("@#{n}")
212
- criteria << "#{n.to_s.color :normal}:#{val.color :bright}"
681
+ val = @global.send(n)
682
+ next unless val
683
+ criteria << "#{n.to_s.slice(0,1).att :normal}:#{val.att :bright}"
213
684
  end
214
- puts '%s -- %s' % [title, criteria.join(", ")]
215
- puts 'UTC: %s' % now_utc.strftime("%Y-%m-%d %H:%M:%S")
216
- puts
217
-
218
- if (@environment == "prod")
219
- msg = %q(=======================================================
220
- =======================================================
221
- !!!!!!!!! YOU ARE PLAYING WITH PRODUCTION !!!!!!!!!
222
- =======================================================
223
- =======================================================)
224
- puts msg.color(:bright, :red, :bg_white), $/ unless @quiet
685
+ puts '%s -- %s UTC' % [title, now_utc] unless @global.quiet
686
+ puts '[%s]' % criteria.join(" ") unless @global.quiet
687
+
688
+ puts unless @global.quiet
689
+
690
+ if (@global.environment == "prod")
691
+ msg = without_indent %q(
692
+ =======================================================
693
+ =======================================================
694
+ !!!!!!!!! YOU ARE PLAYING WITH PRODUCTION !!!!!!!!!
695
+ =======================================================
696
+ =======================================================)
697
+ puts msg.colour(:red).bgcolour(:white).att(:bright), $/ unless @global.quiet
225
698
 
226
699
  end
227
700
 
228
701
  if Rudy.in_situ?
229
702
  msg = %q(============ THIS IS EC2 ============)
230
- puts msg.color(:bright, :blue, :bg_white), $/ unless @quiet
703
+ puts msg.colour(:blue).bgcolour(:white).att(:bright), $/ unless @global.quiet
231
704
  end
232
705
 
233
706
  end
@@ -236,19 +709,10 @@ module Rudy
236
709
 
237
710
  end
238
711
 
239
- # Returns a hash of info for the requested machine. If the requested machine
240
- # is not running, it will raise an exception.
241
- def find_current_machine
242
- machine_list = @ec2.instances.list(machine_group)
243
- machine = machine_list.values.first # NOTE: Only one machine per group, for now...
244
- raise "There's no machine running in #{machine_group}" unless machine
245
- raise "The primary machine in #{machine_group} is not in a running state" unless machine[:aws_state] == 'running'
246
712
 
247
- machine
248
- end
249
713
 
250
714
 
251
- def group_metadata(env=@environment, role=@role)
715
+ def group_metadata(env=@global.environment, role=@global.role)
252
716
  query = "['environment' = '#{env}'] intersection ['role' = '#{role}']"
253
717
  @sdb.query_with_attributes(RUDY_DOMAIN, query)
254
718
  end
@@ -258,7 +722,7 @@ module Rudy
258
722
  # +inst+ is a hash
259
723
  def print_instance(inst)
260
724
  puts '-'*60
261
- puts "Instance: #{inst[:aws_instance_id]} (AMI: #{inst[:aws_image_id]})"
725
+ puts "Instance: #{inst[:aws_instance_id].att(:bright)} (AMI: #{inst[:aws_image_id]})"
262
726
  [:aws_state, :dns_name, :private_dns_name, :aws_availability_zone, :aws_launch_time, :ssh_key_name].each do |key|
263
727
  printf(" %22s: %s#{$/}", key, inst[key]) if inst[key]
264
728
  end
@@ -268,7 +732,7 @@ module Rudy
268
732
 
269
733
  def print_image(img)
270
734
  puts '-'*60
271
- puts "Image: #{img[:aws_id]}"
735
+ puts "Image: #{img[:aws_id].att(:bright)}"
272
736
  img.each_pair do |key, value|
273
737
  printf(" %22s: %s#{$/}", key, value) if value
274
738
  end
@@ -277,7 +741,7 @@ module Rudy
277
741
 
278
742
  def print_disk(disk, backups=[])
279
743
  puts '-'*60
280
- puts "Disk: #{disk.name}"
744
+ puts "Disk: #{disk.name.att(:bright)}"
281
745
  puts disk.to_s
282
746
  puts "#{backups.size} most recent backups:", backups.collect { |back| "#{back.nice_time} (#{back.awsid})" }
283
747
  puts
@@ -286,7 +750,7 @@ module Rudy
286
750
 
287
751
  def print_volume(vol, disk)
288
752
  puts '-'*60
289
- puts "Volume: #{vol[:aws_id]} (disk: #{disk.name})"
753
+ puts "Volume: #{vol[:aws_id].att(:bright)} (disk: #{disk.name if disk})"
290
754
  vol.each_pair do |key, value|
291
755
  printf(" %22s: %s#{$/}", key, value) if value
292
756
  end
@@ -297,7 +761,7 @@ module Rudy
297
761
  # +group+ is an OpenStruct
298
762
  def print_group(group)
299
763
  puts '-'*60
300
- puts "%12s: %s" % ['GROUP', group[:aws_group_name]]
764
+ puts "%12s: %s" % ['GROUP', group[:aws_group_name].att(:bright)]
301
765
  puts
302
766
 
303
767
  group_ip = {}
@@ -317,7 +781,61 @@ module Rudy
317
781
  end
318
782
  end
319
783
 
320
-
784
+ def init_config_dir
785
+ unless File.exists?(RUDY_CONFIG_DIR)
786
+ puts "Creating #{RUDY_CONFIG_DIR}"
787
+ Dir.mkdir(RUDY_CONFIG_DIR, 0700)
788
+ end
789
+
790
+ unless File.exists?(RUDY_CONFIG_FILE)
791
+ puts "Creating #{RUDY_CONFIG_FILE}"
792
+ rudy_config = without_indent %Q{
793
+ # Amazon Web Services
794
+ # Account access indentifiers.
795
+ awsinfo do
796
+ account ""
797
+ accesskey ""
798
+ secretkey ""
799
+ privatekey "~/path/2/pk-xxxx.pem"
800
+ cert "~/path/2/cert-xxxx.pem"
801
+ end
802
+
803
+ # Machine Configuration
804
+ # Specify your private keys here. These can be defined globally
805
+ # or by environment and role like in machines.rb.
806
+ machines do
807
+ users do
808
+ root :keypair => "path/2/root-private-key"
809
+ end
810
+ end
811
+
812
+ # Routine Configuration
813
+ # Define stuff here that you don't want to be stored in version control.
814
+ routines do
815
+ config do
816
+ # ...
817
+ end
818
+ end
819
+
820
+ # Global Defaults
821
+ # Define the values to use unless otherwise specified on the command-line.
822
+ defaults do
823
+ region "us-east-1"
824
+ zone "us-east-1b"
825
+ environment "stage"
826
+ role "app"
827
+ position "01"
828
+ user ENV['USER']
829
+ end
830
+ }
831
+ write_to_file(RUDY_CONFIG_FILE, rudy_config, 'w')
832
+ end
833
+
834
+ #puts "Creating SimpleDB domain called #{RUDY_DOMAIN}"
835
+ #@sdb.domains.create(RUDY_DOMAIN)
836
+ end
321
837
  end
322
838
  end
323
- end
839
+ end
840
+
841
+