| # |
| # 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. |
| |
| $:.unshift File.join(File.dirname(__FILE__), '..') |
| require "deltacloud/test_setup.rb" |
| |
| INSTANCES = "/instances" |
| |
| describe 'Deltacloud API instances collection' do |
| include Deltacloud::Test::Methods |
| need_collection :instances |
| #make sure we have at least one running instance to test |
| if collection_supported :instances |
| #keep track of what we create for deletion after tests: |
| @@created_resources = {:instances=>[], :keys=>[], :images=>[], :firewalls=>[]} |
| image_id = get_a("image") |
| res = post(INSTANCES, :image_id=>image_id) |
| unless res.code == 201 |
| raise Exception.new("Failed to create instance from image_id #{image_id}") |
| end |
| @@my_instance_id = (res.xml/'instance')[0][:id] |
| @@created_resources[:instances] << @@my_instance_id |
| #currently supported providers transition from PENDING to either RUNNING or STOPPED |
| attempts = 0 |
| begin |
| res = get(INSTANCES+"/"+@@my_instance_id) |
| attempts += 1 |
| state = (res.xml/'instance/state').text |
| if state == "STOPPED" |
| start_res = post(INSTANCES+"/"+@@my_instance_id+"/start", "") |
| attempts = 0 |
| elsif state == "PENDING" |
| sleep(10) |
| end |
| end while attempts < 30 and state != "RUNNING" |
| end |
| |
| #stop/destroy the resources we created for the tests |
| MiniTest::Unit.after_tests { |
| puts "CLEANING UP... resources for deletion: #{@@created_resources.inspect}" |
| #instances: |
| #first try to stop them all. They may still be in another state, so keep trying for a while. |
| @@created_resources[:instances].each_index do |i| |
| attempts = 0 |
| begin |
| stop_res = post(INSTANCES+"/"+@@created_resources[:instances][i]+"/stop", "") |
| @@created_resources[:instances][i] = nil if stop_res.code == 202 |
| rescue Exception => e |
| sleep(10) |
| attempts += 1 |
| retry if (attempts <= 5) |
| end |
| end |
| @@created_resources[:instances].compact! |
| #then destroy them. They may still be still stopping, so keep trying for a while. |
| (0..20).each do |
| @@created_resources[:instances].each_index do |i| |
| attempts = 0 |
| begin |
| destroy_res = delete(INSTANCES+"/"+@@created_resources[:instances][i]) |
| @@created_resources[:instances][i] = nil if destroy_res.code == 204 |
| rescue Exception => e |
| if e.http_code == 404 # the stop operation may have destroyed it already |
| @@created_resources[:instances][i] = nil |
| elsif (attempts <= 5) |
| attempts += 1 |
| sleep(10) |
| retry |
| end |
| end |
| end |
| @@created_resources[:instances].compact! |
| break if @@created_resources[:instances].empty? |
| puts "sleeping 10" |
| sleep(10) |
| end |
| @@created_resources.delete(:instances) if @@created_resources[:instances].empty? |
| #keys |
| [:keys, :images, :firewalls].each do |col| |
| @@created_resources[col].each do |k| |
| attempts = 0 |
| begin |
| res = delete("/#{col}/#{k}") |
| @@created_resources[col].delete(k) if res.code == 204 |
| rescue Exception => e |
| sleep(10) |
| attempts += 1 |
| retry if (attempts <=5) |
| end |
| end |
| @@created_resources.delete(col) if @@created_resources[col].empty? |
| 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? |
| } |
| |
| def each_instance_xml(&block) |
| res = get(INSTANCES) |
| (res.xml/'instances/instance').each do |r| |
| instance_res = get(INSTANCES + '/' + r[:id]) |
| yield instance_res.xml |
| end |
| end |
| |
| def wait_until_running_or_stopped(res, instance_id) |
| attempts = 0 |
| state = (res.xml/'instance/state').text |
| while (attempts < 30 and not ["RUNNING","STOPPED"].include?(state)) do |
| sleep(10) |
| res = get(INSTANCES+"/"+instance_id) |
| state = (res.xml/'instance/state').text |
| attempts += 1 |
| end |
| end |
| |
| #Run the 'common' tests for all collections defined in common_tests_collections.rb |
| CommonCollectionsTest::run_collection_and_member_tests_for("instances") |
| |
| #Now run the instances-specific tests: |
| |
| it 'must have a legal "state" element defined for each instance in collection, or no "state" at all' do |
| res = get(INSTANCES) |
| (res.xml/'instances/instance').each do |r| |
| # provider may not return state for each instance in collection because of performance reasons |
| (r/'state').first.must_match /(RUNNING|STOPPED|PENDING)/ unless (r/'state').empty? |
| end |
| end |
| |
| it 'must have the "owner_id" element for each instance and it should match with the one in collection' do |
| res = get(INSTANCES) |
| (res.xml/'instances/instance').each do |r| |
| instance_res = get(INSTANCES + '/' + r[:id]) |
| (instance_res.xml/'owner_id').wont_be_empty |
| (instance_res.xml/'owner_id').first.text.must_equal((r/'owner_id').first.text) |
| end |
| end |
| |
| it 'each instance must link to the realm that was used during instance creation' do |
| each_instance_xml do |instance_xml| |
| (instance_xml/'realm').wont_be_empty |
| (instance_xml/'realm').size.must_equal 1 |
| (instance_xml/'realm').first[:id].wont_be_nil |
| (instance_xml/'realm').first[:href].wont_be_nil |
| (instance_xml/'realm').first[:href].must_match /\/#{(instance_xml/'realm').first[:id]}$/ |
| end |
| end |
| |
| it 'each instance must link to the image that was used to during instance creation' do |
| each_instance_xml do |instance_xml| |
| (instance_xml/'image').wont_be_empty |
| (instance_xml/'image').size.must_equal 1 |
| (instance_xml/'image').first[:id].wont_be_nil |
| (instance_xml/'image').first[:href].wont_be_nil |
| (instance_xml/'image').first[:href].must_match /\/#{(instance_xml/'image').first[:id]}$/ |
| end |
| end |
| |
| it 'each instance must link to the hardware_profile that was used to during instance creation' do |
| each_instance_xml do |instance_xml| |
| (instance_xml/'hardware_profile').wont_be_empty |
| (instance_xml/'hardware_profile').size.must_equal 1 |
| (instance_xml/'hardware_profile').first[:id].wont_be_nil |
| (instance_xml/'hardware_profile').first[:href].wont_be_nil |
| (instance_xml/'hardware_profile').first[:href].must_match /\/#{(instance_xml/'hardware_profile').first[:id]}$/ |
| end |
| end |
| |
| it 'each (NON-STOPPED) instance should advertise the public and private addresses of the instance' do |
| each_instance_xml do |instance_xml| |
| #skip this instance if it is in STOPPED state |
| next if (instance_xml/'instance/state').text == "STOPPED" |
| (instance_xml/'public_addresses').wont_be_empty |
| (instance_xml/'public_addresses').size.must_equal 1 |
| (instance_xml/'public_addresses/address').each do |a| |
| a[:type].wont_be_nil |
| a.text.strip.wont_be_empty |
| end |
| (instance_xml/'private_addresses').wont_be_empty |
| (instance_xml/'private_addresses').size.must_equal 1 |
| (instance_xml/'private_addresses/address').each do |a| |
| a[:type].wont_be_nil |
| a.text.strip.wont_be_empty |
| end |
| end |
| end |
| |
| it 'each instance should advertise the storage volumes used by the instance' do |
| each_instance_xml do |i| |
| (i/'storage_volumes').wont_be_empty |
| end |
| end |
| |
| it 'each instance should advertise the list of actions that can be executed for each instance' do |
| each_instance_xml do |instance_xml| |
| (instance_xml/'actions/link').each do |l| |
| l[:href].wont_be_nil |
| l[:href].must_match /^http/ |
| l[:method].wont_be_nil |
| l[:rel].wont_be_nil |
| end |
| end |
| end |
| |
| it 'should allow to create new instance using image without realm' do |
| #random image and create instance |
| image_id = get_a("image") |
| image_id.wont_be_nil |
| res = post(INSTANCES, :image_id=>image_id) |
| res.code.must_equal 201 |
| res.headers[:location].wont_be_nil |
| created_instance_id = (res.xml/'instance')[0][:id] |
| #GET the instance |
| res = get(INSTANCES+"/"+created_instance_id) |
| res.code.must_equal 200 |
| (res.xml/'instance').first[:id].must_equal created_instance_id |
| (res.xml/'instance/image').first[:id].must_equal image_id |
| #confirm instance progresses from PENDING to initial (non-error) state. |
| wait_until_running_or_stopped(res, created_instance_id) |
| #mark it for stopping/destroying after tests run: |
| @@created_resources[:instances] << created_instance_id |
| end |
| |
| it 'should allow to create new instance using image and realm' do |
| #random image, realm and create instance |
| image_id = get_a("image") |
| image_id.wont_be_nil |
| realm_id = get_a("realm") |
| realm_id.wont_be_nil |
| res = post(INSTANCES, :image_id=>image_id, :realm_id=>realm_id) |
| res.code.must_equal 201 |
| res.headers[:location].wont_be_nil |
| created_instance_id = (res.xml/'instance')[0][:id] |
| #GET the instance |
| res = get(INSTANCES+"/"+created_instance_id) |
| res.code.must_equal 200 |
| (res.xml/'instance').first[:id].must_equal created_instance_id |
| (res.xml/'instance/image').first[:id].must_equal image_id |
| (res.xml/'instance/realm').first[:id].must_equal realm_id |
| #confirm instance progresses from PENDING to initial (non-error) state. |
| wait_until_running_or_stopped(res, created_instance_id) |
| #mark it for stopping/destroying after tests run: |
| @@created_resources[:instances] << created_instance_id |
| end |
| |
| it 'should allow to create new instance using image, realm and hardware_profile' do |
| #random image, realm, hardware_profile and create instance |
| image_id = get_a("image") |
| image_id.wont_be_nil |
| #check if this image defines compatible hw_profiles: |
| res = get("/images/"+image_id) |
| if (res.xml/'image/hardware_profiles').empty? |
| hwp_id = get_a("hardware_profile") |
| else |
| hwp_id = (res.xml/'image/hardware_profiles/hardware_profile').to_a.choice[:id] |
| end |
| hwp_id.wont_be_nil |
| #random realm: |
| realm_id = get_a("realm") |
| realm_id.wont_be_nil |
| res = post(INSTANCES, :image_id=>image_id, :realm_id=>realm_id, :hwp_id => hwp_id) |
| res.code.must_equal 201 |
| res.headers[:location].wont_be_nil |
| created_instance_id = (res.xml/'instance')[0][:id] |
| #GET the instance |
| res = get(INSTANCES+"/"+created_instance_id) |
| res.code.must_equal 200 |
| (res.xml/'instance').first[:id].must_equal created_instance_id |
| (res.xml/'instance/image').first[:id].must_equal image_id |
| (res.xml/'instance/realm').first[:id].must_equal realm_id |
| (res.xml/'instance/hardware_profile').first[:id].must_equal hwp_id |
| #confirm instance progresses from PENDING to initial (non-error) state. |
| wait_until_running_or_stopped(res, created_instance_id) |
| #mark it for stopping/destroying after tests run: |
| @@created_resources[:instances] << created_instance_id |
| end |
| |
| #snapshot (make image of running instance) |
| |
| it 'should allow to snapshot running instance if supported by provider' do |
| res = get(INSTANCES+"/"+@@my_instance_id) |
| #confirm instance is running at all |
| (res.xml/'instance/state').text.must_match /RUNNING/ |
| #check if created instance allows creating image |
| instance_actions = (res.xml/'actions/link').to_a.inject([]){|actions, current| actions << current[:rel]; actions} |
| #check if the create_image action is available for stopped instances |
| unless instance_actions.include?("create_image") |
| stop_res = post(INSTANCES+"/"+@@my_instance_id+"/stop", "") |
| res = get(INSTANCES+"/"+@@my_instance_id) |
| wait_until_running_or_stopped(res, @@my_instance_id) |
| instance_actions = (res.xml/'actions/link').to_a.inject([]){|actions, current| actions << current[:rel]; actions} |
| end |
| skip "no create image support for instance #{@@my_instance_id}" unless instance_actions.include?("create_image") |
| #create image |
| res = post("/images", :instance_id => @@my_instance_id, :name => random_name) |
| res.code.must_equal 201 |
| my_image_id = (res.xml/'image')[0][:id] |
| #mark for deletion later: |
| @@created_resources[:images] << my_image_id |
| res = get(INSTANCES+"/"+@@my_instance_id) |
| #start instance if it was stopped |
| if (res.xml/'instance/state').text == "STOPPED" |
| start_res = post(INSTANCES+"/"+@@my_instance_id+"/start", "") |
| res = get(INSTANCES+"/"+@@my_instance_id) |
| wait_until_running_or_stopped(res, @@my_instance_id) |
| end |
| end |
| |
| # |
| #create with key |
| |
| describe "create instance with auth key" do |
| |
| need_collection :keys |
| need_feature :instances, :authentication_key |
| |
| it 'should allow specification of auth key for created instance when supported' do |
| #create a key to use |
| key_name = random_name |
| key_res = post("/keys", :name=>key_name) |
| key_res.code.must_equal 201 |
| key_id = (key_res.xml/'key')[0][:id] |
| #create instance with this key: |
| image_id = get_a("image") |
| res = post(INSTANCES, :image_id => image_id, :keyname => key_id) |
| res.code.must_equal 201 |
| instance_id = (res.xml/'instance')[0][:id] |
| #check the key: |
| key_used = (res.xml/'instance/authentication/login/keyname')[0].text |
| key_used.must_equal key_id |
| #mark them for deletion after tests run: |
| @@created_resources[:instances] << instance_id |
| @@created_resources[:keys] << key_id |
| end |
| |
| end |
| |
| #specify user name (feature) |
| describe "create instance with user defined name" do |
| |
| need_feature :instances, :user_name |
| |
| it 'should allow specification of name for created instance when supported' do |
| instance_name = random_name |
| image_id = get_a("image") |
| res = post(INSTANCES, :image_id => image_id, :name => instance_name) |
| res.code.must_equal 201 |
| instance_id = (res.xml/'instance')[0][:id] |
| #check the name: |
| created_name = (res.xml/'instance/name')[0].text |
| created_name.must_equal instance_name |
| #confirm instance progresses from PENDING to initial (non-error) state. |
| wait_until_running_or_stopped(res, instance_id) |
| #mark for deletion: |
| @@created_resources[:instances] << instance_id |
| end |
| end |
| |
| #create with firewall (feature) |
| describe "create instance with firewall" do |
| |
| need_collection :firewalls |
| need_feature :instances, :firewalls |
| |
| it 'should be able to create instance using specified firewall' do |
| #create a firewall to use |
| fw_name = random_name |
| fw_res = post("/firewalls", :name=>fw_name, :description=>"firewall created for instances API test on #{Time.now}") |
| fw_res.code.must_equal 201 |
| fw_id = (fw_res.xml/'firewall')[0][:id] |
| ((fw_res.xml/'firewall/name')[0].text).must_equal fw_name |
| #create instance with this firewall: |
| image_id = get_a("image") |
| res = post(INSTANCES, :image_id => image_id, :firewalls1 => fw_id) |
| res.code.must_equal 201 |
| instance_id = (res.xml/'instance')[0][:id] |
| #check the firewall: |
| fw_used = (res.xml/'instance/firewalls/firewall')[0][:id] |
| fw_used.must_equal fw_id |
| #mark for deletion: |
| @@created_resources[:instances] << instance_id |
| @@created_resources[:firewalls] << fw_id |
| end |
| |
| end |
| end |