Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions lib/kitchen/driver/openstack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,19 @@ def status(state)
}
end

# Checks the configuration for the mistakes that otherwise surface part
# way through +create+, once a server may already be building and
# billing.
#
# @param state [Hash] mutable instance and driver state
# @return [Boolean] true when a problem was reported
def doctor(state) # rubocop:disable Lint/UnusedMethodArgument
problems = credential_problems + image_flavor_problems

problems.each { |problem| warn(problem) }
!problems.empty?
end

private

# Looks a server up without turning an unreachable cloud into a failure.
Expand All @@ -232,6 +245,54 @@ def lookup_server(server_id)
nil
end

# The three settings without which Keystone cannot authenticate. They are
# in +required_server_settings+, but that only means Fog is handed them
# even when nil -- nothing rejects a nil before the request goes out.
#
# @return [Array<String>] one problem per unset setting, then an
# authentication check when all three are present
def credential_problems
missing = {
openstack_username: "OS_USERNAME",
openstack_api_key: "OS_PASSWORD",
openstack_auth_url: "OS_AUTH_URL",
}.filter_map do |key, env|
if config[key].to_s.empty?
"#{key} is not set: set it in kitchen.yml, export #{env}, or " \
"put it in clouds.yaml."
end
end
return missing unless missing.empty?

compute.servers.summary
[]
rescue ::StandardError => e
["OpenStack rejected the configured credentials at " \
"#{config[:openstack_auth_url]}: #{e.message}"]
end

# The image and flavor pairs that +create+ refuses, checked here so the
# refusal arrives before the run starts rather than during it.
#
# @return [Array<String>] the conflicting or missing selections
def image_flavor_problems
problems = []

if config[:image_id] && config[:image_ref]
problems << "Both image_id and image_ref are set; create accepts only one."
elsif config[:image_id].nil? && config[:image_ref].nil?
problems << "Neither image_id nor image_ref is set; there is no image to build from."
end

if config[:flavor_id] && config[:flavor_ref]
problems << "Both flavor_id and flavor_ref are set; create accepts only one."
elsif config[:flavor_id].nil? && config[:flavor_ref].nil?
problems << "Neither flavor_id nor flavor_ref is set; there is no flavor to size the server with."
end

problems
end

# Releases a floating IP back to its pool.
#
# A floating IP that Neutron no longer knows about is not an error worth
Expand Down
108 changes: 108 additions & 0 deletions spec/kitchen/driver/openstack_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,114 @@
end
end

describe "#doctor" do
let(:servers) { double("Fog servers collection", summary: []) }
let(:compute) { fog_compute(servers: servers) }
let(:config) do
{
openstack_username: "user", openstack_api_key: "secret",
openstack_auth_url: "https://keystone.example.com/v3",
image_id: "img-1", flavor_id: "flavor-1"
}
end

before { allow(driver).to receive(:compute).and_return(compute) }

def doctor_messages
messages = []
allow(driver).to receive(:warn) { |m| messages << m }
[driver.doctor({}), messages]
end

it "passes on a complete configuration" do
found, messages = doctor_messages

expect(found).to be(false)
expect(messages).to be_empty
end

context "with no credentials at all" do
let(:config) { { image_id: "img-1", flavor_id: "flavor-1" } }

it "names each missing setting and where it can come from" do
found, messages = doctor_messages

expect(found).to be(true)
joined = messages.join("\n")
expect(joined).to include("openstack_username is not set")
expect(joined).to include("OS_PASSWORD")
expect(joined).to include("clouds.yaml")
end

it "does not also complain about a connection it never tried" do
_found, messages = doctor_messages

expect(messages.join("\n")).not_to include("rejected the configured credentials")
end
end

it "reports credentials Keystone rejects" do
allow(servers).to receive(:summary).and_raise(Excon::Errors::Unauthorized.new("401"))

found, messages = doctor_messages

expect(found).to be(true)
expect(messages.join("\n")).to include("rejected the configured credentials")
end

context "with both image_id and image_ref" do
let(:config) do
{
openstack_username: "user", openstack_api_key: "secret",
openstack_auth_url: "https://keystone.example.com/v3",
image_id: "img-1", image_ref: "ubuntu", flavor_id: "flavor-1"
}
end

it "reports the conflict create would raise on" do
found, messages = doctor_messages

expect(found).to be(true)
expect(messages.join("\n")).to include("Both image_id and image_ref are set")
end
end

context "with both flavor_id and flavor_ref" do
let(:config) do
{
openstack_username: "user", openstack_api_key: "secret",
openstack_auth_url: "https://keystone.example.com/v3",
image_id: "img-1", flavor_id: "flavor-1", flavor_ref: "m1.small"
}
end

it "reports the conflict create would raise on" do
found, messages = doctor_messages

expect(found).to be(true)
expect(messages.join("\n")).to include("Both flavor_id and flavor_ref are set")
end
end

context "with neither an image nor a flavor selected" do
let(:config) do
{
openstack_username: "user", openstack_api_key: "secret",
openstack_auth_url: "https://keystone.example.com/v3"
}
end

it "reports both gaps" do
found, messages = doctor_messages

expect(found).to be(true)
joined = messages.join("\n")
expect(joined).to include("Neither image_id nor image_ref is set")
expect(joined).to include("Neither flavor_id nor flavor_ref is set")
end
end
end

describe "#destroy" do
let(:state) { { server_id: "test123", hostname: "1.2.3.4" } }
let(:servers) { double("Fog servers collection", get: server) }
Expand Down
Loading