I've been working on enhancing Deltacloud storage_volume support, and stumbled across a serious bug in the client. The issue is with deltacloud.rb#xml_to_class, and the way it handles actions that are defined in the xml.

For each object, it defines three methods on the class for that object:

* action!, where 'action' is the actual action (so stop!, destroy!, etc) - this performs the action
* actions - returns a list of action names
* actions_urls - returns a hash of { action => action_url }

The issue is that all three of these methods are defined on the class as closures over the action list for a particular object. So if you are iterating over a collection, *all* members of the collection will have the three methods defined for the *last* collection member, since the methods are defined at the class level. This results in:

irb> client.keys.collect {|x| [x.id, x.actions_urls]}
[
    [0] [
        [0] "somekey",
        [1] {
"destroy" => "http://localhost:8080/deltacloud/api/keys/default";
        }
    ],
    [1] [
        [0] "default",
        [1] {
"destroy" => "http://localhost:8080/deltacloud/api/keys/default";
        }
    ]
]

which means a call to client.keys.first.destroy! actually destroys client.keys.last.

xml_to_class has a good bit of meta-programming, and is difficult to decipher. Is there a reason why the client starts with a bare Class, and turns it in to Instance, Key, etc? I understand the motivation of taking some base class and creating Instance, Key, etc. dynamically, but wouldn't it make sense to have a base class that provided some functionality (written off the cuff, and untested):

class DeltaCloud::API::BaseObject

  def action_meta
    @action_meta ||= { }
  end

  def add_action(action, method, url)
    action_meta[action.to_s] = [method, url]
# you could also define_method here instead of relying on method_missing (below)
  end

  def actions
    actions_meta.keys
  end

  def actions_urls
action_meta.inject({}) { |accum, action, method_and_url| accum[action] = method_and_url.last; accum }
  end

  def call_action(action)
    method, url = action_meta[action]
    client.request(method.to_sym, url, {}, {})
    # would need to get status here
  end

  def method_missing(meth, *args, &block)
    if meth.to_s =~ /^(.*)!$/ and actions.include?($1)
      call_action($1)
    else
      super
    end
  end

end


DeltaCloud.define_class could then instead create a new class that subclassed the above class, and xml_to_class (for actions at least) becomes much simpler:

                when "actions":
                  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

would become (ignoring the 'state' method for now):

                when "actions":
                  attribute.xpath('link').each do |link|
obj.add_action(link['rel'].sanitize, link['method'], link[:href])
                  end

There is much more the base class would need to do in order to satisfy all of what xml_to_class, base_object, and other methods on the client set up, but you would end up with an implementation that would be more readable and more easily tested.

I'm certainly new to the Deltacloud code base (I really just started digging in to it yesterday), so let me know if I'm way off base.
Toby

Reply via email to