judo 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/server.rb DELETED
@@ -1,491 +0,0 @@
1
- ### NEEDED for new gem launch
2
-
3
- ### [X] judo init (2 hrs)
4
- ### [X] implement real default config - remove special case code (3 hrs)
5
- ### [X] refactor keypair.pem setup (3 hrs)
6
- ### [X] version in the db - require upgrade of gem if db version ahead (1 hr)
7
- ### [X] implement multiple security groups (1 hr)
8
- ### [-] complete slug compile - load into s3 (4 hrs)
9
- ### [X] compile and put in s3
10
- ### [X] attach and increment version number
11
- ### [X] list version number on "judo list"
12
- ### [X] update kuzushi to pull down a compiled tar.gz
13
- ### [X] error if version is blank
14
- ### [ ] two phase delete (1 hr)
15
- ### [-] refactor availability_zone (2 hrs)
16
- ### [ ] pick availability zone from config "X":"Y" or "X":["Y","Z"]
17
- ### [ ] assign to state on creation ( could delay till volume creation )
18
- ### [ ] implement auto security_group creation and setup (6 hrs)
19
- ### [ ] write some examples - simple postgres/redis/couchdb server (5hrs)
20
- ### [ ] write new README (4 hrs)
21
- ### [ ] bind kuzushi gem version version
22
- ### [ ] realase new gem! (1 hr)
23
-
24
- ### [ ] user a logger service (1 hr)
25
- ### [ ] write specs (5 hr)
26
-
27
- ### Error Handling
28
- ### [ ] no availability zone before making disks
29
- ### [ ] security group does not exists
30
-
31
- ### Do Later
32
- ### [ ] use amazon's new conditional write tools so we never have problems from concurrent updates
33
- ### [ ] is thor really what we want to use here?
34
- ### [ ] need to be able to pin a config to a version of kuzushi - gem updates can/will break a lot of things
35
- ### [ ] I want a "judo monitor" command that will make start servers if they go down, and poke a listed port to make sure a service is listening, would be cool if it also detects wrong ami, wrong secuirity group, missing/extra volumes, missing/extra elastic_ip - might not want to force a reboot quite yet in these cases
36
- ### [ ] Implement "judo snapshot [NAME]" to take a snapshot of the ebs's blocks
37
- ### [ ] ruby 1.9.1 support
38
- ### [ ] find a good way to set the hostname or prompt to :name
39
- ### [ ] remove fog/s3 dependancy
40
- ### [ ] enforce template files end in .erb to make room for other possible templates as defined by the extensions
41
- ### [ ] zerigo integration for automatic DNS setup
42
- ### [ ] How cool would it be if this was all reimplemented in eventmachine and could start lots of boxes in parallel? Would need to evented AWS api calls... Never seen a library to do that - would have to write our own... "Fog Machine?"
43
-
44
- module Judo
45
- class Server
46
- attr_accessor :name, :group
47
-
48
- def initialize(name, group)
49
- @name = name
50
- @group = group
51
- end
52
-
53
- def domain
54
- self.class.domain
55
- end
56
-
57
- def self.domain
58
- "judo_servers"
59
- end
60
-
61
- def sdb
62
- Judo::Config.sdb
63
- end
64
-
65
- def fetch_state
66
- Judo::Config.sdb.get_attributes(domain, name)[:attributes]
67
- end
68
-
69
- def self.fetch_all
70
- @@state = {}
71
- Judo::Config.sdb.select("select * from #{domain}")[:items].each do |group|
72
- group.each do |key,val|
73
- @@state[key] = val
74
- end
75
- end
76
- @@state.map { |k,v| Server.new(k,v["group"].first) }
77
- end
78
-
79
- def self.all
80
- @@all ||= fetch_all
81
- end
82
-
83
- def super_state
84
- @@state ||= {}
85
- end
86
-
87
- def state
88
- super_state[name] ||= fetch_state
89
- end
90
-
91
- def get(key)
92
- state[key] && [state[key]].flatten.first
93
- end
94
-
95
- def instance_id
96
- get "instance_id"
97
- end
98
-
99
- def elastic_ip
100
- get "elastic_ip"
101
- end
102
-
103
- def version_desc
104
- return "" unless running?
105
- if version == group.version
106
- "v#{version}"
107
- else
108
- "v#{version}/#{group.version}"
109
- end
110
- end
111
-
112
- def version
113
- get("version").to_i
114
- end
115
-
116
- def virgin?
117
- get("virgin").to_s == "true" ## I'm going to set it to true and it will come back from the db as "true" -> could be "false" or false or nil also
118
- end
119
-
120
- def secret
121
- get "secret"
122
- end
123
-
124
- def volumes
125
- Hash[ (state["volumes"] || []).map { |a| a.split(":") } ]
126
- end
127
-
128
- def update(attrs)
129
- sdb.put_attributes(domain, name, attrs, :replace)
130
- state.merge! attrs
131
- end
132
-
133
- def add(key, value)
134
- sdb.put_attributes(domain, name, { key => value })
135
- (state[key] ||= []) << value
136
- end
137
-
138
- def remove(key, value = nil)
139
- if value
140
- sdb.delete_attributes(domain, name, key => value)
141
- state[key] - [value]
142
- else
143
- sdb.delete_attributes(domain, name, [ key ])
144
- state.delete(key)
145
- end
146
- end
147
-
148
- def delete
149
- group.delete_server(self)
150
- sdb.delete_attributes(domain, name)
151
- end
152
-
153
- ######## end simple DB access #######
154
-
155
- def instance_size
156
- config["instance_size"]
157
- end
158
-
159
- def config
160
- group.config
161
- end
162
-
163
- def to_s
164
- "#{group}:#{name}"
165
- end
166
-
167
- def allocate_resources
168
- if config["volumes"]
169
- [config["volumes"]].flatten.each do |volume_config|
170
- device = volume_config["device"]
171
- if volume_config["media"] == "ebs"
172
- size = volume_config["size"]
173
- if not volumes[device]
174
- task("Creating EC2 Volume #{device} #{size}") do
175
- ### EC2 create_volume
176
- volume_id = Judo::Config.ec2.create_volume(nil, size, config["availability_zone"])[:aws_id]
177
- add_volume(volume_id, device)
178
- end
179
- else
180
- puts "Volume #{device} already exists."
181
- end
182
- else
183
- puts "device #{device || volume_config["mount"]} is not of media type 'ebs', skipping..."
184
- end
185
- end
186
- end
187
-
188
- begin
189
- if config["elastic_ip"] and not elastic_ip
190
- ### EC2 allocate_address
191
- task("Adding an elastic ip") do
192
- ip = Judo::Config.ec2.allocate_address
193
- add_ip(ip)
194
- end
195
- end
196
- rescue Aws::AwsError => e
197
- if e.message =~ /AddressLimitExceeded/
198
- abort "Failed to allocate ip address: Limit Exceeded"
199
- else
200
- raise
201
- end
202
- end
203
- end
204
-
205
- def task(msg, &block)
206
- self.class.task(msg, &block)
207
- end
208
-
209
- def self.task(msg, &block)
210
- printf "---> %-24s ", "#{msg}..."
211
- STDOUT.flush
212
- start = Time.now
213
- result = block.call
214
- result = "done" unless result.is_a? String
215
- finish = Time.now
216
- time = sprintf("%0.1f", finish - start)
217
- puts "#{result} (#{time}s)"
218
- result
219
- end
220
-
221
- def has_ip?
222
- !!elastic_ip
223
- end
224
-
225
- def has_volumes?
226
- not volumes.empty?
227
- end
228
-
229
- def ec2_volumes
230
- return [] if volumes.empty?
231
- Judo::Config.ec2.describe_volumes( volumes.values )
232
- end
233
-
234
- def remove_ip
235
- Judo::Config.ec2.release_address(elastic_ip) rescue nil
236
- remove "elastic_ip"
237
- end
238
-
239
- def destroy
240
- stop if running?
241
- ### EC2 release_address
242
- task("Deleting Elastic Ip") { remove_ip } if has_ip?
243
- volumes.each { |dev,v| remove_volume(v,dev) }
244
- task("Destroying server #{name}") { delete }
245
- end
246
-
247
- def ec2_state
248
- ec2_instance[:aws_state] rescue "offline"
249
- end
250
-
251
- def ec2_instance
252
- ### EC2 describe_instances
253
- @@ec2_list ||= Config.ec2.describe_instances
254
- @@ec2_list.detect { |e| e[:aws_instance_id] == instance_id } or {}
255
- end
256
-
257
- def running?
258
- ## other options are "terminated" and "nil"
259
- ["pending", "running", "shutting_down", "degraded"].include?(ec2_state)
260
- end
261
-
262
- def start
263
- abort "Already running" if running?
264
- abort "No config has been commited yet, type 'judo commit'" unless group.version > 0
265
- task("Starting server #{name}") { launch_ec2 }
266
- task("Acquire hostname") { wait_for_hostname }
267
- task("Wait for ssh") { wait_for_ssh }
268
- task("Attaching ip") { attach_ip } if elastic_ip
269
- task("Attaching volumes") { attach_volumes } if has_volumes?
270
- end
271
-
272
- def restart
273
- stop if running?
274
- start
275
- end
276
-
277
- def generic_name?
278
- name =~ /^#{group}[.]\d*$/
279
- end
280
-
281
- def generic?
282
- volumes.empty? and not has_ip? and generic_name?
283
- end
284
-
285
- def stop
286
- abort "not running" unless running?
287
- ## EC2 terminate_isntaces
288
- task("Terminating instance") { Config.ec2.terminate_instances([ instance_id ]) }
289
- task("Wait for volumes to detach") { wait_for_volumes_detached } if volumes.size > 0
290
- remove "instance_id"
291
- end
292
-
293
- def launch_ec2
294
- # validate
295
-
296
- ## EC2 launch_instances
297
- ud = user_data
298
- debug(ud)
299
- result = Config.ec2.launch_instances(ami,
300
- :instance_type => config["instance_size"],
301
- :availability_zone => config["availability_zone"],
302
- :key_name => config["key_name"],
303
- :group_ids => security_groups,
304
- :user_data => ud).first
305
-
306
- update "instance_id" => result[:aws_instance_id], "virgin" => false, "version" => group.version
307
- end
308
-
309
- def debug(str)
310
- return unless ENV['JUDO_DEBUG'] == "1"
311
- puts "<JUDO_DEBUG>#{str}</JUDO_DEBUG>"
312
- end
313
-
314
- def security_groups
315
- [ config["security_group"] ].flatten
316
- end
317
-
318
- def console_output
319
- abort "not running" unless running?
320
- Config.ec2.get_console_output(instance_id)[:aws_output]
321
- end
322
-
323
- def ami
324
- ia32? ? config["ami32"] : config["ami64"]
325
- end
326
-
327
- def ia32?
328
- ["m1.small", "c1.medium"].include?(instance_size)
329
- end
330
-
331
- def ia64?
332
- not ia32?
333
- end
334
-
335
- def hostname
336
- ec2_instance[:dns_name] == "" ? nil : ec2_instance[:dns_name]
337
- end
338
-
339
- def wait_for_hostname
340
- loop do
341
- reload
342
- return hostname if hostname
343
- sleep 1
344
- end
345
- end
346
-
347
- def wait_for_volumes_detached
348
- loop do
349
- break if ec2_volumes.reject { |v| v[:aws_status] == "available" }.empty?
350
- sleep 2
351
- end
352
- end
353
-
354
- def wait_for_termination
355
- loop do
356
- reload
357
- break if ec2_instance[:aws_state] == "terminated"
358
- sleep 1
359
- end
360
- end
361
-
362
- def wait_for_ssh
363
- abort "not running" unless running?
364
- loop do
365
- begin
366
- Timeout::timeout(4) do
367
- TCPSocket.new(hostname, 22)
368
- return
369
- end
370
- rescue SocketError, Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
371
- end
372
- end
373
- end
374
-
375
- def add_ip(public_ip)
376
- update "elastic_ip" => public_ip
377
- attach_ip
378
- end
379
-
380
- def attach_ip
381
- return unless running? and elastic_ip
382
- ### EC2 associate_address
383
- Config.ec2.associate_address(instance_id, elastic_ip)
384
- end
385
-
386
- def dns_name
387
- return nil unless elastic_ip
388
- `dig +short -x #{elastic_ip}`.strip
389
- end
390
-
391
- def attach_volumes
392
- return unless running?
393
- volumes.each do |device,volume_id|
394
- ### EC2 attach_volume
395
- Config.ec2.attach_volume(volume_id, instance_id, device)
396
- end
397
- end
398
-
399
- def remove_volume(volume_id, device)
400
- task("Deleting #{device} #{volume_id}") do
401
- ### EC2 delete_volume
402
- Judo::Config.ec2.delete_volume(volume_id)
403
- remove "volumes", "#{device}:#{volume_id}"
404
- end
405
- end
406
-
407
- def add_volume(volume_id, device)
408
- abort("Server already has a volume on that device") if volumes[device]
409
-
410
- add "volumes", "#{device}:#{volume_id}"
411
-
412
- Config.ec2.attach_volume(volume_id, instance_id, device) if running?
413
-
414
- volume_id
415
- end
416
-
417
- def connect_ssh
418
- abort "not running" unless running?
419
- system "chmod 600 #{group.keypair_file}"
420
- system "ssh -i #{group.keypair_file} #{config["user"]}@#{hostname}"
421
- end
422
-
423
- def self.commit
424
- Config.group_dirs.each do |group_dir|
425
- group = File.basename(group_dir)
426
- next if Config.group and Config.group != group
427
- puts "commiting #{group}"
428
- doc = Config.couchdb.get(group) rescue {}
429
- config = Config.read_config(group)
430
- config['_id'] = group
431
- config['_rev'] = doc['_rev'] if doc.has_key?('_rev')
432
- response = Config.couchdb.save_doc(config)
433
- doc = Config.couchdb.get(response['id'])
434
-
435
- # walk subdirs and save as _attachments
436
- ['files', 'templates', 'packages', 'scripts'].each { |subdir|
437
- Dir["#{group_dir}/#{subdir}/*"].each do |f|
438
- puts "storing attachment #{f}"
439
- doc.put_attachment("#{subdir}/#{File.basename(f)}", File.read(f))
440
- end
441
- }
442
- end
443
- end
444
-
445
- def ip
446
- hostname || config["state_ip"]
447
- end
448
-
449
- def reload
450
- @@ec2_list = nil
451
- super_state.delete(name)
452
- end
453
-
454
- def user_data
455
- <<USER_DATA
456
- #!/bin/sh
457
-
458
- export DEBIAN_FRONTEND="noninteractive"
459
- export DEBIAN_PRIORITY="critical"
460
- export SECRET='#{secret}'
461
- apt-get update
462
- apt-get install ruby rubygems ruby-dev irb libopenssl-ruby libreadline-ruby -y
463
- gem install kuzushi --no-rdoc --no-ri
464
- GEM_BIN=`ruby -r rubygems -e "puts Gem.bindir"`
465
- echo "$GEM_BIN/kuzushi #{virgin? && "init" || "start"} '#{url}'" > /var/log/kuzushi.log
466
- $GEM_BIN/kuzushi #{virgin? && "init" || "start"} '#{url}' >> /var/log/kuzushi.log 2>&1
467
- USER_DATA
468
- end
469
-
470
- def url
471
- @url ||= group.s3_url
472
- end
473
-
474
- def validate
475
- ### EC2 create_security_group
476
- Judo::Config.create_security_group
477
-
478
- ### EC2 desctibe_key_pairs
479
- k = Judo::Config.ec2.describe_key_pairs.detect { |kp| kp[:aws_key_name] == config["key_name"] }
480
-
481
- if k.nil?
482
- if config["key_name"] == "judo"
483
- Judo::Config.create_keypair
484
- else
485
- raise "cannot use key_pair #{config["key_name"]} b/c it does not exist"
486
- end
487
- end
488
- end
489
-
490
- end
491
- end