diff --git a/lib/kitchen/driver/openstack.rb b/lib/kitchen/driver/openstack.rb index 922ee242..22c7697d 100755 --- a/lib/kitchen/driver/openstack.rb +++ b/lib/kitchen/driver/openstack.rb @@ -24,6 +24,7 @@ require "kitchen" require "fog/openstack" require "yaml" +require "time" unless defined?(Time.now.iso8601) require_relative "openstack_version" require_relative "openstack/clouds" require_relative "openstack/config" @@ -41,6 +42,11 @@ module Driver # kitchen.yml, `OS_*` environment variables, or a standard # `clouds.yaml` -- see {Clouds} for the precedence rules. class Openstack < Kitchen::Driver::Base + # Nova server states that mean the instance is up and reachable. + # + # @return [Array] + LIVE_STATES = %w{ACTIVE}.freeze + # Settings Fog requires as Strings. Fog re-coerces anything that looks # numeric back to an Integer, so these are stringified on the way in. # @@ -191,8 +197,41 @@ def destroy(state) state.delete(:hostname) end + # Reports what Nova currently thinks of the server. + # + # @param state [Hash] instance state naming the server + # @return [Hash] a Test Kitchen status hash, or the base implementation's + # answer when there is no server or Nova does not know it + def status(state) + return super unless state[:server_id] + + server = lookup_server(state[:server_id]) + return super unless server + + { + live: LIVE_STATES.include?(server.state), + state: server.state, + source: "driver", + resource_id: state[:server_id], + message: "OpenStack reports the server as #{server.state}", + checked_at: Time.now.utc.iso8601, + } + end + private + # Looks a server up without turning an unreachable cloud into a failure. + # + # @param server_id [String] the Nova server ID + # @return [Fog::OpenStack::Compute::Server, nil] the server, or nil when + # Nova does not know it or cannot be reached + def lookup_server(server_id) + disable_ssl_validation if config[:disable_ssl_validation] + compute.servers.get(server_id) + rescue ::StandardError + nil + end + # Releases a floating IP back to its pool. # # A floating IP that Neutron no longer knows about is not an error worth diff --git a/spec/kitchen/driver/openstack_spec.rb b/spec/kitchen/driver/openstack_spec.rb index ae43ff72..a90556f8 100755 --- a/spec/kitchen/driver/openstack_spec.rb +++ b/spec/kitchen/driver/openstack_spec.rb @@ -271,6 +271,62 @@ end end + describe "#status" do + let(:servers) { double("Fog servers collection", get: server) } + let(:compute) { fog_compute(servers: servers) } + + before { allow(driver).to receive(:compute).and_return(compute) } + + it "reports an unknown status when state names no server" do + expect(driver.status({})).to include(live: nil, state: "unknown") + end + + it "reports an unknown status when Nova does not know the server" do + allow(servers).to receive(:get).with("gone").and_return(nil) + + expect(driver.status(server_id: "gone")).to include(state: "unknown") + end + + context "with an ACTIVE server" do + let(:server) { fog_server(state: "ACTIVE") } + + it "reports it as live" do + expect(driver.status(server_id: "test123")).to include( + live: true, state: "ACTIVE", source: "driver", resource_id: "test123" + ) + end + + it "stamps when the check happened" do + expect(driver.status(server_id: "test123")[:checked_at]) + .to match(/\A\d{4}-\d{2}-\d{2}T/) + end + end + + context "with a server Nova has not finished building" do + let(:server) { fog_server(state: "BUILD") } + + it "reports it as not live" do + expect(driver.status(server_id: "test123")) + .to include(live: false, state: "BUILD") + end + end + + context "with a server in ERROR" do + let(:server) { fog_server(state: "ERROR") } + + it "reports it as not live but names the state" do + expect(driver.status(server_id: "test123")) + .to include(live: false, state: "ERROR") + end + end + + it "reports an unknown status when the cloud cannot be reached" do + allow(servers).to receive(:get).and_raise(Excon::Errors::SocketError.new(StandardError.new("boom"))) + + expect(driver.status(server_id: "test123")).to include(state: "unknown") + end + end + describe "#destroy" do let(:state) { { server_id: "test123", hostname: "1.2.3.4" } } let(:servers) { double("Fog servers collection", get: server) }