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