judo 0.0.9 → 0.1.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/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