cft_smartcloud 0.2.2 → 0.3.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.
Files changed (93) hide show
  1. data/.gitignore +1 -0
  2. data/CHANGELOG +11 -0
  3. data/README.md +106 -0
  4. data/VERSION +1 -1
  5. data/bin/cft_smartcloud +33 -7
  6. data/bin/smartcloud +33 -7
  7. data/cft_smartcloud.gemspec +84 -20
  8. data/lib/config/config.yml +2 -0
  9. data/lib/curl_client.rb +42 -0
  10. data/lib/rest-client-1.6.6-master/.gitignore +6 -0
  11. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/README.rdoc +10 -1
  12. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/Rakefile +0 -0
  13. data/lib/rest-client-1.6.6-master/VERSION +1 -0
  14. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/bin/restclient +5 -4
  15. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/history.md +22 -0
  16. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/lib/rest-client.rb +0 -0
  17. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/lib/rest_client.rb +0 -0
  18. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/lib/restclient/abstract_response.rb +0 -0
  19. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/lib/restclient/exceptions.rb +0 -0
  20. data/lib/rest-client-1.6.6-master/lib/restclient/net_http_ext.rb +55 -0
  21. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/lib/restclient/payload.rb +17 -2
  22. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/lib/restclient/raw_response.rb +0 -0
  23. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/lib/restclient/request.rb +21 -19
  24. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/lib/restclient/resource.rb +0 -0
  25. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/lib/restclient/response.rb +0 -0
  26. data/lib/{rest-client-1.6.3 → rest-client-1.6.6-master}/lib/restclient.rb +0 -0
  27. data/lib/rest-client-1.6.6-master/rest-client.gemspec +76 -0
  28. data/lib/rest-client-1.6.6-master/spec/abstract_response_spec.rb +85 -0
  29. data/lib/rest-client-1.6.6-master/spec/base.rb +16 -0
  30. data/lib/rest-client-1.6.6-master/spec/exceptions_spec.rb +98 -0
  31. data/lib/rest-client-1.6.6-master/spec/integration/certs/equifax.crt +19 -0
  32. data/lib/rest-client-1.6.6-master/spec/integration/certs/verisign.crt +14 -0
  33. data/lib/rest-client-1.6.6-master/spec/integration/request_spec.rb +25 -0
  34. data/lib/rest-client-1.6.6-master/spec/integration_spec.rb +38 -0
  35. data/lib/rest-client-1.6.6-master/spec/master_shake.jpg +0 -0
  36. data/lib/rest-client-1.6.6-master/spec/payload_spec.rb +234 -0
  37. data/lib/rest-client-1.6.6-master/spec/raw_response_spec.rb +17 -0
  38. data/lib/rest-client-1.6.6-master/spec/request2_spec.rb +40 -0
  39. data/lib/rest-client-1.6.6-master/spec/request_spec.rb +536 -0
  40. data/lib/rest-client-1.6.6-master/spec/resource_spec.rb +134 -0
  41. data/lib/rest-client-1.6.6-master/spec/response_spec.rb +169 -0
  42. data/lib/rest-client-1.6.6-master/spec/restclient_spec.rb +73 -0
  43. data/lib/slop-2.3.1/.gemtest +0 -0
  44. data/lib/slop-2.3.1/.gitignore +6 -0
  45. data/lib/slop-2.3.1/.yardopts +6 -0
  46. data/lib/slop-2.3.1/CHANGES.md +137 -0
  47. data/lib/slop-2.3.1/LICENSE +20 -0
  48. data/lib/slop-2.3.1/README.md +293 -0
  49. data/lib/slop-2.3.1/Rakefile +6 -0
  50. data/lib/slop-2.3.1/lib/slop.rb +1022 -0
  51. data/lib/slop-2.3.1/slop.gemspec +11 -0
  52. data/lib/slop-2.3.1/test/commands_test.rb +151 -0
  53. data/lib/slop-2.3.1/test/helper.rb +13 -0
  54. data/lib/slop-2.3.1/test/option_test.rb +198 -0
  55. data/lib/slop-2.3.1/test/slop_test.rb +574 -0
  56. data/lib/smartcloud.rb +186 -116
  57. data/lib/terminal-table-1.4.4/History.rdoc +53 -0
  58. data/lib/terminal-table-1.4.4/Manifest +24 -0
  59. data/lib/terminal-table-1.4.4/README.rdoc +240 -0
  60. data/lib/terminal-table-1.4.4/Rakefile +15 -0
  61. data/lib/terminal-table-1.4.4/Todo.rdoc +14 -0
  62. data/lib/terminal-table-1.4.4/examples/examples.rb +80 -0
  63. data/lib/terminal-table-1.4.4/lib/terminal-table/cell.rb +88 -0
  64. data/lib/terminal-table-1.4.4/lib/terminal-table/core_ext.rb +8 -0
  65. data/lib/terminal-table-1.4.4/lib/terminal-table/import.rb +4 -0
  66. data/lib/terminal-table-1.4.4/lib/terminal-table/row.rb +48 -0
  67. data/lib/terminal-table-1.4.4/lib/terminal-table/separator.rb +14 -0
  68. data/lib/terminal-table-1.4.4/lib/terminal-table/style.rb +61 -0
  69. data/lib/terminal-table-1.4.4/lib/terminal-table/table.rb +217 -0
  70. data/lib/terminal-table-1.4.4/lib/terminal-table/table_helper.rb +9 -0
  71. data/lib/terminal-table-1.4.4/lib/terminal-table/version.rb +6 -0
  72. data/lib/terminal-table-1.4.4/lib/terminal-table.rb +27 -0
  73. data/lib/terminal-table-1.4.4/spec/cell_spec.rb +54 -0
  74. data/lib/terminal-table-1.4.4/spec/core_ext_spec.rb +18 -0
  75. data/lib/terminal-table-1.4.4/spec/import_spec.rb +11 -0
  76. data/lib/terminal-table-1.4.4/spec/spec.opts +1 -0
  77. data/lib/terminal-table-1.4.4/spec/spec_helper.rb +8 -0
  78. data/lib/terminal-table-1.4.4/spec/table_spec.rb +525 -0
  79. data/lib/terminal-table-1.4.4/tasks/docs.rake +13 -0
  80. data/lib/terminal-table-1.4.4/tasks/gemspec.rake +3 -0
  81. data/lib/terminal-table-1.4.4/tasks/spec.rake +25 -0
  82. data/lib/terminal-table-1.4.4/terminal-table.gemspec +30 -0
  83. data/responses/addresses +26 -0
  84. data/responses/addresses.blank +2 -0
  85. data/responses/instances +74 -0
  86. data/responses/keys +33 -0
  87. data/responses/locations +142 -0
  88. data/responses/offerings_image +3780 -0
  89. data/responses/storage +379 -0
  90. metadata +86 -22
  91. data/README.rdoc +0 -75
  92. data/lib/rest-client-1.6.3/VERSION +0 -1
  93. data/lib/rest-client-1.6.3/lib/restclient/net_http_ext.rb +0 -21
data/lib/smartcloud.rb CHANGED
@@ -22,32 +22,39 @@ require 'restclient_fix'
22
22
  require 'hash_fix'
23
23
  require 'xmlsimple'
24
24
  require 'smartcloud_logger'
25
+ require 'curl_client'
26
+ require 'terminal-table'
25
27
 
26
28
  IBM_TOOLS_HOME=File.join(File.dirname(__FILE__), "cli_tools") unless defined?(IBM_TOOLS_HOME)
27
29
 
28
30
  # Encapsulates communications with IBM SmartCloud via REST
29
- class IBMSmartCloud
30
31
 
32
+ class IBMSmartCloud
33
+
31
34
  attr_accessor :logger
32
35
 
33
- def initialize(username, password, logger=nil, debug=false)
36
+ def initialize(opts={})
37
+ @save_response = opts[:save_response]
38
+ @simulated_response = opts[:simulated_response_file] && File.read(opts[:simulated_response_file])
39
+
34
40
  # For handling errors
35
- @retries = 120
36
- @sleep_interval = 30
41
+ @retries = (opts[:retries] || 120).to_i
42
+ @sleep_interval = (opts[:sleep_interval] || 30).to_i
37
43
 
38
- @username = username
39
- @password = password
40
- @logger = logger || SmartcloudLogger.new(STDOUT)
41
- @debug = debug
44
+ @username = opts[:username] || ENV['SMARTCLOUD_USERNAME'] || raise(RuntimeError, "Please specify username in an option or as ENV variable SMARTCLOUD_USERNAME")
45
+ @password = opts[:password]|| ENV['SMARTCLOUD_PASSWORD'] || raise(RuntimeError, "Please specify password in an option or as ENV variable SMARTCLOUD_PASSWORD")
46
+ @logger = opts[:logger] || SmartcloudLogger.new(STDOUT)
47
+ @debug = opts[:debug] || false
42
48
 
43
49
  @config = YAML.load_file(File.join(File.dirname(__FILE__), "config/config.yml"))
44
50
  @states = @config["states"]
45
51
 
46
- @api_url = @config["api_url"]
47
- @api_url.gsub!("https://", "https://#{CGI::escape(username)}:#{CGI::escape(password)}@")
48
-
49
- RestClient.timeout = 120 # ibm requests can be very slow
50
- RestClient.log = @logger if @debug
52
+ @api_url = (opts[:api_url] || @config["api_url"]).to_s.dup # gotta dup it because the option string is frozen
53
+ @api_url.gsub!("https://", "https://#{CGI::escape(@username)}:#{CGI::escape(@password)}@")
54
+
55
+ @http_client = Kernel.const_get(@config["http_client"])
56
+ @http_client.timeout = 120 # ibm requests can be very slow
57
+ @http_client.log = @logger if @debug
51
58
  end
52
59
 
53
60
  class << self
@@ -57,7 +64,7 @@ class IBMSmartCloud
57
64
  attr_reader :config
58
65
  end
59
66
 
60
- def self.args(method, args, extra_help={})
67
+ def self.help_for(method, args, extra_help={})
61
68
  @method_help||={}
62
69
  @method_help_supplemental||={}
63
70
  @method_help[method.to_s] = args
@@ -71,10 +78,7 @@ class IBMSmartCloud
71
78
  return "Sorry, I don't know method: #{method}"
72
79
  end
73
80
 
74
- if args.nil?
75
- puts method.to_s
76
- else
77
- args = args.map do |arg|
81
+ args = args && args.map do |arg|
78
82
  if arg.is_a?(Hash)
79
83
  # If an argument is required, just list it
80
84
  if arg.values.first==:req
@@ -93,11 +97,11 @@ class IBMSmartCloud
93
97
 
94
98
  extra_help = self.class.method_help_supplemental[method.to_s] || ""
95
99
 
96
- puts "#{method.to_s}(#{args})\n#{extra_help}"
97
- end
100
+ puts %{ * #{method.to_s}#{'(' + args + ')' if args}#{extra_help + "\n" if extra_help}}
98
101
  else
99
102
  methods = public_methods - Object.public_methods - ['post','get','put','delete','logger','logger=','help']
100
- puts methods.sort.join("\n")
103
+ methods.sort.each {|m| help(m)}
104
+ nil
101
105
  end
102
106
  end
103
107
 
@@ -111,15 +115,22 @@ class IBMSmartCloud
111
115
  end
112
116
 
113
117
  def display_locations
114
- log = describe_locations.map {|loc| "#{loc.ID.ljust(4)} | #{loc.Location.ljust(15)} | #{loc.Name}"}.join("\n")
115
- logger.info "\n#{log}"
118
+
119
+ table = Terminal::Table.new do |t|
120
+ t.headings = ['ID', 'Loc', 'Name']
121
+ describe_locations.each do |loc|
122
+ t.add_row [loc.ID, loc.Location, loc.Name]
123
+ end
124
+ end
125
+
126
+ logger.info table
116
127
  end
117
128
 
118
129
  # Create an instance. The instance_params hash can follow
119
130
  # CLI-style attribute naming. I.e. "image-id" or "data-center"
120
131
  # or API-style (imageID, location). If the CLI-style params are
121
132
  # provided, they will be remapped to the correct API params.
122
- args :create_instance, [:instance_params_hash], %{
133
+ help_for :create_instance, [:instance_params_hash], %{
123
134
  Available hash keys: :name, :imageID, :instanceType, :location, :publicKey, :ip, :volumeID,
124
135
  :ConfigurationData, :vlanID, :antiCollocationInstance, :isMiniEphemeral
125
136
 
@@ -161,7 +172,7 @@ class IBMSmartCloud
161
172
  end
162
173
 
163
174
  # Find a location by its name. Assumes locations have unique names (not verified by ibm docs)
164
- args :get_locations_by_name, [:name]
175
+ help_for :get_location_by_name, [:name]
165
176
  def get_location_by_name(name)
166
177
  locations = describe_locations
167
178
  locations.detect {|loc| loc.Name == name}
@@ -169,7 +180,7 @@ class IBMSmartCloud
169
180
 
170
181
  # find all ip address offerings. If a location is specified, give the offering for the location
171
182
  # assumption: only one offering per location. This is assumed, not verified in docs.
172
- args :describe_address_offerings, [{:location => :opt}]
183
+ help_for :describe_address_offerings, [{:location => :opt}]
173
184
  def describe_address_offerings(location=nil)
174
185
  response=get("/offerings/address").Offerings
175
186
  if location
@@ -181,7 +192,7 @@ class IBMSmartCloud
181
192
 
182
193
  # Allocate IP address. Offering ID is optional. if not specified, will look up offerings
183
194
  # for this location and pick one
184
- args :allocate_address, [{:location => :req}, {:offering_id => :opt}, {:foo => [:bar, :baz, :quux]}]
195
+ help_for :allocate_address, [{:location => :req}, {:offering_id => :opt}, {:foo => [:bar, :baz, :quux]}]
185
196
  def allocate_address(location, offering_id=nil)
186
197
  if offering_id.nil?
187
198
  offering_id = describe_address_offerings(location).ID
@@ -190,7 +201,7 @@ class IBMSmartCloud
190
201
  end
191
202
 
192
203
 
193
- args :describe_address, [:address_id]
204
+ help_for :describe_address, [:address_id]
194
205
  def describe_address(address_id)
195
206
  address = get("/addresses/#{address_id}").Address
196
207
  address["State"] = @config.states.ip[address.State]
@@ -198,37 +209,56 @@ class IBMSmartCloud
198
209
  end
199
210
 
200
211
  # Launches a clone image call from CLI
201
- args :clone_image, [:name, :description, :image_id]
212
+ help_for :clone_image, [:name, :description, :image_id]
202
213
  def clone_image(name, description, image_id)
203
214
  post("/offerings/image/#{image_id}", :name => name, :description => description).ImageID
204
215
  end
205
216
 
217
+ # Export an image to a volume
218
+ help_for :export_image, [{:name=>:req}, {:size => ['Small','Medium','Large']}, {:image_id => :req}, {:location => :req}]
219
+ def export_image(name, size, image_id, location)
220
+ # Note we figure out the correct size based on the name and location
221
+ storage_offering=describe_storage_offerings(location, size)
222
+
223
+ response = post("/storage", :name => name, :size => storage_offering.Capacity, :format => 'EXT3', :offeringID => storage_offering.ID, :location => location, :imageID => image_id)
224
+ response.Volume.ID
225
+ end
226
+
227
+ help_for :import_image, [{:name=>:req, :volume_id => :req}]
228
+ def import_image(name, volume_id)
229
+ # TODO: this is a complete guess as we have no info from IBM as to the URL for this api, only the parameters
230
+ response = post("/offerings/image", :volumeID => volume_id, :name => name)
231
+ response.ImageID
232
+ end
233
+
206
234
  # Launches a clone request and returns ID of new volume
207
235
  # TODO: the API call does not work, we have to use the cli for now
208
- args :clone_volume, [:name, :source_disk_id]
236
+ help_for :clone_volume, [:name, :source_disk_id]
209
237
  def clone_volume(name, source_disk_id)
210
238
  result = post("/storage", :name => name, :sourceDiskID => source_disk_id, :type => 'clone')
211
239
  result.Volume.ID
212
240
  end
213
241
 
214
- args :attach_volume, [:instance_id, :volume_id]
242
+ help_for :attach_volume, [:instance_id, :volume_id]
215
243
  def attach_volume(instance_id, volume_id)
216
244
  put("/instances/#{instance_id}", :volumeID => volume_id, :attach => true)
217
245
  end
218
246
 
219
- args :detach_volume, [:instance_id, :volume_id]
247
+ help_for :detach_volume, [:instance_id, :volume_id]
220
248
  def detach_volume(instance_id, volume_id)
221
249
  put("/instances/#{instance_id}", :volumeID => volume_id, :detach => true)
222
250
  end
223
251
 
224
252
  # Delete the volume
225
- args :delete_volume, [:vol_id]
253
+ help_for :delete_volume, [:vol_id]
226
254
  def delete_volume(vol_id)
227
255
  delete("/storage/#{vol_id}")
228
256
  true
229
257
  end
230
258
 
231
- args :delete_volumes, [:volume_ids], %{Example: smartcloud "delete_volumes(12345,12346,12347)"}
259
+ help_for :delete_volumes, [:volume_ids], %{
260
+ Example: smartcloud "delete_volumes(12345,12346,12347)"
261
+ }
232
262
  def delete_volumes(*vol_id_list)
233
263
  threads=[]
234
264
  vol_id_list.each {|vol|
@@ -239,7 +269,10 @@ class IBMSmartCloud
239
269
  end
240
270
 
241
271
  # generates a keypair and returns the private key
242
- args :generate_keypair, [{:name=>:req}, {:publicKey=>:opt}], %{ If publicKey parameter is given, creates a key with that public key, and returns nothing. If public key is omitted, generates a new key and returns the pirvate key.}
272
+ help_for :generate_keypair, [{:name=>:req}, {:publicKey=>:opt}], %{
273
+ If publicKey parameter is given, creates a key with that public key, and returns nothing.
274
+ If public key is omitted, generates a new key and returns the pirvate key.
275
+ }
243
276
  def generate_keypair(name, publicKey=nil)
244
277
  options = {:name => name}
245
278
  options[:publicKey] = publicKey if publicKey
@@ -251,26 +284,28 @@ class IBMSmartCloud
251
284
  end
252
285
  end
253
286
 
254
- args :remove_keypair, [:name]
287
+ help_for :remove_keypair, [:name]
255
288
  def remove_keypair(name)
256
289
  delete("/keys/#{name}")
257
290
  true
258
291
  end
292
+ alias remove_key remove_keypair
293
+ alias delete_key remove_keypair
259
294
 
260
- args :describe_key, [:name]
295
+ help_for :describe_key, [:name]
261
296
  def describe_key(name)
262
297
  get("/keys/#{name}").PublicKey
263
298
  end
264
299
 
265
- args :update_key, [:name, :publicKey]
300
+ help_for :update_key, [:name, :publicKey]
266
301
  def update_key(name, key)
267
302
  put("/keys/#{name}", :publicKey => key)
268
303
  end
269
304
 
270
305
  # If address_id is supplied, will return info on that address only
271
- args :describe_addresses, [{:address_id=>:opt}]
306
+ help_for :describe_addresses, [{:address_id=>:opt}]
272
307
  def describe_addresses(address_id=nil)
273
- response = get("/addresses").Address
308
+ response = arrayize(get("/addresses").Address)
274
309
  response.each do |a|
275
310
  a["State"] = @states["ip"][a.State.to_i]
276
311
  end
@@ -283,49 +318,48 @@ class IBMSmartCloud
283
318
  def display_addresses
284
319
  addresses = describe_addresses
285
320
 
286
- logger.info "\nID | Loc | State | IP \n" + addresses.map {|a|
287
- a.ID.ljust(6) + " | " + a.Location.ljust(4) + " | " + (a.State[0..9].ljust(10) rescue '?'.ljust(10)) + " | " + (a.IP.to_s || "")
288
- }.join("\n")
321
+ table = Terminal::Table.new do |t|
322
+ t.headings = ['ID', 'Loc', 'State', 'IP']
323
+ addresses.each do |a|
324
+ t.add_row [a.ID, a.Location, a.State, a.IP]
325
+ end
326
+ end
327
+
328
+ logger.info table
289
329
  end
290
330
 
291
331
  # Allows you to poll for a specific storage state
292
332
  # ex: storage_state_is?("123456", "UNMOUNTED")
293
333
  # storage state string can be given as a string or symbol
294
- args :describe_addresses, [{:address_id=>:req}, {:state_string=>%w(new attached etc...)}]
334
+ help_for :describe_addresses, [{:address_id=>:req}, {:state_string=>%w(new attached etc...)}]
295
335
  def address_state_is?(address_id, state_string)
296
336
  v = describe_addresses(address_id)
297
337
  v.State.to_s == state_string.to_s.upcase
298
338
  end
299
339
 
300
340
  def describe_keys
301
- get("/keys").PublicKey
341
+ arrayize(get("/keys").PublicKey)
302
342
  end
303
343
 
304
344
  def describe_unused_keys
305
345
  describe_keys.select {|key| key.Instances == {}}
306
346
  end
307
347
 
308
- def delete_unused_keys(auto=false)
309
- describe_unused_keys.each do |key|
310
- if auto
311
- remove_keypair(key.KeyName)
312
- else
313
- puts "Remove key #{key.KeyName}? (type 'y' to continue): "
314
- input = gets.strip
315
- if input == 'y'
316
- remove_keypair(key.KeyName)
317
- else
318
- puts "Not removing key #{key.KeyName}"
319
- end
348
+ def display_keys
349
+ keys = describe_keys
350
+
351
+ table = Terminal::Table.new do |t|
352
+ t.headings = ['KeyName', 'Used By Instances', 'Last Modified']
353
+ keys.each do |k|
354
+ t.add_row [k.KeyName, (k.Instances.empty? ? '[NONE]' : arrayize(k.Instances.InstanceID).join("\n")), time_format( k.LastModifiedTime )]
320
355
  end
321
356
  end
357
+
358
+ logger.info table
322
359
  end
360
+ alias display_keypairs display_keys
323
361
 
324
- def display_keys
325
- logger.info "\nKeyName".ljust(50) + " | Instance ID's\n" + describe_keys.map {|key| key.KeyName.strip.ljust(50) + " | " + (key.Instances.empty? ? '[]' : key.Instances.InstanceID.inspect )}.join("\n")
326
- end
327
-
328
- args :supported_instance_types, [:image_id]
362
+ help_for :supported_instance_types, [:image_id]
329
363
  def supported_instance_types(image_id)
330
364
  img=describe_image(image_id)
331
365
  arrayize(img.SupportedInstanceTypes.InstanceType).map(&:ID)
@@ -337,8 +371,8 @@ class IBMSmartCloud
337
371
 
338
372
  # Optionally supply a location to get offerings filtered to a particular location
339
373
  # Optionally supply a name (Small, Medium, Large) to get the specific offering
340
- args :describe_volume_offerings, [{:location => :opt}, {:name => :opt}]
341
- args :describe_storage_offerings, [{:location => :opt}, {:name => :opt}]
374
+ help_for :describe_volume_offerings, [{:location => :opt}, {:name => :opt}]
375
+ help_for :describe_storage_offerings, [{:location => :opt}, {:name => :opt}]
342
376
  def describe_storage_offerings(location=nil, name=nil)
343
377
  response = get("/offerings/storage")
344
378
  if location
@@ -361,13 +395,18 @@ class IBMSmartCloud
361
395
  # ex: create_volume("yan", 61, "Small", "20001208", 61)
362
396
  #
363
397
  # NOTE: storage area id is not currently supported by IBM (according to docs)
364
- args :create_volume, [{:name => :req},{:location_id => :req},{:size => ['Small','Medium','Large']}, {:offering_id => :opt}, {:format => :opt}]
398
+ help_for :create_volume, [{:name => :req},{:location_id => :req},{:size => ['Small','Medium','Large']}, {:offering_id => :opt}, {:format => :opt}]
365
399
  def create_volume(name, location, size, offering_id=nil, format="EXT3")
366
400
 
367
401
  # figure out the offering ID automatically based on location and size
368
402
  if offering_id.nil?
369
403
  logger.debug "Looking up volume offerings based on location: #{location} and size: #{size}"
370
- offering_id = describe_volume_offerings(location, size).ID
404
+ offering = describe_volume_offerings(location, size)
405
+ offering_id = if offering
406
+ offering.ID
407
+ else
408
+ raise "Unable to locate offering with location #{location}, size #{size}."
409
+ end
371
410
  end
372
411
 
373
412
  logger.debug "Creating volume...please wait."
@@ -376,21 +415,13 @@ class IBMSmartCloud
376
415
  end
377
416
 
378
417
  # Optionally takes a volume id, if none supplied, will show all volumes
379
- args :describe_storage, [{:volume_id => :opt}]
418
+ help_for :describe_storage, [{:volume_id => :opt}]
380
419
  def describe_storage(volume_id=nil)
381
420
  response = volume_id ? get("/storage/#{volume_id}") : get("/storage")
382
421
 
383
- if response.Volume.is_a?(Array)
384
- response.Volume.each do |v|
385
- v["State"] = @states["storage"][v.State.to_i]
386
- end
387
- elsif response.Volume
388
- response.Volume["State"] = @states["storage"][response.Volume.State.to_i]
389
- else
390
- return []
422
+ arrayize(response.Volume).each do |v|
423
+ v["State"] = @states["storage"][v.State.to_i]
391
424
  end
392
-
393
- response.Volume
394
425
  end
395
426
 
396
427
  alias describe_volumes describe_storage
@@ -398,16 +429,21 @@ class IBMSmartCloud
398
429
 
399
430
  # This does a human-usable version of describe_volumes with the critical info (name, id, state)
400
431
  # Optionally takes a filter (currently supports state). i.e. display_volumes(:state => :mounted)
401
- args :display_volumes, [{:filter => :opt}]
432
+ help_for :display_volumes, [{:filters => :opt}], %{
433
+ Example filters: {:Name => "match-name", :Location => 101}
434
+ }
402
435
  def display_volumes(filter={})
403
436
  vols = describe_volumes
404
437
  vols = filter_and_sort(vols, filter)
405
438
 
406
- log = "\nVolume | State | Loc | Name\n"
407
- volsz = vols.map {|vol| vol.ID.ljust(6) + " | " + (vol.State[0..9].ljust(10) rescue '?'.ljust(10)) + " | " + vol.Location.ljust(4) + " | " + vol.Name }.join("\n")
408
- log << volsz
409
- logger.info log
410
- true
439
+ table = Terminal::Table.new do |t|
440
+ t.headings = %w(Volume State Loc Name CreatedTime)
441
+ vols.each do |vol|
442
+ t.add_row [vol.ID, vol.State, vol.Location, vol.Name, time_format(vol.CreatedTime)]
443
+ end
444
+ end
445
+
446
+ logger.info table
411
447
  end
412
448
 
413
449
  alias display_storage display_volumes
@@ -415,9 +451,10 @@ class IBMSmartCloud
415
451
  # Allows you to poll for a specific storage state
416
452
  # ex: storage_state_is?("123456", "UNMOUNTED")
417
453
  # storage state string can be given as a string or symbol
418
- args :storage_state_is?, [{:volume_id => :req}, {:state_string => %w(mounted unmounted etc...)}]
454
+ help_for :storage_state_is?, [{:volume_id => :req}, {:state_string => %w(mounted unmounted etc...)}]
419
455
  def storage_state_is?(volume_id, state_string)
420
456
  v = describe_storage(volume_id)
457
+ v = v.first if v.is_a?(Array)
421
458
 
422
459
  @last_storage_state||={}
423
460
  if @last_storage_state[volume_id.to_s] != v.State
@@ -433,7 +470,7 @@ class IBMSmartCloud
433
470
 
434
471
  end
435
472
 
436
- args :instance_state_is?, [{:instance_id=> :req}, {:state_string => %w(active stopping etc...)}]
473
+ help_for :instance_state_is?, [{:instance_id=> :req}, {:state_string => %w(active stopping etc...)}]
437
474
  def instance_state_is?(instance_id, state_string)
438
475
  v = describe_instance(instance_id)
439
476
 
@@ -451,7 +488,7 @@ class IBMSmartCloud
451
488
  end
452
489
 
453
490
  # Polls until volume state is matched. When it is, returns entire volume descriptor hash.
454
- args :poll_for_volume_state, [{:volume_id => :req}, {:state_string => %w(mounted unmounted etc...)}]
491
+ help_for :poll_for_volume_state, [{:volume_id => :req}, {:state_string => %w(mounted unmounted etc...)}]
455
492
  def poll_for_volume_state(volume_id, state_string, polling_interval=5)
456
493
  logger.debug "Polling for volume #{volume_id} to acquire state #{state_string} (interval: #{polling_interval})..."
457
494
  while(true)
@@ -462,7 +499,7 @@ class IBMSmartCloud
462
499
  end
463
500
 
464
501
  # Polls until instance state is matched. When it is, returns entire instance descriptor hash.
465
- args :poll_for_instance_state, [{:instance_id => :req}, {:state_string => %w(active stopped etc...)}]
502
+ help_for :poll_for_instance_state, [{:instance_id => :req}, {:state_string => %w(active stopped etc...)}]
466
503
  def poll_for_instance_state(instance_id, state_string, polling_interval=5)
467
504
  logger.debug "Polling for instance #{instance_id} to acquire state #{state_string} (interval: #{polling_interval})..."
468
505
  while(true)
@@ -473,7 +510,9 @@ class IBMSmartCloud
473
510
  end
474
511
 
475
512
  # Deletes many instances in a thread
476
- args :delete_instances, [:instance_ids], %{Example: smartcloud "delete_instances(12345,12346,12347)"}
513
+ help_for :delete_instances, [:instance_ids], %{
514
+ Example: smartcloud "delete_instances(12345,12346,12347)"
515
+ }
477
516
  def delete_instances(*instance_ids)
478
517
  threads=[]
479
518
  instance_ids.each {|id| logger.info "Sending delete request for: #{id}..."; threads << Thread.new { delete_instance(id) }; logger.info "Finished delete request for #{id}" }
@@ -481,24 +520,25 @@ class IBMSmartCloud
481
520
  true
482
521
  end
483
522
 
484
- args :rename_instance, [:instance_id, :new_name]
523
+ help_for :rename_instance, [:instance_id, :new_name]
485
524
  def rename_instance(instance_id, new_name)
486
525
  put("/instances/#{instance_id}", :name => new_name)
526
+ true
487
527
  end
488
528
 
489
- args :delete_instance, [:instance_id]
529
+ help_for :delete_instance, [:instance_id]
490
530
  def delete_instance(instance_id)
491
531
  delete("/instances/#{instance_id}")
492
532
  true
493
533
  end
494
534
 
495
- args :restart_instance, [:instance_id]
535
+ help_for :restart_instance, [:instance_id]
496
536
  def restart_instance(instance_id)
497
537
  put("/instances/#{instance_id}", :state => "restart")
498
538
  true
499
539
  end
500
540
 
501
- args :describe_instance, [:instance_id]
541
+ help_for :describe_instance, [:instance_id]
502
542
  def describe_instance(instance_id)
503
543
  response = get("instances/#{instance_id}").Instance
504
544
  response["Status"] = @states["instance"][response.Status.to_i]
@@ -508,7 +548,7 @@ class IBMSmartCloud
508
548
  # You can filter by any instance attributes such as
509
549
  # describe_instances(:name => "FOO_BAR", :status => 'ACTIVE')
510
550
  # in the case of status you can also use symbols like :status => :active
511
- args :describe_instances, [:filters => :opt]
551
+ help_for :describe_instances, [:filters => :opt]
512
552
  def describe_instances(filters={})
513
553
  instances = arrayize(get("instances").Instance)
514
554
 
@@ -525,30 +565,34 @@ class IBMSmartCloud
525
565
  # display_instances(:order => "Name") or :order => "LaunchTime"
526
566
  #
527
567
 
528
- args :display_instances, [:filters => :opt], %{example: smartcloud "display_instances(:name=>'matchMe')"}
568
+ help_for :display_instances, [:filters => :opt], %{
569
+ Example: smartcloud "display_instances(:name=>'matchMe')"
570
+ }
529
571
  def display_instances(filters={})
530
572
  instances = describe_instances(filters)
531
573
 
532
- log = %{#{"Started".ljust(18)} | #{"Instance".ljust(8)} | #{"Image".ljust(9)} | #{"Loc".ljust(3)} | #{"Status".ljust(10)} | #{"KeyName".ljust(15)} | #{"IP".ljust(15)} | #{"Volume".ljust(6)} | Name\n}
533
- log << instances.map do |ins|
534
- ip = (ins.IP && ins.IP.to_s) || ""
535
- ip = (ip.strip == "") ? "[NONE]" : ip.strip
536
- "#{DateTime.parse(ins.LaunchTime).strftime("%Y-%m-%d %I:%M%p")} | #{ins.ID.ljust(8)} | #{ins.ImageID.ljust(9)} | #{ins.Location.ljust(3)} | #{ins.Status[0..9].ljust(10)} | #{(ins.KeyName || "").strip[0..14].ljust(15)} | #{ip.ljust(15)} | #{(ins.Volume && ins.Volume || "").ljust(6)} | #{ins.Name}"
537
- end.join("\n")
538
- logger.info "\n#{log}"
574
+ table = Terminal::Table.new do |t|
575
+ t.headings = ['ID', 'LaunchTime', 'Name', 'Key', 'ID', 'Loc', 'Status', 'IP', 'Volume']
576
+ instances.each do |ins|
577
+ launchTime = time_format(ins.LaunchTime)
578
+ t.add_row [ins.ID, launchTime, ins.Name, ins.KeyName, ins.ImageID, ins.Location, ins.Status, ins.IP && ins.IP.strip, ins.Volume]
579
+ end
580
+ end
581
+ table.style = {:padding_left => 0, :padding_right => 0, :border_y => ' ', :border_x => '-'}
582
+ logger.info table
539
583
  end
540
584
 
541
585
 
542
- args :describe_image, [:image_id]
586
+ help_for :describe_image, [:image_id]
543
587
  def describe_image(image_id)
544
588
  image = get("offerings/image/#{image_id}").Image
545
589
  image["State"] = @states["image"][image.State.to_i]
546
590
  image
547
591
  end
548
592
 
549
- args :describe_images, [:filters => :opt]
593
+ help_for :describe_images, [:filters => :opt]
550
594
  def describe_images(filters={})
551
- images = arrayize(get("offerings/image/").Image)
595
+ images = arrayize(get("offerings/image").Image)
552
596
  images.each {|img| img["State"] = @states["image"][img.State.to_i]}
553
597
  filters[:order] ||= "Location"
554
598
  images = filter_and_sort(images, filters)
@@ -558,20 +602,25 @@ class IBMSmartCloud
558
602
  get("offerings/image/#{image_id}/manifest").Manifest
559
603
  end
560
604
 
561
- args :display_images, [:filters => :opt]
605
+ help_for :display_images, [:filters => :opt]
562
606
  def display_images(filters={})
563
607
  images = describe_images(filters)
564
608
 
565
- log = images.map do |i|
566
- types = arrayize(i.SupportedInstanceTypes.InstanceType).map(&:ID).join(", ") rescue "[INSTANCE TYPE UNKNOWN]"
567
- "#{i.ID} | #{i.Location} | #{i.Name} | #{types}"
568
- end.join("\n")
569
- logger.info "\n#{log}"
609
+ table = Terminal::Table.new do |t|
610
+ t.headings = ['ID', 'Loc', 'Name & Type', 'Platform', 'Arch']
611
+ images.each do |i|
612
+ types = arrayize(i.SupportedInstanceTypes.InstanceType).map(&:ID).join("\n") rescue "[INSTANCE TYPE UNKNOWN]"
613
+ t.add_row [i.ID, i.Location, i.Name + "\n" + types, i.Platform, i.Architecture]
614
+ t.add_separator unless i == images.last
615
+ end
616
+ end
617
+
618
+ logger.info table
570
619
  end
571
620
 
572
621
  def delete(path)
573
622
  rescue_and_retry_errors do
574
- output = RestClient.delete File.join(@api_url, path), :accept => :response
623
+ output = @http_client.delete File.join(@api_url, path), :accept => :response
575
624
  response = XmlSimple.xml_in(output, {'ForceArray' => nil})
576
625
  end
577
626
  end
@@ -579,22 +628,38 @@ class IBMSmartCloud
579
628
  def put(path, params={}, param_remap={})
580
629
  rescue_and_retry_errors do
581
630
  param_string = make_param_string(params, param_remap)
582
- output = RestClient.put File.join(@api_url, path), param_string, :accept => :response
631
+ output = @http_client.put File.join(@api_url, path), param_string, :accept => :response
583
632
  response = XmlSimple.xml_in(output, {'ForceArray' => nil})
584
633
  end
585
634
  end
586
635
 
587
636
  def get(path)
588
637
  rescue_and_retry_errors do
589
- output = RestClient.get File.join(@api_url, path), :accept => :response
590
- response = XmlSimple.xml_in(output, {'ForceArray' => nil})
638
+ output = if @simulated_response
639
+ @simulated_response
640
+ else
641
+ @http_client.get File.join(@api_url, path)
642
+ end
643
+
644
+ # Save Response for posterity
645
+ if @save_response && !output.empty?
646
+ response_file = "responses/#{path.gsub('/','_').gsub(/^_/,'')}.#{Time.now.to_i}"
647
+ logger.info "Saving response to: #{response_file}"
648
+ File.open(response_file,'w') {|f| f.write(output)}
649
+ end
650
+
651
+ if output && !output.empty?
652
+ XmlSimple.xml_in(output, {'ForceArray' => nil})
653
+ else
654
+ raise "Empty response!"
655
+ end
591
656
  end
592
657
  end
593
658
 
594
659
  def post(path, params={}, param_remap=nil)
595
660
  rescue_and_retry_errors do
596
661
  param_string = make_param_string(params, param_remap)
597
- output = RestClient.post File.join(@api_url, path), param_string, :accept => :response
662
+ output = @http_client.post File.join(@api_url, path), param_string, :accept => :response
598
663
  response = XmlSimple.xml_in(output, {'ForceArray' => nil})
599
664
  end
600
665
  end
@@ -618,6 +683,8 @@ class IBMSmartCloud
618
683
  def rescue_and_retry_errors(&block)
619
684
  block.call
620
685
  rescue timeouts_and_500s => e
686
+ logger.warn e.inspect
687
+
621
688
  is_500 = e.message =~ /500/
622
689
 
623
690
  @retries -= 1
@@ -687,7 +754,10 @@ class IBMSmartCloud
687
754
  array_or_object.is_a?(Array) ? array_or_object : [array_or_object]
688
755
  end
689
756
 
757
+ def time_format(time)
758
+ DateTime.parse(time).strftime("%Y-%m-%d %I:%M%p")
759
+ end
690
760
  end
691
761
 
692
762
  # predefine an instance for convenience
693
- @smartcloud = IBMSmartCloud.new(ENV['SMARTCLOUD_USERNAME'], ENV['SMARTCLOUD_PASSWORD']) if ENV['SMARTCLOUD_USERNAME'] && ENV['SMARTCLOUD_PASSWORD']
763
+ @smartcloud = IBMSmartCloud.new if ENV['SMARTCLOUD_USERNAME'] && ENV['SMARTCLOUD_PASSWORD']