Here are a few patches that implement the beginnings of an API for OVirt. They cover handling hosts and storage and hardware pools so far. This patch is mostly meant for discussion, since there's still a few things that need to be tweaked. If you're only interested in seeing how the API is used, jump straight to patch 5/5; it contains all the client side (example) code. To see what gets sent across the wire, you can request stuff like /ovirt/hosts or /ovirt/hardware_pools?path=/default/foo/bar in your browser and admire the XML. In doing this, it became apparent that there are various things in the existing OVirt server code that need to be adapted to better accomodate the API: * Enable simple HTTP authentication It should be possible for people to run scripts without hooking the client into the krb5 infrastructure; instead, scripts should be able to use simple username/password authentication. * Fold the Rest:: controllers into the main controllers Once we have non-krb5 authn, the controllers I wrote to support the API should be folded into the controllers that drive the WUI. There's some annoying duplication of code right now, and having just one controller for each aspect of the API/WUI would eliminate that. I don't think there's a need to have 'base' controllers with separate subclasses for WUI and API - for the most part, WUI and API just differ in how the results are rendered, and should be able to have exactly the same logic to load/manipulate model objects. But as with anything, the devil will be in the details. One sideeffect of this would be that the API is accessible under /ovirt, and not /ovirt/rest as is the case with these patches. * Rename some of the WUI controllers The REST support (ActionController::Resources) is pretty specific about the names of controllers; for example, hardware pools in the API are served by a HardwarePoolsController, not a HardwareController as in the WUI. It's possible to work around that, but ultimately easiest if we just follow the preferred naming conventions of Rails. * Reconcile some of the differences in parameter naming With REST, the names of attributes in objects maps directly to parameter names in certain requests, and the naming in the WUI sometimes differs from that. For example, when creating a hardware pool, the WUI sends parameters for that in params[:pool], whereas the API sends it in params[:hardware_pool] Again, this can be overcome on a case-by-case basis, but it's easiest if the WUI uses the naming conventions preferred by the Rails' REST support. * Too many attributes are dumped into the XML I haven't tried to prune the attributes that are sent across the wire for each object to a reasonable subset. For example, right now attributes of very questionable use to an API user, like updated_at, lock_version, lft, rgt etc. are all sent. * Where's the CLI ? I intended to accompany this with a nice clean CLI client that you can use for lots of things. Even with the current subset of the API, there are lots of interesting queries and actions one could perform, even with the current subset of the API. I'd like to avoid offering a million options from the CLI though. If anybody has pressing needs for CLI functionality, let me know, otherwise, I'll start making stuff up. Especially for querying, it's questionable if CLI access to query functionality is useful at all - if you want to see all the hosts in hardware pool 7, you can just wget http://management.priv.ovirt.org:3000/ovirt/rest/hardware_pools/7/hosts and get a nice XML document. Is there a need for a CLI client for interactive use that lets you avoid using a browser ? Not sure how important that is.
- List hosts and filter the list by state, arch, hostname, uuid and hardware_pool_id - Retrieve an individual host The routing maps this to /ovirt/rest/hosts and /ovirt/rest/hosts/#{id} --- wui/src/app/controllers/rest/hosts_controller.rb | 26 ++++++++++++++++++++ wui/src/config/routes.rb | 5 ++++ .../test/functional/rest/hosts_controller_test.rb | 8 ++++++ 3 files changed, 39 insertions(+), 0 deletions(-) create mode 100644 wui/src/app/controllers/rest/hosts_controller.rb create mode 100644 wui/src/test/functional/rest/hosts_controller_test.rb diff --git a/wui/src/app/controllers/rest/hosts_controller.rb b/wui/src/app/controllers/rest/hosts_controller.rb new file mode 100644 index 0000000..2e85f65 --- /dev/null +++ b/wui/src/app/controllers/rest/hosts_controller.rb @@ -0,0 +1,26 @@ +class Rest::HostsController < ApplicationController + + EQ_ATTRIBUTES = [ :state, :arch, :hostname, :uuid, + :hardware_pool_id ] + def index + conditions = [] + EQ_ATTRIBUTES.each do |attr| + if params[attr] + conditions << "#{attr} = :#{attr}" + end + end + @hosts = Host.find(:all, + :conditions => [conditions.join(" and "), params], + :order => "id") + respond_to do |format| + format.xml { render :xml => @hosts.to_xml } + end + end + + def show + @host = Host.find(params[:id]) + respond_to do |format| + format.xml { render :xml => @host.to_xml } + end + end +end diff --git a/wui/src/config/routes.rb b/wui/src/config/routes.rb index a93ba4b..e7aa0be 100644 --- a/wui/src/config/routes.rb +++ b/wui/src/config/routes.rb @@ -19,6 +19,11 @@ ActionController::Routing::Routes.draw do |map| + map.namespace(:rest) do |rest| + rest.resources :hosts + end + + # The priority is based upon order of creation: first created -> highest priority. # Sample of regular route: diff --git a/wui/src/test/functional/rest/hosts_controller_test.rb b/wui/src/test/functional/rest/hosts_controller_test.rb new file mode 100644 index 0000000..32e7931 --- /dev/null +++ b/wui/src/test/functional/rest/hosts_controller_test.rb @@ -0,0 +1,8 @@ +require File.dirname(__FILE__) + '/../../test_helper' + +class Rest::HostsControllerTest < ActionController::TestCase + # Replace this with your real tests. + def test_truth + assert true + end +end -- 1.5.5.1
- List and filter storage pools by ipaddr, path, target and hardware_pool_id - Show an individual storage pool - Create a new storage pool - Destroy a storage pool --- .../controllers/rest/storage_pools_controller.rb | 60 ++++++++++++++++++++ wui/src/config/routes.rb | 1 + 2 files changed, 61 insertions(+), 0 deletions(-) create mode 100644 wui/src/app/controllers/rest/storage_pools_controller.rb diff --git a/wui/src/app/controllers/rest/storage_pools_controller.rb b/wui/src/app/controllers/rest/storage_pools_controller.rb new file mode 100644 index 0000000..99eb158 --- /dev/null +++ b/wui/src/app/controllers/rest/storage_pools_controller.rb @@ -0,0 +1,60 @@ +class Rest::StoragePoolsController < ApplicationController + + EQ_ATTRIBUTES = [ :ip_addr, :export_path, :target, + :hardware_pool_id ] + + def index + conditions = [] + EQ_ATTRIBUTES.each do |attr| + if params[attr] + conditions << "#{attr} = :#{attr}" + end + end + + @storage = StoragePool.find(:all, + :conditions => [conditions.join(" and "), params], + :order => "id") + + respond_to do |format| + format.xml { render :xml => @storage.to_xml } + end + end + + def show + @storage = StoragePool.find(params[:id]) + respond_to do |format| + format.xml { render :xml => @storage.to_xml } + end + end + + def create + # Somehow the attribute 'type' never makes it through + # Maybe because there is a (deprecated) Object.type ? + pool = params[:storage_pool] + type = pool[:storage_type] + pool.delete(:storage_type) + @storage_pool = StoragePool.factory(type, pool) + respond_to do |format| + if @storage_pool + if @storage_pool.save + format.xml { render :xml => @storage_pool, + :status => :created, + :location => rest_storage_pool_url(@storage_pool) } + else + format.xml { render :xml => @storage_pool.errors, + :status => :unprocessable_entity } + end + else + format.xml { render :xml => "Illegal storage type #{params[:storage_type]}", :status => :unprocessable_entity } + end + end + end + + def destroy + @storage_pool = StoragePool.find(params[:id]) + @storage_pool.destroy + respond_to do |format| + format.xml { head :ok } + end + end +end diff --git a/wui/src/config/routes.rb b/wui/src/config/routes.rb index e7aa0be..993a885 100644 --- a/wui/src/config/routes.rb +++ b/wui/src/config/routes.rb @@ -21,6 +21,7 @@ ActionController::Routing::Routes.draw do |map| map.namespace(:rest) do |rest| rest.resources :hosts + rest.resources :storage_pools end -- 1.5.5.1
- List and filter HW pools by name - CRUD of individual HW pool - Hosts and storgae pools for a HW pool are accessible as nested resources, e.g. /ovirt/hardware_pools/1/hosts?hostname=myhost.example.com --- .../controllers/rest/hardware_pools_controller.rb | 85 ++++++++++++++++++++ wui/src/config/routes.rb | 4 + 2 files changed, 89 insertions(+), 0 deletions(-) create mode 100644 wui/src/app/controllers/rest/hardware_pools_controller.rb diff --git a/wui/src/app/controllers/rest/hardware_pools_controller.rb b/wui/src/app/controllers/rest/hardware_pools_controller.rb new file mode 100644 index 0000000..49a8fcc --- /dev/null +++ b/wui/src/app/controllers/rest/hardware_pools_controller.rb @@ -0,0 +1,85 @@ +class Rest::HardwarePoolsController < ApplicationController + OPTS = { + :include => [ :storage_pools, :hosts, :quota ] + } + + EQ_ATTRIBUTES = [ :name ] + + def index + conditions = [] + EQ_ATTRIBUTES.each do |attr| + if params[attr] + conditions << "#{attr} = :#{attr}" + end + end + + @pools = HardwarePool.find(:all, + :conditions => [conditions.join(" and "), params], + :order => "id") + + respond_to do |format| + format.xml { render :xml => @pools.to_xml(OPTS) } + end + end + + def show + @pool = HardwarePool.find(params[:id]) + respond_to do |format| + format.xml { render :xml => @pool.to_xml(OPTS) } + end + end + + def create + # FIXME: Why can't I just use HardwarePool.create here ? + # Shouldn't that find the parent_id param ? + @parent = Pool.find(params[:hardware_pool][:parent_id]) + @pool = HardwarePool.new(params[:hardware_pool]) + # FIXME: How do we check for errors here ? + @pool.create_with_parent(@parent) + respond_to do |format| + format.xml { render :xml => @pool.to_xml(OPTS), + :status => :created, + :location => rest_hardware_pool_url(@pool) } + end + end + + def update + # We allow updating direct attributes of the HW pool and moving + # hosts/storage into it here. + @pool = HardwarePool.find(params[:id]) + [:hosts, :storage_pools].each do |k| + objs = params[:hardware_pool].delete(k) + ids = objs.reject{ |obj| obj[:hardware_pool_id] == @pool.id}. + collect{ |obj| obj[:id] } + if ids.size > 0 + if k == :hosts + # FIXME: Why does move_hosts need an explicit pool_id ? + @pool.move_hosts(ids, @pool.id) + else + @pool.move_storage(ids, @pool.id) + end + end + end + + @pool.update_attributes(params[:hardware_pool]) + respond_to do |format| + if @pool.save + format.xml { render :xml => @pool.to_xml(OPTS), + :status => :created, + :location => rest_hardware_pool_url(@pool) } + else + format.xml { render :xml => @pool.errors, + :status => :unprocessable_entity } + end + end + end + + def destroy + @pool = HardwarePool.find(params[:id]) + @pool.destroy + respond_to do |format| + format.xml { head :ok } + end + end + +end diff --git a/wui/src/config/routes.rb b/wui/src/config/routes.rb index 993a885..322d6f9 100644 --- a/wui/src/config/routes.rb +++ b/wui/src/config/routes.rb @@ -22,6 +22,10 @@ ActionController::Routing::Routes.draw do |map| map.namespace(:rest) do |rest| rest.resources :hosts rest.resources :storage_pools + rest.resources :hardware_pools do |pools| + pools.resources :hosts + pools.resources :storage_pools + end end -- 1.5.5.1
David Lutterkort
2008-Aug-07 00:00 UTC
[Ovirt-devel] [PATCH 4/5] Find hardware_pool by path; search by parent_id
Get individual hardware pool with a path, e.g. with a GET to /ovirt/hardware_pools?path=/default/foo/bar Get children of a hardware pool from /ovirt/hardware_pools?parent_id=1 Get specific child with /ovirt/hardware_pools?parent_id=1&name=foo --- .../controllers/rest/hardware_pools_controller.rb | 25 ++++++++++++------- wui/src/app/models/hardware_pool.rb | 12 +++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/wui/src/app/controllers/rest/hardware_pools_controller.rb b/wui/src/app/controllers/rest/hardware_pools_controller.rb index 49a8fcc..836d5f2 100644 --- a/wui/src/app/controllers/rest/hardware_pools_controller.rb +++ b/wui/src/app/controllers/rest/hardware_pools_controller.rb @@ -3,19 +3,26 @@ class Rest::HardwarePoolsController < ApplicationController :include => [ :storage_pools, :hosts, :quota ] } - EQ_ATTRIBUTES = [ :name ] + EQ_ATTRIBUTES = [ :name, :parent_id ] def index - conditions = [] - EQ_ATTRIBUTES.each do |attr| - if params[attr] - conditions << "#{attr} = :#{attr}" + if params[:path] + @pools = [] + pool = HardwarePool.find_by_path(params[:path]) + @pools << pool if pool + logger.info "POOLS: #{@pools.inspect}" + else + conditions = [] + EQ_ATTRIBUTES.each do |attr| + if params[attr] + conditions << "#{attr} = :#{attr}" + end end - end - @pools = HardwarePool.find(:all, - :conditions => [conditions.join(" and "), params], - :order => "id") + @pools = HardwarePool.find(:all, + :conditions => [conditions.join(" and "), params], + :order => "id") + end respond_to do |format| format.xml { render :xml => @pools.to_xml(OPTS) } diff --git a/wui/src/app/models/hardware_pool.rb b/wui/src/app/models/hardware_pool.rb index 276779f..249d744 100644 --- a/wui/src/app/models/hardware_pool.rb +++ b/wui/src/app/models/hardware_pool.rb @@ -97,4 +97,16 @@ class HardwarePool < Pool return {:total => total, :labels => labels} end + def self.find_by_path(path) + segs = path.split("/") + unless segs.shift.empty? + raise "Path must be absolute, but is #{path}" + end + if segs.shift == "default" + segs.inject(get_default_pool) do |pool, seg| + pool.sub_hardware_pools.find { |p| p.name == seg } if pool + end + end + end + end -- 1.5.5.1
David Lutterkort
2008-Aug-07 00:00 UTC
[Ovirt-devel] [PATCH 5/5] Sample code that shows how to use the API
--- wui/client/bin/ovirt-cli | 104 ++++++++++++++++++++++++++++++++++++++++++++++ wui/client/lib/ovirt.rb | 63 ++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 0 deletions(-) create mode 100755 wui/client/bin/ovirt-cli create mode 100644 wui/client/lib/ovirt.rb diff --git a/wui/client/bin/ovirt-cli b/wui/client/bin/ovirt-cli new file mode 100755 index 0000000..cfb4697 --- /dev/null +++ b/wui/client/bin/ovirt-cli @@ -0,0 +1,104 @@ +#! /usr/bin/ruby + +# This is not really a CLI, it's just an example how +# the API can be used for a variety of tasks +# Eventually, this needs to be replaced by the real CLI + +# Run this on any machine that has activeresource installed. Since there is +# no support for authentication yet, you need to connect to Mongrel +# directly (and therefore expose port 3000 on the OVirt Server to the wider +# network) + +require 'pp' +require 'rubygems' +require 'activeresource' +require 'optparse' + +require 'ovirt' + +def move_random_host(hosts, pool) + host = hosts[rand(hosts.size)] + puts "Move #{host.hostname} to #{pool.name}" + pool.hosts << host + pool.save +end + +def element_path(obj) + "[#{obj.class.element_path(obj.id)}]" +end + +def print_pool(pool) + puts "\n\nPool #{pool.name}: #{pool.hosts.size} hosts, #{pool.storage_pools.size} storage pools #{element_path(pool)} " + puts "=" * 75 + pool.hosts.each do |h| + printf "%-36s %s\n", h.hostname, element_path(h) + end + pool.storage_pools.each do |sp| + type = sp.nfs? ? "NFS" : "iSCSI" + printf "%-5s %-30s %s\n", type, sp.label, element_path(sp) + end + puts "-" * 75 +end + +# Plumbing so we can find the OVirt server +# "http://ovirt.watzmann.net:3000/ovirt/rest" +OVirt::Base.site = ENV["OVIRT_SERVER"] +opts = OptionParser.new("ovirt-cli GLOBAL_OPTS") +opts.separator(" Run some things against an OVirt server") +opts.separator("") +opts.separator "Global options:" +opts.on("-s", "--server=URL", "The OVirt server. Since there is no auth\n" + + "#{" "*37}yet, must be the mongrel server port.\n" + + "#{" "*37}Overrides env var OVIRT_SERVER") do |val| + OVirt::Base.site = val +end + +opts.order(ARGV) + +unless OVirt::Base.site + $stderr.puts <<EOF +You must specify the OVirt server to connect to, either with the +--server option or through the OVIRT_SERVER environment variable +EOF + exit 1 +end + +# Get a single host by name +host = OVirt::Host.find_by_hostname("node3.priv.ovirt.org") +puts "#{host.uuid} has id #{host.id}" + +# What's in the default pool +defpool = OVirt::HardwarePool.default_pool +print_pool(defpool) + +# Create a new hardware pool +mypool = OVirt::HardwarePool.find_by_path("/default/mypool") +unless mypool + puts "Create mypool" + mypool = OVirt::HardwarePool.create( { :parent_id => defpool.id, + :name => "mypool" } ) +end + +# Move some hosts around +puts +if defpool.hosts.size > 1 + move_random_host(defpool.hosts, mypool) +elsif mypool.hosts.size > 0 + move_random_host(mypool.hosts, defpool) +end + +# Delete all storage pools for mypool and add a new one +mypool.storage_pools.each do |sp| + puts "Delete storage pool #{sp.id}" + sp.destroy +end + +storage_pool = OVirt::StoragePool.create( { :storage_type => "NFS", + :hardware_pool_id => mypool.id, + :ip_addr => "192.168.122.50", + :export_path => "/exports/pool1" } ) +puts "Created storage pool #{storage_pool.id}" + +# For some reason, mypool.reload doesn't work here +mypool = OVirt::HardwarePool.find_by_path("/default/mypool") +print_pool(mypool) diff --git a/wui/client/lib/ovirt.rb b/wui/client/lib/ovirt.rb new file mode 100644 index 0000000..48739f4 --- /dev/null +++ b/wui/client/lib/ovirt.rb @@ -0,0 +1,63 @@ +require 'pp' +require 'rubygems' +require 'activeresource' + +module OVirt + class Base < ActiveResource::Base ; end + + class HardwarePool < Base + def self.find_by_path(path) + find(:first, :params => { :path => path }) + end + + def self.default_pool + find(:first, :params => { :path => "/default" }) + end + end + + class StoragePool < Base + def iscsi? + attributes["type"] == "IscsiStoragePool" + end + + def nfs? + attributes["type"] == "NfsStoragePool" + end + + def label + if iscsi? + "#{ip_addr}:#{port}:#{target}" + elsif nfs? + "#{ip_addr}:#{export_path}" + else + raise "Unknown type #{attributes["type"]}" + end + end + end + + class IscsiStoragePool < StoragePool + def initialize(attributes = {}) + super(attributes.update( "type" => "IscsiStoragePool" )) + end + end + + class NfsStoragePool < StoragePool + def initialize(attributes = {}) + super(attributes.update( "type" => "NfsStoragePool" )) + end + end + + class Host < Base + def self.find_by_uuid(uuid) + find(:first, :params => { :uuid => uuid }) + end + + def self.find_by_hostname(hostname) + find(:first, :params => { :hostname => hostname }) + end + + def hardware_pool + HardwarePool.find(hardware_pool_id) + end + end +end -- 1.5.5.1
On Wed, Aug 6, 2008 at 5:00 PM, David Lutterkort <dlutter at redhat.com> wrote: ...snip...> > Is there a need for a CLI client for interactive use that lets you > avoid using a browser ? Not sure how important that is.It would be really nice. Not everyone wants to write everything in ruby. -- Jeff Schroeder Don't drink and derive, alcohol and analysis don't mix. http://www.digitalprognosis.com
David Lutterkort wrote:> Here are a few patches that implement the beginnings of an API for > OVirt. They cover handling hosts and storage and hardware pools so > far. This patch is mostly meant for discussion, since there's still a few > things that need to be tweaked. > > If you're only interested in seeing how the API is used, jump straight to > patch 5/5; it contains all the client side (example) code. To see what gets > sent across the wire, you can request stuff like /ovirt/hosts or > /ovirt/hardware_pools?path=/default/foo/bar in your browser and admire the > XML. > > In doing this, it became apparent that there are various things in the > existing OVirt server code that need to be adapted to better accomodate the > API: > > * Enable simple HTTP authentication > > It should be possible for people to run scripts without hooking the > client into the krb5 infrastructure; instead, scripts should be able to > use simple username/password authentication.Jay is working on this. Once this is in the next branch you can modify your stuff to use the user/pass auth. [snip]> > * Where's the CLI ? > > I intended to accompany this with a nice clean CLI client that you can > use for lots of things. Even with the current subset of the API, there > are lots of interesting queries and actions one could perform, even > with the current subset of the API. I'd like to avoid offering a > million options from the CLI though. If anybody has pressing needs for > CLI functionality, let me know, otherwise, I'll start making stuff up.Start making stuff up for now :) Perry
David Lutterkort wrote:> Here are a few patches that implement the beginnings of an API for > OVirt. They cover handling hosts and storage and hardware pools so > far. This patch is mostly meant for discussion, since there's still a few > things that need to be tweaked. >> * Where's the CLI ? > > I intended to accompany this with a nice clean CLI client that you can > use for lots of things. Even with the current subset of the API, there > are lots of interesting queries and actions one could perform, even > with the current subset of the API. I'd like to avoid offering a > million options from the CLI though. If anybody has pressing needs for > CLI functionality, let me know, otherwise, I'll start making stuff up. > > Especially for querying, it's questionable if CLI access to query > functionality is useful at all - if you want to see all the hosts in > hardware pool 7, you can just > > wget http://management.priv.ovirt.org:3000/ovirt/rest/hardware_pools/7/hosts > > and get a nice XML document. > > Is there a need for a CLI client for interactive use that lets you > avoid using a browser ? Not sure how important that is. >I believe that a lot of admin work tends to be performed via scripts. bash and perl are two that I use. I would think that adding new equipment to pools or moving stuff between pools would be done via scripts quicker than via the wui, especially when done in bulk. In addition, I can envision admins writing scripts to respond to events / alerts. Resources on hostX are running out, migrate vm-Y to hostA -mark> _______________________________________________ > Ovirt-devel mailing list > Ovirt-devel at redhat.com > https://www.redhat.com/mailman/listinfo/ovirt-devel