blob: 8641ebe231327353ccb3cccf913e3c138d38f18a [file] [log] [blame]
#
# 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 'rubygems'
require 'require_relative' if RUBY_VERSION < '1.9'
require_relative '../helpers/common.rb'
require 'singleton'
ENV['API_FRONTEND'] = 'cimi'
require_relative '../../server/lib/initialize'
require_relative "../../server/lib/cimi/models"
# Add CIMI specific config stuff
module CIMI
module Test
CIMI_NAMESPACE = "http://schemas.dmtf.org/cimi/1"
class Config
include Singleton
def initialize
@hash = Deltacloud::Test::yaml_config
@cimi = @hash["cimi"]
# Pull in settings for driver reported by server, if any
# Only relevant when running against Deltacloud
@hash["cimi"].update(@hash[name] || {})
end
def cep_url
@cimi["cep"]
end
def base_uri
xml.xpath("/c:CloudEntryPoint/c:baseURI", ns).text
end
def name
xml.xpath("/c:CloudEntryPoint/c:name", ns).text
end
def basic_auth(u = nil, p = nil)
u ||= @cimi["user"]
p ||= @cimi["password"]
"Basic #{Base64.encode64("#{u}:#{p}")}"
end
def auth_header
if @cimi["user"]
{ "Authorization" => basic_auth }
else
{}
end
end
def preferred
@cimi["preferred"] || {}
end
def collections
xml.xpath("/c:CloudEntryPoint/c:*[@href]", ns).map { |c| c.name.to_sym }
end
def features
{}
end
def ns
{ "c" => CIMI_NAMESPACE }
end
private
def xml
unless @xml
h = auth_header
h['Accept'] = 'application/xml'
@xml = RestClient.get(cep_url, h).xml
end
@xml
end
end
def self.config
Config::instance
end
end
end
module CIMI::Test::Methods
module Global
def api
CIMI::Test::config
end
def cep(params = {})
get(api.cep_url, params)
end
def discover_uri_for(op, collection, operations = nil)
unless operations
if collection
cep_json = cep(:accept => :json)
#get the collection operations:
operations = get(cep_json.json["#{collection}"]["href"], {:accept=> :json}).json["operations"]
else
operations = []
end
end
op_regex = Regexp.new(op, Regexp::IGNORECASE) # "add" == /add/i
op_uri = operations.inject(""){|res,current| res = current["href"] if current["rel"] =~ op_regex; res} unless operations.nil?
raise "Couldn't discover the #{collection} Collection #{op} URI" if op_uri.nil? || op_uri.empty?
op_uri
end
def discover_uri_for_subcollection(op, resource_id, subcollection)
subcollection_uri = get(resource_id, {:accept=> :json}).json[subcollection]["href"]
subcollection_ops = get(subcollection_uri, {:accept=> :json}).json["operations"]
discover_uri_for(op, "", subcollection_ops)
end
def discover_uri_for_rmd(resource_uri, rmd_type, rmd_uri)
cep_json = cep(:accept => :json)
rmd_coll = get cep_json.json["resourceMetadata"]["href"], :accept => :json
#get the collection index:
collection_index = rmd_coll.json["resourceMetadata"].index {|rmd| rmd["typeUri"] == resource_uri}
unless rmd_coll.json["resourceMetadata"][collection_index][rmd_type].nil?()
rmd_index = rmd_coll.json["resourceMetadata"][collection_index][rmd_type].index {|rmd| rmd["uri"] == rmd_uri}
end
raise "Couldn't discover the #{rmd_uri} URI" unless rmd_index
end
def get_a(cep, item)
if api.preferred[item]
item_id = cep.json[item.pluralize]["href"] + "/" + api.preferred[item]
else
item_id = get(cep.json[item.pluralize]["href"], {:accept=> :json}).json[full_name(item).pluralize][0]["id"]
end
end
def full_name(item)
if item.include?("Config")
full_name = item.gsub("Config", "Configuration")
else
full_name = item
end
end
def get(path, params = {})
RestClient.get absolute_url(path), headers(params)
end
def post(path, body, params = {})
log_request(:post, path, :params => params, :body => body)
resp = RestClient.post absolute_url(path), body, headers(params)
log_response(:post, path, resp)
resp
end
def delete(path, params={})
log_request(:delete, path, :params=>params)
resp = RestClient.delete absolute_url(path), headers(params)
log_response(:delete, path, resp)
resp
end
# Find the model class that can process the body of the HTTP response
# +resp+
def model_class(resp)
resource = nil
ct = resp.content_type
if ct == "application/json"
resp.json["resourceURI"].wont_be_nil
resource = resp.json["resourceURI"].split("/").last
elsif ct == "application/xml"
if resp.xml.root.name == "Collection"
resource = resp.xml.root["resourceURI"].split("/").last
else
resource = resp.xml.root.name
end
elsif resp.body.nil? || resp.body.size == 0
raise "Can not construct model from empty body"
else
raise "Unexpected content type #{resp.content_type}"
end
CIMI::Model::const_get(resource)
end
private
def absolute_url(path)
if path.start_with?("http")
path
elsif path.start_with?("/")
api.base_uri + path[1, path.size]
else
api.base_uri + "#{path}"
end
end
def headers(params)
headers = api.auth_header
if params[:accept]
headers["Accept"] = "application/#{params[:accept]}"
else
# @content_type is set by the harness below
# if it isn't, default to XML
headers["Accept"] = @content_type || "application/xml"
end
if params[:content_type]
headers["Content-Type"] = "application/#{params[:content_type]}"
end
headers
end
# Adding logging capability
def log
unless @log
@log = Logger.new(STDOUT)
if ENV['LOG_LEVEL'].nil?
@log.level = Logger::WARN
else
@log.level = Logger.const_get ENV['LOG_LEVEL']
end
@log.datetime_format = "%H:%M:%S"
RestClient.log = @log if @log.level == Logger::DEBUG
end
@log
end
def log_request(method, path, opts = {})
log.debug("#{method.to_s.upcase} #{absolute_url(path)}")
if opts[:params]
h = headers(opts[:params])
h.keys.sort.each { |k| log.debug " #{k}: #{h[k]}" }
end
log.debug opts[:body] if opts[:body]
end
def log_response(method, path, resp)
log.debug "--> #{resp.code} #{resp.headers[:content_type]}"
resp.headers.keys.each { |k| log.debug "#{k}: /#{resp.headers[k]}/" }
log.debug resp.body
log.debug "/#{method.to_s.upcase} #{absolute_url(path)}"
end
def poll_state(res, *states)
while not states.include?(res.state.upcase)
log.info "waiting for resource #{res.id} to go from #{res.state} to #{states}"
sleep(10)
res = fetch(res.id)
end
return res
end
def machine_stop_start(machine, action, state)
uri = discover_uri_for(action, "", machine.operations)
response = post( uri,
"<Action xmlns=\"http://schemas.dmtf.org/cimi/1\">" +
"<action> http://http://schemas.dmtf.org/cimi/1/action/" + action + "</action>" +
"</Action>",
:accept => :xml, :content_type => :xml)
response.code.must_equal 202
poll_state(machine(:refetch => true), state)
machine(:refetch => true).state.upcase.must_equal state
end
end
module ClassMethods
def need_collection(name)
before :each do
unless api.collections.include?(name.to_sym)
skip "Server at #{api.cep_url} doesn't support #{name}"
end
end
end
#convenience method for checking if collection :foo is supported:
def collection_supported(name)
api.collections.include?(name.to_sym)
end
def need_capability(op, collection)
before :each do
begin
discover_uri_for(op, collection)
rescue RuntimeError => e
skip "Server at #{api.cep_url} doesn't support #{op} for #{collection} collection. #{e.message}"
end
end
end
def need_rmd(resource_uri, rmd_type, rmd_uri)
before :each do
begin
discover_uri_for_rmd(resource_uri, rmd_type, rmd_uri)
rescue RuntimeError => e
skip "Server at #{api.cep_url} doesn't support #{rmd_uri}. #{e.message}"
end
end
end
# Perform basic collection checks; +model_name+ is the name of the
# method returning the collection model
def check_collection(model_name)
it "must have the \"id\" and \"count\" attributes" do
coll = self.send(model_name)
coll.count.wont_be_nil
coll.count.to_i.must_equal coll.entries.size
coll.id.must_be_uri
end
it "must have a valid id and name for each member" do
self.send(model_name).entries.each do |entry|
entry.id.must_be_uri
member = fetch(entry.id)
member.id.must_equal entry.id
member.name.must_equal entry.name
end
end
end
# Cleanup: stop/destroy the resources created for the tests
def teardown(created_resources, api_basic_auth)
@@created_resources = created_resources
puts "CLEANING UP... resources for deletion: #{@@created_resources.inspect}"
#systems:
if @@created_resources[:systems]
@@created_resources[:systems].each do |sys_id|
sys = get(sys_id, :accept=>:json)
delete_op = sys.json["operations"].find { |op| op["rel"] =~ /delete$/ }
if delete_op
delete_res = RestClient.delete( delete_op["href"],
{'Authorization' => api_basic_auth, :accept => :json} )
@@created_resources[:systems].delete(sys_id) if (200..207).include? delete_res.code
@@created_resources.delete(:systems) if @@created_resources[:systems].empty?
end
end
end
# machines:
if not @@created_resources[:machines].nil?
@@created_resources[:machines].each_index do |i|
machine = get(@@created_resources[:machines][i], :accept => :json)
unless machine.json["state"].upcase.eql?("STOPPED")
stop_op = machine.json["operations"].find { |op| op["rel"] =~ /stop$/ }
stop_res = post( stop_op["href"],
"<Action xmlns=\"http://schemas.dmtf.org/cimi/1\">" +
"<action>http://schemas.dmtf.org/cimi/1/action/stop</action>" +
"</Action>",
:accept => :xml, :content_type => :xml )
machine = get(machine.json["id"], :accept => :json)
end
cep_json = cep(:accept => :json)
while (get(cep_json.json["machines"]["href"], {:accept=>:json}).include?(machine.json["id"]) && (not machine.json["state"].upcase.eql?("STOPPED")))
puts 'waiting for machine to be STOPPED'
sleep(3)
unless (not get(cep_json.json["machines"]["href"], {:accept=>:json}).include?(machine.json["id"]))
machine = get(machine.json["id"], :accept => :json)
end
end
if get(cep_json.json["machines"]["href"], {:accept=>:json}).include?(machine.json["id"])
delete_op = machine.json["operations"].find { |op| op["rel"] =~ /delete$/ }
if delete_op
delete_res = RestClient.delete( delete_op["href"],
{'Authorization' => api_basic_auth, :accept => :json} )
@@created_resources[:machines][i] = nil if (200..207).include? delete_res.code
end
else
@@created_resources[:machines][i] = nil
end
end
@@created_resources[:machines].compact!
@@created_resources.delete(:machines) if @@created_resources[:machines].empty?
end
# machine_image, machine_volumes, other collections
if (not @@created_resources[:machine_images].nil?) &&
(not @@created_resources[:volumes].nil?)
[:machine_images, :volumes, :machine_templates].each do |col|
@@created_resources[col].each do |k|
attempts = 0
begin
puts "#{k}"
res = RestClient.delete( "#{k}",
{'Authorization' => api_basic_auth, :accept => :json} )
@@created_resources[col].delete(k) if res.code == 200
rescue Exception => e
sleep(10)
attempts += 1
retry if (attempts <= 5)
end
end
@@created_resources.delete(col) if @@created_resources[col].empty?
end
end
puts "CLEANUP attempt finished... resources looks like: #{@@created_resources.inspect}"
raise Exception.new("Unable to delete all created resources - please check: #{@@created_resources.inspect}") unless @@created_resources.empty?
end
def query_the_cep(collections = [])
it "should have root collections" do
cep = self.send(:subject)
collections.each do |root|
r = root.underscore.to_sym
if cep.respond_to?(r)
log.info( "Testing collection: " + root )
coll = cep.send(r)
coll.must_respond_to :href, "#{root} collection"
unless coll.href.nil?
coll.href.must_be_uri "#{root} collection"
model = fetch(coll.href)
last_response.code.must_equal 200
if last_response.content_type.eql?("application/json")
last_response.json["resourceURI"].wont_be_nil
end
else
log.info( root + " is not supported by this provider." )
end
end
end
end
end
end
def self.included(base)
base.extend ClassMethods
base.extend Global
base.send(:include, Global)
end
end
# Special spec class for 'behavior' tests that need to be run once
# for XML and once for JSON
class CIMI::Test::Spec < MiniTest::Spec
include CIMI::Test::Methods
attr_reader :format, :content_type
CONTENT_TYPES = { :xml => "application/xml",
:json => "application/json" }
def use_format(fmt)
@format = fmt
@content_type = CONTENT_TYPES[fmt]
end
def fetch(uri)
resp = retrieve(uri) { |fmt| get(uri, :accept => fmt) }
parse(resp)
end
def self.it desc = "anonymous", opts = {}, &block
block ||= proc { skip "(no tests defined)" }
if opts[:only]
super("#{desc}") do
use_format(opts[:only])
instance_eval &block
end
else
CONTENT_TYPES.keys.each do |fmt|
super("#{desc} [#{fmt}]") do
use_format(fmt)
instance_eval &block
end
end
end
end
def self.model(name, opts = {}, &block)
define_method name do |*args|
@_memoized ||= {}
@@_cache ||= {}
if args[0].is_a?(Hash)
if args[0][:refetch]
k = "#{name}_#{@format}"
@_memoized.delete(k)
@@_cache.delete(k)
end
end
resp = @_memoized.fetch("#{name}_#{@format}") do |k|
if opts[:cache]
@_memoized[k] = @@_cache.fetch(k) do |k|
@@_cache[k] = retrieve(k, &block)
end
else
@_memoized[k] = retrieve(k, &block)
end
end
parse(resp)
end
end
def last_response
@@_cache ||= {}
@@_cache[:last_response] ||= {}
@@_cache[:last_response][@format]
end
def setup
unless defined? @@created_resources
# Keep track of what collections were created for deletion after tests:
@@created_resources = {:machines=>[], :machine_images=>[], :volumes=>[], :machine_templates=>[]}
end
@@created_resources
end
private
def parse(response)
model_class(response).parse(response.body, @content_type)
end
def retrieve(k, &block)
response = instance_exec(@format, &block)
if response.body && response.body.size > 0
assert_equal @content_type, response.content_type
if @format == :xml
response.xml.namespaces["xmlns"].must_equal CIMI::Test::CIMI_NAMESPACE
end
end
@@_cache ||= {}
@@_cache[:last_response] ||= {}
@@_cache[:last_response][@format] = response
response
end
end
MiniTest::Spec.register_spec_type(/Behavior$/, CIMI::Test::Spec)