solutious-rudy 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+