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.
- data/CHANGES.txt +53 -3
- data/README.rdoc +9 -5
- data/bin/rudy +115 -292
- data/bin/rudy-ec2 +107 -0
- data/lib/console.rb +322 -278
- data/lib/rudy.rb +78 -55
- data/lib/rudy/aws/ec2.rb +63 -5
- data/lib/rudy/command/addresses.rb +18 -13
- data/lib/rudy/command/backups.rb +175 -0
- data/lib/rudy/command/base.rb +664 -146
- data/lib/rudy/command/config.rb +77 -0
- data/lib/rudy/command/deploy.rb +12 -0
- data/lib/rudy/command/disks.rb +165 -195
- data/lib/rudy/command/environment.rb +42 -64
- data/lib/rudy/command/groups.rb +21 -19
- data/lib/rudy/command/images.rb +34 -19
- data/lib/rudy/command/instances.rb +46 -92
- data/lib/rudy/command/machines.rb +161 -0
- data/lib/rudy/command/metadata.rb +14 -30
- data/lib/rudy/command/release.rb +174 -0
- data/lib/rudy/command/volumes.rb +26 -10
- data/lib/rudy/config.rb +93 -0
- data/lib/rudy/metadata/backup.rb +1 -1
- data/lib/rudy/metadata/disk.rb +15 -50
- data/lib/rudy/scm/svn.rb +32 -21
- data/lib/rudy/utils.rb +2 -3
- data/lib/storable.rb +4 -0
- data/lib/tryouts.rb +40 -0
- data/rudy.gemspec +25 -9
- data/support/mailtest +40 -0
- data/support/rudy-ec2-startup +41 -15
- data/tryouts/console_tryout.rb +91 -0
- metadata +86 -11
- data/lib/drydock.rb +0 -524
- data/lib/rudy/command/stage.rb +0 -45
- data/lib/rudy/metadata/config.rb +0 -8
- data/lib/rudy/metadata/environment.rb +0 -0
data/lib/rudy/command/base.rb
CHANGED
@@ -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
|
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
|
-
@
|
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
|
-
@
|
75
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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 #{
|
101
|
-
raise "SSH key provided but cannot be found! (
|
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
|
-
(@
|
106
|
-
@
|
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
|
-
(@
|
120
|
+
(@global.accesskey && !@global.accesskey.empty? && @global.secretkey && !@global.secretkey.empty?)
|
111
121
|
end
|
112
122
|
|
113
|
-
def
|
114
|
-
|
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
|
-
|
118
|
-
|
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
|
-
|
122
|
-
|
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
|
-
|
126
|
-
|
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
|
-
#
|
130
|
-
|
131
|
-
|
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
|
135
|
-
[
|
215
|
+
def machine_group
|
216
|
+
[@global.environment, @global.role].join(RUDY_DELIM)
|
136
217
|
end
|
137
218
|
|
138
|
-
def
|
139
|
-
|
140
|
-
|
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
|
161
|
-
|
162
|
-
|
163
|
-
|
251
|
+
def wait_for_machine(id)
|
252
|
+
|
253
|
+
print "Waiting for #{id} to become available"
|
254
|
+
STDOUT.flush
|
164
255
|
|
165
|
-
|
166
|
-
|
256
|
+
while @ec2.instances.pending?(id)
|
257
|
+
sleep 2
|
258
|
+
print '.'
|
259
|
+
STDOUT.flush
|
167
260
|
end
|
168
261
|
|
169
|
-
|
170
|
-
|
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
|
-
|
289
|
+
puts "Running #{action.to_s.capitalize} DISK routines".att(:bright)
|
174
290
|
|
175
|
-
|
176
|
-
@
|
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
|
-
|
294
|
+
unless routines
|
295
|
+
puts "No #{action} disk routines for #{machine_group}"
|
296
|
+
return
|
297
|
+
end
|
180
298
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
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
|
-
|
192
|
-
|
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 =
|
212
|
-
|
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,
|
215
|
-
puts '
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
=======================================================
|
223
|
-
|
224
|
-
|
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.
|
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
|
+
|