deltacloud-client 0.0.4 → 0.0.5

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.
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