You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@deltacloud.apache.org by mf...@redhat.com on 2010/10/15 14:18:52 UTC

Fixed xml_to_class method in client

Hi,

This patch include a major change in client code, in term of
meta-programming usage. Some of you already recognized some limitations
of metaprogramming in xml_to_class implementation and it would be
extremely difficult to fix all issues and this fixes will add additional
code here, which will make this method even worse.
So I decided to get rid of metaprogramming and instead of doing some
method_define mumbo-jumbo I used 'method_missing' approach.
So now we have three basic classes:

'BaseObject' -> Defines basic methods for REST objects like ID, URL, etc
'ActionObject' -> Extend BaseObject with actions (.stop!, .start!)
'StateFullObject' -> Extend ActionObject with states (reloading, etc)

This approach will make our client stable and safe. So now for example calls
like 'client.realms' will return correct realm names.

There are also some other additions to client. For now, all backend errors
throwed by API will be 're-raised' in client.

Another change was that I moved 'create_*' methods to method_missing.

Last important change is about versioning. Right now our version model is
0.0.9.x but now, version of client will need to be bumped to 0.0.9.9, so
I changed version number to 0.0.1, which I hope will be easy to maintain
and I will definitely looks better in RPMs.

PS:

Our test suite in client is perfectly green with all changes, so this should
break nothing in terms of API.

(rake spec):
...........................................

Finished in 98.923537 seconds
43 examples, 0 failures

I would really appreciate if guys from CloudEngine can test this new gem[1]
under their test frameworks.

[1] http://mifo.sk/tmp/deltacloud-client-0.0.1.gem

  -- Michal


[PATCH core 1/3] Fix for case/when statement compatibility with 1.9.2

Posted by mf...@redhat.com.
From: Ladislav Martincik <lm...@redhat.com>

---
 client/lib/deltacloud.rb    |   10 +++++-----
 client/lib/documentation.rb |    6 +++---
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/client/lib/deltacloud.rb b/client/lib/deltacloud.rb
index 117a04b..e44556f 100644
--- a/client/lib/deltacloud.rb
+++ b/client/lib/deltacloud.rb
@@ -194,7 +194,7 @@ module DeltaCloud
                 # When response cointains 'link' block, declare
                 # methods to call links inside. This is used for instance
                 # to dynamicaly create .stop!, .start! methods
-                when "actions":
+                when "actions" then
                   actions = []
                   attribute.xpath('link').each do |link|
                     actions << [link['rel'], link[:href]]
@@ -217,7 +217,7 @@ module DeltaCloud
                     urls
                   end
                 # Property attribute is handled differently
-                when "property":
+                when "property" then
                   attr_accessor :"#{attribute['name'].sanitize}"
                   if attribute['value'] =~ /^(\d+)$/
                     obj.send(:"#{attribute['name'].sanitize}=",
@@ -227,7 +227,7 @@ module DeltaCloud
                       DeltaCloud::HWP::Property.new(attribute, attribute['name']))
                   end
                 # Public and private addresses are returned as Array
-                when "public_addresses", "private_addresses":
+                when "public_addresses", "private_addresses" then
                   attr_accessor :"#{attribute.name.sanitize}"
                   obj.send(:"#{attribute.name.sanitize}=",
                     attribute.xpath('address').collect { |address| address.text })
@@ -523,12 +523,12 @@ module DeltaCloud
 
       def declare_ranges(xml)
         case xml['kind']
-          when 'range':
+          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':
+          when 'enum' then
             self.class.instance_eval do
               attr_reader :options
             end
diff --git a/client/lib/documentation.rb b/client/lib/documentation.rb
index f727cec..6fb5779 100644
--- a/client/lib/documentation.rb
+++ b/client/lib/documentation.rb
@@ -20,11 +20,11 @@ def read_method_description(c, method)
     "    # Read #{c.downcase} collection from Deltacloud API"
   else
     case method
-      when "uri"
+      when "uri" then
         "    # Return URI to API for this object"
-      when "action_urls"
+      when "action_urls" then
         "    # Return available actions API URL"
-      when "client"
+      when "client" then
         "    # Return instance of API client"
       else
         "    # Get #{method} attribute value from #{c.downcase}"
-- 
1.7.2.3


[PATCH core 2/3] Fixed xml_to_class method in client

Posted by mf...@redhat.com.
From: Michal Fojtik <mf...@redhat.com>

---
 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
 
-@dc.entry_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 << "# #{@dc.documentation(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 << "# #{@dc.documentation(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


Re: Fixed xml_to_class method in client

Posted by Michal Fojtik <mf...@redhat.com>.
On 15/10/10 08:31 -0400, Benjamin Browning wrote:
>
>On Oct 15, 2010, at 8:18 AM, mfojtik@redhat.com wrote:
>
>> Last important change is about versioning. Right now our version model is
>> 0.0.9.x but now, version of client will need to be bumped to 0.0.9.9, so
>> I changed version number to 0.0.1, which I hope will be easy to maintain
>> and I will definitely looks better in RPMs.
>> 
>
>Should that not be 0.0.10 or 0.1.0? 0.0.1 < 0.0.9.x, which means most software and people will assume 0.0.1 is an older version.

Yeah, you're absolutely right. I voting for 0.1.0

   -- Michal

-- 
--------------------------------------------------------
Michal Fojtik, mfojtik@redhat.com
Deltacloud API: http://deltacloud.org
--------------------------------------------------------

Re: Fixed xml_to_class method in client

Posted by Benjamin Browning <bb...@redhat.com>.
On Oct 15, 2010, at 8:18 AM, mfojtik@redhat.com wrote:

> Last important change is about versioning. Right now our version model is
> 0.0.9.x but now, version of client will need to be bumped to 0.0.9.9, so
> I changed version number to 0.0.1, which I hope will be easy to maintain
> and I will definitely looks better in RPMs.
> 

Should that not be 0.0.10 or 0.1.0? 0.0.1 < 0.0.9.x, which means most software and people will assume 0.0.1 is an older version.

Ben

[PATCH core 3/3] Bumped gem version to 0.0.1

Posted by mf...@redhat.com.
From: Michal Fojtik <mf...@redhat.com>

---
 client/deltacloud-client.gemspec |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diff --git a/client/deltacloud-client.gemspec b/client/deltacloud-client.gemspec
index 110ad83..6359c4e 100644
--- a/client/deltacloud-client.gemspec
+++ b/client/deltacloud-client.gemspec
@@ -23,7 +23,7 @@ Gem::Specification.new do |s|
   s.email = 'deltacloud-users@lists.fedorahosted.org'
   s.name = 'deltacloud-client'
   s.description = %q{Deltacloud REST Client for API}
-  s.version = '0.0.9.8'
+  s.version = '0.0.1'
   s.summary = %q{Deltacloud REST Client}
   s.files = Dir['Rakefile', 'lib/**/*.rb', 'init.rb', 'bin/deltacloudc']
   s.bindir = 'bin'
-- 
1.7.2.3