Merge "Restructures authentication for resource providers"
This commit is contained in:
@@ -12,185 +12,73 @@ class Puppet::Provider::Openstack < Puppet::Provider
|
||||
initvars # so commands will work
|
||||
commands :openstack => 'openstack'
|
||||
|
||||
def request(service, action, object, credentials, *properties)
|
||||
if password_credentials_set?(credentials)
|
||||
auth_args = password_auth_args(credentials)
|
||||
elsif openrc_set?(credentials)
|
||||
credentials = get_credentials_from_openrc(credentials['openrc'])
|
||||
auth_args = password_auth_args(credentials)
|
||||
elsif service_credentials_set?(credentials)
|
||||
auth_args = token_auth_args(credentials)
|
||||
elsif env_vars_set?
|
||||
# noop; auth needs no extra arguments
|
||||
auth_args = nil
|
||||
else # All authentication efforts failed
|
||||
raise(Puppet::Error::OpenstackAuthInputError, 'No credentials provided.')
|
||||
end
|
||||
args = [object, properties, auth_args].flatten.compact
|
||||
authenticate_request(service, action, args)
|
||||
end
|
||||
|
||||
def self.request(service, action, object, *properties)
|
||||
if env_vars_set?
|
||||
# noop; auth needs no extra arguments
|
||||
auth_args = nil
|
||||
else # All authentication efforts failed
|
||||
raise(Puppet::Error::OpenstackAuthInputError, 'No credentials provided.')
|
||||
end
|
||||
args = [object, properties, auth_args].flatten.compact
|
||||
authenticate_request(service, action, args)
|
||||
end
|
||||
|
||||
# Returns an array of hashes, where the keys are the downcased CSV headers
|
||||
# with underscores instead of spaces
|
||||
def self.authenticate_request(service, action, *args)
|
||||
rv = nil
|
||||
timeout = 10
|
||||
end_time = Time.now.to_i + timeout
|
||||
loop do
|
||||
begin
|
||||
if(action == 'list')
|
||||
response = openstack(service, action, '--quiet', '--format', 'csv', args)
|
||||
response = parse_csv(response)
|
||||
keys = response.delete_at(0) # ID,Name,Description,Enabled
|
||||
rv = response.collect do |line|
|
||||
hash = {}
|
||||
keys.each_index do |index|
|
||||
key = keys[index].downcase.gsub(/ /, '_').to_sym
|
||||
hash[key] = line[index]
|
||||
def self.request(service, action, properties, credentials=nil)
|
||||
env = credentials ? credentials.to_env : {}
|
||||
Puppet::Util.withenv(env) do
|
||||
rv = nil
|
||||
timeout = 10
|
||||
end_time = Time.now.to_i + timeout
|
||||
loop do
|
||||
begin
|
||||
if(action == 'list')
|
||||
response = openstack(service, action, '--quiet', '--format', 'csv', properties)
|
||||
response = parse_csv(response)
|
||||
keys = response.delete_at(0) # ID,Name,Description,Enabled
|
||||
rv = response.collect do |line|
|
||||
hash = {}
|
||||
keys.each_index do |index|
|
||||
key = keys[index].downcase.gsub(/ /, '_').to_sym
|
||||
hash[key] = line[index]
|
||||
end
|
||||
hash
|
||||
end
|
||||
elsif(action == 'show' || action == 'create')
|
||||
rv = {}
|
||||
# shell output is name="value"\nid="value2"\ndescription="value3" etc.
|
||||
openstack(service, action, '--format', 'shell', properties).split("\n").each do |line|
|
||||
# key is everything before the first "="
|
||||
key, val = line.split("=", 2)
|
||||
next unless val # Ignore warnings
|
||||
# value is everything after the first "=", with leading and trailing double quotes stripped
|
||||
val = val.gsub(/\A"|"\Z/, '')
|
||||
rv[key.downcase.to_sym] = val
|
||||
end
|
||||
hash
|
||||
end
|
||||
elsif(action == 'show' || action == 'create')
|
||||
rv = {}
|
||||
# shell output is name="value"\nid="value2"\ndescription="value3" etc.
|
||||
openstack(service, action, '--format', 'shell', args).split("\n").each do |line|
|
||||
# key is everything before the first "="
|
||||
key, val = line.split("=", 2)
|
||||
next unless val # Ignore warnings
|
||||
# value is everything after the first "=", with leading and trailing double quotes stripped
|
||||
val = val.gsub(/\A"|"\Z/, '')
|
||||
rv[key.downcase.to_sym] = val
|
||||
end
|
||||
else
|
||||
rv = openstack(service, action, args)
|
||||
end
|
||||
break
|
||||
rescue Puppet::ExecutionFailure => e
|
||||
if e.message =~ /HTTP 401/
|
||||
raise(Puppet::Error::OpenstackUnauthorizedError, 'Could not authenticate.')
|
||||
elsif e.message =~ /Unable to establish connection/
|
||||
current_time = Time.now.to_i
|
||||
if current_time > end_time
|
||||
break
|
||||
else
|
||||
wait = end_time - current_time
|
||||
Puppet::debug("Non-fatal error: \"#{e.message}\"; retrying for #{wait} more seconds.")
|
||||
if wait > timeout - 2 # Only notice the first time
|
||||
notice("#{service} service is unavailable. Will retry for up to #{wait} seconds.")
|
||||
end
|
||||
rv = openstack(service, action, properties)
|
||||
end
|
||||
break
|
||||
rescue Puppet::ExecutionFailure => e
|
||||
if e.message =~ /HTTP 401/
|
||||
raise(Puppet::Error::OpenstackUnauthorizedError, 'Could not authenticate.')
|
||||
elsif e.message =~ /Unable to establish connection/
|
||||
current_time = Time.now.to_i
|
||||
if current_time > end_time
|
||||
break
|
||||
else
|
||||
wait = end_time - current_time
|
||||
Puppet::debug("Non-fatal error: \"#{e.message}\"; retrying for #{wait} more seconds.")
|
||||
if wait > timeout - 2 # Only notice the first time
|
||||
notice("#{service} service is unavailable. Will retry for up to #{wait} seconds.")
|
||||
end
|
||||
end
|
||||
sleep(2)
|
||||
else
|
||||
raise e
|
||||
end
|
||||
sleep(2)
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
return rv
|
||||
end
|
||||
return rv
|
||||
end
|
||||
|
||||
def authenticate_request(service, action, *args)
|
||||
self.class.authenticate_request(service, action, *args)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def password_credentials_set?(auth_params)
|
||||
auth_params && auth_params['username'] && auth_params['password'] && auth_params['project_name'] && auth_params['auth_url']
|
||||
end
|
||||
|
||||
def openrc_set?(auth_params)
|
||||
auth_params && auth_params['openrc']
|
||||
end
|
||||
|
||||
def service_credentials_set?(auth_params)
|
||||
auth_params && auth_params['token'] && auth_params['url']
|
||||
end
|
||||
|
||||
def self.env_vars_set?
|
||||
ENV['OS_USERNAME'] && ENV['OS_PASSWORD'] && ENV['OS_PROJECT_NAME'] && ENV['OS_AUTH_URL']
|
||||
end
|
||||
|
||||
def env_vars_set?
|
||||
self.class.env_vars_set?
|
||||
end
|
||||
|
||||
def self.password_auth_args(credentials)
|
||||
creds = [ '--os-username', credentials['username'],
|
||||
'--os-password', credentials['password'],
|
||||
'--os-project-name', credentials['project_name'],
|
||||
'--os-auth-url', credentials['auth_url'] ]
|
||||
|
||||
if credentials.include?('project_domain_name')
|
||||
creds << '--os-project-domain-name'
|
||||
creds << credentials['project_domain_name']
|
||||
end
|
||||
|
||||
if credentials.include?('user_domain_name')
|
||||
creds << '--os-user-domain-name'
|
||||
creds << credentials['user_domain_name']
|
||||
end
|
||||
|
||||
creds
|
||||
end
|
||||
|
||||
def password_auth_args(credentials)
|
||||
self.class.password_auth_args(credentials)
|
||||
end
|
||||
|
||||
def self.token_auth_args(credentials)
|
||||
args = [ '--os-token', credentials['token'],
|
||||
'--os-url', credentials['url'] ]
|
||||
# Add the api version only if requested by the caller
|
||||
if credentials['version']
|
||||
args << [ '--os-identity-api-version', credentials['version'] ]
|
||||
end
|
||||
args
|
||||
end
|
||||
|
||||
def token_auth_args(credentials)
|
||||
self.class.token_auth_args(credentials)
|
||||
end
|
||||
|
||||
def get_credentials_from_openrc(file)
|
||||
creds = {}
|
||||
File.open(file).readlines.delete_if{|l| l=~ /^#/}.each do |line|
|
||||
key, value = line.split('=')
|
||||
key = key.split(' ').last.downcase.sub(/^os_/, '')
|
||||
value = value.chomp.gsub(/'/, '')
|
||||
creds[key] = value
|
||||
end
|
||||
return creds
|
||||
end
|
||||
|
||||
def self.get_credentials_from_env
|
||||
env = ENV.to_hash.dup.delete_if { |key, _| ! (key =~ /^OS_/) }
|
||||
credentials = {}
|
||||
env.each do |name, value|
|
||||
credentials[name.downcase.sub(/^os_/, '')] = value
|
||||
end
|
||||
credentials
|
||||
end
|
||||
|
||||
def get_credentials_from_env
|
||||
self.class.get_credentials_from_env
|
||||
end
|
||||
|
||||
def self.parse_csv(text)
|
||||
# Ignore warnings - assume legitimate output starts with a double quoted
|
||||
# string. Errors will be caught and raised prior to this
|
||||
text = text.split("\n").drop_while { |line| line !~ /^\".*\"/ }.join("\n")
|
||||
return CSV.parse(text + "\n")
|
||||
end
|
||||
|
||||
end
|
||||
|
49
lib/puppet/provider/openstack/auth.rb
Normal file
49
lib/puppet/provider/openstack/auth.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
require 'puppet/provider/openstack/credentials'
|
||||
|
||||
module Puppet::Provider::Openstack::Auth
|
||||
|
||||
RCFILENAME = "#{ENV['HOME']}/openrc"
|
||||
|
||||
def get_os_vars_from_env
|
||||
env = {}
|
||||
ENV.each { |k,v| env.merge!(k => v) if k =~ /^OS_/ }
|
||||
return env
|
||||
end
|
||||
|
||||
def get_os_vars_from_rcfile(filename)
|
||||
env = {}
|
||||
if File.exists?(filename)
|
||||
File.open(filename).readlines.delete_if{|l| l=~ /^#|^$/ }.each do |line|
|
||||
key, value = line.split('=')
|
||||
key = key.split(' ').last
|
||||
value = value.chomp.gsub(/'/, '')
|
||||
env.merge!(key => value) if key =~ /OS_/
|
||||
end
|
||||
end
|
||||
return env
|
||||
end
|
||||
|
||||
def rc_filename
|
||||
RCFILENAME
|
||||
end
|
||||
|
||||
def request(service, action, properties=nil)
|
||||
properties ||= []
|
||||
set_credentials(@credentials, get_os_vars_from_env)
|
||||
unless @credentials.set?
|
||||
@credentials.unset
|
||||
set_credentials(@credentials, get_os_vars_from_rcfile(rc_filename))
|
||||
end
|
||||
unless @credentials.set?
|
||||
raise(Puppet::Error::OpenstackAuthInputError, 'Insufficient credentials to authenticate')
|
||||
end
|
||||
super(service, action, properties, @credentials)
|
||||
end
|
||||
|
||||
def set_credentials(creds, env)
|
||||
env.each do |key, val|
|
||||
var = key.sub(/^OS_/,'').downcase
|
||||
creds.set(var, val)
|
||||
end
|
||||
end
|
||||
end
|
56
lib/puppet/provider/openstack/credentials.rb
Normal file
56
lib/puppet/provider/openstack/credentials.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
require 'puppet'
|
||||
require 'puppet/provider/openstack'
|
||||
|
||||
class Puppet::Provider::Openstack::Credentials
|
||||
|
||||
KEYS = [
|
||||
:auth_url, :password, :project_name, :username,
|
||||
:token, :url,
|
||||
:identity_api_version
|
||||
]
|
||||
|
||||
KEYS.each { |var| attr_accessor var }
|
||||
|
||||
def self.defined?(name)
|
||||
KEYS.include?(name.to_sym)
|
||||
end
|
||||
|
||||
def set(key, val)
|
||||
if self.class.defined?(key.to_sym)
|
||||
self.instance_variable_set("@#{key}".to_sym, val)
|
||||
end
|
||||
end
|
||||
|
||||
def set?
|
||||
return true if user_password_set? || service_token_set?
|
||||
end
|
||||
|
||||
def service_token_set?
|
||||
return true if @token && @url
|
||||
end
|
||||
|
||||
def to_env
|
||||
env = {}
|
||||
self.instance_variables.each do |var|
|
||||
name = var.to_s.sub(/^@/,'OS_').upcase
|
||||
env.merge!(name => self.instance_variable_get(var))
|
||||
end
|
||||
env
|
||||
end
|
||||
|
||||
def user_password_set?
|
||||
return true if @username && @password && @project_name && @auth_url
|
||||
end
|
||||
|
||||
def unset
|
||||
list = KEYS.delete_if { |key, val| key == :identity_api_version }
|
||||
list.each { |key, val| self.set(key, '') if self.class.defined?("@#{key}".to_sym) }
|
||||
end
|
||||
|
||||
def version
|
||||
self.class.to_s.sub(/.*V/,'').sub('_','.')
|
||||
end
|
||||
end
|
||||
|
||||
class Puppet::Provider::Openstack::CredentialsV2_0 < Puppet::Provider::Openstack::Credentials
|
||||
end
|
@@ -1,66 +0,0 @@
|
||||
# Add the auth parameter to whatever type is given
|
||||
module Puppet::Util::Openstack
|
||||
def self.add_openstack_type_methods(type, comment)
|
||||
|
||||
type.newparam(:auth) do
|
||||
|
||||
desc <<EOT
|
||||
Hash of authentication credentials. Credentials can be specified as either :
|
||||
|
||||
1. Using a project/user with a password
|
||||
|
||||
For Keystone API V2:
|
||||
auth => {
|
||||
'username' => 'test',
|
||||
'password' => 'changeme',
|
||||
'project_name' => 'test',
|
||||
'auth_url' => 'http://localhost:35357/v2.0'
|
||||
}
|
||||
|
||||
or altenatively for Keystone API V3:
|
||||
auth => {
|
||||
'username' => 'test',
|
||||
'password' => 'changeme',
|
||||
'project_name' => 'test',
|
||||
'project_domain_name' => 'domain1',
|
||||
'user_domain_name' => 'domain1',
|
||||
'auth_url' => 'http://localhost:35357/v3'
|
||||
}
|
||||
|
||||
2. Using a path to an openrc file containing these credentials
|
||||
|
||||
auth => {
|
||||
'openrc' => '/root/openrc'
|
||||
}
|
||||
|
||||
3. Using a service token
|
||||
|
||||
For Keystone API V2:
|
||||
auth => {
|
||||
'token' => 'example',
|
||||
'url' => 'http://localhost:35357/v2.0'
|
||||
}
|
||||
|
||||
Alternatively for Keystone API V3:
|
||||
auth => {
|
||||
'token' => 'example',
|
||||
'url' => 'http://localhost:35357/v3.0'
|
||||
}
|
||||
|
||||
If not present, the provider will look for environment variables for
|
||||
password credentials.
|
||||
|
||||
#{comment}
|
||||
EOT
|
||||
|
||||
validate do |value|
|
||||
raise(Puppet::Error, 'This property must be a hash') unless value.is_a?(Hash)
|
||||
end
|
||||
end
|
||||
|
||||
type.autorequire(:package) do
|
||||
'python-openstackclient'
|
||||
end
|
||||
|
||||
end
|
||||
end
|
181
spec/unit/provider/openstack/auth_spec.rb
Normal file
181
spec/unit/provider/openstack/auth_spec.rb
Normal file
@@ -0,0 +1,181 @@
|
||||
require 'puppet'
|
||||
require 'spec_helper'
|
||||
require 'puppet/provider/openstack'
|
||||
require 'puppet/provider/openstack/auth'
|
||||
require 'tempfile'
|
||||
|
||||
class Puppet::Provider::Openstack::AuthTester < Puppet::Provider::Openstack
|
||||
extend Puppet::Provider::Openstack::Auth
|
||||
end
|
||||
|
||||
klass = Puppet::Provider::Openstack::AuthTester
|
||||
|
||||
describe Puppet::Provider::Openstack::Auth do
|
||||
|
||||
let(:type) do
|
||||
Puppet::Type.newtype(:test_resource) do
|
||||
newparam(:name, :namevar => true)
|
||||
newparam(:log_file)
|
||||
end
|
||||
end
|
||||
|
||||
let(:resource_attrs) do
|
||||
{
|
||||
:name => 'stubresource'
|
||||
}
|
||||
end
|
||||
|
||||
let(:provider) do
|
||||
klass.new(type.new(resource_attrs))
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
ENV['OS_USERNAME'] = nil
|
||||
ENV['OS_PASSWORD'] = nil
|
||||
ENV['OS_PROJECT_NAME'] = nil
|
||||
ENV['OS_AUTH_URL'] = nil
|
||||
ENV['OS_TOKEN'] = nil
|
||||
ENV['OS_URL'] = nil
|
||||
end
|
||||
|
||||
describe '#set_credentials' do
|
||||
it 'adds keys to the object' do
|
||||
credentials = Puppet::Provider::Openstack::CredentialsV2_0.new
|
||||
set = { 'OS_USERNAME' => 'user', 'OS_PASSWORD' => 'secret',
|
||||
'OS_PROJECT_NAME' => 'tenant',
|
||||
'OS_AUTH_URL' => 'http://127.0.0.1:5000',
|
||||
'OS_TOKEN' => 'token',
|
||||
'OS_URL' => 'http://127.0.0.1:35357',
|
||||
'OS_IDENTITY_API_VERSION' => '2.0'
|
||||
}
|
||||
klass.set_credentials(credentials, set)
|
||||
expect(credentials.to_env).to eq("OS_AUTH_URL" => "http://127.0.0.1:5000",
|
||||
"OS_IDENTITY_API_VERSION" => '2.0',
|
||||
"OS_PASSWORD" => "secret",
|
||||
"OS_PROJECT_NAME" => "tenant",
|
||||
"OS_TOKEN" => "token",
|
||||
"OS_URL" => "http://127.0.0.1:35357",
|
||||
"OS_USERNAME" => "user")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#rc_filename' do
|
||||
it 'returns RCFILENAME' do
|
||||
expect(klass.rc_filename).to eq("#{ENV['HOME']}/openrc")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_os_from_env' do
|
||||
context 'with Openstack environment variables set' do
|
||||
it 'provides a hash' do
|
||||
ENV['OS_AUTH_URL'] = 'http://127.0.0.1:5000'
|
||||
ENV['OS_PASSWORD'] = 'abc123'
|
||||
ENV['OS_PROJECT_NAME'] = 'test'
|
||||
ENV['OS_USERNAME'] = 'test'
|
||||
response = klass.get_os_vars_from_env
|
||||
expect(response).to eq({"OS_AUTH_URL" => "http://127.0.0.1:5000","OS_PASSWORD" => "abc123","OS_PROJECT_NAME" => "test","OS_USERNAME" => "test"})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_os_vars_from_rcfile' do
|
||||
context 'with a valid RC file' do
|
||||
it 'provides a hash' do
|
||||
mock = "export OS_USERNAME='test'\nexport OS_PASSWORD='abc123'\nexport OS_PROJECT_NAME='test'\nexport OS_AUTH_URL='http://127.0.0.1:5000'"
|
||||
filename = 'file'
|
||||
File.expects(:exists?).with('file').returns(true)
|
||||
File.expects(:open).with('file').returns(StringIO.new(mock))
|
||||
|
||||
response = klass.get_os_vars_from_rcfile(filename)
|
||||
expect(response).to eq({"OS_AUTH_URL" => "http://127.0.0.1:5000","OS_PASSWORD" => "abc123","OS_PROJECT_NAME" => "test","OS_USERNAME" => "test"})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an empty file' do
|
||||
it 'provides an empty hash' do
|
||||
filename = 'file'
|
||||
File.expects(:exists?).with(filename).returns(true)
|
||||
File.expects(:open).with(filename).returns(StringIO.new(""))
|
||||
|
||||
response = klass.get_os_vars_from_rcfile(filename)
|
||||
expect(response).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
class Puppet::Provider::Openstack::AuthTester
|
||||
@credentials = Puppet::Provider::Openstack::CredentialsV2_0.new
|
||||
end
|
||||
end
|
||||
|
||||
describe '#request' do
|
||||
context 'with no valid credentials' do
|
||||
it 'fails to authenticate' do
|
||||
expect { klass.request('project', 'list', ['--long']) }.to raise_error(Puppet::Error::OpenstackAuthInputError, "Insufficient credentials to authenticate")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with user credentials in env' do
|
||||
it 'is successful' do
|
||||
klass.expects(:get_os_vars_from_env)
|
||||
.returns({ 'OS_USERNAME' => 'test',
|
||||
'OS_PASSWORD' => 'abc123',
|
||||
'OS_PROJECT_NAME' => 'test',
|
||||
'OS_AUTH_URL' => 'http://127.0.0.1:5000' })
|
||||
klass.expects(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', ['--long'])
|
||||
.returns('"ID","Name","Description","Enabled"
|
||||
"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True
|
||||
')
|
||||
response = klass.request('project', 'list', ['--long'])
|
||||
expect(response.first[:description]).to eq("Test tenant")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with service token credentials in env' do
|
||||
it 'is successful' do
|
||||
klass.expects(:get_os_vars_from_env)
|
||||
.returns({ 'OS_TOKEN' => 'test',
|
||||
'OS_URL' => 'http://127.0.0.1:5000' })
|
||||
klass.expects(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', ['--long'])
|
||||
.returns('"ID","Name","Description","Enabled"
|
||||
"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True
|
||||
')
|
||||
response = klass.request('project', 'list', ['--long'])
|
||||
expect(response.first[:description]).to eq("Test tenant")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a RC file containing user credentials' do
|
||||
it 'is successful' do
|
||||
mock = "export OS_USERNAME='test'\nexport OS_PASSWORD='abc123'\nexport OS_PROJECT_NAME='test'\nexport OS_AUTH_URL='http://127.0.0.1:5000'"
|
||||
File.expects(:exists?).with("#{ENV['HOME']}/openrc").returns(true)
|
||||
File.expects(:open).with("#{ENV['HOME']}/openrc").returns(StringIO.new(mock))
|
||||
klass.expects(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', ['--long'])
|
||||
.returns('"ID","Name","Description","Enabled"
|
||||
"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True
|
||||
')
|
||||
response = provider.class.request('project', 'list', ['--long'])
|
||||
expect(response.first[:description]).to eq("Test tenant")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a RC file containing service token credentials' do
|
||||
it 'is successful' do
|
||||
mock = "export OS_TOKEN='test'\nexport OS_URL='abc123'\n"
|
||||
File.expects(:exists?).with("#{ENV['HOME']}/openrc").returns(true)
|
||||
File.expects(:open).with("#{ENV['HOME']}/openrc").returns(StringIO.new(mock))
|
||||
klass.expects(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', ['--long'])
|
||||
.returns('"ID","Name","Description","Enabled"
|
||||
"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True
|
||||
')
|
||||
response = klass.request('project', 'list', ['--long'])
|
||||
expect(response.first[:description]).to eq("Test tenant")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
63
spec/unit/provider/openstack/credentials_spec.rb
Normal file
63
spec/unit/provider/openstack/credentials_spec.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
require 'puppet'
|
||||
require 'spec_helper'
|
||||
require 'puppet/provider/openstack'
|
||||
require 'puppet/provider/openstack/credentials'
|
||||
|
||||
|
||||
describe Puppet::Provider::Openstack::Credentials do
|
||||
|
||||
let(:creds) do
|
||||
creds = Puppet::Provider::Openstack::CredentialsV2_0.new
|
||||
end
|
||||
|
||||
describe '#service_token_set?' do
|
||||
context "with service credentials" do
|
||||
it 'is successful' do
|
||||
creds.token = 'token'
|
||||
creds.url = 'url'
|
||||
expect(creds.service_token_set?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#password_set?' do
|
||||
context "with user credentials" do
|
||||
it 'is successful' do
|
||||
creds.auth_url = 'auth_url'
|
||||
creds.password = 'password'
|
||||
creds.project_name = 'project_name'
|
||||
creds.username = 'username'
|
||||
expect(creds.user_password_set?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#set?' do
|
||||
context "without any credential" do
|
||||
it 'fails' do
|
||||
expect(creds.set?).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_env' do
|
||||
context "with an exhaustive data set" do
|
||||
it 'successfully returns content' do
|
||||
creds.auth_url = 'auth_url'
|
||||
creds.password = 'password'
|
||||
creds.project_name = 'project_name'
|
||||
creds.username = 'username'
|
||||
creds.token = 'token'
|
||||
creds.url = 'url'
|
||||
creds.identity_api_version = 'identity_api_version'
|
||||
expect(creds.auth_url).to eq("auth_url")
|
||||
expect(creds.password).to eq("password")
|
||||
expect(creds.project_name).to eq("project_name")
|
||||
expect(creds.username).to eq("username")
|
||||
expect(creds.token).to eq('token')
|
||||
expect(creds.url).to eq('url')
|
||||
expect(creds.identity_api_version).to eq('identity_api_version')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@@ -1,12 +1,8 @@
|
||||
# Load libraries from aviator here to simulate how they live together in a real puppet run
|
||||
$LOAD_PATH.push(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'modules', 'aviator', 'lib'))
|
||||
require 'puppet'
|
||||
require 'spec_helper'
|
||||
require 'puppet/provider/openstack'
|
||||
|
||||
|
||||
describe Puppet::Provider::Openstack do
|
||||
|
||||
before(:each) do
|
||||
ENV['OS_USERNAME'] = nil
|
||||
ENV['OS_PASSWORD'] = nil
|
||||
@@ -17,239 +13,48 @@ describe Puppet::Provider::Openstack do
|
||||
let(:type) do
|
||||
Puppet::Type.newtype(:test_resource) do
|
||||
newparam(:name, :namevar => true)
|
||||
newparam(:auth)
|
||||
newparam(:log_file)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'authenticating with environment variables using API v2' do
|
||||
it 'makes a successful request' do
|
||||
ENV['OS_USERNAME'] = 'test'
|
||||
ENV['OS_PASSWORD'] = 'abc123'
|
||||
ENV['OS_PROJECT_NAME'] = 'test'
|
||||
ENV['OS_AUTH_URL'] = 'http://127.0.0.1:35357/v2.0'
|
||||
if provider.class == Class
|
||||
provider.stubs(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', [[ '--long' ]])
|
||||
.returns('"ID","Name","Description","Enabled"
|
||||
"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True
|
||||
')
|
||||
else
|
||||
provider.class.stubs(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', [[ '--long' ]])
|
||||
.returns('"ID","Name","Description","Enabled"
|
||||
"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True
|
||||
')
|
||||
end
|
||||
response = provider.request('project', 'list', nil, nil, '--long' )
|
||||
expect(response.first[:description]).to match /Test tenant/
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'authenticating with environment variables using API v3' do
|
||||
it 'makes a successful request' do
|
||||
ENV['OS_USERNAME'] = 'test'
|
||||
ENV['OS_PASSWORD'] = 'abc123'
|
||||
ENV['OS_PROJECT_NAME'] = 'test'
|
||||
ENV['OS_AUTH_URL'] = 'http://127.0.0.1:35357/v3'
|
||||
if provider.class == Class
|
||||
provider.stubs(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', [[ '--long' ]])
|
||||
.returns('"ID","Name","Description","Enabled"
|
||||
"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True
|
||||
')
|
||||
else
|
||||
provider.class.stubs(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', [[ '--long' ]])
|
||||
.returns('"ID","Name","Description","Enabled"
|
||||
"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True
|
||||
')
|
||||
end
|
||||
response = provider.request('project', 'list', nil, nil, '--long' )
|
||||
expect(response.first[:description]).to match /Test tenant/
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'it has no credentials' do
|
||||
it 'fails to authenticate' do
|
||||
expect{ provider.request('project', 'list', nil, nil, '--long') }.to raise_error(Puppet::Error::OpenstackAuthInputError, /No credentials provided/)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#request' do
|
||||
let(:resource_attrs) do
|
||||
{
|
||||
:name => 'stubresource',
|
||||
}
|
||||
end
|
||||
|
||||
context 'with valid password credentials in parameters' do
|
||||
let(:resource_attrs) do
|
||||
{
|
||||
:name => 'stubresource',
|
||||
:auth => {
|
||||
'username' => 'test',
|
||||
'password' => 'abc123',
|
||||
'project_name' => 'test',
|
||||
'auth_url' => 'http://127.0.0.1:5000/v2.0',
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:provider) do
|
||||
Puppet::Provider::Openstack.new(type.new(resource_attrs))
|
||||
end
|
||||
let(:provider) do
|
||||
Puppet::Provider::Openstack.new(type.new(resource_attrs))
|
||||
end
|
||||
|
||||
it 'makes a successful request' do
|
||||
provider.class.stubs(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', [['--long', '--os-username', 'test', '--os-password', 'abc123', '--os-project-name', 'test', '--os-auth-url', 'http://127.0.0.1:5000/v2.0']])
|
||||
.returns('"ID","Name","Description","Enabled"
|
||||
it 'makes a successful request' do
|
||||
provider.class.stubs(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', ['--long'])
|
||||
.returns('"ID","Name","Description","Enabled"
|
||||
"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True
|
||||
')
|
||||
response = provider.request('project', 'list', nil, resource_attrs[:auth], '--long')
|
||||
expect(response.first[:description]).to match /Test tenant/
|
||||
end
|
||||
response = Puppet::Provider::Openstack.request('project', 'list', ['--long'])
|
||||
expect(response.first[:description]).to eq("Test tenant")
|
||||
end
|
||||
|
||||
context 'with valid openrc file in parameters' do
|
||||
mock = "export OS_USERNAME='test'\nexport OS_PASSWORD='abc123'\nexport OS_PROJECT_NAME='test'\nexport OS_AUTH_URL='http://127.0.0.1:5000/v2.0'"
|
||||
let(:resource_attrs) do
|
||||
{
|
||||
:name => 'stubresource',
|
||||
:auth => {
|
||||
'openrc' => '/root/openrc'
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:provider) do
|
||||
Puppet::Provider::Openstack.new(type.new(resource_attrs))
|
||||
end
|
||||
|
||||
it 'makes a successful request' do
|
||||
File.expects(:open).with('/root/openrc').returns(StringIO.new(mock))
|
||||
provider.class.stubs(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', [['--long', '--os-username', 'test', '--os-password', 'abc123', '--os-project-name', 'test', '--os-auth-url', 'http://127.0.0.1:5000/v2.0']])
|
||||
.returns('"ID","Name","Description","Enabled"
|
||||
"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True
|
||||
')
|
||||
response = provider.request('project', 'list', nil, resource_attrs[:auth], '--long')
|
||||
expect(response.first[:description]).to match /Test tenant/
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid service token in parameters' do
|
||||
let(:resource_attrs) do
|
||||
{
|
||||
:name => 'stubresource',
|
||||
:auth => {
|
||||
'token' => 'secrettoken',
|
||||
'url' => 'http://127.0.0.1:5000/v3',
|
||||
'version' => '3'
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:provider) do
|
||||
Puppet::Provider::Openstack.new(type.new(resource_attrs))
|
||||
end
|
||||
|
||||
it 'makes a successful request' do
|
||||
provider.class.stubs(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', [['--long', '--os-token', 'secrettoken', '--os-url', 'http://127.0.0.1:5000/v3', '--os-identity-api-version', '3']])
|
||||
.returns('"ID","Name","Description","Enabled"
|
||||
"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True
|
||||
')
|
||||
response = provider.request('project', 'list', nil, resource_attrs[:auth], '--long')
|
||||
expect(response.first[:description]).to match /Test tenant/
|
||||
end
|
||||
|
||||
it 'makes a successful show request' do
|
||||
provider.class.stubs(:openstack)
|
||||
.with('project', 'show', '--format', 'shell', [['test', '--os-token', 'secrettoken', '--os-url', 'http://127.0.0.1:5000/v3', '--os-identity-api-version', '3']])
|
||||
.returns('ID="1cb05cfed7c24279be884ba4f6520262"
|
||||
Name="test"
|
||||
Description="Test Tenant"
|
||||
Enabled="True"
|
||||
')
|
||||
response = provider.request('project', 'show', 'test', resource_attrs[:auth])
|
||||
expect(response[:description]).to match /Test Tenant/
|
||||
expect(response[:id]).to match /1cb05cfed7c24279be884ba4f6520262/
|
||||
expect(response[:name]).to match /test/
|
||||
expect(response[:enabled]).to match /True/
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'with valid password credentials in environment variables' do
|
||||
it_behaves_like 'authenticating with environment variables using API v2' do
|
||||
let(:resource_attrs) do
|
||||
{
|
||||
:name => 'stubresource',
|
||||
}
|
||||
end
|
||||
let(:provider) do
|
||||
Puppet::Provider::Openstack.new(type.new(resource_attrs))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no valid credentials' do
|
||||
it_behaves_like 'it has no credentials' do
|
||||
let(:resource_attrs) do
|
||||
{
|
||||
:name => 'stubresource',
|
||||
}
|
||||
end
|
||||
let(:provider) do
|
||||
Puppet::Provider::Openstack.new(type.new(resource_attrs))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'it retries on connection errors' do
|
||||
let(:resource_attrs) do
|
||||
{
|
||||
:name => 'stubresource',
|
||||
:auth => {
|
||||
'username' => 'test',
|
||||
'password' => 'abc123',
|
||||
'project_name' => 'test',
|
||||
'auth_url' => 'http://127.0.0.1:5000/v2.0',
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:provider) do
|
||||
Puppet::Provider::Openstack.new(type.new(resource_attrs))
|
||||
end
|
||||
context 'on connection errors' do
|
||||
it 'retries' do
|
||||
ENV['OS_USERNAME'] = 'test'
|
||||
ENV['OS_PASSWORD'] = 'abc123'
|
||||
ENV['OS_PROJECT_NAME'] = 'test'
|
||||
ENV['OS_AUTH_URL'] = 'http://127.0.0.1:5000'
|
||||
provider.class.stubs(:openstack)
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', [['--long', '--os-username', 'test', '--os-password', 'abc123', '--os-project-name', 'test', '--os-auth-url', 'http://127.0.0.1:5000/v2.0']])
|
||||
.with('project', 'list', '--quiet', '--format', 'csv', ['--long'])
|
||||
.raises(Puppet::ExecutionFailure, 'Unable to establish connection')
|
||||
.then
|
||||
.returns('')
|
||||
provider.class.expects(:sleep).with(2).returns(nil)
|
||||
provider.request('project', 'list', nil, resource_attrs[:auth], '--long')
|
||||
Puppet::Provider::Openstack.request('project', 'list', ['--long'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe '::request' do
|
||||
|
||||
context 'with valid password credentials in environment variables' do
|
||||
it_behaves_like 'authenticating with environment variables using API v2' do
|
||||
let(:resource_attrs) do
|
||||
{
|
||||
:name => 'stubresource',
|
||||
}
|
||||
end
|
||||
let(:provider) do
|
||||
Puppet::Provider::Openstack.dup
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no valid credentials' do
|
||||
it_behaves_like 'it has no credentials' do
|
||||
let(:provider) { Puppet::Provider::Openstack.dup }
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'parse_csv' do
|
||||
context 'with mixed stderr' do
|
||||
text = "ERROR: Testing\n\"field\",\"test\",1,2,3\n"
|
||||
@@ -281,5 +86,4 @@ Enabled="True"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
Reference in New Issue
Block a user