cft_smartcloud 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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']