deltacloud-client 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -16,20 +16,24 @@
16
16
  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
18
  require 'rake/gempackagetask'
19
- require 'spec/rake/spectask'
20
19
 
21
20
  load 'deltacloud-client.gemspec'
22
21
 
22
+ desc "Generate documentation"
23
+ task 'documentation' do
24
+ load 'lib/documentation.rb'
25
+ end
26
+
23
27
  Rake::GemPackageTask.new(@spec) do |pkg|
24
28
  pkg.need_tar = true
25
29
  end
26
30
 
27
- desc "Run all examples"
28
- Spec::Rake::SpecTask.new('spec') do |t|
29
- t.spec_files = FileList['specs/**/*_spec.rb']
30
- t.spec_opts = [
31
- '--format html:spec_report.html'
32
- ]
31
+ if Gem.available?('rspec')
32
+ require 'spec/rake/spectask'
33
+ desc "Run all examples"
34
+ Spec::Rake::SpecTask.new('spec') do |t|
35
+ t.spec_files = FileList['specs/**/*_spec.rb']
36
+ end
33
37
  end
34
38
 
35
39
  desc "Setup Fixtures"
@@ -19,7 +19,10 @@
19
19
  require 'rubygems'
20
20
  require 'optparse'
21
21
  require 'uri'
22
- require 'deltacloud'
22
+ require 'lib/deltacloud'
23
+ require 'lib/plain_formatter'
24
+
25
+ include DeltaCloud::PlainFormatter
23
26
 
24
27
  options = {
25
28
  :verbose => false
@@ -66,7 +69,7 @@ options[:collection] = ARGV[0]
66
69
  options[:operation] = ARGV[1]
67
70
 
68
71
  # Connect to Deltacloud API and fetch all entry points
69
- client = DeltaCloud.new(url.user || ENV['API_USER'], url.password || ENV['API_PASSWORD'], api_url, { :verbose => options[:verbose] })
72
+ client = DeltaCloud.new(url.user || ENV['API_USER'], url.password || ENV['API_PASSWORD'], api_url)
70
73
  collections = client.entry_points.keys
71
74
 
72
75
  # Exclude collection which don't have methods in client library yet
@@ -106,7 +109,7 @@ if options[:collection] and ( options[:operation].nil? or options[:operation].eq
106
109
  params.merge!(:id => options[:id]) if options[:id]
107
110
  params.merge!(:state => options[:state]) if options[:state]
108
111
  client.send(options[:collection].to_s, params).each do |model|
109
- puts model.to_plain
112
+ puts format(model)
110
113
  end
111
114
  exit(0)
112
115
  end
@@ -121,7 +124,7 @@ if options[:collection] and options[:operation]
121
124
  # If collection is set and requested operation is 'show' just 'singularize'
122
125
  # collection name and print item with specified id (-i parameter)
123
126
  if options[:operation].eql?('show')
124
- puts client.send(options[:collection].gsub(/s$/, ''), options[:id] ).to_plain
127
+ puts format(client.send(options[:collection].gsub(/s$/, ''), options[:id]))
125
128
  exit(0)
126
129
  end
127
130
 
@@ -137,7 +140,7 @@ if options[:collection] and options[:operation]
137
140
  params.merge!(:image_id => options[:image_id]) if options[:image_id]
138
141
  params.merge!(:hwp_id => options[:hwp_id]) if options[:hwp_id]
139
142
  instance = client.create_instance(options[:image_id], params)
140
- puts instance.to_plain
143
+ puts format(instance)
141
144
  exit(0)
142
145
  end
143
146
 
@@ -146,7 +149,7 @@ if options[:collection] and options[:operation]
146
149
  instance = client.instance(options[:id])
147
150
  instance.send("#{options[:operation]}!".to_s)
148
151
  instance = client.instance(options[:id])
149
- puts instance.to_plain
152
+ puts format(instance)
150
153
  exit(0)
151
154
  end
152
155
  end
@@ -15,404 +15,494 @@
15
15
  # License along with this library; if not, write to the Free Software
16
16
  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
+ require 'nokogiri'
18
19
  require 'rest_client'
19
- require 'rexml/document'
20
- require 'logger'
21
- require 'dcloud/hardware_profile'
22
- require 'dcloud/realm'
23
- require 'dcloud/image'
24
- require 'dcloud/instance'
25
- require 'dcloud/storage_volume'
26
- require 'dcloud/storage_snapshot'
27
- require 'dcloud/state'
28
- require 'dcloud/transition'
29
20
  require 'base64'
21
+ require 'logger'
30
22
 
31
- class DeltaCloud
23
+ module DeltaCloud
32
24
 
33
- attr_accessor :logger
34
- attr_reader :api_uri
35
- attr_reader :entry_points
36
- attr_reader :driver_name
37
- attr_reader :last_request_xml
38
- attr_reader :features
25
+ # Get a new API client instance
26
+ #
27
+ # @param [String, user_name] API user name
28
+ # @param [String, password] API password
29
+ # @param [String, user_name] API URL (eg. http://localhost:3001/api)
30
+ # @return [DeltaCloud::API]
31
+ def self.new(user_name, password, api_url, &block)
32
+ API.new(user_name, password, api_url, &block)
33
+ end
39
34
 
35
+ # Return a API driver for specified URL
36
+ #
37
+ # @param [String, url] API URL (eg. http://localhost:3001/api)
40
38
  def self.driver_name(url)
41
- DeltaCloud.new( nil, nil, url) do |client|
42
- return client.driver_name
39
+ API.new(nil, nil, url).driver_name
40
+ end
41
+
42
+ class API
43
+ attr_accessor :logger
44
+ attr_reader :api_uri, :driver_name, :api_version, :features, :entry_points
45
+ attr_reader :classes
46
+
47
+ def initialize(user_name, password, api_url, opts={}, &block)
48
+ @logger = opts[:verbose] ? Logger.new(STDERR) : []
49
+ @username, @password = user_name, password
50
+ @api_uri = URI.parse(api_url)
51
+ @features, @entry_points = {}, {}
52
+ @classes = []
53
+ @verbose = opts[:verbose] || false
54
+ discover_entry_points
55
+ yield self if block_given?
43
56
  end
44
- end
45
-
46
- def initialize(name, password, api_uri, opts={}, &block)
47
- @logger = Logger.new( STDERR )
48
- @name = name
49
- @password = password
50
- @api_uri = URI.parse( api_uri )
51
- @entry_points = {}
52
- @verbose = opts[:verbose]
53
- @features = {}
54
- discover_entry_points
55
- connect( &block )
56
- self
57
- end
58
-
59
-
60
- def connect(&block)
61
- @http = RestClient::Resource.new( api_uri.to_s , :accept => 'application/xml' )
62
- discover_entry_points
63
- block.call( self ) if block
64
- self
65
- end
66
-
67
- def api_host
68
- @api_uri.host
69
- end
70
-
71
- def api_port
72
- @api_uri.port
73
- end
74
57
 
75
- def api_path
76
- @api_uri.path
77
- end
78
-
79
- def feature?(collection, name)
80
- @features.has_key?(collection) && @features[collection].include?(name)
81
- end
58
+ def connect(&block)
59
+ yield self
60
+ end
82
61
 
83
- def hardware_profiles(opts={})
84
- hardware_profiles = []
85
- request(entry_points[:hardware_profiles], :get, opts) do |response|
86
- doc = REXML::Document.new( response )
87
- doc.get_elements( 'hardware-profiles/hardware-profile' ).each do |hwp|
88
- uri = hwp.attributes['href']
89
- hardware_profiles << DCloud::HardwareProfile.new( self, uri, hwp )
62
+ # Return API hostname
63
+ def api_host; @api_uri.host ; end
64
+
65
+ # Return API port
66
+ def api_port; @api_uri.port ; end
67
+
68
+ # Return API path
69
+ def api_path; @api_uri.path ; end
70
+
71
+ # Define methods based on 'rel' attribute in entry point
72
+ # Two methods are declared: 'images' and 'image'
73
+ def declare_entry_points_methods(entry_points)
74
+ logger = @logger
75
+ API.instance_eval do
76
+ entry_points.keys.select {|k| [:instance_states].include?(k)==false }.each do |model|
77
+ define_method model do |*args|
78
+ request(:get, "/#{model}", args.first) do |response|
79
+ # Define a new class based on model name
80
+ c = Kernel.define_class("#{model.to_s.classify}")
81
+ # Create collection from index operation
82
+ base_object_collection(c, model, response)
83
+ end
84
+ end
85
+ logger << "[API] Added method #{model}\n"
86
+ define_method :"#{model.to_s.singularize}" do |*args|
87
+ request(:get, "/#{model}/#{args[0]}") do |response|
88
+ # Define a new class based on model name
89
+ c = Kernel.define_class("#{model.to_s.classify}")
90
+ # Build class for returned object
91
+ base_object(c, model, response)
92
+ end
93
+ end
94
+ logger << "[API] Added method #{model.to_s.singularize}\n"
95
+ define_method :"fetch_#{model.to_s.singularize}" do |url|
96
+ id = url.grep(/\/#{model}\/(.*)$/)
97
+ self.send(model.to_s.singularize.to_sym, $1)
98
+ end
99
+ end
90
100
  end
91
101
  end
92
- hardware_profiles
93
- end
94
102
 
95
- def hardware_profile(id)
96
- request( entry_points[:hardware_profiles], :get, {:id=>id } ) do |response|
97
- doc = REXML::Document.new( response )
98
- doc.get_elements( '/hardware-profile' ).each do |hwp|
99
- uri = hwp.attributes['href']
100
- return DCloud::HardwareProfile.new( self, uri, hwp )
103
+ def base_object_collection(c, model, response)
104
+ collection = []
105
+ Nokogiri::XML(response).xpath("#{model}/#{model.to_s.singularize}").each do |item|
106
+ c.instance_eval do
107
+ attr_accessor :id
108
+ attr_accessor :uri
109
+ end
110
+ collection << xml_to_class(c, item)
101
111
  end
112
+ return collection
102
113
  end
103
- end
104
-
105
- def fetch_hardware_profile(uri)
106
- xml = fetch_resource( :hardware_profile, uri )
107
- return DCloud::HardwareProfile.new( self, uri, xml ) if xml
108
- nil
109
- end
110
114
 
111
- def fetch_resource(type, uri)
112
- request( uri ) do |response|
113
- doc = REXML::Document.new( response )
114
- if ( doc.root && ( doc.root.name == type.to_s.gsub( /_/, '-' ) ) )
115
- return doc.root
115
+ # Add default attributes [id and href] to class
116
+ def base_object(c, model, response)
117
+ obj = nil
118
+ Nokogiri::XML(response).xpath("#{model.to_s.singularize}").each do |item|
119
+ c.instance_eval do
120
+ attr_accessor :id
121
+ attr_accessor :uri
122
+ end
123
+ obj = xml_to_class(c, item)
116
124
  end
125
+ return obj
117
126
  end
118
- nil
119
- end
120
127
 
121
- def fetch_documentation(collection, operation=nil)
122
- response = @http["docs/#{collection}#{operation ? "/#{operation}" : ''}"].get(:accept => "application/xml")
123
- doc = REXML::Document.new( response.to_s )
124
- if operation.nil?
125
- docs = {
126
- :name => doc.get_elements('docs/collection').first.attributes['name'],
127
- :description => doc.get_elements('docs/collection/description').first.text,
128
- :operations => []
129
- }
130
- doc.get_elements('docs/collection/operations/operation').each do |operation|
131
- p = {}
132
- p[:name] = operation.attributes['name']
133
- p[:description] = operation.get_elements('description').first.text
134
- p[:parameters] = []
135
- operation.get_elements('parameter').each do |param|
136
- p[:parameters] << param.attributes['name']
128
+ # Convert XML response to defined Ruby Class
129
+ def xml_to_class(c, item)
130
+ obj = c.new
131
+ # Set default attributes
132
+ obj.id = item['id']
133
+ api = self
134
+ c.instance_eval do
135
+ define_method :client do
136
+ api
137
137
  end
138
- docs[:operations] << p
139
138
  end
140
- else
141
- docs = {
142
- :name => doc.get_elements('docs/operation').attributes['name'],
143
- :description => doc.get_elements('docs/operation/description').first.text,
144
- :parameters => []
145
- }
146
- doc.get_elements('docs/operation/parameter').each do |param|
147
- docs[:parameters] << param.attributes['name']
139
+ obj.uri = item['href']
140
+ logger = @logger
141
+ logger << "[DC] Creating class #{obj.class.name}\n"
142
+ obj.instance_eval do
143
+ # Declare methods for all attributes in object
144
+ item.xpath('./*').each do |attribute|
145
+ # If attribute is a link to another object then
146
+ # create a method which request this object from API
147
+ if api.entry_points.keys.include?(:"#{attribute.name}s")
148
+ c.instance_eval do
149
+ define_method :"#{attribute.name.sanitize}" do
150
+ client.send(:"#{attribute.name}", attribute['id'] )
151
+ end
152
+ logger << "[DC] Added #{attribute.name} to class #{obj.class.name}\n"
153
+ end
154
+ else
155
+ # Define methods for other attributes
156
+ c.instance_eval do
157
+ case attribute.name
158
+ # When response cointains 'link' block, declare
159
+ # methods to call links inside. This is used for instance
160
+ # to dynamicaly create .stop!, .start! methods
161
+ when "actions":
162
+ actions = []
163
+ attribute.xpath('link').each do |link|
164
+ actions << [link['rel'], link[:href]]
165
+ define_method :"#{link['rel'].sanitize}!" do
166
+ client.request(:"#{link['method']}", link['href'], {}, {})
167
+ client.send(:"#{item.name}", item['id'])
168
+ end
169
+ end
170
+ define_method :actions do
171
+ actions.collect { |a| a.first }
172
+ end
173
+ define_method :actions_urls do
174
+ urls = {}
175
+ actions.each { |a| urls[a.first] = a.last }
176
+ urls
177
+ end
178
+ # Property attribute is handled differently
179
+ when "property":
180
+ define_method :"#{attribute['name'].sanitize}" do
181
+ if attribute['value'] =~ /^(\d+)$/
182
+ DeltaCloud::HWP::FloatProperty.new(attribute, attribute['name'])
183
+ else
184
+ DeltaCloud::HWP::Property.new(attribute, attribute['name'])
185
+ end
186
+ end
187
+ # Public and private addresses are returned as Array
188
+ when "public_addresses", "private_addresses":
189
+ define_method :"#{attribute.name.sanitize}" do
190
+ attribute.xpath('address').collect { |address| address.text }
191
+ end
192
+ # Value for other attributes are just returned using
193
+ # method with same name as attribute (eg. .owner_id, .state)
194
+ else
195
+ define_method :"#{attribute.name.sanitize}" do
196
+ attribute.text.convert
197
+ end
198
+ logger << "[DC] Added method #{attribute.name} to #{obj.class.name}\n"
199
+ end
200
+ end
201
+ end
202
+ end
148
203
  end
204
+ add_class_record(obj)
205
+ return obj
149
206
  end
150
- docs
151
- end
152
207
 
153
- def instance_states
154
- states = []
155
- request( entry_points[:instance_states] ) do |response|
156
- doc = REXML::Document.new( response )
157
- doc.get_elements( 'states/state' ).each do |state_elem|
158
- state = DCloud::State.new( state_elem.attributes['name'] )
159
- state_elem.get_elements( 'transition' ).each do |transition_elem|
160
- state.transitions << DCloud::Transition.new(
161
- transition_elem.attributes['to'],
162
- transition_elem.attributes['action']
163
- )
208
+ # Get /api and parse entry points
209
+ def discover_entry_points
210
+ return if discovered?
211
+ request(:get, @api_uri.to_s) do |response|
212
+ api_xml = Nokogiri::XML(response)
213
+ @driver_name = api_xml.xpath('/api').first['driver']
214
+ @api_version = api_xml.xpath('/api').first['version']
215
+ logger << "[API] Version #{@api_version}\n"
216
+ logger << "[API] Driver #{@driver_name}\n"
217
+ api_xml.css("api > link").each do |entry_point|
218
+ rel, href = entry_point['rel'].to_sym, entry_point['href']
219
+ @entry_points.store(rel, href)
220
+ logger << "[API] Entry point '#{rel}' added\n"
221
+ entry_point.css("feature").each do |feature|
222
+ @features[rel] ||= []
223
+ @features[rel] << feature['name'].to_sym
224
+ logger << "[API] Feature #{feature['name']} added to #{rel}\n"
225
+ end
164
226
  end
165
- states << state
166
227
  end
228
+ declare_entry_points_methods(@entry_points)
167
229
  end
168
- states
169
- end
170
230
 
171
- def instance_state(name)
172
- found = instance_states.find{|e| e.name.to_s == name.to_s}
173
- found
174
- end
231
+ # Create a new instance, using image +image_id+. Possible optiosn are
232
+ #
233
+ # name - a user-defined name for the instance
234
+ # realm - a specific realm for placement of the instance
235
+ # hardware_profile - either a string giving the name of the
236
+ # hardware profile or a hash. The hash must have an
237
+ # entry +id+, giving the id of the hardware profile,
238
+ # and may contain additional names of properties,
239
+ # e.g. 'storage', to override entries in the
240
+ # hardware profile
241
+ def create_instance(image_id, opts={}, &block)
242
+ name = opts[:name]
243
+ realm_id = opts[:realm]
244
+ user_data = opts[:user_data]
245
+
246
+ params = {}
247
+ ( params[:realm_id] = realm_id ) if realm_id
248
+ ( params[:name] = name ) if name
249
+ ( params[:user_data] = user_data ) if user_data
250
+
251
+ if opts[:hardware_profile].is_a?(String)
252
+ params[:hwp_id] = opts[:hardware_profile]
253
+ elsif opts[:hardware_profile].is_a?(Hash)
254
+ opts[:hardware_profile].each do |k,v|
255
+ params[:"hwp_#{k}"] = v
256
+ end
257
+ end
175
258
 
176
- def realms(opts={})
177
- realms = []
178
- request( entry_points[:realms], :get, opts ) do |response|
179
- doc = REXML::Document.new( response )
180
- doc.get_elements( 'realms/realm' ).each do |realm|
181
- uri = realm.attributes['href']
182
- realms << DCloud::Realm.new( self, uri, realm )
259
+ params[:image_id] = image_id
260
+ instance = nil
261
+
262
+ request(:post, entry_points[:instances], {}, params) do |response|
263
+ c = Kernel.define_class("Instance")
264
+ instance = base_object(c, :instance, response)
265
+ yield instance if block_given?
183
266
  end
267
+
268
+ return instance
184
269
  end
185
- realms
186
- end
187
270
 
188
- def realm(id)
189
- request( entry_points[:realms], :get, {:id=>id } ) do |response|
190
- doc = REXML::Document.new( response )
191
- doc.get_elements( 'realm' ).each do |realm|
192
- uri = realm.attributes['href']
193
- return DCloud::Realm.new( self, uri, realm )
271
+ # Basic request method
272
+ #
273
+ def request(*args, &block)
274
+ conf = {
275
+ :method => (args[0] || 'get').to_sym,
276
+ :path => (args[1]=~/^http/) ? args[1] : "#{api_uri.to_s}#{args[1]}",
277
+ :query_args => args[2] || {},
278
+ :form_data => args[3] || {}
279
+ }
280
+ if conf[:query_args] != {}
281
+ conf[:path] += '?' + URI.escape(conf[:query_args].collect{ |key, value| "#{key}=#{value}" }.join('&')).to_s
282
+ end
283
+ logger << "[#{conf[:method].to_s.upcase}] #{conf[:path]}\n"
284
+ if conf[:method].eql?(:post)
285
+ RestClient.send(:post, conf[:path], conf[:form_data], default_headers) do |response|
286
+ yield response.body if block_given?
287
+ end
288
+ else
289
+ RestClient.send(conf[:method], conf[:path], default_headers) do |response|
290
+ yield response.body if block_given?
291
+ end
194
292
  end
195
293
  end
196
- nil
197
- end
198
294
 
199
- def fetch_realm(uri)
200
- xml = fetch_resource( :realm, uri )
201
- return DCloud::Realm.new( self, uri, xml ) if xml
202
- nil
203
- end
295
+ # Check if specified collection have wanted feature
296
+ def feature?(collection, name)
297
+ @feature.has_key?(collection) && @feature[collection].include?(name)
298
+ end
204
299
 
205
- def images(opts={})
206
- images = []
207
- request_path = entry_points[:images]
208
- request( request_path, :get, opts ) do |response|
209
- doc = REXML::Document.new( response )
210
- doc.get_elements( 'images/image' ).each do |image|
211
- uri = image.attributes['href']
212
- images << DCloud::Image.new( self, uri, image )
300
+ # List available instance states and transitions between them
301
+ def instance_states
302
+ states = []
303
+ request(:get, entry_points[:instance_states]) do |response|
304
+ Nokogiri::XML(response).xpath('states/state').each do |state_el|
305
+ state = DeltaCloud::InstanceState::State.new(state_el['name'])
306
+ state_el.xpath('transition').each do |transition_el|
307
+ state.transitions << DeltaCloud::InstanceState::Transition.new(
308
+ transition_el['to'],
309
+ transition_el['action']
310
+ )
311
+ end
312
+ states << state
313
+ end
213
314
  end
315
+ states
214
316
  end
215
- images
216
- end
217
317
 
218
- def image(id)
219
- request( entry_points[:images], :get, {:id=>id } ) do |response|
220
- doc = REXML::Document.new( response )
221
- doc.get_elements( 'image' ).each do |instance|
222
- uri = instance.attributes['href']
223
- return DCloud::Image.new( self, uri, instance )
224
- end
318
+ # Select instance state specified by name
319
+ def instance_state(name)
320
+ instance_states.select { |s| s.name.to_s.eql?(name.to_s) }.first
225
321
  end
226
- nil
227
- end
228
322
 
229
- def instances(opts={})
230
- instances = []
231
- request( entry_points[:instances], :get, opts ) do |response|
232
- doc = REXML::Document.new( response )
233
- doc.get_elements( 'instances/instance' ).each do |instance|
234
- uri = instance.attributes['href']
235
- instances << DCloud::Instance.new( self, uri, instance )
236
- end
323
+ # Skip parsing /api when we already got entry points
324
+ def discovered?
325
+ true if @entry_points!={}
237
326
  end
238
- instances
239
- end
240
327
 
241
- def instance(id)
242
- request( entry_points[:instances], :get, {:id=>id } ) do |response|
243
- doc = REXML::Document.new( response )
244
- doc.get_elements( 'instance' ).each do |instance|
245
- uri = instance.attributes['href']
246
- return DCloud::Instance.new( self, uri, instance )
328
+ def documentation(collection, operation=nil)
329
+ data = {}
330
+ request(:get, "/docs/#{collection}") do |body|
331
+ document = Nokogiri::XML(body)
332
+ if operation
333
+ data[:description] = document.xpath('/docs/collection/operations/operation[@name = "'+operation+'"]/description').first
334
+ return false unless data[:description]
335
+ data[:params] = []
336
+ (document/"/docs/collection/operations/operation[@name='#{operation}']/parameter").each do |param|
337
+ data[:params] << {
338
+ :name => param['name'],
339
+ :required => param['type'] == 'optional',
340
+ :type => (param/'class').text
341
+ }
342
+ end
343
+ else
344
+ data[:description] = (document/'/docs/collection/description').text
345
+ end
247
346
  end
347
+ return Documentation.new(data)
248
348
  end
249
- nil
250
- end
251
349
 
252
- def post_instance(uri)
253
- request( uri, :post ) do |response|
254
- return true
350
+ private
351
+
352
+ def default_headers
353
+ {
354
+ :authorization => "Basic "+Base64.encode64("#{@username}:#{@password}"),
355
+ :accept => "application/xml"
356
+ }
357
+ end
358
+
359
+ def add_class_record(obj)
360
+ return if self.classes.include?(obj.class)
361
+ self.classes << obj.class
255
362
  end
256
- return false
257
- end
258
363
 
259
- def fetch_instance(uri)
260
- xml = fetch_resource( :instance, uri )
261
- return DCloud::Instance.new( self, uri, xml ) if xml
262
- nil
263
364
  end
264
365
 
265
- # Create a new instance, using image +image_id+. Possible optiosn are
266
- #
267
- # name - a user-defined name for the instance
268
- # realm - a specific realm for placement of the instance
269
- # hardware_profile - either a string giving the name of the
270
- # hardware profile or a hash. The hash must have an
271
- # entry +id+, giving the id of the hardware profile,
272
- # and may contain additional names of properties,
273
- # e.g. 'storage', to override entries in the
274
- # hardware profile
275
- def create_instance(image_id, opts={})
276
- name = opts[:name]
277
- realm_id = opts[:realm]
278
-
279
- params = {}
280
- ( params[:realm_id] = realm_id ) if realm_id
281
- ( params[:name] = name ) if name
282
-
283
- if opts[:hardware_profile].is_a?(String)
284
- params[:hwp_id] = opts[:hardware_profile]
285
- elsif opts[:hardware_profile].is_a?(Hash)
286
- opts[:hardware_profile].each do |k,v|
287
- params[:"hwp_#{k}"] = v
366
+ class Documentation
367
+ attr_reader :description
368
+ attr_reader :params
369
+
370
+ def initialize(opts={})
371
+ @description = opts[:description]
372
+ @params = parse_parameters(opts[:params]) if opts[:params]
373
+ self
374
+ end
375
+
376
+ class OperationParameter
377
+ attr_reader :name
378
+ attr_reader :type
379
+ attr_reader :required
380
+ attr_reader :description
381
+
382
+ def initialize(data)
383
+ @name, @type, @required, @description = data[:name], data[:type], data[:required], data[:description]
384
+ end
385
+
386
+ def to_comment
387
+ " # @param [#{@type}, #{@name}] #{@description}"
288
388
  end
389
+
289
390
  end
290
391
 
291
- params[:image_id] = image_id
292
- request( entry_points[:instances], :post, {}, params ) do |response|
293
- doc = REXML::Document.new( response )
294
- instance = doc.root
295
- uri = instance.attributes['href']
296
- return DCloud::Instance.new( self, uri, instance )
392
+ private
393
+
394
+ def parse_parameters(params)
395
+ params.collect { |p| OperationParameter.new(p) }
297
396
  end
397
+
298
398
  end
299
399
 
300
- def storage_volumes(opts={})
301
- storage_volumes = []
302
- request( entry_points[:storage_volumes] ) do |response|
303
- doc = REXML::Document.new( response )
304
- doc.get_elements( 'storage-volumes/storage-volume' ).each do |instance|
305
- uri = instance.attributes['href']
306
- storage_volumes << DCloud::StorageVolume.new( self, uri, instance )
400
+ module InstanceState
401
+
402
+ class State
403
+ attr_reader :name
404
+ attr_reader :transitions
405
+
406
+ def initialize(name)
407
+ @name, @transitions = name, []
307
408
  end
308
409
  end
309
- storage_volumes
310
- end
311
410
 
312
- def storage_volume(id)
313
- request( entry_points[:storage_volumes], :get, {:id=>id } ) do |response|
314
- doc = REXML::Document.new( response )
315
- doc.get_elements( 'storage-volume' ).each do |storage_volume|
316
- uri = storage_volume.attributes['href']
317
- return DCloud::StorageVolume.new( self, uri, storage_volume )
411
+ class Transition
412
+ attr_reader :to
413
+ attr_reader :action
414
+
415
+ def initialize(to, action)
416
+ @to = to
417
+ @action = action
418
+ end
419
+
420
+ def auto?
421
+ @action.nil?
318
422
  end
319
423
  end
320
- nil
321
424
  end
322
425
 
323
- def fetch_storage_volume(uri)
324
- xml = fetch_resource( :storage_volume, uri )
325
- return DCloud::StorageVolume.new( self, uri, xml ) if xml
326
- nil
327
- end
426
+ module HWP
427
+
428
+ class Property
429
+ attr_reader :name, :unit, :value, :kind
328
430
 
329
- def storage_snapshots(opts={})
330
- storage_snapshots = []
331
- request( entry_points[:storage_snapshots] ) do |response|
332
- doc = REXML::Document.new( response )
333
- doc.get_elements( 'storage-snapshots/storage-snapshot' ).each do |instance|
334
- uri = instance.attributes['href']
335
- storage_snapshots << DCloud::StorageSnapshot.new( self, uri, instance )
431
+ def initialize(xml, name)
432
+ @kind, @value, @unit = xml['kind'].to_sym, xml['value'], xml['unit']
433
+ declare_ranges(xml)
434
+ self
336
435
  end
436
+
437
+ def present?
438
+ ! @value.nil?
439
+ end
440
+
441
+ private
442
+
443
+ def declare_ranges(xml)
444
+ case xml['kind']
445
+ when 'range':
446
+ self.class.instance_eval do
447
+ attr_reader :range
448
+ end
449
+ @range = { :from => xml.xpath('range').first['first'], :to => xml.xpath('range').first['last'] }
450
+ when 'enum':
451
+ self.class.instance_eval do
452
+ attr_reader :options
453
+ end
454
+ @options = xml.xpath('enum/entry').collect { |e| e['value'] }
455
+ end
456
+ end
457
+
337
458
  end
338
- storage_snapshots
339
- end
340
459
 
341
- def storage_snapshot(id)
342
- request( entry_points[:storage_snapshots], :get, {:id=>id } ) do |response|
343
- doc = REXML::Document.new( response )
344
- doc.get_elements( 'storage-snapshot' ).each do |storage_snapshot|
345
- uri = storage_snapshot.attributes['href']
346
- return DCloud::StorageSnapshot.new( self, uri, storage_snapshot )
460
+ # FloatProperty is like Property but return value is Float instead of String.
461
+ class FloatProperty < Property
462
+ def initialize(xml, name)
463
+ super(xml, name)
464
+ @value = @value.to_f if @value
347
465
  end
348
466
  end
349
- nil
350
467
  end
351
468
 
352
- def fetch_storage_snapshot(uri)
353
- xml = fetch_resource( :storage_snapshot, uri )
354
- return DCloud::StorageSnapshot.new( self, uri, xml ) if xml
355
- nil
469
+ end
470
+
471
+ class String
472
+
473
+ # Create a class name from string
474
+ def classify
475
+ self.singularize.camelize
356
476
  end
357
477
 
358
- def fetch_image(uri)
359
- xml = fetch_resource( :image, uri )
360
- return DCloud::Image.new( self, uri, xml ) if xml
361
- nil
478
+ # Camelize converts strings to UpperCamelCase
479
+ def camelize
480
+ self.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
362
481
  end
363
482
 
364
- private
365
-
366
- attr_reader :http
367
-
368
- def discover_entry_points
369
- return if @discovered
370
- request(api_uri.to_s) do |response|
371
- doc = REXML::Document.new( response )
372
- @driver_name = doc.root.attributes['driver']
373
- doc.get_elements( 'api/link' ).each do |link|
374
- rel = link.attributes['rel'].to_sym
375
- uri = link.attributes['href']
376
- @entry_points[rel] = uri
377
- @features[rel] ||= []
378
- link.get_elements('feature').each do |feature|
379
- @features[rel] << feature.attributes['name'].to_sym
380
- end
381
- end
382
- end
383
- @discovered = true
483
+ # Strip 's' character from end of string
484
+ def singularize
485
+ self.gsub(/s$/, '')
384
486
  end
385
487
 
386
- def request(path='', method=:get, query_args={}, form_data={}, &block)
387
- if ( path =~ /^http/ )
388
- request_path = path
389
- else
390
- request_path = "#{api_uri.to_s}#{path}"
391
- end
392
- if query_args[:id]
393
- request_path += "/#{query_args[:id]}"
394
- query_args.delete(:id)
395
- end
396
- query_string = URI.escape(query_args.collect{|k,v| "#{k}=#{v}"}.join('&'))
397
- request_path += "?#{query_string}" unless query_string==''
398
- headers = {
399
- :authorization => "Basic "+Base64.encode64("#{@name}:#{@password}"),
400
- :accept => "application/xml"
401
- }
402
-
403
- logger << "Request [#{method.to_s.upcase}] #{request_path}]\n" if @verbose
404
-
405
- if method.eql?(:get)
406
- RestClient.send(method, request_path, headers) do |response|
407
- @last_request_xml = response
408
- yield response.to_s
409
- end
410
- else
411
- RestClient.send(method, request_path, form_data, headers) do |response|
412
- @last_request_xml = response
413
- yield response.to_s
414
- end
415
- end
488
+ # Convert string to float if string value seems like Float
489
+ def convert
490
+ return self.to_f if self.strip =~ /^([\d\.]+$)/
491
+ self
492
+ end
493
+
494
+ # Simply converts whitespaces and - symbols to '_' which is safe for Ruby
495
+ def sanitize
496
+ self.gsub(/(\W+)/, '_')
497
+ end
498
+
499
+ end
500
+
501
+ module Kernel
502
+
503
+ # Get defined class or declare a new one, when class was not declared before
504
+ def define_class(name)
505
+ DeltaCloud.const_get(name) rescue DeltaCloud.const_set(name, Class.new)
416
506
  end
417
507
 
418
508
  end