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