From: Michal Fojtik <[email protected]> --- client/lib/base_object.rb | 281 +++++++++++++++++++++ client/lib/deltacloud.rb | 434 ++++++++------------------------ client/lib/documentation.rb | 136 ++++------- client/lib/hwp_properties.rb | 64 +++++ client/lib/instance_state.rb | 29 +++ client/lib/string.rb | 53 ++++ client/specs/hardware_profiles_spec.rb | 2 +- client/specs/instances_spec.rb | 4 +- 8 files changed, 589 insertions(+), 414 deletions(-) create mode 100644 client/lib/base_object.rb create mode 100644 client/lib/hwp_properties.rb create mode 100644 client/lib/instance_state.rb create mode 100644 client/lib/string.rb
diff --git a/client/lib/base_object.rb b/client/lib/base_object.rb new file mode 100644 index 0000000..b269bb8 --- /dev/null +++ b/client/lib/base_object.rb @@ -0,0 +1,281 @@ +# +# Copyright (C) 2010 Red Hat, Inc. +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +require 'lib/string' + +module DeltaCloud + + class BaseObjectParamError < Exception; end + class NoHandlerForMethod < Exception; end + + # BaseObject model basically provide the basic operation around + # REST model, like defining a links between different objects, + # element with text values, or collection of these elements + class BaseObject + attr_reader :id, :url, :client, :base_name + attr_reader :objects + + alias :uri :url + + # For initializing new object you require to set + # id, url, client and name attribute. + def initialize(opts={}, &block) + @id, @url, @client, @base_name = opts[:id], opts[:url], opts[:client], opts[:name] + @objects = [] + raise BaseObjectParamError if @id.nil? or @url.nil? or @client.nil? or @base_name.nil? + yield self if block_given? + end + + # This method add link to another object in REST model + # XML syntax: <link rel="destroy" href="http://localhost/api/resource" method="post"/> + def add_link!(object_name, id) + @objects << { + :type => :link, + :method_name => object_name.sanitize, + :id => id + } + @objects << { + :type => :text, + :method_name => "#{object_name.sanitize}_id", + :value => id + } + end + + # Method add property for hardware profile + def add_hwp_property!(name, property, type) + hwp_property=case type + when :float then DeltaCloud::HWP::FloatProperty.new(property, name) + when :integer then DeltaCloud::HWP::Property.new(property, name) + end + @objects << { + :type => :property, + :method_name => name.sanitize, + :property => hwp_property + } + end + + # This method define text object in REST model + # XML syntax: <name>Instance 1</name> + def add_text!(object_name, value) + @objects << { + :type => :text, + :method_name => object_name.sanitize, + :value => value + } + end + + # This method define collection of text elements inside REST model + # XML syntax: <addresses> + # <address>127.0.0.1</address> + # <address>127.0.0.2</address> + # </addresses> + def add_collection!(collection_name, values=[]) + @objects << { + :type => :collection, + :method_name => collection_name.sanitize, + :values => values + } + end + + # Basic method hander. This define a way how value from property + # will be returned + def method_handler(m, args=[]) + case m[:type] + when :link then return @client.send(m[:method_name].singularize, m[:id]) + when :text then return m[:value] + when :property then return m[:property] + when :collection then return m[:values] + else raise NoHandlerForMethod + end + end + + def method_missing(method_name, *args) + # First of all search throught array for method name + m = search_for_method(method_name) + if m.nil? + warn "[WARNING] Method unsupported by API: '#{self.class}.#{method_name}(#{args.inspect})'" + return nil + else + # Call appropriate handler for method + method_handler(m, args) + end + end + + private + + def search_for_method(name) + @objects.select { |o| o[:method_name] == "#{name}" }.first + end + + end + + class ActionObject < BaseObject + + def initialize(opts={}, &block) + super(opts) + @action_urls = opts[:action_urls] || [] + @actions = [] + end + + # This trigger is called right after action. + # This method does nothing inside ActionObject + # but it can be redifined and used in meta-programming + def action_trigger(action) + end + + def add_action_link!(id, link) + m = { + :type => :action_link, + :method_name => "#{link['rel'].sanitize}!", + :id => id, + :href => link['href'], + :rel => link['rel'].sanitize, + :method => link['method'].sanitize + } + @objects << m + @actions << [m[:rel], m[:href]] + @action_urls << m[:href] + end + + def actions + @objects.select {|o| o[:type].eql?(:action_link) }.collect { |o| [o[:rel], o[:href]] } + end + + def action_urls + actions.collect { |a| a.last } + end + + alias :base_method_handler :method_handler + + # First call BaseObject method handler, + # then, if not method found try ActionObject handler + def method_handler(m, args=[]) + begin + base_method_handler(m, args) + rescue NoHandlerForMethod + case m[:type] + when :action_link then do_action(m) + else raise NoHandlerForMethod + end + end + end + + private + + def do_action(m) + @client.request(:"#{m[:method]}", m[:href], {}, {}) + action_trigger(m[:rel]) + end + + end + + class StateFullObject < ActionObject + attr_reader :state + + def initialize(opts={}, &block) + super(opts) + @state = opts[:initial_state] || '' + add_default_states! + end + + def add_default_states! + @objects << { + :method_name => 'stopped?', + :type => :state, + :state => 'STOPPED' + } + @objects << { + :method_name => 'running?', + :type => :state, + :state => 'RUNNING' + } + @objects << { + :method_name => 'pending?', + :type => :state, + :state => 'PENDING' + } + @objects << { + :method_name => 'shutting_down?', + :type => :state, + :state => 'SHUTTING_DOWN' + } + end + + def action_trigger(action) + # Refresh object state after action + @new_state_object = @client.send(self.base_name, self.id) + @state = @new_state_object.state + self.update_actions! + end + + alias :action_method_handler :method_handler + + def method_handler(m, args=[]) + begin + action_method_handler(m, args) + rescue NoHandlerForMethod + case m[:type] + when :state then evaluate_state(m[:state], @state) + else raise NoHandlerForMethod + end + end + end + +# private + + def evaluate_state(method_state, current_state) + method_state.eql?(current_state) + end + + def action_objects + @objects.select { |o| o[:type] == :action_link } + end + + def update_actions! + new_actions = @new_state_object.action_objects + @objects.reject! { |o| o[:type] == :action_link } + @objects = (@objects + new_actions) + end + + end + + def self.add_class(name, parent=:base) + parent_class = case parent + when :base then BaseObject + when :action then ActionObject + when :state then StateFullObject + end + begin + return API.const_get(name.classify) + rescue NameError + API.module_eval("class #{name.classify} < #{parent_class.to_s}; end") + new_class = API.const_get(name.classify) + @defined_classes ||= [] + @defined_classes << new_class + new_class + end + end + + def self.guess_model_type(response) + response = Nokogiri::XML(response.to_s) + return :action if ((response/'//actions').length == 1) and ((response/'//state').length == 0) + return :state if ((response/'//actions').length == 1) and ((response/'//state').length == 1) + return :base + end + +end diff --git a/client/lib/deltacloud.rb b/client/lib/deltacloud.rb index e44556f..5c224e5 100644 --- a/client/lib/deltacloud.rb +++ b/client/lib/deltacloud.rb @@ -20,6 +20,10 @@ require 'nokogiri' require 'rest_client' require 'base64' require 'logger' +require 'lib/hwp_properties' +require 'lib/instance_state' +require 'lib/documentation' +require 'lib/base_object' module DeltaCloud @@ -56,27 +60,11 @@ module DeltaCloud API.new(nil, nil, url).driver_name end - def self.define_class(name) - @defined_classes ||= [] - if @defined_classes.include?(name) - self.module_eval("API::#{name}") - else - @defined_classes << name unless @defined_classes.include?(name) - API.const_set(name, Class.new) - end - end - - def self.classes - @defined_classes || [] - end - class API - attr_accessor :logger attr_reader :api_uri, :driver_name, :api_version, :features, :entry_points def initialize(user_name, password, api_url, opts={}, &block) opts[:version] = true - @logger = opts[:verbose] ? Logger.new(STDERR) : [] @username, @password = user_name, password @api_uri = URI.parse(api_url) @features, @entry_points = {}, {} @@ -101,147 +89,91 @@ module DeltaCloud # Define methods based on 'rel' attribute in entry point # Two methods are declared: 'images' and 'image' def declare_entry_points_methods(entry_points) - logger = @logger + API.instance_eval do entry_points.keys.select {|k| [:instance_states].include?(k)==false }.each do |model| + define_method model do |*args| request(:get, entry_points[model], args.first) do |response| - # Define a new class based on model name - c = DeltaCloud.define_class("#{model.to_s.classify}") - # Create collection from index operation - base_object_collection(c, model, response) + base_object_collection(model, response) end end - logger << "[API] Added method #{model}\n" + define_method :"#{model.to_s.singularize}" do |*args| request(:get, "#{entry_points[model]}/#{args[0]}") do |response| - # Define a new class based on model name - c = DeltaCloud.define_class("#{model.to_s.classify}") - # Build class for returned object - base_object(c, model, response) + base_object(model, response) end end - logger << "[API] Added method #{model.to_s.singularize}\n" + define_method :"fetch_#{model.to_s.singularize}" do |url| id = url.grep(/\/#{model}\/(.*)$/) self.send(model.to_s.singularize.to_sym, $1) end + end end end - def base_object_collection(c, model, response) - collection = [] - Nokogiri::XML(response).xpath("#{model}/#{model.to_s.singularize}").each do |item| - c.instance_eval do - attr_accessor :id - attr_accessor :uri - end - collection << xml_to_class(c, item) + def base_object_collection(model, response) + Nokogiri::XML(response).xpath("#{model}/#{model.to_s.singularize}").collect do |item| + base_object(model, item.to_s) end - return collection end # Add default attributes [id and href] to class - def base_object(c, model, response) - obj = nil - Nokogiri::XML(response).xpath("#{model.to_s.singularize}").each do |item| - c.instance_eval do - attr_accessor :id - attr_accessor :uri - - - end - obj = xml_to_class(c, item) - end - return obj + def base_object(model, response) + c = DeltaCloud.add_class("#{model}", DeltaCloud::guess_model_type(response)) + xml_to_class(c, Nokogiri::XML(response).xpath("#{model.to_s.singularize}").first) end # Convert XML response to defined Ruby Class - def xml_to_class(c, item) - obj = c.new - # Set default attributes - obj.id = item['id'] - api = self - c.instance_eval do - define_method :method_missing do |method| - warn "[WARNING] Method '#{method}' is not available for this resource (#{c.name})." - return nil - end - define_method :client do - api + def xml_to_class(base_object, item) + + return nil unless item + + params = { + :id => item['id'], + :url => item['href'], + :name => item.name, + :client => self + } + params.merge!({ :initial_state => (item/'state').text.sanitize }) if (item/'state').length > 0 + + obj = base_object.new(params) + + # Traverse across XML document and deal with elements + item.xpath('./*').each do |attribute| + + # Do a link for elements which are links to other REST models + if self.entry_points.keys.include?(:"#{attribute.name}s") + obj.add_link!(attribute.name, attribute['id']) && next end - end - obj.uri = item['href'] - logger = @logger - logger << "[DC] Creating class #{obj.class.name}\n" - obj.instance_eval do - # Declare methods for all attributes in object - item.xpath('./*').each do |attribute| - # If attribute is a link to another object then - # create a method which request this object from API - if api.entry_points.keys.include?(:"#{attribute.name}s") - c.instance_eval do - define_method :"#{attribute.name.sanitize}" do - client.send(:"#{attribute.name}", attribute['id'] ) - end - logger << "[DC] Added #{attribute.name} to class #{obj.class.name}\n" - end + + # Do a HWP property for hardware profile properties + if attribute.name == 'property' + if attribute['value'] =~ /^(\d+)$/ + obj.add_hwp_property!(attribute['name'], attribute, :float) && next else - # Define methods for other attributes - c.instance_eval do - case attribute.name - # When response cointains 'link' block, declare - # methods to call links inside. This is used for instance - # to dynamicaly create .stop!, .start! methods - when "actions" then - actions = [] - attribute.xpath('link').each do |link| - actions << [link['rel'], link[:href]] - define_method :"#{link['rel'].sanitize}!" do - client.request(:"#{link['method']}", link['href'], {}, {}) - @current_state = client.send(:"#{item.name}", item['id']).state - obj.instance_eval do |o| - def state - @current_state - end - end - end - end - define_method :actions do - actions.collect { |a| a.first } - end - define_method :actions_urls do - urls = {} - actions.each { |a| urls[a.first] = a.last } - urls - end - # Property attribute is handled differently - when "property" then - attr_accessor :"#{attribute['name'].sanitize}" - if attribute['value'] =~ /^(\d+)$/ - obj.send(:"#{attribute['name'].sanitize}=", - DeltaCloud::HWP::FloatProperty.new(attribute, attribute['name'])) - else - obj.send(:"#{attribute['name'].sanitize}=", - DeltaCloud::HWP::Property.new(attribute, attribute['name'])) - end - # Public and private addresses are returned as Array - when "public_addresses", "private_addresses" then - attr_accessor :"#{attribute.name.sanitize}" - obj.send(:"#{attribute.name.sanitize}=", - attribute.xpath('address').collect { |address| address.text }) - # Value for other attributes are just returned using - # method with same name as attribute (eg. .owner_id, .state) - else - attr_accessor :"#{attribute.name.sanitize}" - obj.send(:"#{attribute.name.sanitize}=", attribute.text.convert) - logger << "[DC] Added method #{attribute.name}[#{attribute.text}] to #{obj.class.name}\n" - end - end + obj.add_hwp_property!(attribute['name'], attribute, :integer) && next end end + + # If there are actions, add they to ActionObject/StateFullObject + if attribute.name == 'actions' + (attribute/'link').each do |link| + obj.add_action_link!(item['id'], link) + end && next + end + + # Deal with collections like public-addresses, private-addresses + if (attribute/'./*').length > 0 + obj.add_collection!(attribute.name, (attribute/'*').collect { |value| value.text }) && next + end + + # Anything else is treaten as text object + obj.add_text!(attribute.name, attribute.text.convert) end + return obj end @@ -252,73 +184,54 @@ module DeltaCloud api_xml = Nokogiri::XML(response) @driver_name = api_xml.xpath('/api').first['driver'] @api_version = api_xml.xpath('/api').first['version'] - logger << "[API] Version #...@api_version}\n" - logger << "[API] Driver #...@driver_name}\n" + api_xml.css("api > link").each do |entry_point| rel, href = entry_point['rel'].to_sym, entry_point['href'] @entry_points.store(rel, href) - logger << "[API] Entry point '#{rel}' added\n" + entry_point.css("feature").each do |feature| @features[rel] ||= [] @features[rel] << feature['name'].to_sym - logger << "[API] Feature #{feature['name']} added to #{rel}\n" + end end end declare_entry_points_methods(@entry_points) end - def create_key(opts={}, &block) - params = { :name => opts[:name] } - key = nil - request(:post, entry_points[:keys], {}, params) do |response| - c = DeltaCloud.define_class("Key") - key = base_object(c, :key, response) - yield key if block_given? - end - return key - end - - # Create a new instance, using image +image_id+. Possible optiosn are + # Generate create_* methods dynamically # - # name - a user-defined name for the instance - # realm - a specific realm for placement of the instance - # hardware_profile - either a string giving the name of the - # hardware profile or a hash. The hash must have an - # entry +id+, giving the id of the hardware profile, - # and may contain additional names of properties, - # e.g. 'storage', to override entries in the - # hardware profile - def create_instance(image_id, opts={}, &block) - name = opts[:name] - realm_id = opts[:realm] - user_data = opts[:user_data] - key_name = opts[:key_name] - - params = {} - ( params[:realm_id] = realm_id ) if realm_id - ( params[:name] = name ) if name - ( params[:user_data] = user_data ) if user_data - ( params[:keyname] = key_name ) if key_name - - if opts[:hardware_profile].is_a?(String) - params[:hwp_id] = opts[:hardware_profile] - elsif opts[:hardware_profile].is_a?(Hash) - opts[:hardware_profile].each do |k,v| - params[:"hwp_#{k}"] = v + def method_missing(name, *args) + if name.to_s =~ /create_(\w+)/ + params = args[0] if args[0] and args[0].class.eql?(Hash) + params ||= args[1] if args[1] and args[1].class.eql?(Hash) + params ||= {} + + # FIXME: This fixes are related to Instance model and should be + # replaced by 'native' parameter names + + params[:realm_id] ||= params[:realm] if params[:realm] + params[:keyname] ||= params[:key_name] if params[:key_name] + + if params[:hardware_profile] and params[:hardware_profile].class.eql?(Hash) + params[:hardware_profile].each do |k,v| + params[:"hwp_#{k}"] ||= v + end + else + params[:hwp_id] ||= params[:hardware_profile] end - end - params[:image_id] = image_id - instance = nil + params[:image_id] ||= params[:image_id] || args[0] if args[0].class!=Hash - request(:post, entry_points[:instances], {}, params) do |response| - c = DeltaCloud.define_class("Instance") - instance = base_object(c, :instance, response) - yield instance if block_given? - end + obj = nil - return instance + request(:post, entry_points[:"#{$1}s"], {}, params) do |response| + obj = base_object(:"#{$1}", response) + yield obj if block_given? + end + return obj + end + raise NoMethodError end # Basic request method @@ -333,9 +246,10 @@ module DeltaCloud if conf[:query_args] != {} conf[:path] += '?' + URI.escape(conf[:query_args].collect{ |key, value| "#{key}=#{value}" }.join('&')).to_s end - logger << "[#{conf[:method].to_s.upcase}] #{conf[:path]}\n" + if conf[:method].eql?(:post) RestClient.send(:post, conf[:path], conf[:form_data], default_headers) do |response, request, block| + handle_backend_error(response) if response.code.eql?(500) if response.respond_to?('body') yield response.body if block_given? else @@ -344,6 +258,7 @@ module DeltaCloud end else RestClient.send(conf[:method], conf[:path], default_headers) do |response, request, block| + handle_backend_error(response) if response.code.eql?(500) if conf[:method].eql?(:get) and [301, 302, 307].include? response.code response.follow_redirection(request) do |response, request, block| if response.respond_to?('body') @@ -363,6 +278,21 @@ module DeltaCloud end end + # Re-raise backend errors as on exception in client with message from + # backend + class BackendError < Exception + def initialize(opts={}) + @message = opts[:message] + end + def message + @message + end + end + + def handle_backend_error(response) + raise BackendError.new(:message => (Nokogiri::XML(response)/'error/message').text) + end + # Check if specified collection have wanted feature def feature?(collection, name) @features.has_key?(collection) && @features[collection].include?(name) @@ -396,6 +326,7 @@ module DeltaCloud true if @entry_points!={} end + # This method will retrieve API documentation for given collection def documentation(collection, operation=nil) data = {} request(:get, "/docs/#{collection}") do |body| @@ -436,151 +367,4 @@ module DeltaCloud end - class Documentation - - attr_reader :api, :description, :params, :collection_operations - attr_reader :collection, :operation - - def initialize(api, opts={}) - @description, @api = opts[:description], api - @params = parse_parameters(opts[:params]) if opts[:params] - @collection_operations = opts[:operations] if opts[:operations] - @collection = opts[:collection] - @operation = opts[:operation] - self - end - - def operations - @collection_operations.collect { |o| api.documentation(@collection, o) } - end - - class OperationParameter - attr_reader :name - attr_reader :type - attr_reader :required - attr_reader :description - - def initialize(data) - @name, @type, @required, @description = data[:name], data[:type], data[:required], data[:description] - end - - def to_comment - " # @param [...@type}, #...@name}] #...@description}" - end - - end - - private - - def parse_parameters(params) - params.collect { |p| OperationParameter.new(p) } - end - - end - - module InstanceState - - class State - attr_reader :name - attr_reader :transitions - - def initialize(name) - @name, @transitions = name, [] - end - end - - class Transition - attr_reader :to - attr_reader :action - - def initialize(to, action) - @to = to - @action = action - end - - def auto? - @action.nil? - end - end - end - - module HWP - - class Property - attr_reader :name, :unit, :value, :kind - - def initialize(xml, name) - @name, @kind, @value, @unit = xml['name'], xml['kind'].to_sym, xml['value'], xml['unit'] - declare_ranges(xml) - self - end - - def present? - ! @value.nil? - end - - private - - def declare_ranges(xml) - case xml['kind'] - when 'range' then - self.class.instance_eval do - attr_reader :range - end - @range = { :from => xml.xpath('range').first['first'], :to => xml.xpath('range').first['last'] } - when 'enum' then - self.class.instance_eval do - attr_reader :options - end - @options = xml.xpath('enum/entry').collect { |e| e['value'] } - end - end - - end - - # FloatProperty is like Property but return value is Float instead of String. - class FloatProperty < Property - def initialize(xml, name) - super(xml, name) - @value = @value.to_f if @value - end - end - end - -end - -class String - - unless method_defined?(:classify) - # Create a class name from string - def classify - self.singularize.camelize - end - end - - unless method_defined?(:camelize) - # Camelize converts strings to UpperCamelCase - def camelize - self.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase } - end - end - - unless method_defined?(:singularize) - # Strip 's' character from end of string - def singularize - self.gsub(/s$/, '') - end - end - - # Convert string to float if string value seems like Float - def convert - return self.to_f if self.strip =~ /^([\d\.]+$)/ - self - end - - # Simply converts whitespaces and - symbols to '_' which is safe for Ruby - def sanitize - self.gsub(/(\W+)/, '_') - end - end diff --git a/client/lib/documentation.rb b/client/lib/documentation.rb index 6fb5779..c6255af 100644 --- a/client/lib/documentation.rb +++ b/client/lib/documentation.rb @@ -1,98 +1,62 @@ -require 'lib/deltacloud' +# +# Copyright (C) 2010 Red Hat, Inc. +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +module DeltaCloud + class Documentation + + attr_reader :api, :description, :params, :collection_operations + attr_reader :collection, :operation + + def initialize(api, opts={}) + @description, @api = opts[:description], api + @params = parse_parameters(opts[:params]) if opts[:params] + @collection_operations = opts[:operations] if opts[:operations] + @collection = opts[:collection] + @operation = opts[:operation] + self + end -skip_methods = [ "id=", "uri=" ] + def operations + @collection_operations.collect { |o| api.documentation(@collection, o) } + end -begin - @dc=DeltaCloud.new('mockuser', 'mockpassword', 'http://localhost:3001/api') -rescue - puts "Please make sure that Deltacloud API is running with Mock driver" - exit(1) -end + class OperationParameter + attr_reader :name + attr_reader :type + attr_reader :required + attr_reader :description [email protected]_points.keys.each do |ep| - @dc.send(ep) -end + def initialize(data) + @name, @type, @required, @description = data[:name], data[:type], data[:required], data[:description] + end -class_list = DeltaCloud::classes.collect { |c| DeltaCloud::module_eval("::DeltaCloud::API::#{c}")} + def to_comment + " # @param [...@type}, #...@name}] #...@description}" + end -def read_method_description(c, method) - if method =~ /es$/ - " # Read #{c.downcase} collection from Deltacloud API" - else - case method - when "uri" then - " # Return URI to API for this object" - when "action_urls" then - " # Return available actions API URL" - when "client" then - " # Return instance of API client" - else - " # Get #{method} attribute value from #{c.downcase}" end - end -end -def read_parameters(c, method) - out = [] - if method =~ /es$/ - out << " # @param [String, #id] Filter by ID" - end - out.join("\n") -end + private -def read_return_value(c, method) - if method =~ /es$/ - rt = "Array" - else - rt = "String" - end - " # @return [String] Value of #{method}" -end - -out = [] - -class_list.each do |c| - class_name = "#{c}".gsub(/^DeltaCloud::/, '') - out << "module DeltaCloud" - out << " class API" - @dc.entry_points.keys.each do |ep| - out << "# Return #{ep.to_s.classify} object with given id\n" - out << "# " - out << "# #[email protected](ep.to_s).description.split("\n").join("\n# ")}" - out << "# @return [#{ep.to_s.classify}]" - out << "def #{ep.to_s.gsub(/s$/, '')}" - out << "end" - out << "# Return collection of #{ep.to_s.classify} objects" - out << "# " - out << "# #[email protected](ep.to_s).description.split("\n").join("\n# ")}" - @dc.documentation(ep.to_s, 'index').params.each do |p| - out << p.to_comment + def parse_parameters(params) + params.collect { |p| OperationParameter.new(p) } end - out << "# @return [Array] [#{ep.to_s.classify}]" - out << "def #{ep}(opts={})" - out << "end" - end - out << " end" - out << " class #{class_name}" - c.instance_methods(false).each do |method| - next if skip_methods.include?(method) - params = read_parameters(class_name, method) - retval = read_return_value(class_name, method) - out << read_method_description(class_name, method) - out << params if params - out << retval if retval - out << " def #{method}" - out << " # This method was generated dynamically from API" - out << " end\n" + end - out << " end" - out << "end" -end -FileUtils.rm_r('doc') rescue nil -FileUtils.mkdir_p('doc') -File.open('doc/deltacloud.rb', 'w') do |f| - f.puts(out.join("\n")) end -system("yardoc -m markdown --readme README --title 'Deltacloud Client Library' 'lib/*.rb' 'doc/deltacloud.rb' --verbose") -FileUtils.rm('doc/deltacloud.rb') diff --git a/client/lib/hwp_properties.rb b/client/lib/hwp_properties.rb new file mode 100644 index 0000000..86ccf00 --- /dev/null +++ b/client/lib/hwp_properties.rb @@ -0,0 +1,64 @@ +# +# Copyright (C) 2009 Red Hat, Inc. +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +module DeltaCloud + + module HWP + + class Property + attr_reader :name, :unit, :value, :kind + + def initialize(xml, name) + @name, @kind, @value, @unit = xml['name'], xml['kind'].to_sym, xml['value'], xml['unit'] + declare_ranges(xml) + self + end + + def present? + ! @value.nil? + end + + private + + def declare_ranges(xml) + case xml['kind'] + when 'range' then + self.class.instance_eval do + attr_reader :range + end + @range = { :from => xml.xpath('range').first['first'], :to => xml.xpath('range').first['last'] } + when 'enum' then + self.class.instance_eval do + attr_reader :options + end + @options = xml.xpath('enum/entry').collect { |e| e['value'] } + end + end + + end + + # FloatProperty is like Property but return value is Float instead of String. + class FloatProperty < Property + def initialize(xml, name) + super(xml, name) + @value = @value.to_f if @value + end + end + end + +end diff --git a/client/lib/instance_state.rb b/client/lib/instance_state.rb new file mode 100644 index 0000000..ebc59c4 --- /dev/null +++ b/client/lib/instance_state.rb @@ -0,0 +1,29 @@ +module DeltaCloud + module InstanceState + + class State + attr_reader :name + attr_reader :transitions + + def initialize(name) + @name, @transitions = name, [] + end + end + + class Transition + attr_reader :to + attr_reader :action + + def initialize(to, action) + @to = to + @action = action + end + + def auto? + @action.nil? + end + end + + end + +end diff --git a/client/lib/string.rb b/client/lib/string.rb new file mode 100644 index 0000000..72cc259 --- /dev/null +++ b/client/lib/string.rb @@ -0,0 +1,53 @@ +# +# Copyright (C) 2010 Red Hat, Inc. +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +class String + + unless method_defined?(:classify) + # Create a class name from string + def classify + self.singularize.camelize + end + end + + unless method_defined?(:camelize) + # Camelize converts strings to UpperCamelCase + def camelize + self.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase } + end + end + + unless method_defined?(:singularize) + # Strip 's' character from end of string + def singularize + self.gsub(/s$/, '') + end + end + + # Convert string to float if string value seems like Float + def convert + return self.to_f if self.strip =~ /^([\d\.]+$)/ + self + end + + # Simply converts whitespaces and - symbols to '_' which is safe for Ruby + def sanitize + self.strip.gsub(/(\W+)/, '_') + end + +end diff --git a/client/specs/hardware_profiles_spec.rb b/client/specs/hardware_profiles_spec.rb index d11eb36..7957e99 100644 --- a/client/specs/hardware_profiles_spec.rb +++ b/client/specs/hardware_profiles_spec.rb @@ -64,7 +64,7 @@ describe "hardware_profiles" do it "should allow fetching different hardware_profiles" do client = DeltaCloud.new( API_NAME, API_PASSWORD, API_URL ) hwp1 = client.hardware_profile( 'm1-small' ) - hwp2 = client.hardware_profile( 'm1-xlarge' ) + hwp2 = client.hardware_profile( 'm1-large' ) hwp1.storage.value.should_not eql(hwp2.storage.value) hwp1.memory.value.should_not eql(hwp2.memory.value) end diff --git a/client/specs/instances_spec.rb b/client/specs/instances_spec.rb index c4995ae..bfe29bb 100644 --- a/client/specs/instances_spec.rb +++ b/client/specs/instances_spec.rb @@ -85,7 +85,7 @@ describe "instances" do it "should allow creation of new instances with reasonable defaults" do DeltaCloud.new( API_NAME, API_PASSWORD, API_URL ) do |client| - instance = client.create_instance( 'img1', :name=>'TestInstance' ) + instance = client.create_instance( 'img1', :name=>'TestInstance', :hardware_profile => 'm1-large' ) instance.should_not be_nil instance.uri.should match( %r{#{API_URL}/instances/inst[0-9]+} ) instance.id.should match( /inst[0-9]+/ ) @@ -98,7 +98,7 @@ describe "instances" do it "should allow creation of new instances with specific realm" do DeltaCloud.new( API_NAME, API_PASSWORD, API_URL ) do |client| - instance = client.create_instance( 'img1', :realm=>'eu' ) + instance = client.create_instance( 'img1', :realm=>'eu', :hardware_profile => 'm1-large' ) instance.should_not be_nil instance.uri.should match( %r{#{API_URL}/instances/inst[0-9]+} ) instance.id.should match( /inst[0-9]+/ ) -- 1.7.2.3
