From cf2ca97dede402af8336821448792375e53aa8c6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?V=C3=ADt=20Ondruch?= <vondruch@redhat.com>
Date: Thu, 6 Mar 2014 10:45:16 +0100
Subject: [PATCH] added support for https peer authentication.
---
lib/rbovirt.rb | 53 +++++++++++++++++++++++++++--------
lib/restclient_ext/request.rb | 60 ++++++++++++++++++++++++++++++++++++++++
spec/endpoint.yml.example | 3 +-
spec/integration/api_spec.rb | 30 +++++++++++++++++---
spec/integration/vm_crud_spec.rb | 20 ++++++++------
spec/lib/endpoint.rb | 7 +----
spec/spec_helper.rb | 16 ++++++++++-
spec/unit/client_spec.rb | 25 +++++++++++++++++
8 files changed, 182 insertions(+), 32 deletions(-)
create mode 100644 lib/restclient_ext/request.rb
create mode 100644 spec/unit/client_spec.rb
diff --git a/lib/rbovirt.rb b/lib/rbovirt.rb
index 7204bc7..9efdec5 100644
--- a/lib/rbovirt.rb
+++ b/lib/rbovirt.rb
@@ -18,6 +18,7 @@ require "client/storage_domain_api"
require "nokogiri"
require "rest_client"
+require "restclient_ext/request"
module OVIRT
@@ -35,14 +36,35 @@ module OVIRT
class Client
- attr_reader :credentials, :api_entrypoint, :datacenter_id, :cluster_id, :filtered_api
-
- def initialize(username, password, api_entrypoint, datacenter_id=nil, cluster_id=nil, filtered_api = false)
- @credentials = { :username => username, :password => password }
- @datacenter_id = datacenter_id
- @cluster_id = cluster_id
+ attr_reader :credentials, :api_entrypoint, :datacenter_id, :cluster_id, :filtered_api, :ca_cert_file, :ca_cert_store
+
+ # Construct a new ovirt client class.
+ # mandatory parameters
+ # username, password, api_entrypoint - for example 'me@internal', 'secret', 'https://example.com/api'
+ # optional parameters
+ # datacenter_id, cluster_id and filtered_api can be sent in this order for backward
+ # compatibility, or as a hash in the 4th parameter.
+ # datacenter_id - setting the datacenter at initialization will add a default scope to any subsequent call
+ # to the client to the specified datacenter.
+ # cluster_id - setting the cluster at initialization will add a default scope to any subsequent call
+ # to the client to the specified cluster.
+ # filtered_api - when set to false (default) will use ovirt administrator api, else it will use the user
+ # api mode.
+ #
+ def initialize(username, password, api_entrypoint, options={}, backward_compatibility_cluster=nil, backward_compatibility_filtered=nil )
+ if !options.is_a?(Hash)
+ # backward compatibility optional parameters
+ options = {:datacenter_id => options,
+ :cluster_id => backward_compatibility_cluster,
+ :filtered_api => backward_compatibility_filtered}
+ end
@api_entrypoint = api_entrypoint
- @filtered_api = filtered_api
+ @credentials = { :username => username, :password => password }
+ @datacenter_id = options[:datacenter_id]
+ @cluster_id = options[:cluster_id]
+ @filtered_api = options[:filtered_api]
+ @ca_cert_file = options[:ca_cert_file]
+ @ca_cert_store = options[:ca_cert_store]
end
def api_version
@@ -76,7 +98,7 @@ module OVIRT
def http_get(suburl, headers={})
begin
- Nokogiri::XML(RestClient::Resource.new(@api_entrypoint)[suburl].get(http_headers(headers)))
+ Nokogiri::XML(rest_client(suburl).get(http_headers(headers)))
rescue
handle_fault $!
end
@@ -84,7 +106,7 @@ module OVIRT
def http_post(suburl, body, headers={})
begin
- Nokogiri::XML(RestClient::Resource.new(@api_entrypoint)[suburl].post(body, http_headers(headers)))
+ Nokogiri::XML(rest_client(suburl).post(body, http_headers(headers)))
rescue
handle_fault $!
end
@@ -92,7 +114,7 @@ module OVIRT
def http_put(suburl, body, headers={})
begin
- Nokogiri::XML(RestClient::Resource.new(@api_entrypoint)[suburl].put(body, http_headers(headers)))
+ Nokogiri::XML(rest_client(suburl).put(body, http_headers(headers)))
rescue
handle_fault $!
end
@@ -101,7 +123,7 @@ module OVIRT
def http_delete(suburl)
begin
headers = {:accept => 'application/xml'}.merge(auth_header).merge(filter_header)
- Nokogiri::XML(RestClient::Resource.new(@api_entrypoint)[suburl].delete(headers))
+ Nokogiri::XML(rest_client(suburl).delete(headers))
rescue
handle_fault $!
end
@@ -113,6 +135,15 @@ module OVIRT
{ :authorization => "Basic " + encoded_credentials }
end
+ def rest_client(suburl)
+ if (URI.parse(@api_entrypoint)).scheme == 'https'
+ verify_options = {:verify_ssl => OpenSSL::SSL::VERIFY_PEER}
+ verify_options[:ssl_cert_store] = ca_cert_store if ca_cert_store
+ verify_options[:ssl_ca_file] = ca_cert_file if ca_cert_file
+ end
+ RestClient::Resource.new(@api_entrypoint, verify_options)[suburl]
+ end
+
def filter_header
filtered_api ? { :filter => "true" } : {}
end
diff --git a/lib/restclient_ext/request.rb b/lib/restclient_ext/request.rb
new file mode 100644
index 0000000..0070b6b
--- /dev/null
+++ b/lib/restclient_ext/request.rb
@@ -0,0 +1,60 @@
+# rest-client extension
+module RestClient
+ # This class enhance the rest-client request by accepting a parameter for ca certificate store,
+ # this file can be removed once https://github.com/rest-client/rest-client/pull/254
+ # get merged upstream.
+ #
+ # :ssl_cert_store - an x509 certificate store.
+ class Request
+
+ def transmit uri, req, payload, & block
+ setup_credentials req
+
+ net = net_http_class.new(uri.host, uri.port)
+ net.use_ssl = uri.is_a?(URI::HTTPS)
+ if (@verify_ssl == false) || (@verify_ssl == OpenSSL::SSL::VERIFY_NONE)
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ elsif @verify_ssl.is_a? Integer
+ net.verify_mode = @verify_ssl
+ net.verify_callback = lambda do |preverify_ok, ssl_context|
+ if (!preverify_ok) || ssl_context.error != 0
+ err_msg = "SSL Verification failed -- Preverify: #{preverify_ok}, Error: #{ssl_context.error_string} (#{ssl_context.error})"
+ raise SSLCertificateNotVerified.new(err_msg)
+ end
+ true
+ end
+ end
+ net.cert = @ssl_client_cert if @ssl_client_cert
+ net.key = @ssl_client_key if @ssl_client_key
+ net.ca_file = @ssl_ca_file if @ssl_ca_file
+ net.cert_store = args[:ssl_cert_store] if args[:ssl_cert_store]
+ net.read_timeout = @timeout if @timeout
+ net.open_timeout = @open_timeout if @open_timeout
+
+ # disable the timeout if the timeout value is -1
+ net.read_timeout = nil if @timeout == -1
+ net.out_timeout = nil if @open_timeout == -1
+
+ RestClient.before_execution_procs.each do |before_proc|
+ before_proc.call(req, args)
+ end
+
+ log_request
+
+ net.start do |http|
+ if @block_response
+ http.request(req, payload ? payload.to_s : nil, & @block_response)
+ else
+ res = http.request(req, payload ? payload.to_s : nil) { |http_response| fetch_body(http_response) }
+ log_response res
+ process_result res, & block
+ end
+ end
+ rescue EOFError
+ raise RestClient::ServerBrokeConnection
+ rescue Timeout::Error
+ raise RestClient::RequestTimeout
+ end
+
+ end
+end
diff --git a/spec/endpoint.yml.example b/spec/endpoint.yml.example
index 4730d00..77224aa 100644
--- a/spec/endpoint.yml.example
+++ b/spec/endpoint.yml.example
@@ -1,5 +1,4 @@
user: "admin@internal"
password: "secret"
-hostname: "ovirt.example.com"
-port: ""
+url: "http://ovirt.example.com/api"
diff --git a/spec/integration/api_spec.rb b/spec/integration/api_spec.rb
index 3fcb21a..1aa0b2e 100644
--- a/spec/integration/api_spec.rb
+++ b/spec/integration/api_spec.rb
@@ -38,11 +38,32 @@ shared_examples_for "API" do
end
end
+describe OVIRT, "Https authentication" do
+ context 'authenticate using the server ca certificate' do
+
+ it "test_should_get_ca_certificate" do
+ user, password, url, datacenter = endpoint
+ ::OVIRT::RSpec.ca_cert(url).class.should eql(String)
+ end
+
+ it "should_authenticate_with_ca_certificate" do
+ user, password, url, datacenter = endpoint
+ cert = ::OVIRT::RSpec.ca_cert(url)
+ store = OpenSSL::X509::Store.new().add_cert(
+ OpenSSL::X509::Certificate.new(cert))
+
+ client = ::OVIRT::Client.new(user, password, url, {:ca_cert_store => store})
+ client.api_version.class.should eql(String)
+ end
+ end
+end
+
describe OVIRT, "Admin API" do
before(:all) do
- user, password, url = endpoint
- @client = ::OVIRT::Client.new(user, password, url, nil, nil, false)
+ user, password, url, datacenter = endpoint
+ opts = {:datacenter_id => datacenter, :ca_cert_file => "#{File.dirname(__FILE__)}/../ca_cert.pem"}
+ @client = ::OVIRT::Client.new(user, password, url, opts )
end
after(:all) do
@@ -61,8 +82,9 @@ end
describe OVIRT, "User API" do
before(:all) do
- user, password, url = endpoint
- @client = ::OVIRT::Client.new(user, password, url, nil, nil, support_user_level_api)
+ user, password, url, datacenter = endpoint
+ opts = {:datacenter_id => datacenter, :ca_cert_file => "#{File.dirname(__FILE__)}/../ca_cert.pem", :filtered_api => support_user_level_api}
+ @client = ::OVIRT::Client.new(user, password, url, opts)
end
after(:all) do
diff --git a/spec/integration/vm_crud_spec.rb b/spec/integration/vm_crud_spec.rb
index 42e9ca0..61c5293 100644
--- a/spec/integration/vm_crud_spec.rb
+++ b/spec/integration/vm_crud_spec.rb
@@ -3,12 +3,12 @@ require "#{File.dirname(__FILE__)}/../spec_helper"
shared_examples_for "Basic VM Life cycle" do
before(:all) do
- @blank_template_id = "00000000-0000-0000-0000-000000000000"
- @cluster = @client.clusters.first.id
+ @cluster = @client.clusters.last.id
+ @template_id = "00000000-0000-0000-0000-000000000000"
name = 'vm-'+Time.now.to_i.to_s
- @vm = @client.create_vm(:name => name, :template => @blank_template_id, :cluster => @cluster)
+ @vm = @client.create_vm(:name => name, :template => @template_id, :cluster => @cluster)
@client.add_volume(@vm.id)
- @client.add_interface(@vm.id)
+ @client.add_interface(@vm.id, :network_name => 'rhevm')
while !@client.vm(@vm.id).ready? do
end
end
@@ -71,8 +71,9 @@ end
describe "Admin API VM Life cycle" do
before(:all) do
- user, password, url = endpoint
- @client = ::OVIRT::Client.new(user, password, url, nil, nil, false)
+ user, password, url, datacenter = endpoint
+ opts = {:datacenter_id => datacenter, :ca_cert_file => "#{File.dirname(__FILE__)}/../ca_cert.pem"}
+ @client = ::OVIRT::Client.new(user, password, url, opts)
end
context 'admin basic vm and templates operations' do
@@ -83,8 +84,11 @@ end
describe "User API VM Life cycle" do
before(:all) do
- user, password, url = endpoint
- @client = ::OVIRT::Client.new(user, password, url, nil, nil, support_user_level_api)
+ user, password, url, datacenter = endpoint
+ opts = {:datacenter_id => datacenter,
+ :ca_cert_file => "#{File.dirname(__FILE__)}/../ca_cert.pem",
+ :filtered_api => support_user_level_api}
+ @client = ::OVIRT::Client.new(user, password, url, opts)
end
context 'user basic vm and templates operations' do
diff --git a/spec/lib/endpoint.rb b/spec/lib/endpoint.rb
index c85fbd1..e1f8e34 100644
--- a/spec/lib/endpoint.rb
+++ b/spec/lib/endpoint.rb
@@ -3,12 +3,7 @@ module OVIRT::RSpec::Endpoint
def endpoint
file = File.expand_path("../endpoint.yml", File.dirname(__FILE__))
@endpoint ||= YAML.load(File.read(file))
- user = @endpoint['user']
- password= @endpoint['password']
- hostname = @endpoint['hostname']
- port = @endpoint['port']
- url = "http://#{hostname}:#{port}/api"
- return user, password, url
+ return @endpoint['user'], @endpoint['password'], @endpoint['url'] , @endpoint['datacenter']
end
def support_user_level_api
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 564b024..b89cc2e 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -4,7 +4,21 @@
require 'rspec'
require 'rbovirt'
-module OVIRT::RSpec end
+module OVIRT::RSpec
+
+ # get ovirt ca certificate public key
+ # * url - ovirt server url
+ def self.ca_cert(url)
+ ca_url = URI.parse(url)
+ ca_url.path = "/ca.crt"
+ http = Net::HTTP.new(ca_url.host, ca_url.port)
+ http.use_ssl = (ca_url.scheme == 'https')
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ request = Net::HTTP::Get.new(ca_url.path)
+ http.request(request).body
+ end
+
+end
require "#{File.dirname(__FILE__)}/lib/endpoint"
diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb
new file mode 100644
index 0000000..9d2f7c8
--- /dev/null
+++ b/spec/unit/client_spec.rb
@@ -0,0 +1,25 @@
+require "#{File.dirname(__FILE__)}/../spec_helper"
+
+describe OVIRT::Client do
+ context 'client initialization' do
+ it 'should accept no option' do
+ OVIRT::Client::new('mockuser','mockpass','http://example.com/api')
+ end
+
+ it 'should accept no datacenter_id in options' do
+ OVIRT::Client::new('mockuser','mockpass','http://example.com/api', :datacenter_id => '123123')
+ end
+
+ it 'should support backward compatibility' do
+ OVIRT::Client::new('mockuser','mockpass','http://example.com/api', '123123', '123123', false)
+ end
+
+ it 'should support options hash in 4th parameter' do
+ OVIRT::Client::new('mockuser','mockpass','http://example.com/api',
+ {:datacenter_id => '123123',
+ :cluster_id => '123123',
+ :filtered_api => false,
+ :ca_cert_file => 'ca_cert.pem'})
+ end
+ end
+end
--
1.8.5.3