cft_smartcloud 0.1.4

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 (135) hide show
  1. data/.gitignore +2 -0
  2. data/CHANGELOG +8 -0
  3. data/LICENSE +178 -0
  4. data/README.rdoc +71 -0
  5. data/Rakefile +53 -0
  6. data/VERSION +1 -0
  7. data/bin/smartcloud +37 -0
  8. data/cft_smartcloud.gemspec +176 -0
  9. data/lib/cli_tools/README.txt +50 -0
  10. data/lib/cli_tools/ic-add-keypair.cmd +29 -0
  11. data/lib/cli_tools/ic-add-keypair.sh +15 -0
  12. data/lib/cli_tools/ic-allocate-address.cmd +29 -0
  13. data/lib/cli_tools/ic-allocate-address.sh +14 -0
  14. data/lib/cli_tools/ic-attach-volume.cmd +27 -0
  15. data/lib/cli_tools/ic-attach-volume.sh +27 -0
  16. data/lib/cli_tools/ic-clone-image.cmd +27 -0
  17. data/lib/cli_tools/ic-clone-image.sh +14 -0
  18. data/lib/cli_tools/ic-clone-volume.cmd +27 -0
  19. data/lib/cli_tools/ic-clone-volume.sh +60 -0
  20. data/lib/cli_tools/ic-cmd.cmd +41 -0
  21. data/lib/cli_tools/ic-cmd.sh +38 -0
  22. data/lib/cli_tools/ic-create-instance.cmd +27 -0
  23. data/lib/cli_tools/ic-create-instance.sh +14 -0
  24. data/lib/cli_tools/ic-create-password.cmd +27 -0
  25. data/lib/cli_tools/ic-create-password.sh +14 -0
  26. data/lib/cli_tools/ic-create-volume.cmd +27 -0
  27. data/lib/cli_tools/ic-create-volume.sh +14 -0
  28. data/lib/cli_tools/ic-delete-image.cmd +27 -0
  29. data/lib/cli_tools/ic-delete-image.sh +14 -0
  30. data/lib/cli_tools/ic-delete-instance.cmd +27 -0
  31. data/lib/cli_tools/ic-delete-instance.sh +14 -0
  32. data/lib/cli_tools/ic-delete-volume.cmd +27 -0
  33. data/lib/cli_tools/ic-delete-volume.sh +14 -0
  34. data/lib/cli_tools/ic-describe-address-offerings.cmd +27 -0
  35. data/lib/cli_tools/ic-describe-address-offerings.sh +14 -0
  36. data/lib/cli_tools/ic-describe-addresses.cmd +27 -0
  37. data/lib/cli_tools/ic-describe-addresses.sh +14 -0
  38. data/lib/cli_tools/ic-describe-image-agreement.cmd +27 -0
  39. data/lib/cli_tools/ic-describe-image-agreement.sh +14 -0
  40. data/lib/cli_tools/ic-describe-image.cmd +27 -0
  41. data/lib/cli_tools/ic-describe-image.sh +14 -0
  42. data/lib/cli_tools/ic-describe-images.cmd +27 -0
  43. data/lib/cli_tools/ic-describe-images.sh +14 -0
  44. data/lib/cli_tools/ic-describe-instance.cmd +27 -0
  45. data/lib/cli_tools/ic-describe-instance.sh +14 -0
  46. data/lib/cli_tools/ic-describe-instances.cmd +27 -0
  47. data/lib/cli_tools/ic-describe-instances.sh +14 -0
  48. data/lib/cli_tools/ic-describe-keypair.cmd +27 -0
  49. data/lib/cli_tools/ic-describe-keypair.sh +14 -0
  50. data/lib/cli_tools/ic-describe-keypairs.cmd +27 -0
  51. data/lib/cli_tools/ic-describe-keypairs.sh +14 -0
  52. data/lib/cli_tools/ic-describe-location.cmd +14 -0
  53. data/lib/cli_tools/ic-describe-location.sh +14 -0
  54. data/lib/cli_tools/ic-describe-locations.cmd +14 -0
  55. data/lib/cli_tools/ic-describe-locations.sh +14 -0
  56. data/lib/cli_tools/ic-describe-request.cmd +27 -0
  57. data/lib/cli_tools/ic-describe-request.sh +14 -0
  58. data/lib/cli_tools/ic-describe-vlans.cmd +27 -0
  59. data/lib/cli_tools/ic-describe-vlans.sh +14 -0
  60. data/lib/cli_tools/ic-describe-volume-offerings.cmd +27 -0
  61. data/lib/cli_tools/ic-describe-volume-offerings.sh +14 -0
  62. data/lib/cli_tools/ic-describe-volume.cmd +27 -0
  63. data/lib/cli_tools/ic-describe-volume.sh +14 -0
  64. data/lib/cli_tools/ic-describe-volumes.cmd +27 -0
  65. data/lib/cli_tools/ic-describe-volumes.sh +14 -0
  66. data/lib/cli_tools/ic-detach-volume.cmd +27 -0
  67. data/lib/cli_tools/ic-detach-volume.sh +27 -0
  68. data/lib/cli_tools/ic-extend-reservation.cmd +27 -0
  69. data/lib/cli_tools/ic-extend-reservation.sh +14 -0
  70. data/lib/cli_tools/ic-generate-keypair.cmd +27 -0
  71. data/lib/cli_tools/ic-generate-keypair.sh +14 -0
  72. data/lib/cli_tools/ic-release-address.cmd +27 -0
  73. data/lib/cli_tools/ic-release-address.sh +14 -0
  74. data/lib/cli_tools/ic-remove-keypair.cmd +27 -0
  75. data/lib/cli_tools/ic-remove-keypair.sh +14 -0
  76. data/lib/cli_tools/ic-restart-instance.cmd +27 -0
  77. data/lib/cli_tools/ic-restart-instance.sh +14 -0
  78. data/lib/cli_tools/ic-save-instance.cmd +27 -0
  79. data/lib/cli_tools/ic-save-instance.sh +14 -0
  80. data/lib/cli_tools/ic-set-default-key.cmd +27 -0
  81. data/lib/cli_tools/ic-set-default-key.sh +14 -0
  82. data/lib/cli_tools/ic-update-instance.cmd +27 -0
  83. data/lib/cli_tools/ic-update-instance.sh +14 -0
  84. data/lib/cli_tools/ic-update-keypair.cmd +27 -0
  85. data/lib/cli_tools/ic-update-keypair.sh +14 -0
  86. data/lib/cli_tools/lib/DeveloperCloud_API_Client_JAR.jar +0 -0
  87. data/lib/cli_tools/lib/DeveloperCloud_CMD_Tool.jar +0 -0
  88. data/lib/cli_tools/lib/commons-beanutils-1.6.1.jar +0 -0
  89. data/lib/cli_tools/lib/commons-cli-1.2.jar +0 -0
  90. data/lib/cli_tools/lib/commons-codec-1.3.jar +0 -0
  91. data/lib/cli_tools/lib/commons-collections-3.2.1.jar +0 -0
  92. data/lib/cli_tools/lib/commons-digester-1.8.jar +0 -0
  93. data/lib/cli_tools/lib/commons-httpclient-3.1.jar +0 -0
  94. data/lib/cli_tools/lib/commons-lang-2.3.jar +0 -0
  95. data/lib/cli_tools/lib/commons-logging-1.1.1.jar +0 -0
  96. data/lib/cli_tools/logging.properties +7 -0
  97. data/lib/cli_tools/manifest.rmd +26 -0
  98. data/lib/config/config.yml +50 -0
  99. data/lib/hash_fix.rb +37 -0
  100. data/lib/mime-types-1.16/History.txt +107 -0
  101. data/lib/mime-types-1.16/Install.txt +17 -0
  102. data/lib/mime-types-1.16/Licence.txt +15 -0
  103. data/lib/mime-types-1.16/Manifest.txt +12 -0
  104. data/lib/mime-types-1.16/README.txt +28 -0
  105. data/lib/mime-types-1.16/Rakefile +316 -0
  106. data/lib/mime-types-1.16/lib/mime/types.rb +751 -0
  107. data/lib/mime-types-1.16/lib/mime/types.rb.data +1324 -0
  108. data/lib/mime-types-1.16/mime-types.gemspec +43 -0
  109. data/lib/mime-types-1.16/setup.rb +1585 -0
  110. data/lib/mime-types-1.16/test/test_mime_type.rb +356 -0
  111. data/lib/mime-types-1.16/test/test_mime_types.rb +122 -0
  112. data/lib/mock_smartcloud.rb +53 -0
  113. data/lib/rest-client-1.6.3/README.rdoc +276 -0
  114. data/lib/rest-client-1.6.3/Rakefile +66 -0
  115. data/lib/rest-client-1.6.3/VERSION +1 -0
  116. data/lib/rest-client-1.6.3/bin/restclient +92 -0
  117. data/lib/rest-client-1.6.3/history.md +112 -0
  118. data/lib/rest-client-1.6.3/lib/rest-client.rb +2 -0
  119. data/lib/rest-client-1.6.3/lib/rest_client.rb +2 -0
  120. data/lib/rest-client-1.6.3/lib/restclient/abstract_response.rb +106 -0
  121. data/lib/rest-client-1.6.3/lib/restclient/exceptions.rb +193 -0
  122. data/lib/rest-client-1.6.3/lib/restclient/net_http_ext.rb +21 -0
  123. data/lib/rest-client-1.6.3/lib/restclient/payload.rb +220 -0
  124. data/lib/rest-client-1.6.3/lib/restclient/raw_response.rb +34 -0
  125. data/lib/rest-client-1.6.3/lib/restclient/request.rb +314 -0
  126. data/lib/rest-client-1.6.3/lib/restclient/resource.rb +169 -0
  127. data/lib/rest-client-1.6.3/lib/restclient/response.rb +24 -0
  128. data/lib/rest-client-1.6.3/lib/restclient.rb +174 -0
  129. data/lib/restclient_fix.rb +41 -0
  130. data/lib/smartcloud.rb +616 -0
  131. data/lib/smartcloud_logger.rb +20 -0
  132. data/lib/xml-simple-1.0.12/lib/xmlsimple.rb +1028 -0
  133. data/script/console +3 -0
  134. data/test/helper.rb +22 -0
  135. metadata +196 -0
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+ #####################################################################################
3
+ # Copyright (c) 2011, Cohesive Flexible Technologies, Inc.
4
+ # This copyrighted material is the property of Cohesive Flexible Technologies and
5
+ # is subject to the license terms of the product it is contained within, whether
6
+ # in text or compiled form. It is licensed under the terms expressed in the
7
+ # accompanying README and LICENSE files.
8
+ #
9
+ # This program is AS IS and WITHOUT ANY WARRANTY; without even the implied warranty
10
+ # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+ #####################################################################################
12
+
13
+ # Enables modification of timeout in RestClient
14
+ module RestClient
15
+
16
+ class << self
17
+ attr_accessor :timeout
18
+ end
19
+
20
+ def self.get(url, headers={}, &block)
21
+ Request.execute(:method => :get, :url => url, :headers => headers, :timeout => @timeout, &block)
22
+ end
23
+
24
+ def self.post(url, payload, headers={}, &block)
25
+ Request.execute(:method => :post, :url => url, :payload => payload, :headers => headers, :timeout => @timeout, &block)
26
+ end
27
+
28
+ def self.patch(url, payload, headers={}, &block)
29
+ Request.execute(:method => :patch, :url => url, :payload => payload, :headers => headers, :timeout => @timeout, &block)
30
+ end
31
+
32
+ def self.put(url, payload, headers={}, &block)
33
+ Request.execute(:method => :put, :url => url, :payload => payload, :headers => headers, :timeout => @timeout, &block)
34
+ end
35
+
36
+ def self.delete(url, headers={}, &block)
37
+ Request.execute(:method => :delete, :url => url, :headers => headers, :timeout => @timeout, &block)
38
+ end
39
+
40
+ end
41
+
data/lib/smartcloud.rb ADDED
@@ -0,0 +1,616 @@
1
+ #!/usr/bin/env ruby
2
+ #####################################################################################
3
+ # Copyright (c) 2011, Cohesive Flexible Technologies, Inc.
4
+ # This copyrighted material is the property of Cohesive Flexible Technologies and
5
+ # is subject to the license terms of the product it is contained within, whether
6
+ # in text or compiled form. It is licensed under the terms expressed in the
7
+ # accompanying README and LICENSE files.
8
+ #
9
+ # This program is AS IS and WITHOUT ANY WARRANTY; without even the implied warranty
10
+ # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+ #####################################################################################
12
+
13
+ Dir["#{File.dirname(__FILE__)}/**/**"].each {|dir| $LOAD_PATH << dir }
14
+ $LOAD_PATH << File.dirname(__FILE__)
15
+
16
+ require 'tempfile'
17
+ require 'logger'
18
+ require 'cgi'
19
+ require 'rest_client'
20
+ require 'yaml'
21
+ require 'restclient_fix'
22
+ require 'hash_fix'
23
+ require 'xmlsimple'
24
+ require 'smartcloud_logger'
25
+
26
+ IBM_TOOLS_HOME=File.join(File.dirname(__FILE__), "cli_tools") unless defined?(IBM_TOOLS_HOME)
27
+
28
+ # Encapsulates communications with IBM SmartCloud via REST
29
+ class IBMSmartCloud
30
+
31
+ attr_accessor :logger
32
+
33
+ def initialize(username, password, logger=nil, debug=false)
34
+ @username = username
35
+ @password = password
36
+ @logger = logger || SmartcloudLogger.new(STDOUT)
37
+ @debug = debug
38
+
39
+ @config = YAML.load_file(File.join(File.dirname(__FILE__), "config/config.yml"))
40
+ @states = @config["states"]
41
+
42
+ @api_url = @config["api_url"]
43
+ @api_url.gsub!("https://", "https://#{CGI::escape(username)}:#{CGI::escape(password)}@")
44
+
45
+ RestClient.timeout = 120 # ibm requests can be very slow
46
+ RestClient.log = @logger if @debug
47
+ end
48
+
49
+ class << self
50
+ @config = YAML.load_file(File.join(File.dirname(__FILE__), "config/config.yml"))
51
+ attr_reader :method_help
52
+ attr_reader :config
53
+ end
54
+
55
+ def self.args(method, args)
56
+ @method_help||={}
57
+ @method_help[method.to_s] = args
58
+ end
59
+
60
+ def help(method=nil)
61
+ if method
62
+ args = (self.class.method_help[method.to_s])
63
+ if args.nil?
64
+ puts method.to_s
65
+ else
66
+ args = args.map do |arg|
67
+ if arg.is_a?(Hash)
68
+ # If an argument is required, just list it
69
+ if arg.values.first==:req
70
+ arg.keys.first.to_s
71
+ # If it's optional, list it in brackets
72
+ elsif arg.values.first==:opt
73
+ "[#{arg.keys.first.to_s}]"
74
+ # If there is an array of options, list them
75
+ else
76
+ "#{arg.keys.first.to_s}=>#{arg.values.first.inspect}"
77
+ end
78
+ else
79
+ arg
80
+ end
81
+ end.join(", ")
82
+
83
+ puts "#{method.to_s}(#{args})"
84
+ end
85
+ else
86
+ methods = public_methods - Object.public_methods - ['post','get','put','delete','logger','logger=','help']
87
+ puts methods.sort.join("\n")
88
+ end
89
+ end
90
+
91
+ # Get a list of data centers
92
+ def describe_locations
93
+ get("/locations").Location
94
+ end
95
+
96
+ def display_locations
97
+ log = describe_locations.map {|loc| "#{loc.ID} #{loc.Location}"}.join("\n")
98
+ logger.info "\n#{log}"
99
+ end
100
+
101
+ # Create an instance. The instance_params hash can follow
102
+ # CLI-style attribute naming. I.e. "image-id" or "data-center"
103
+ # or API-style (imageID, location). If the CLI-style params are
104
+ # provided, they will be remapped to the correct API params.
105
+ args :create_instance, [:instance_params_hash]
106
+ def create_instance(instance_params)
107
+ param_remap = { "name" => "name",
108
+ "image-id" => "imageID",
109
+ "instance-type" => "instanceType",
110
+ "data-center" => "location",
111
+ "key-name" => "publicKey",
112
+ "ip-address-id" => "ip",
113
+ "volume-id" => "volumeID",
114
+ "configuration" => "ConfigurationData",
115
+ "vlan-id" => "vlanID",
116
+ "antiCollocationInstance" => "antiCollocationInstance",
117
+ "isMiniEphemeral" => "isMiniEphemeral"
118
+ }
119
+
120
+ # api does not take description
121
+ instance_params.delete("description")
122
+
123
+ # configuration data has to be changed from a string like
124
+ # <configuration>{contextmanager:baml-c3-master.cohesiveft.com,clustername:BAML_poc_pk0515,role:[nfs-client-setup|newyork_master_refdata_member|install-luci|rhel-openlikewise-client-setup|join-domain],hostname:r550n107}</configuration>
125
+ # to a standard list of POST params like
126
+ # contextmanager=baml-c3-mager&clustername=BAML...
127
+ configuration_data = instance_params.delete("configuration") || instance_params.delete("ConfigurationData")
128
+ if configuration_data
129
+ if configuration_data =~ /\s+/
130
+ logger.warn "<configuration> tag should not contain spaces! Correct format looks like: <configuration>{foo:bar,baz:quux}</configuration>. Spaces will be removed."
131
+ end
132
+ configuration_data.delete!("{}") # get rid of curly braces
133
+ config_data_params = configuration_data.split(",").map{|param| param.split(":")} # split each foo:bar into key and value
134
+ config_data_params.each {|k,v| instance_params[k]=v} # add it into the standard instance params array
135
+ end
136
+
137
+ post "/instances", instance_params, param_remap
138
+ end
139
+
140
+ # Find a location by its name. Assumes locations have unique names (not verified by ibm docs)
141
+ args :get_locations_by_name, [:name]
142
+ def get_location_by_name(name)
143
+ locations = describe_locations
144
+ locations.detect {|loc| loc.Name == name}
145
+ end
146
+
147
+ # find all ip address offerings. If a location is specified, give the offering for the location
148
+ # assumption: only one offering per location. This is assumed, not verified in docs.
149
+ args :describe_address_offerings, [{:location => :opt}]
150
+ def describe_address_offerings(location=nil)
151
+ response=get("/offerings/address").Offerings
152
+ if location
153
+ response.detect {|offering| offering.Location.to_s == location.to_s}
154
+ else
155
+ response
156
+ end
157
+ end
158
+
159
+ # Allocate IP address. Offering ID is optional. if not specified, will look up offerings
160
+ # for this location and pick one
161
+ args :allocate_address, [{:location => :req}, {:offering_id => :opt}, {:foo => [:bar, :baz, :quux]}]
162
+ def allocate_address(location, offering_id=nil)
163
+ if offering_id.nil?
164
+ offering_id = describe_address_offerings(location).ID
165
+ end
166
+ post("/addresses", :offeringID => offering_id, :location => location).Address
167
+ end
168
+
169
+
170
+ args :describe_address, [:address_id]
171
+ def describe_address(address_id)
172
+ address = get("/addresses/#{address_id}").Address
173
+ address["State"] = @config.states.ip[address.State]
174
+ address
175
+ end
176
+
177
+ # Launches a clone image call from CLI
178
+ args :clone_image, [:name, :description, :image_id]
179
+ def clone_image(name, description, image_id)
180
+ post("/offerings/image/#{image_id}", :name => name, :description => description).ImageID
181
+ end
182
+
183
+ # Launches a clone request and returns ID of new volume
184
+ # TODO: the API call does not work, we have to use the cli for now
185
+ args :clone_volume, [:name, :source_disk_id]
186
+ def clone_volume(name, source_disk_id)
187
+ # result = post("/storage", :name => name, :SourceDiskID => source_disk_id)
188
+ # result.Volume.ID
189
+ create_password_file
190
+ result = run_ibm_tool("ic-clone-volume.sh -n #{name} -S #{source_disk_id}")
191
+ result =~ /ID : (.*)/
192
+ return $1 # grab the id of the created volume
193
+ end
194
+
195
+ # Delete the volume
196
+ args :delete_volume, [:vol_id]
197
+ def delete_volume(vol_id)
198
+ delete("/storage/#{vol_id}")
199
+ true
200
+ end
201
+
202
+ # generates a keypair and returns the private key
203
+ args :generate_keypair, [:name]
204
+ def generate_keypair(name)
205
+ response = post("/keys", :name => name)
206
+ response.PrivateKey.KeyMaterial
207
+ end
208
+
209
+ args :remove_keypair, [:name]
210
+ def remove_keypair(name)
211
+ delete("/keys/#{name}")
212
+ true
213
+ end
214
+
215
+ args :describe_key, [:name]
216
+ def describe_key(name)
217
+ get("/keys/#{name}").PublicKey
218
+ end
219
+
220
+ # If address_id is supplied, will return info on that address only
221
+ args :describe_addresses, [{:address_id=>:opt}]
222
+ def describe_addresses(address_id=nil)
223
+ response = get("/addresses").Address
224
+ response.each do |a|
225
+ a["State"] = @states["ip"][a.State.to_i]
226
+ end
227
+ if address_id
228
+ response = response.detect {|address| address.ID.to_s == address_id.to_s}
229
+ end
230
+ response
231
+ end
232
+
233
+ # Allows you to poll for a specific storage state
234
+ # ex: storage_state_is?("123456", "UNMOUNTED")
235
+ # storage state string can be given as a string or symbol
236
+ args :describe_addresses, [{:address_id=>:req}, {:state_string=>%w(new attached etc...)}]
237
+ def address_state_is?(address_id, state_string)
238
+ v = describe_addresses(address_id)
239
+ v.State.to_s == state_string.to_s.upcase
240
+ end
241
+
242
+ def describe_keys
243
+ get("/keys").PublicKey
244
+ end
245
+
246
+ def describe_unused_keys
247
+ describe_keys.select {|key| key.Instances == {}}
248
+ end
249
+
250
+ def delete_unused_keys(auto=false)
251
+ describe_unused_keys.each do |key|
252
+ if auto
253
+ remove_keypair(key.KeyName)
254
+ else
255
+ puts "Remove key #{key.KeyName}? (type 'y' to continue): "
256
+ input = gets.strip
257
+ if input == 'y'
258
+ remove_keypair(key.KeyName)
259
+ else
260
+ puts "Not removing key #{key.KeyName}"
261
+ end
262
+ end
263
+ end
264
+ end
265
+
266
+ def display_keys
267
+ 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")
268
+ end
269
+
270
+ args :supported_instance_types, [:image_id]
271
+ def supported_instance_types(image_id)
272
+ img=describe_image(image_id)
273
+ arrayize(img.SupportedInstanceTypes.InstanceType).map(&:ID)
274
+ end
275
+ # NOTE: doesn't seem to be supported
276
+ # def describe_vlans
277
+ # get("/offerings/vlan").Vlan
278
+ # end
279
+
280
+ # Optionally supply a location to get offerings filtered to a particular location
281
+ # Optionally supply a name (Small, Medium, Large) to get the specific offering
282
+ args :describe_volume_offerings, [{:location => :opt}, {:name => :opt}]
283
+ def describe_volume_offerings(location=nil, name=nil)
284
+ response = get("/offerings/storage")
285
+ if location
286
+ filtered_by_location = response.Offerings.select {|o| o.Location.to_s == location.to_s}
287
+ if name
288
+ filtered_by_location.detect {|o| o.Name == name}
289
+ else
290
+ filtered_by_location
291
+ end
292
+ else
293
+ response.Offerings
294
+ end
295
+ end
296
+
297
+ # Create a volume. offering_id is optional and will be determined automatically
298
+ # for you based on the location and volume size.
299
+ #
300
+ # ex: create_volume("yan", 61, "Small", "20001208", 61)
301
+ #
302
+ # NOTE: storage area id is not currently supported by IBM (according to docs)
303
+ args :create_volume, [{:name => :req},{:location_id => :req},{:size => ['Small','Medium','Large']}, {:offering_id => :opt}, {:format => :opt}]
304
+ def create_volume(name, location, size, offering_id=nil, format="EXT3")
305
+
306
+ # figure out the offering ID automatically based on location and size
307
+ if offering_id.nil?
308
+ logger.debug "Looking up volume offerings based on location: #{location} and size: #{size}"
309
+ offering_id = describe_volume_offerings(location, size).ID
310
+ end
311
+
312
+ logger.debug "Creating volume...please wait."
313
+ result = post("/storage", :format => format, :location => location, :name => name, :size => size, :offeringID => offering_id)
314
+ result.Volume.ID
315
+ end
316
+
317
+ # Optionally takes a volume id, if none supplied, will show all volumes
318
+ args :describe_storage, [{:volume_id => :opt}]
319
+ def describe_storage(volume_id=nil)
320
+ response = volume_id ? get("/storage/#{volume_id}") : get("/storage")
321
+
322
+ if response.Volume.is_a?(Array)
323
+ response.Volume.each do |v|
324
+ v["State"] = @states["storage"][v.State.to_i]
325
+ end
326
+ elsif response.Volume
327
+ response.Volume["State"] = @states["storage"][response.Volume.State.to_i]
328
+ else
329
+ return []
330
+ end
331
+
332
+ response.Volume
333
+ end
334
+
335
+ alias describe_volumes describe_storage
336
+ alias describe_volume describe_storage
337
+
338
+ # This does a human-usable version of describe_volumes with the critical info (name, id, state)
339
+ # Optionally takes a filter (currently supports state). i.e. display_volumes(:state => :mounted)
340
+ args :display_volumes, [{:filter => :opt}]
341
+ def display_volumes(filter={})
342
+ vols = describe_volumes
343
+ vols = filter_and_sort(vols, filter)
344
+
345
+ log = "\nVolume | State | Loc | Name\n"
346
+ 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")
347
+ log << volsz
348
+ logger.info log
349
+ true
350
+ end
351
+
352
+ alias display_storage display_volumes
353
+
354
+ # Allows you to poll for a specific storage state
355
+ # ex: storage_state_is?("123456", "UNMOUNTED")
356
+ # storage state string can be given as a string or symbol
357
+ args :storage_state_is?, [{:volume_id => :req}, {:state_string => %w(mounted unmounted etc...)}]
358
+ def storage_state_is?(volume_id, state_string)
359
+ v = describe_storage(volume_id)
360
+
361
+ @last_storage_state||={}
362
+ if @last_storage_state[volume_id.to_s] != v.State
363
+ logger.debug "Volume: #{volume_id}; Current State: #{v.State}; Waiting for: #{state_string.to_s.upcase} " # log it every time it changes
364
+ end
365
+ @last_storage_state[volume_id.to_s] = v.State
366
+
367
+ if v.State.to_s == state_string.to_s.upcase
368
+ v
369
+ else
370
+ false
371
+ end
372
+
373
+ end
374
+
375
+ args :instance_state_is?, [{:instance_id=> :req}, {:state_string => %w(active stopping etc...)}]
376
+ def instance_state_is?(instance_id, state_string)
377
+ v = describe_instance(instance_id)
378
+
379
+ @last_instance_state||={}
380
+ if @last_instance_state[instance_id.to_s] != v.Status
381
+ logger.debug "Instance: #{instance_id}; Current State: #{v.Status}; Waiting for: #{state_string.to_s.upcase}" # log it every time it changes
382
+ end
383
+ @last_instance_state[instance_id.to_s] = v.Status
384
+
385
+ if v.Status.to_s == state_string.to_s.upcase
386
+ v
387
+ else
388
+ false
389
+ end
390
+ end
391
+
392
+ # Polls until volume state is matched. When it is, returns entire volume descriptor hash.
393
+ args :poll_for_volume_state, [{:volume_id => :req}, {:state_string => %w(mounted unmounted etc...)}]
394
+ def poll_for_volume_state(volume_id, state_string, polling_interval=5)
395
+ logger.debug "Polling for volume #{volume_id} to acquire state #{state_string} (interval: #{polling_interval})..."
396
+ while(true)
397
+ descriptor = storage_state_is?(volume_id, state_string)
398
+ return descriptor if descriptor
399
+ sleep(polling_interval)
400
+ end
401
+ end
402
+
403
+ # Polls until instance state is matched. When it is, returns entire instance descriptor hash.
404
+ args :poll_for_instance_state, [{:instance_id => :req}, {:state_string => %w(active stopped etc...)}]
405
+ def poll_for_instance_state(instance_id, state_string, polling_interval=5)
406
+ logger.debug "Polling for instance #{instance_id} to acquire state #{state_string} (interval: #{polling_interval})..."
407
+ while(true)
408
+ descriptor = instance_state_is?(instance_id, state_string)
409
+ return descriptor if descriptor
410
+ sleep(polling_interval)
411
+ end
412
+ end
413
+
414
+ # Deletes many instances in a thread
415
+ args :delete_instances, [:instance_ids => [12345,12346,12347, '...']]
416
+ def delete_instances(instance_ids)
417
+ instance_ids.each {|id| delete_instance(id) }
418
+ end
419
+
420
+
421
+ args :delete_instance, [:instance_id]
422
+ def delete_instance(instance_id)
423
+ delete("/instances/#{instance_id}")
424
+ true
425
+ end
426
+
427
+ args :restart_instance, [:instance_id]
428
+ def restart_instance(instance_id)
429
+ put("/instances/#{instance_id}", :state => "restart")
430
+ true
431
+ end
432
+
433
+ args :describe_instance, [:instance_id]
434
+ def describe_instance(instance_id)
435
+ response = get("instances/#{instance_id}").Instance
436
+ response["Status"] = @states["instance"][response.Status.to_i]
437
+ response
438
+ end
439
+
440
+ # You can filter by any instance attributes such as
441
+ # describe_instances(:name => "FOO_BAR", :status => 'ACTIVE')
442
+ # in the case of status you can also use symbols like :status => :active
443
+ args :describe_instances, [:filters => :opt]
444
+ def describe_instances(filters={})
445
+ instances = arrayize(get("instances").Instance)
446
+
447
+ instances.each do |instance|
448
+ instance["Status"] = @states["instance"][instance.Status.to_i]
449
+ end
450
+
451
+ filters[:order] ||= "LaunchTime"
452
+ instances = filter_and_sort(instances, filters)
453
+ end
454
+
455
+ # Same as describe_instances except prints a human readable summary
456
+ # Also takes an :order param, examples:
457
+ # display_instances(:order => "Name") or :order => "LaunchTime"
458
+ #
459
+ args :display_instances, [:filters => :opt]
460
+ def display_instances(filters={})
461
+ instances = describe_instances(filters)
462
+
463
+ log = %{#{"Started".ljust(18)} | #{"Instance".ljust(8)} | #{"Image".ljust(9)} | #{"Loc".ljust(3)} | #{"Status".ljust(10)} | #{"KeyName".ljust(15)} | #{"IP".ljust(15)} | Name\n}
464
+ log << instances.map do |ins|
465
+ "#{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)} | #{(ins.IP.strip=="" ? '[NONE]' : ins.IP.strip).to_s.ljust(15)} | #{ins.Name}"
466
+ end.join("\n")
467
+ logger.info "\n#{log}"
468
+ end
469
+
470
+
471
+ args :describe_image, [:image_id]
472
+ def describe_image(image_id)
473
+ image = get("offerings/image/#{image_id}").Image
474
+ image["State"] = @states["image"][image.State.to_i]
475
+ image
476
+ end
477
+
478
+ args :describe_images, [:filters => :opt]
479
+ def describe_images(filters={})
480
+ images = arrayize(get("offerings/image/").Image)
481
+ images.each {|img| img["State"] = @states["image"][img.State.to_i]}
482
+ filters[:order] ||= "Location"
483
+ images = filter_and_sort(images, filters)
484
+ end
485
+
486
+ args :display_images, [:filters => :opt]
487
+ def display_images(filters={})
488
+ images = describe_images(filters)
489
+
490
+ log = images.map do |i|
491
+ types = arrayize(i.SupportedInstanceTypes.InstanceType).map(&:ID).join(", ") rescue "[INSTANCE TYPE UNKNOWN]"
492
+ "#{i.ID} | #{i.Location} | #{i.Name} | #{types}"
493
+ end.join("\n")
494
+ logger.info "\n#{log}"
495
+ end
496
+
497
+ def delete(path)
498
+ output = RestClient.delete File.join(@api_url, path), :accept => :response
499
+ response = XmlSimple.xml_in(output, {'ForceArray' => nil})
500
+ rescue => e
501
+ raise_restclient_error(e)
502
+ end
503
+
504
+ def put(path, params={}, param_remap={})
505
+ param_string = make_param_string(params, param_remap)
506
+ output = RestClient.put File.join(@api_url, path), param_string, :accept => :response
507
+ response = XmlSimple.xml_in(output, {'ForceArray' => nil})
508
+ rescue => e
509
+ raise_restclient_error(e)
510
+ end
511
+
512
+ def get(path)
513
+ output = RestClient.get File.join(@api_url, path), :accept => :response
514
+ response = XmlSimple.xml_in(output, {'ForceArray' => nil})
515
+ rescue => e
516
+ raise_restclient_error(e)
517
+ end
518
+
519
+ def post(path, params={}, param_remap=nil)
520
+ param_string = make_param_string(params, param_remap)
521
+ output = RestClient.post File.join(@api_url, path), param_string, :accept => :response
522
+ response = XmlSimple.xml_in(output, {'ForceArray' => nil})
523
+ response
524
+ rescue => e
525
+ raise_restclient_error(e)
526
+ end
527
+
528
+ private
529
+ # rest client error details are in the response so we want to
530
+ # display that as the error, otherwise we lose that info
531
+ def raise_restclient_error(e)
532
+ if e.respond_to?(:response) && !e.is_a?(RestClient::RequestTimeout)
533
+ raise "#{e.message} - #{e.response}"
534
+ else
535
+ raise e
536
+ end
537
+ end
538
+
539
+ def make_param_string(params, param_remap)
540
+ param_string = params.map do |k,v|
541
+ k=k.to_s # symbol keys turn to string
542
+
543
+ # logger.debug "Removing all spaces from parameters, smartcloud API does not allow spaces."
544
+ # v = v.gsub(/\\s+/,'') # remove all spaces! smartcloud does not like spaces in params
545
+ if param_remap && param_remap[k]
546
+ k = param_remap[k]
547
+ end
548
+
549
+ "#{CGI.escape(k)}=#{CGI.escape(v.to_s)}"
550
+ end.compact.join("&")
551
+ end
552
+
553
+ def filter_and_sort(instances=[], filters={})
554
+ order_by = filters.delete(:order)
555
+
556
+ filters.each do |filter, value|
557
+ value = value.to_s.upcase if (filter==:status || filter==:state)
558
+ if filter == :name || filter == :Name
559
+ instances = instances.select {|inst| inst.send(filter.to_s.capitalize) =~ /#{value}/}
560
+ else
561
+ instances = instances.select {|inst| inst.send(filter.to_s.capitalize) == value.to_s}
562
+ end
563
+ end
564
+
565
+ instances = instances.sort_by{|ins| ins.has_key?(order_by) ? ins.send(order_by) : 0 }
566
+ if order_by == "LaunchTime"
567
+ instances = instances.reverse
568
+ end
569
+
570
+ instances
571
+ end
572
+
573
+ def arrayize(array_or_object)
574
+ return [] unless array_or_object
575
+ array_or_object.is_a?(Array) ? array_or_object : [array_or_object]
576
+ end
577
+
578
+ # TODO: all methods below here can be nuked once CLI tools are removed
579
+ def run_ibm_tool(command)
580
+ cmd = "cd #{IBM_TOOLS_HOME} && ./#{command} -u #{@username} -w #{passphrase} -g #{password_file}"
581
+ logger.debug cmd
582
+ output = if @debug
583
+ IBMMockResponseGenerator.respond(command)
584
+ else
585
+ `#{cmd}`
586
+ end
587
+
588
+ logger.debug output
589
+
590
+ if output =~ /ErrorMessage : (.*)/
591
+ raise $1 # raise the error message matched above
592
+ else
593
+ return output
594
+ end
595
+ end
596
+
597
+ def create_password_file
598
+ # if no passphrase or password file is supplied, we'll generate it
599
+ run_ibm_tool("ic-create-password.sh -u #{@username} -p #{@password}")
600
+ end
601
+
602
+ def password_file
603
+ @password_file ||= "/tmp/smrtcloud-pwfile-#{Time.now.to_i.to_s}"
604
+ end
605
+
606
+ def passphrase
607
+ @passphrase ||= Time.now.to_i.to_s
608
+ end
609
+ # End CLI tool helpers
610
+
611
+
612
+
613
+ end
614
+
615
+ # predefine an instance for convenience
616
+ @smartcloud = IBMSmartCloud.new(ENV['SMARTCLOUD_USERNAME'], ENV['SMARTCLOUD_PASSWORD']) if ENV['SMARTCLOUD_USERNAME'] && ENV['SMARTCLOUD_PASSWORD']
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ #####################################################################################
3
+ # Copyright (c) 2011, Cohesive Flexible Technologies, Inc.
4
+ # This copyrighted material is the property of Cohesive Flexible Technologies and
5
+ # is subject to the license terms of the product it is contained within, whether
6
+ # in text or compiled form. It is licensed under the terms expressed in the
7
+ # accompanying README and LICENSE files.
8
+ #
9
+ # This program is AS IS and WITHOUT ANY WARRANTY; without even the implied warranty
10
+ # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+ #####################################################################################
12
+
13
+ class SmartcloudLogger < Logger
14
+ def format_message(severity, timestamp, foobar, message)
15
+ message = message.to_s
16
+
17
+ "#{timestamp.strftime("%b %d %H:%M:%S")} #{severity}: #{message}\n"
18
+ end
19
+
20
+ end