Class: Judo::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/judo/server.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(base, name, group, version = nil) ⇒ Server

Returns a new instance of Server.



5
6
7
8
9
# File 'lib/judo/server.rb', line 5

def initialize(base, name, group, version = nil)
  @base = base
  @name = name
  @group_name = group
end

Instance Attribute Details

#nameObject

Returns the value of attribute name.



3
4
5
# File 'lib/judo/server.rb', line 3

def name
  @name
end

Class Method Details

.commitObject



466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/judo/server.rb', line 466

def self.commit
  ## FIXME
  Config.group_dirs.each do |group_dir|
    group = File.basename(group_dir)
    next if Config.group and Config.group != group
    puts "commiting #{group}"
    doc = Config.couchdb.get(group) rescue {}
    config = Config.read_config(group)
    config['_id'] = group
    config['_rev'] = doc['_rev'] if doc.has_key?('_rev')
    response = Config.couchdb.save_doc(config)
    doc = Config.couchdb.get(response['id'])

    # walk subdirs and save as _attachments
    ['files', 'templates', 'packages', 'scripts'].each { |subdir|
      Dir["#{group_dir}/#{subdir}/*"].each do |f|
        puts "storing attachment #{f}"
        doc.put_attachment("#{subdir}/#{File.basename(f)}", File.read(f))
      end
    }
  end
end

.domainObject



122
123
124
# File 'lib/judo/server.rb', line 122

def self.domain
  "judo_servers"
end

.task(msg, &block) ⇒ Object



226
227
228
229
230
231
232
233
234
235
236
# File 'lib/judo/server.rb', line 226

def self.task(msg, &block)
  printf "---> %-24s ", "#{msg}..."
  STDOUT.flush
  start = Time.now
  result = block.call
  result = "done" unless result.is_a? String
  finish = Time.now
  time = sprintf("%0.1f", finish - start)
  puts "#{result} (#{time}s)"
  result
end

Instance Method Details

#<=>(s) ⇒ Object



543
544
545
# File 'lib/judo/server.rb', line 543

def <=>(s)
  [group.name, name] <=> [s.group.name, s.name]
end

#add(key, value) ⇒ Object



131
132
133
134
# File 'lib/judo/server.rb', line 131

def add(key, value)
  @base.sdb.put_attributes(self.class.domain, name, { key => value })
  (state[key] ||= []) << value
end

#add_ip(public_ip) ⇒ Object



418
419
420
421
# File 'lib/judo/server.rb', line 418

def add_ip(public_ip)
  update "elastic_ip" => public_ip
  attach_ip
end

#add_volume(volume_id, device) ⇒ Object



450
451
452
453
454
455
456
457
458
# File 'lib/judo/server.rb', line 450

def add_volume(volume_id, device)
  invalid("Server already has a volume on that device") if volumes[device]

  add "volumes", "#{device}:#{volume_id}"

  @base.ec2.attach_volume(volume_id, instance_id, device) if running?

  volume_id
end

#allocate_disk(snapshots) ⇒ Object



165
166
167
168
169
170
171
# File 'lib/judo/server.rb', line 165

def allocate_disk(snapshots)
  if snapshots
    clone_snapshots(snapshots)
  else
    create_volumes
  end
end

#allocate_ipObject



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/judo/server.rb', line 204

def allocate_ip
  begin
    if config["elastic_ip"] and not elastic_ip
      ### EC2 allocate_address
      task("Adding an elastic ip") do
        ip = @base.ec2.allocate_address
        add_ip(ip)
      end
    end
  rescue Aws::AwsError => e
    if e.message =~ /AddressLimitExceeded/
      invalid "Failed to allocate ip address: Limit Exceeded"
    else
      raise
    end
  end
end

#amiObject



351
352
353
# File 'lib/judo/server.rb', line 351

def ami
  ia32? ? config["ami32"] : config["ami64"]
end

#attach_ipObject



423
424
425
426
427
# File 'lib/judo/server.rb', line 423

def attach_ip
  return unless running? and elastic_ip
  ### EC2 associate_address
  @base.ec2.associate_address(instance_id, elastic_ip)
end

#attach_volumesObject



434
435
436
437
438
439
440
# File 'lib/judo/server.rb', line 434

def attach_volumes
  return unless running?
  volumes.each do |device,volume_id|
    ### EC2 attach_volume
    @base.ec2.attach_volume(volume_id, instance_id, device)
  end
end

#cloneObject



98
99
100
# File 'lib/judo/server.rb', line 98

def clone
  get("clone")
end

#clone_snapshots(snapshots) ⇒ Object



173
174
175
176
177
178
179
180
# File 'lib/judo/server.rb', line 173

def clone_snapshots(snapshots)
  snapshots.each do |device,snap_id| 
    task("Creating EC2 Volume #{device} from #{snap_id}") do
      volume_id = @base.ec2.create_volume(snap_id, nil, config["availability_zone"])[:aws_id]
      add_volume(volume_id, device)
    end
  end
end

#cloned?Boolean

Returns:

  • (Boolean)


102
103
104
# File 'lib/judo/server.rb', line 102

def cloned?
  !!clone
end

#configObject



157
158
159
# File 'lib/judo/server.rb', line 157

def config
  group.config
end

#connect_sshObject



460
461
462
463
464
# File 'lib/judo/server.rb', line 460

def connect_ssh
  wait_for_ssh
  system "chmod 600 #{group.keypair_file}"
  system "ssh -i #{group.keypair_file} #{config["user"]}@#{hostname}"
end

#console_outputObject



346
347
348
349
# File 'lib/judo/server.rb', line 346

def console_output
  invalid "not running" unless running?
  @base.ec2.get_console_output(instance_id)[:aws_output]
end

#create(options) ⇒ Object

Raises:



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/judo/server.rb', line 11

def create(options)
  raise JudoError, "no group specified" unless @group_name

  snapshots = options[:snapshots]
  note = options[:note]

  version = options[:version]
  version ||= group.version

  if @name.nil?
    index = @base.servers.map { |s| (s.name =~ /^#{s.group.name}.(\d*)$/); $1.to_i }.sort.last.to_i + 1
    @name = "#{group.name}.#{index}"
  end

  raise JudoError, "there is already a server named #{name}" if @base.servers.detect { |s| s.name == @name and s != self}

  task("Creating server #{name}") do
    update "name" => name, "group" => @group_name, "note" => note, "virgin" => true, "secret" => rand(2 ** 128).to_s(36), "version" => version
    @base.sdb.put_attributes("judo_config", "groups", @group_name => name)
  end

  allocate_disk(snapshots)
  allocate_ip

  self
end

#create_volumesObject



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/judo/server.rb', line 182

def create_volumes
  if config["volumes"]
    [config["volumes"]].flatten.each do |volume_config|
      device = volume_config["device"]
      if volume_config["media"] == "ebs"
        size = volume_config["size"]
        if not volumes[device]
          task("Creating EC2 Volume #{device} #{size}") do
            ### EC2 create_volume
            volume_id = @base.ec2.create_volume(nil, size, config["availability_zone"])[:aws_id]
            add_volume(volume_id, device)
          end
        else
          puts "Volume #{device} already exists."
        end
      else
        puts "device #{device || volume_config["mount"]} is not of media type 'ebs', skipping..."
      end
    end
  end
end

#debug(str) ⇒ Object



337
338
339
340
# File 'lib/judo/server.rb', line 337

def debug(str)
  return unless ENV['JUDO_DEBUG'] == "1"
  puts "<JUDO_DEBUG>#{str}</JUDO_DEBUG>"
end

#deleteObject



146
147
148
149
# File 'lib/judo/server.rb', line 146

def delete
  group.delete_server(self) if group
  @base.sdb.delete_attributes(self.class.domain, name)
end

#destroyObject



256
257
258
259
260
261
262
# File 'lib/judo/server.rb', line 256

def destroy
  stop if running?
  ### EC2 release_address
  task("Deleting Elastic Ip") { remove_ip } if has_ip?
  volumes.each { |dev,v| remove_volume(v,dev) }
  task("Destroying server #{name}") { delete }
end

#dns_nameObject



429
430
431
432
# File 'lib/judo/server.rb', line 429

def dns_name
  return nil unless elastic_ip
  `dig +short -x #{elastic_ip}`.strip
end

#ec2_instanceObject



268
269
270
271
# File 'lib/judo/server.rb', line 268

def ec2_instance
  ### EC2 describe_instances
  @base.ec2_instances.detect { |e| e[:aws_instance_id] == instance_id } or {}
end

#ec2_instance_typeObject



489
490
491
# File 'lib/judo/server.rb', line 489

def ec2_instance_type
  ec2_instance[:aws_instance_type] rescue nil
end

#ec2_stateObject



264
265
266
# File 'lib/judo/server.rb', line 264

def ec2_state
  ec2_instance[:aws_state] rescue "offline"
end

#ec2_volumesObject



246
247
248
249
# File 'lib/judo/server.rb', line 246

def ec2_volumes
  return [] if volumes.empty?
  @base.ec2.describe_volumes( volumes.values )
end

#elastic_ipObject



58
59
60
# File 'lib/judo/server.rb', line 58

def elastic_ip
  get "elastic_ip"
end

#fetch_stateObject



42
43
44
# File 'lib/judo/server.rb', line 42

def fetch_state
  @base.sdb.get_attributes(self.class.domain, name)[:attributes]
end

#force_detach_volumesObject



305
306
307
308
309
310
311
# File 'lib/judo/server.rb', line 305

def force_detach_volumes
  volumes.each do |device,volume_id| 
    task("Force detaching #{volume_id}") do
      @base.ec2.detach_volume(volume_id, instance_id, device, true) rescue nil
    end
  end
end

#generic?Boolean

Returns:

  • (Boolean)


297
298
299
# File 'lib/judo/server.rb', line 297

def generic?
  volumes.empty? and not has_ip? and generic_name?
end

#generic_name?Boolean

Returns:

  • (Boolean)


293
294
295
# File 'lib/judo/server.rb', line 293

def generic_name?
  name =~ /^#{group}[.]\d*$/
end

#get(key) ⇒ Object



50
51
52
# File 'lib/judo/server.rb', line 50

def get(key)
  state[key] && [state[key]].flatten.first
end

#groupObject



38
39
40
# File 'lib/judo/server.rb', line 38

def group
  @group ||= @base.groups.detect { |g| g.name == @group_name }
end

#has_ip?Boolean

Returns:

  • (Boolean)


238
239
240
# File 'lib/judo/server.rb', line 238

def has_ip?
  !!elastic_ip
end

#has_volumes?Boolean

Returns:

  • (Boolean)


242
243
244
# File 'lib/judo/server.rb', line 242

def has_volumes?
  not volumes.empty?
end

#hostnameObject



363
364
365
# File 'lib/judo/server.rb', line 363

def hostname
  ec2_instance[:dns_name] == "" ? nil : ec2_instance[:dns_name]
end

#ia32?Boolean

Returns:

  • (Boolean)


355
356
357
# File 'lib/judo/server.rb', line 355

def ia32?
  ["m1.small", "c1.medium"].include?(instance_size)
end

#ia64?Boolean

Returns:

  • (Boolean)


359
360
361
# File 'lib/judo/server.rb', line 359

def ia64?
  not ia32?
end

#instance_idObject



54
55
56
# File 'lib/judo/server.rb', line 54

def instance_id
  get "instance_id"
end

#instance_sizeObject

end simple DB access #######



153
154
155
# File 'lib/judo/server.rb', line 153

def instance_size
  config["instance_size"]
end

#invalid(str) ⇒ Object

Raises:



301
302
303
# File 'lib/judo/server.rb', line 301

def invalid(str)
  raise JudoInvalid, str
end

#ipObject



493
494
495
# File 'lib/judo/server.rb', line 493

def ip
  hostname || config["state_ip"]
end

#kuzushi_actionObject



82
83
84
85
86
87
88
89
90
91
92
# File 'lib/judo/server.rb', line 82

def kuzushi_action
  if virgin?
    if cloned?
      "start"
    else
      "init"
    end
  else
    "start"
  end
end

#launch_ec2Object



322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/judo/server.rb', line 322

def launch_ec2
#      validate

  ## EC2 launch_instances
  ud = user_data
  debug(ud)
  result = @base.ec2.launch_instances(ami,
    :instance_type => config["instance_size"],
    :availability_zone => config["availability_zone"],
    :key_name => config["key_name"],
    :group_ids => security_groups,
    :user_data => ud).first
  update "instance_id" => result[:aws_instance_id], "virgin" => false
end

#noteObject



94
95
96
# File 'lib/judo/server.rb', line 94

def note
  get("note")
end

#reloadObject



497
498
499
500
# File 'lib/judo/server.rb', line 497

def reload
  @base.reload_ec2_instances
  @base.servers_state.delete(name)
end

#remove(key, value = nil) ⇒ Object



136
137
138
139
140
141
142
143
144
# File 'lib/judo/server.rb', line 136

def remove(key, value = nil)
  if value
    @base.sdb.delete_attributes(self.class.domain, name, key => value)
    state[key] - [value]
  else
    @base.sdb.delete_attributes(self.class.domain, name, [ key ])
    state.delete(key)
  end
end

#remove_ipObject



251
252
253
254
# File 'lib/judo/server.rb', line 251

def remove_ip
  @base.ec2.release_address(elastic_ip) rescue nil
  remove "elastic_ip"
end

#remove_volume(volume_id, device) ⇒ Object



442
443
444
445
446
447
448
# File 'lib/judo/server.rb', line 442

def remove_volume(volume_id, device)
  task("Deleting #{device} #{volume_id}") do
    ### EC2 delete_volume
    @base.ec2.delete_volume(volume_id)
    remove "volumes", "#{device}:#{volume_id}"
  end
end

#restart(force = false) ⇒ Object



288
289
290
291
# File 'lib/judo/server.rb', line 288

def restart(force = false)
  stop(force) if running?
  start
end

#running?Boolean

Returns:

  • (Boolean)


273
274
275
276
# File 'lib/judo/server.rb', line 273

def running?
  ## other options are "terminated" and "nil"
  ["pending", "running", "shutting_down", "degraded"].include?(ec2_state)
end

#secretObject



110
111
112
# File 'lib/judo/server.rb', line 110

def secret
  get "secret"
end

#security_groupsObject



342
343
344
# File 'lib/judo/server.rb', line 342

def security_groups
  [ config["security_group"] ].flatten
end

#size_descObject



62
63
64
65
66
67
68
# File 'lib/judo/server.rb', line 62

def size_desc
  if not running? or ec2_instance_type == instance_size
    instance_size
  else
    "#{ec2_instance_type}/#{instance_size}"
  end
end

#snapshot(name) ⇒ Object



538
539
540
541
# File 'lib/judo/server.rb', line 538

def snapshot(name)
  snap = @base.new_snapshot(name, self.name)
  snap.create
end

#snapshotsObject



114
115
116
# File 'lib/judo/server.rb', line 114

def snapshots
  @base.snapshots.select { |s| s.server == self }
end

#start(new_version = nil) ⇒ Object



278
279
280
281
282
283
284
285
286
# File 'lib/judo/server.rb', line 278

def start(new_version = nil)
  invalid "Already running" if running?
  invalid "No config has been commited yet, type 'judo commit'" unless group.version > 0
  task("Updating server version")      { update_version(new_version) } if new_version
  task("Starting server #{name}")      { launch_ec2 }
  task("Wait for server")              { wait_for_running } if elastic_ip or has_volumes?
  task("Attaching ip")                 { attach_ip } if elastic_ip
  task("Attaching volumes")            { attach_volumes } if has_volumes?
end

#stateObject



46
47
48
# File 'lib/judo/server.rb', line 46

def state
  @base.servers_state[name] ||= fetch_state
end

#stop(force = false) ⇒ Object



313
314
315
316
317
318
319
320
# File 'lib/judo/server.rb', line 313

def stop(force = false)
  invalid "not running" unless running?
  ## EC2 terminate_isntaces
  task("Terminating instance") { @base.ec2.terminate_instances([ instance_id ]) }
  force_detach_volumes if force
  task("Wait for volumes to detach") { wait_for_volumes_detached } if volumes.size > 0
  remove "instance_id"
end

#task(msg, &block) ⇒ Object



222
223
224
# File 'lib/judo/server.rb', line 222

def task(msg, &block)
  @base.task(msg, &block)
end

#to_sObject



161
162
163
# File 'lib/judo/server.rb', line 161

def to_s
  "#{name}:#{@group_name}"
end

#update(attrs) ⇒ Object



126
127
128
129
# File 'lib/judo/server.rb', line 126

def update(attrs)
  @base.sdb.put_attributes(self.class.domain, name, attrs, :replace)
  state.merge! attrs
end

#update_version(new_version) ⇒ Object



78
79
80
# File 'lib/judo/server.rb', line 78

def update_version(new_version)
    update "version" => new_version
end

#urlObject



518
519
520
# File 'lib/judo/server.rb', line 518

def url
  @url ||= group.s3_url
end

#user_dataObject



502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
# File 'lib/judo/server.rb', line 502

def user_data
  <<USER_DATA
#!/bin/sh

export DEBIAN_FRONTEND="noninteractive"
export DEBIAN_PRIORITY="critical"
export SECRET='#{secret}'
apt-get update
apt-get install ruby rubygems ruby-dev irb libopenssl-ruby libreadline-ruby -y
gem install kuzushi --no-rdoc --no-ri
GEM_BIN=`ruby -r rubygems -e "puts Gem.bindir"`
echo "$GEM_BIN/kuzushi #{virgin? && "init" || "start"} '#{url}'" > /var/log/kuzushi.log
$GEM_BIN/kuzushi #{virgin? && "init" || "start"} '#{url}' >> /var/log/kuzushi.log 2>&1
USER_DATA
end

#validateObject



522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
# File 'lib/judo/server.rb', line 522

def validate
  ### EC2 create_security_group
  @base.create_security_group

  ### EC2 desctibe_key_pairs
  k = @base.ec2.describe_key_pairs.detect { |kp| kp[:aws_key_name] == config["key_name"] }

  if k.nil?
    if config["key_name"] == "judo"
      @base.create_keypair
    else
      raise "cannot use key_pair #{config["key_name"]} b/c it does not exist"
    end
  end
end

#versionObject



74
75
76
# File 'lib/judo/server.rb', line 74

def version
  get("version").to_i
end

#version_descObject



70
71
72
# File 'lib/judo/server.rb', line 70

def version_desc
  group.version_desc(version)
end

#virgin?Boolean

Returns:

  • (Boolean)


106
107
108
# File 'lib/judo/server.rb', line 106

def virgin?
  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
end

#volumesObject



118
119
120
# File 'lib/judo/server.rb', line 118

def volumes
  Hash[ (state["volumes"] || []).map { |a| a.split(":") } ]
end

#wait_for_hostnameObject



375
376
377
378
379
380
381
# File 'lib/judo/server.rb', line 375

def wait_for_hostname
  loop do
    reload
    return hostname if hostname
    sleep 1
  end
end

#wait_for_runningObject



367
368
369
370
371
372
373
# File 'lib/judo/server.rb', line 367

def wait_for_running
  loop do
    return if ec2_state == "running"
    reload
    sleep 1
  end
end

#wait_for_sshObject



404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/judo/server.rb', line 404

def wait_for_ssh
  invalid "not running" unless running?
  loop do
    begin
      reload
      Timeout::timeout(4) do
        TCPSocket.new(hostname, 22)
        return
      end
    rescue SocketError, Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
    end
  end
end

#wait_for_terminationObject



396
397
398
399
400
401
402
# File 'lib/judo/server.rb', line 396

def wait_for_termination
  loop do
    reload
    break if ec2_instance[:aws_state] == "terminated"
    sleep 1
  end
end

#wait_for_volumes_detachedObject



383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/judo/server.rb', line 383

def wait_for_volumes_detached
  begin
    Timeout::timeout(30) do
      loop do
        break if ec2_volumes.reject { |v| v[:aws_status] == "available" }.empty?
        sleep 2
      end
    end
  rescue Timeout::Error
    force_detach_volumes
  end
end