Class: IBMSmartCloud

Inherits:
Object
  • Object
show all
Includes:
DynamicHelpGenerator
Defined in:
lib/smartcloud.rb

Overview

Encapsulates communications with IBM SmartCloud via REST

Class Attribute Summary collapse

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from DynamicHelpGenerator

#help, included

Constructor Details

#initialize(opts = {}) ⇒ IBMSmartCloud

Returns a new instance of IBMSmartCloud.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/smartcloud.rb', line 37

def initialize(opts={})
  @save_response = opts[:save_response]
  @simulated_response = opts[:simulated_response_file] && File.read(opts[:simulated_response_file])

  # For handling errors
  @retries = (opts[:retries] || 120).to_i
  @sleep_interval = (opts[:sleep_interval] || 30).to_i

  @username = opts[:username] || ENV['SMARTCLOUD_USERNAME'] || raise(RuntimeError, "Please specify username in an option or as ENV variable SMARTCLOUD_USERNAME")
  @password = opts[:password]|| ENV['SMARTCLOUD_PASSWORD'] || raise(RuntimeError, "Please specify password in an option or as ENV variable SMARTCLOUD_PASSWORD")
  @logger = opts[:logger] || SmartcloudLogger.new(STDOUT)
  @debug = opts[:debug] || false

  @config = YAML.load_file(File.join(File.dirname(__FILE__), "config/config.yml"))
  @states = @config["states"]

  @api_url = (opts[:api_url] || @config["api_url"]).to_s.dup # gotta dup it because the option string is frozen
  @api_url.gsub!("https://", "https://#{CGI::escape(@username)}:#{CGI::escape(@password)}@")

  @http_client = Kernel.const_get(@config["http_client"])
  @http_client.timeout = 120 # ibm requests can be very slow
  @http_client.log = @logger if @debug
end

Class Attribute Details

.configObject (readonly)

Returns the value of attribute config.



63
64
65
# File 'lib/smartcloud.rb', line 63

def config
  @config
end

Instance Attribute Details

#loggerObject

Returns the value of attribute logger.



35
36
37
# File 'lib/smartcloud.rb', line 35

def logger
  @logger
end

Instance Method Details

#address_state_is?(address_id, state_string) ⇒ Boolean

Returns:

  • (Boolean)


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

def address_state_is?(address_id, state_string)
  v = describe_addresses(address_id)
  v.State.to_s == state_string.to_s.upcase
end

#allocate_address(location, offering_id = nil) ⇒ Object



162
163
164
165
166
167
# File 'lib/smartcloud.rb', line 162

def allocate_address(location, offering_id=nil)
  if offering_id.nil?
    offering_id = describe_address_offerings(location).ID
  end
  post("/addresses", :offeringID => offering_id, :location => location).Address
end

#attach_volume(instance_id, volume_id) ⇒ Object



232
233
234
# File 'lib/smartcloud.rb', line 232

def attach_volume(instance_id, volume_id)
  put("/instances/#{instance_id}", :storageID => volume_id, :type => 'attach')
end

#clone_image(name, description, image_id) ⇒ Object



179
180
181
# File 'lib/smartcloud.rb', line 179

def clone_image(name, description, image_id)
  post("/offerings/image/#{image_id}", :name => name, :description => description).ImageID
end

#clone_volume(name, source_disk_id) ⇒ Object



226
227
228
229
# File 'lib/smartcloud.rb', line 226

def clone_volume(name, source_disk_id)
  result = post("/storage", :name => name, :sourceDiskID => source_disk_id, :type => 'clone')
  result.Volume.ID
end

#create_instance(instance_params) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/smartcloud.rb', line 105

def create_instance(instance_params)
  param_remap = { "name" => "name",
    "image-id" => "imageID",
    "instance-type" => "instanceType",
    "data-center" => "location",
    "key-name" => "publicKey",
    "ip-address-id" => "ip",
    "volume-id" => "volumeID",
    "configuration" => "ConfigurationData",
    "vlan-id" => "vlanID",
    "antiCollocationInstance" => "antiCollocationInstance",
    "isMiniEphemeral" => "isMiniEphemeral"
  }

  # api does not take description
  instance_params.delete("description")

  # configuration data has to be changed from a string like
  # <configuration>{contextmanager:test-c3-master.cohesiveft.com,clustername:TEST_poc_pk0515,role:[nfs-client-setup|newyork_master_refdata_member|install-luci|rhel-openlikewise-client-setup|join-domain],hostname:r550n107}</configuration>
  # to a standard list of POST params like
  # contextmanager=test-c3-mager&clustername=TEST...
  configuration_data = instance_params.delete("configuration") || instance_params.delete("ConfigurationData")
  if configuration_data
    if configuration_data =~ /\s+/
      logger.warn "<configuration> tag should not contain spaces! Correct format looks like: <configuration>{foo:bar,baz:quux}</configuration>. Spaces will be removed."
    end
    configuration_data.delete!("{}") # get rid of curly braces
    config_data_params = configuration_data.split(",").map{|param| param.split(":")} # split each foo:bar into key and value
    config_data_params.each {|k,v| instance_params[k]=v} # add it into the standard instance params array
  end

  result = post "/instances", instance_params, param_remap
  result.Instance
end

#create_volume(name, location, size, offering_id = nil, format = "EXT3") ⇒ Object



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/smartcloud.rb', line 391

def create_volume(name, location, size, offering_id=nil, format="EXT3")

  # figure out the offering ID automatically based on location and size
  if offering_id.nil?
    logger.debug "Looking up volume offerings based on location: #{location} and size: #{size}"

    filter_size = ( ["Small", "Medium", "Large"].include?( size )) ? size: "Storage"
    offering = describe_volume_offerings(location, filter_size)
    if( offering && offering.SupportedSizes.split(",").include?(size) || ["Small", "Medium", "Large"].include?( size ))
      offering_id = offering.ID
    else
      raise "Unable to locate offering with location #{location}, size #{size}."
    end
  end

  logger.debug "Creating volume...please wait."
  result = post("/storage", :format => format, :location => location, :name => name, :size => size, :offeringID => offering_id)
  result.Volume.ID
end

#delete(path) ⇒ Object



621
622
623
624
625
626
# File 'lib/smartcloud.rb', line 621

def delete(path)
  rescue_and_retry_errors do
    output = @http_client.delete File.join(@api_url, path), :accept => :response
    response = XmlSimple.xml_in(output, {'ForceArray' => nil})
  end
end

#delete_images(*image_id_list) ⇒ Object



214
215
216
217
218
219
220
221
# File 'lib/smartcloud.rb', line 214

def delete_images(*image_id_list)
  threads=[]
  image_id_list.each {|image|
    threads << Thread.new { logger.info "Sending delete request for: #{image}..."; delete_image(image); logger.info "Finished delete request for #{image}" }
  }
  threads.each(&:join)
  true
end

#delete_instance(instance_id) ⇒ Object



524
525
526
527
# File 'lib/smartcloud.rb', line 524

def delete_instance(instance_id)
  delete("/instances/#{instance_id}")
  true
end

#delete_instances(*instance_ids) ⇒ Object



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

def delete_instances(*instance_ids)
  threads=[]
  instance_ids.each {|id| logger.info "Sending delete request for: #{id}..."; threads << Thread.new { delete_instance(id) }; logger.info "Finished delete request for #{id}" }
  threads.each(&:join)
  true
end

#delete_volume(vol_id) ⇒ Object



243
244
245
246
# File 'lib/smartcloud.rb', line 243

def delete_volume(vol_id)
  delete("/storage/#{vol_id}")
  true
end

#delete_volumes(*vol_id_list) ⇒ Object



251
252
253
254
255
256
257
258
# File 'lib/smartcloud.rb', line 251

def delete_volumes(*vol_id_list)
  threads=[]
  vol_id_list.each {|vol|
    threads << Thread.new { logger.info "Sending delete request for: #{vol}..."; delete_volume(vol); logger.info "Finished delete request for #{vol}" }
  }
  threads.each(&:join)
  true
end

#describe_address(address_id) ⇒ Object



171
172
173
174
175
# File 'lib/smartcloud.rb', line 171

def describe_address(address_id)
  address = get("/addresses/#{address_id}").Address
  address["State"] = @config.states.ip[address.State]
  address
end

#describe_address_offerings(location = nil) ⇒ Object



150
151
152
153
154
155
156
157
# File 'lib/smartcloud.rb', line 150

def describe_address_offerings(location=nil)
  response=get("/offerings/address").Offerings
  if location
    response.detect {|offering| offering.Location.to_s == location.to_s}
  else
    response
  end
end

#describe_addresses(address_id = nil) ⇒ Object



294
295
296
297
298
299
300
301
302
303
# File 'lib/smartcloud.rb', line 294

def describe_addresses(address_id=nil)
  response = arrayize(get("/addresses").Address)
  response.each do |a|
    a["State"] = @states["ip"][a.State.to_i]
  end
  if address_id
    response = response.detect {|address| address.ID.to_s == address_id.to_s}
  end
  response
end

#describe_image(image_id) ⇒ Object



587
588
589
590
591
# File 'lib/smartcloud.rb', line 587

def describe_image(image_id)
  image = get("offerings/image/#{image_id}").Image
  image["State"] = @states["image"][image.State.to_i]
  image
end

#describe_images(filters = {}) ⇒ Object



594
595
596
597
598
599
# File 'lib/smartcloud.rb', line 594

def describe_images(filters={})
  images = arrayize(get("offerings/image").Image)
  images.each {|img| img["State"] = @states["image"][img.State.to_i]}
  filters[:order] ||= "Location"
  images = filter_and_sort(images, filters)
end

#describe_instance(instance_id) ⇒ Object



542
543
544
545
546
# File 'lib/smartcloud.rb', line 542

def describe_instance(instance_id)
  response = get("instances/#{instance_id}").Instance
  response["Status"] = @states["instance"][response.Status.to_i]
  response
end

#describe_instances(filters = {}) ⇒ Object



552
553
554
555
556
557
558
559
560
561
# File 'lib/smartcloud.rb', line 552

def describe_instances(filters={})
  instances = arrayize(get("instances").Instance)

  instances.each do |instance|
    instance["Status"] = @states["instance"][instance.Status.to_i]
  end

  filters[:order] ||= "LaunchTime"
  instances = filter_and_sort(instances, filters)
end

#describe_key(name) ⇒ Object



283
284
285
# File 'lib/smartcloud.rb', line 283

def describe_key(name)
  get("/keys/#{name}").PublicKey
end

#describe_keysObject



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

def describe_keys
  arrayize(get("/keys").PublicKey)
end

#describe_location(location_id) ⇒ Object



79
80
81
# File 'lib/smartcloud.rb', line 79

def describe_location(location_id)
  get("/locations/#{location_id}").Location
end

#describe_locations(name = nil) ⇒ Object



70
71
72
73
74
75
76
77
# File 'lib/smartcloud.rb', line 70

def describe_locations(name=nil)
  locations = get("/locations").Location
  if name
    locations.detect {|loc| loc.Name =~ /#{name}/}
  else
    locations
  end
end

#describe_manifest(image_id) ⇒ Object



601
602
603
# File 'lib/smartcloud.rb', line 601

def describe_manifest(image_id)
  get("offerings/image/#{image_id}/manifest").Manifest
end

#describe_storage(volume_id = nil) ⇒ Object Also known as: describe_volumes, describe_volume



413
414
415
416
417
418
419
# File 'lib/smartcloud.rb', line 413

def describe_storage(volume_id=nil)
  response = volume_id ? get("/storage/#{volume_id}") : get("/storage")

  arrayize(response.Volume).each do |v|
    v["State"] = @states["storage"][v.State.to_i]
  end
end

#describe_storage_offerings(location = nil, name = nil) ⇒ Object Also known as: describe_volume_offerings



359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/smartcloud.rb', line 359

def describe_storage_offerings(location=nil, name=nil)
  response = get("/offerings/storage")
  if location
    filtered_by_location = response.Offerings.select {|o| o.Location.to_s == location.to_s}
    if name
      filtered_by_location.detect {|o| o.Name == name}
    else
      filtered_by_location
    end
  else
    response.Offerings
  end
end

#detach_volume(instance_id, volume_id) ⇒ Object



237
238
239
# File 'lib/smartcloud.rb', line 237

def detach_volume(instance_id, volume_id)
  put("/instances/#{instance_id}", :storageID => volume_id, :type => 'detach')
end

#display_addressesObject



305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/smartcloud.rb', line 305

def display_addresses
  addresses = describe_addresses

  table = Terminal::Table.new do |t|
    t.headings = ['ID', 'Loc', 'State', 'IP']
    addresses.each do |a|
      t.add_row [a.ID, a.Location, a.State, a.IP]
    end
  end

  logger.info table
end

#display_images(filters = {}) ⇒ Object



606
607
608
609
610
611
612
613
614
615
616
617
618
619
# File 'lib/smartcloud.rb', line 606

def display_images(filters={})
  images = describe_images(filters)

  table = Terminal::Table.new do |t|
    t.headings = ['ID', 'Loc', 'Name & Type', 'Platform', 'Arch']
    images.each do |i|
      types = arrayize(i.SupportedInstanceTypes.InstanceType).map(&:ID).join("\n") rescue "[INSTANCE TYPE UNKNOWN]"
      t.add_row [i.ID, i.Location, i.Name + "\n" + types, i.Platform, i.Architecture]
      t.add_separator unless i == images.last
    end
  end

  logger.info table
end

#display_instances(filters = {}) ⇒ Object



571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'lib/smartcloud.rb', line 571

def display_instances(filters={})
  instances = describe_instances(filters)

  table = Terminal::Table.new do |t|
    t.headings = ['ID', 'LaunchTime', 'Name', 'Key', 'ID', 'Loc', 'Status', 'IP', 'Volume']
    instances.each do |ins|
      launchTime = time_format(ins.LaunchTime)
      t.add_row [ins.ID, launchTime, ins.Name, ins.KeyName, ins.ImageID, ins.Location, ins.Status, ins.IP && ins.IP.strip, ins.Volume]
    end
  end
  table.style = {:padding_left => 0, :padding_right => 0, :border_y => ' ', :border_x => '-'}
  logger.info table
end

#display_keysObject Also known as: display_keypairs



331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/smartcloud.rb', line 331

def display_keys
  keys = describe_keys

  table = Terminal::Table.new do |t|
    t.headings = ['KeyName', 'Used By Instances', 'Last Modified']
    keys.each do |k|
      t.add_row [k.KeyName, (k.Instances.empty? ? '[NONE]' : arrayize(k.Instances.InstanceID).join("\n")), time_format( k.LastModifiedTime )]
    end
  end

  logger.info table
end

#display_locationsObject



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/smartcloud.rb', line 83

def display_locations

  table = Terminal::Table.new do |t|
    t.headings = ['ID', 'Loc', 'Name']
    describe_locations.each do |loc|
      t.add_row [loc.ID, loc.Location, loc.Name]
    end
  end

  logger.info table
end

#display_volumes(filter = {}) ⇒ Object Also known as: display_storage



429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/smartcloud.rb', line 429

def display_volumes(filter={})
  vols = describe_volumes
  vols = filter_and_sort(vols, filter)

  table = Terminal::Table.new do |t|
    t.headings = %w(Volume State Loc Name CreatedTime)
    vols.each do |vol|
      t.add_row [vol.ID, vol.State, vol.Location, vol.Name, time_format(vol.CreatedTime)]
    end
  end

  logger.info table
end

#export_image(name, size, image_id, location) ⇒ Object



185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/smartcloud.rb', line 185

def export_image(name, size, image_id, location)
  # Note we figure out the correct size based on the name and location
  storage_offering=describe_storage_offerings(location, size)

  response = post("/storage", :name => name, :size => storage_offering.Capacity, :format => 'EXT3', :offeringID => storage_offering.ID, :location => location)
  volumeID = response.Volume.ID

  poll_for_volume_state(volumeID, :unmounted)

  response = put("/storage/#{volumeID}", :imageId => image_id)
  response.Volume.ID
end

#generate_keypair(name, publicKey = nil) ⇒ Object



265
266
267
268
269
270
271
272
273
274
# File 'lib/smartcloud.rb', line 265

def generate_keypair(name, publicKey=nil)
  options = {:name => name}
  options[:publicKey] = publicKey if publicKey
  response = post("/keys", options)
  if publicKey
    true
  else
    response.PrivateKey.KeyMaterial
  end
end

#get(path) ⇒ Object



636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
# File 'lib/smartcloud.rb', line 636

def get(path)
  rescue_and_retry_errors do
    output = if @simulated_response
      @simulated_response
    else
      @http_client.get File.join(@api_url, path), :accept => :response, :headers => "User-Agent: cloudapi"
    end

    # Save Response for posterity
    if @save_response && !output.empty?
      logger.info "Saving response to: #{@save_response}"
      File.open(@save_response,'w') {|f| f.write(output)}
    end

    if output && !output.empty?
      XmlSimple.xml_in(output, {'ForceArray' => nil})
    else
      raise "Empty response!"
    end
  end
end

#get_location_by_name(name) ⇒ Object



142
143
144
145
# File 'lib/smartcloud.rb', line 142

def get_location_by_name(name)
  locations = describe_locations
  locations.detect {|loc| loc.Name == name}
end

#import_image(name, volume_id) ⇒ Object



199
200
201
202
203
# File 'lib/smartcloud.rb', line 199

def import_image(name, volume_id)
  # TODO: this is a complete guess as we have no info from IBM as to the URL for this api, only the parameters
  response = post("/offerings/image", :volumeId => volume_id, :name => name)
  response.Image.ID
end

#instance_state_is?(instance_id, state_string) ⇒ Boolean

Returns:

  • (Boolean)


468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/smartcloud.rb', line 468

def instance_state_is?(instance_id, state_string)
  v = describe_instance(instance_id)

  @last_instance_state||={}
  if @last_instance_state[instance_id.to_s] != v.Status
    logger.debug "Instance: #{instance_id}; Current State: #{v.Status}; Waiting for: #{state_string.to_s.upcase}" # log it every time it changes
  end
  @last_instance_state[instance_id.to_s] = v.Status

  if v.Status.to_s == state_string.to_s.upcase
    v
  else
    false
  end
end

#poll_for_instance_state(instance_id, state_string, polling_interval = 5) ⇒ Object



497
498
499
500
501
502
503
504
# File 'lib/smartcloud.rb', line 497

def poll_for_instance_state(instance_id, state_string, polling_interval=5)
  logger.debug "Polling for instance #{instance_id} to acquire state #{state_string} (interval: #{polling_interval})..."
  while(true)
    descriptor = instance_state_is?(instance_id, state_string)
    return descriptor if descriptor
    sleep(polling_interval)
  end
end

#poll_for_volume_state(volume_id, state_string, polling_interval = 5) ⇒ Object



486
487
488
489
490
491
492
493
# File 'lib/smartcloud.rb', line 486

def poll_for_volume_state(volume_id, state_string, polling_interval=5)
  logger.debug "Polling for volume #{volume_id} to acquire state #{state_string} (interval: #{polling_interval})..."
  while(true)
    descriptor = storage_state_is?(volume_id, state_string)
    return descriptor if descriptor
    sleep(polling_interval)
  end
end

#post(path, params = {}, param_remap = nil) ⇒ Object



658
659
660
661
662
663
664
# File 'lib/smartcloud.rb', line 658

def post(path, params={}, param_remap=nil)
  rescue_and_retry_errors do
    param_string = make_param_string(params, param_remap)
    output = @http_client.post File.join(@api_url, path), param_string, :accept => :response
    response = XmlSimple.xml_in(output, {'ForceArray' => nil})
  end
end

#put(path, params = {}, param_remap = {}) ⇒ Object



628
629
630
631
632
633
634
# File 'lib/smartcloud.rb', line 628

def put(path, params={}, param_remap={})
  rescue_and_retry_errors do
    param_string = make_param_string(params, param_remap)
    output = @http_client.put File.join(@api_url, path), param_string, :accept => :response
    response = XmlSimple.xml_in(output, {'ForceArray' => nil})
  end
end

#remove_keypair(name) ⇒ Object



277
278
279
280
# File 'lib/smartcloud.rb', line 277

def remove_keypair(name)
  delete("/keys/#{name}")
  true
end

#rename_instance(instance_id, new_name) ⇒ Object



518
519
520
521
# File 'lib/smartcloud.rb', line 518

def rename_instance(instance_id, new_name)
  put("/instances/#{instance_id}", :name => new_name)
  true
end

#restart_instance(instance_id) ⇒ Object



530
531
532
533
# File 'lib/smartcloud.rb', line 530

def restart_instance(instance_id)
  put("/instances/#{instance_id}", :state => "restart")
  true
end

#save_instance(instance_id, name, desc = "") ⇒ Object



536
537
538
539
# File 'lib/smartcloud.rb', line 536

def save_instance(instance_id, name, desc = "")
  put("/instances/#{instance_id}", :state => "save", :name => name, :description => desc)
  true
end

#storage_state_is?(volume_id, state_string) ⇒ Boolean

Returns:

  • (Boolean)


449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# File 'lib/smartcloud.rb', line 449

def storage_state_is?(volume_id, state_string)
  v = describe_storage(volume_id)
  v = v.first if v.is_a?(Array)

  @last_storage_state||={}
  if @last_storage_state[volume_id.to_s] != v.State
    logger.debug "Volume: #{volume_id}; Current State: #{v.State}; Waiting for: #{state_string.to_s.upcase} " # log it every time it changes
  end
  @last_storage_state[volume_id.to_s] = v.State

  if v.State.to_s == state_string.to_s.upcase
    v
  else
    false
  end

end

#supported_instance_types(image_id) ⇒ Object



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

def supported_instance_types(image_id)
  img=describe_image(image_id)
  arrayize(img.SupportedInstanceTypes.InstanceType).map(&:ID)
end

#update_key(name, key) ⇒ Object



288
289
290
# File 'lib/smartcloud.rb', line 288

def update_key(name, key)
  put("/keys/#{name}", :publicKey => key)
end