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, id, group_name, version = nil) ⇒ Server

Returns a new instance of Server.



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

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

Instance Attribute Details

#baseObject

Returns the value of attribute base.



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

def base
  @base
end

#group_nameObject

Returns the value of attribute group_name.



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

def group_name
  @group_name
end

#idObject

Returns the value of attribute id.



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

def id
  @id
end

Class Method Details

.task(msg, &block) ⇒ Object



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

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



585
586
587
# File 'lib/judo/server.rb', line 585

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

#add(key, value) ⇒ Object



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

def add(key, value)
  @base.sdb.put_attributes(@base.server_domain, id, { key => value })
  (state[key] ||= []) << value
end

#add_ip(public_ip) ⇒ Object



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

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

#add_volume(volume_id, device) ⇒ Object



493
494
495
496
497
498
499
500
501
# File 'lib/judo/server.rb', line 493

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



194
195
196
197
198
199
200
# File 'lib/judo/server.rb', line 194

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

#allocate_ipObject



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/judo/server.rb', line 232

def allocate_ip
  begin
    if config["elastic_ip"] and not elastic_ip
      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



386
387
388
# File 'lib/judo/server.rb', line 386

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

#attach_ipObject



466
467
468
469
# File 'lib/judo/server.rb', line 466

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

#attach_volumesObject

TODO: got this error once — /Library/Ruby/Gems/1.8/gems/aws-2.3.8/lib/awsbase/right_awsbase.rb:696:in ‘request_info_impl’: VolumeInUse: vol-09c44760 is already attached to an instance (Aws::AwsError)



479
480
481
482
483
484
# File 'lib/judo/server.rb', line 479

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

#bootObject



589
590
591
# File 'lib/judo/server.rb', line 589

def boot
  @boot || {}
end

#cloneObject



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

def clone
  get("clone")
end

#clone_snapshots(snapshots) ⇒ Object



202
203
204
205
206
207
208
209
# File 'lib/judo/server.rb', line 202

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, ec2_availability_zone)[:aws_id]
      add_volume(volume_id, device)
    end
  end
end

#cloned?Boolean

Returns:

  • (Boolean)


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

def cloned?
  !!clone
end

#configObject



182
183
184
# File 'lib/judo/server.rb', line 182

def config
  group.config(version)
end

#connect_sshObject



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

def connect_ssh
  wait_for_ssh
  @base.keypair_file do |file|
    system "ssh -q -i #{file} #{config["user"]}@#{hostname}"
  end
end

#console_outputObject



381
382
383
384
# File 'lib/judo/server.rb', line 381

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

#create(name, 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
37
# File 'lib/judo/server.rb', line 11

def create(name, options)
  raise JudoError, "group '#{group_name}' does not exists" unless group
  raise JudoError, "there is already a server named #{name}" if @base.servers.detect { |s| s.name == name and s != self}
  raise JudoError, "there is already a server with id #{id}" if @base.servers.detect { |s| s.id == id and s != self}

  virgin        = !(options[:virgin] == false)
  snapshots     = options[:snapshots]
        = options[:metadata]
  ip            = options[:elastic_ip]
  clone         = options[:clone]
  instance_type = options[:instance_type] || 'm1.small'
  version       = options[:version] || group.version

  task("Creating server #{name}") do
    update("name" => name,         "group" => group_name,
           "virgin" => virgin,     "elastic_ip" => ip,
           "version" => version,   "clone" => clone,
           "instance_type" => instance_type,
           "created_at" => Time.now.to_i)
    () if 
  end

  allocate_disk(snapshots)
  allocate_ip

  self
end

#create_volumesObject



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/judo/server.rb', line 211

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
            volume_id = @base.ec2.create_volume(nil, size, ec2_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

#created_atObject



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

def created_at
  Time.at(get("created_at").to_i)
end

#debug(str) ⇒ Object



370
371
372
373
374
375
# File 'lib/judo/server.rb', line 370

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

#deleteObject



172
173
174
# File 'lib/judo/server.rb', line 172

def delete
  @base.sdb.delete_attributes(@base.server_domain, id)
end

#destroyObject



283
284
285
286
287
288
# File 'lib/judo/server.rb', line 283

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

#displaynameObject



186
187
188
# File 'lib/judo/server.rb', line 186

def displayname
  "#{name}:#{group_name}"
end

#dns_nameObject



471
472
473
474
# File 'lib/judo/server.rb', line 471

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

#ec2_availability_zoneObject



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

def ec2_availability_zone
  ec2_instance[:aws_availability_zone]
end

#ec2_instanceObject



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

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

#ec2_instance_typeObject



524
525
526
# File 'lib/judo/server.rb', line 524

def ec2_instance_type
  ec2_instance[:aws_instance_type] rescue nil
end

#ec2_stateObject



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

def ec2_state
  ec2_instance[:aws_state]
end

#ec2_volumesObject



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

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

#elastic_ipObject



90
91
92
# File 'lib/judo/server.rb', line 90

def elastic_ip
  get "elastic_ip"
end

#fetch_stateObject



62
63
64
# File 'lib/judo/server.rb', line 62

def fetch_state
  @base.sdb.get_attributes(@base.server_domain, id)[:attributes]
end

#first_boot?Boolean

Returns:

  • (Boolean)


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

def first_boot?
  virgin?
end

#force_detach_volumesObject



339
340
341
342
343
344
345
# File 'lib/judo/server.rb', line 339

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)


331
332
333
# File 'lib/judo/server.rb', line 331

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

#generic_name?Boolean

Returns:

  • (Boolean)


327
328
329
# File 'lib/judo/server.rb', line 327

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

#get(key) ⇒ Object

begin simple DB access #######



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

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

#get_metadataObject



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

def 
   = get("metadata_json")
  JSON.load() if 
end

#groupObject



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

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

#has_ip?Boolean

Returns:

  • (Boolean)


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

def has_ip?
  !!elastic_ip
end

#has_volumes?Boolean

Returns:

  • (Boolean)


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

def has_volumes?
  not volumes.empty?
end

#hostnameObject



398
399
400
# File 'lib/judo/server.rb', line 398

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

#ia32?Boolean

Returns:

  • (Boolean)


390
391
392
# File 'lib/judo/server.rb', line 390

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

#ia64?Boolean

Returns:

  • (Boolean)


394
395
396
# File 'lib/judo/server.rb', line 394

def ia64?
  not ia32?
end

#instance_idObject



86
87
88
# File 'lib/judo/server.rb', line 86

def instance_id
  get "instance_id"
end

#instance_typeObject

end simple DB access #######



178
179
180
# File 'lib/judo/server.rb', line 178

def instance_type
 get("instance_type")
end

#invalid(str) ⇒ Object

Raises:



335
336
337
# File 'lib/judo/server.rb', line 335

def invalid(str)
  raise JudoInvalid, str
end

#ipObject



528
529
530
# File 'lib/judo/server.rb', line 528

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

#kuzushi_actionObject



114
115
116
117
118
119
120
# File 'lib/judo/server.rb', line 114

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

#launch_ec2Object



358
359
360
361
362
363
364
365
366
367
368
# File 'lib/judo/server.rb', line 358

def launch_ec2
  ud = user_data
  debug(ud)
  result = @base.ec2.launch_instances(ami,
    :instance_type => instance_type,
    :availability_zone => config["availability_zone"],
    :key_name => "judo",
    :group_ids => security_groups,
    :user_data => ud).first
  update "instance_id" => result[:aws_instance_id], "virgin" => false, "started_at" => Time.now.to_i
end

#metadataObject



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

def 
  @metadata ||= 
end

#nameObject



82
83
84
# File 'lib/judo/server.rb', line 82

def name
  get "name"
end

#reloadObject



532
533
534
535
# File 'lib/judo/server.rb', line 532

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

#remove(key, value = nil) ⇒ Object



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

def remove(key, value = nil)
  if value
    @base.sdb.delete_attributes(@base.server_domain, id, key => value)
    state[key] - [value]
  else
    @base.sdb.delete_attributes(@base.server_domain, id, [ key ])
    state.delete(key)
  end
end

#remove_ipObject



278
279
280
281
# File 'lib/judo/server.rb', line 278

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

#remove_volume(volume_id, device) ⇒ Object



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

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

#rename(newname) ⇒ Object

Raises:



553
554
555
556
557
558
# File 'lib/judo/server.rb', line 553

def rename(newname)
  raise JudoError, "Already a server with that name"  if @base.servers.detect { |s| s.name == newname }
  task("Renaming to #{newname}") do
    update "name" => newname
  end
end

#restart(options = {}) ⇒ Object



322
323
324
325
# File 'lib/judo/server.rb', line 322

def restart(options = {})
  stop(options) if running?
  start(options)
end

#running?Boolean

Returns:

  • (Boolean)


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

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

#security_groupsObject



377
378
379
# File 'lib/judo/server.rb', line 377

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

#set_metadata(new_metadata) ⇒ Object



39
40
41
42
43
44
45
46
47
# File 'lib/judo/server.rb', line 39

def ()
   = .to_json
  if (.size > 1000)
    raise "metadata json too big: '#{}'"
  else
    @metadata = 
    update("metadata_json" => )
  end
end

#size_descObject



94
95
96
97
98
99
100
# File 'lib/judo/server.rb', line 94

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

#snapshot(name) ⇒ Object



547
548
549
550
551
# File 'lib/judo/server.rb', line 547

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

#snapshotsObject



138
139
140
# File 'lib/judo/server.rb', line 138

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

#ssh?Boolean

Returns:

  • (Boolean)


443
444
445
446
447
448
449
450
451
452
# File 'lib/judo/server.rb', line 443

def ssh?
  begin
    Timeout::timeout(2) do
      TCPSocket.new(hostname, 22)
      true
    end
  rescue SocketError, Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
    false
  end
end

#ssh_command(cmd) ⇒ Object



503
504
505
506
507
508
# File 'lib/judo/server.rb', line 503

def ssh_command(cmd)
  wait_for_ssh
  @base.keypair_file do |file|
    Kernel.system "ssh -q -i #{file} #{config["user"]}@#{hostname} '#{cmd}'"
  end
end

#ssh_command!(cmd) ⇒ Object



510
511
512
513
514
515
# File 'lib/judo/server.rb', line 510

def ssh_command!(cmd)
  wait_for_ssh
  @base.keypair_file do |file|
    Util.system_confirmed "ssh -q -i #{file} #{config["user"]}@#{hostname} '#{cmd}'"
  end
end

#start(options = {}) ⇒ Object



307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/judo/server.rb', line 307

def start(options = {})
  @boot = options[:boot]
  new_version = options[:version]
  set_instance_type(options[:instance_type]) if options[:instance_type]

  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(options[:version]) } if options[: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

#started_atObject



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

def started_at
  Time.at(get("started_at").to_i)
end

#stateObject



66
67
68
# File 'lib/judo/server.rb', line 66

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

#stop(options = {}) ⇒ Object



347
348
349
350
351
352
353
354
355
356
# File 'lib/judo/server.rb', line 347

def stop(options = {})
  force = options[:force]
  invalid "not running" unless running?
  ## EC2 terminate_isntaces
  task("Terminating instance") { @base.ec2.terminate_instances([ instance_id ]) }
  force_detach_volumes if force
  wait_for_volumes_detached if volumes.size > 0
  remove "instance_id"
  update "stopped_at" => Time.now.to_i
end

#stopped_atObject



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

def stopped_at
  Time.at(get("stopped_at").to_i)
end

#swap(other) ⇒ Object

Raises:



560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'lib/judo/server.rb', line 560

def swap(other)
  ip1 = elastic_ip
  ip2 = other.elastic_ip

  raise JudoError, "Server must have an elastic IP to swap" unless ip1 and ip2

  task("Swapping Ip Addresses") do
    @base.ec2.disassociate_address(ip1)
    @base.ec2.disassociate_address(ip2)

    @base.ec2.associate_address(instance_id, ip2)
    @base.ec2.associate_address(other.instance_id, ip1)

    update "elastic_ip" => ip2
    other.update "elastic_ip" => ip1
  end

  task("Swapping Names") do
    name1 = name
    name2 = other.name
    update "name" => name2
    other.update "name" => name1
  end
end

#task(msg, &block) ⇒ Object



249
250
251
# File 'lib/judo/server.rb', line 249

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

#to_sObject



190
191
192
# File 'lib/judo/server.rb', line 190

def to_s
  displayname
end

#update(attrs) ⇒ Object



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

def update(attrs)
  @base.sdb.put_attributes(@base.server_domain, id, attrs, :replace)
  state.merge! attrs
end

#update_version(new_version) ⇒ Object



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

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

#urlObject



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

def url
  group.s3_url(version)
end

#user_dataObject



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

def user_data
  erb = group.userdata(version)
  debug erb
  ERB.new(erb, 0, '<>').result(binding)
end

#versionObject



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

def version
  get("version").to_i
end

#version_descObject



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

def version_desc
  group.version_desc(version)
end

#virgin?Boolean

Returns:

  • (Boolean)


134
135
136
# File 'lib/judo/server.rb', line 134

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



142
143
144
# File 'lib/judo/server.rb', line 142

def volumes
  (state["volumes"] || []).inject({}) { |out, kv| k, v = kv.split(':'); out[k] = v; out }
end

#wait_for_hostnameObject



410
411
412
413
414
415
416
# File 'lib/judo/server.rb', line 410

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

#wait_for_runningObject



402
403
404
405
406
407
408
# File 'lib/judo/server.rb', line 402

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

#wait_for_sshObject



454
455
456
457
458
459
# File 'lib/judo/server.rb', line 454

def wait_for_ssh
  invalid "not running" unless running?
  loop do
    ssh? ? return : sleep(1)
  end
end

#wait_for_terminationObject



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

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

#wait_for_volumes_detachedObject



418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/judo/server.rb', line 418

def wait_for_volumes_detached
  begin
    task("Wait for volumes to detach") do
      Timeout::timeout(60) do
        loop do
          break if ec2_volumes.reject { |v| v[:aws_status] == "available" }.empty?
          sleep 2
        end
      end
    end
  rescue Timeout::Error
    puts "failed!"
    force_detach_volumes
    retry
  end
end