Use clouds.yaml for puppet

This change introduces the capability to load clouds.yaml file in
the base Puppet::Provider::Openstack::Auth module, so that each
providers can look up credentials from clouds.yaml instead of rc file.
When SRBAC is enforced, services require appropriate scope for each
operation and this makes it difficult to use rc files which can store
only one credential per file. Usage of clouds.yaml allows us to store
multiple credentials in a single file and switch scopes according to
the API request used.

The new implementation loads the clouds.yaml file for admin user, which
is created by puppet-keystoe. It also allows overriding the credential
by a user-created clouds.file.

We expect clouds.yaml file is created under /etc/openstack, which is
the location openstackclient searches to look up clouds.yaml. To avoid
unexpected conjunction with existing files, the files used by puppet
are located in an independent 'puppet' directory at this moment.

Change-Id: I7587f6e0c2486cbfaf2cbafeb64e9db56a817106
This commit is contained in:
Takashi Kajinami
2022-02-06 16:52:42 +09:00
parent 47fe665d21
commit 08bf393ee4
4 changed files with 116 additions and 14 deletions

View File

@@ -5,12 +5,31 @@ module Puppet::Provider::Openstack::Auth
RCFILENAME = "#{ENV['HOME']}/openrc" RCFILENAME = "#{ENV['HOME']}/openrc"
CLOUDSFILENAMES = [
# This allows overrides by users
"/etc/openstack/puppet/clouds.yaml",
# This is created by puppet-keystone
"/etc/openstack/puppet/admin-clouds.yaml",
]
def get_os_vars_from_env def get_os_vars_from_env
env = {} env = {}
ENV.each { |k,v| env.merge!(k => v) if k =~ /^OS_/ } ENV.each { |k,v| env.merge!(k => v) if k =~ /^OS_/ }
return env return env
end end
def get_os_vars_from_cloudsfile(scope)
cloudsfile = clouds_filenames.detect { |f| File.exists? f}
unless cloudsfile.nil?
{
'OS_CLOUD' => scope,
'OS_CLIENT_CONFIG_FILE' => cloudsfile
}
else
{}
end
end
def get_os_vars_from_rcfile(filename) def get_os_vars_from_rcfile(filename)
env = {} env = {}
rcfile = [filename, '/root/openrc'].detect { |f| File.exists? f } rcfile = [filename, '/root/openrc'].detect { |f| File.exists? f }
@@ -32,13 +51,30 @@ module Puppet::Provider::Openstack::Auth
RCFILENAME RCFILENAME
end end
def clouds_filenames
CLOUDSFILENAMES
end
def request(service, action, properties=nil, options={}, scope='project') def request(service, action, properties=nil, options={}, scope='project')
properties ||= [] properties ||= []
# First, check environments
set_credentials(@credentials, get_os_vars_from_env) set_credentials(@credentials, get_os_vars_from_env)
unless @credentials.set? and (!@credentials.scope_set? or @credentials.scope == scope) unless @credentials.set? and (!@credentials.scope_set? or @credentials.scope == scope)
# Then look for clouds.yaml
@credentials.unset
clouds_env = get_os_vars_from_cloudsfile(scope)
if ! clouds_env.empty?
set_credentials(@credentials, clouds_env)
else
# If it fails then check rc files, to keep backword compatibility.
warning('Usage of rc file is deprecated and will be removed in a future release.')
@credentials.unset @credentials.unset
set_credentials(@credentials, get_os_vars_from_rcfile(rc_filename)) set_credentials(@credentials, get_os_vars_from_rcfile(rc_filename))
end end
end
unless @credentials.set? and (!@credentials.scope_set? or @credentials.scope == scope) unless @credentials.set? and (!@credentials.scope_set? or @credentials.scope == scope)
raise(Puppet::Error::OpenstackAuthInputError, 'Insufficient credentials to authenticate') raise(Puppet::Error::OpenstackAuthInputError, 'Insufficient credentials to authenticate')
end end

View File

@@ -35,7 +35,10 @@ class Puppet::Provider::Openstack::Credentials
env = {} env = {}
self.instance_variables.each do |var| self.instance_variables.each do |var|
name = var.to_s.sub(/^@/,'OS_').upcase name = var.to_s.sub(/^@/,'OS_').upcase
env.merge!(name => self.instance_variable_get(var)) value = self.instance_variable_get(var)
unless value.nil?
env.merge!(name => value)
end
end end
env env
end end
@@ -62,7 +65,7 @@ class Puppet::Provider::Openstack::Credentials
self.instance_variables.each do |var| self.instance_variables.each do |var|
if var.to_s != '@identity_api_version' && if var.to_s != '@identity_api_version' &&
self.instance_variable_defined?(var.to_s) self.instance_variable_defined?(var.to_s)
set(var.to_s.sub(/^@/,''), '') set(var.to_s.sub(/^@/,''), nil)
end end
end end
end end

View File

@@ -85,6 +85,43 @@ describe Puppet::Provider::Openstack::Auth do
end end
end end
describe '#get_os_vars_from_cloudsfile' do
context 'with a clouds.yaml present' do
it 'provides a hash' do
File.expects(:exists?).with('/etc/openstack/puppet/clouds.yaml').returns(true)
response = klass.get_os_vars_from_cloudsfile('project')
expect(response).to eq({
'OS_CLOUD' => 'project',
'OS_CLIENT_CONFIG_FILE' => '/etc/openstack/puppet/clouds.yaml'
})
end
end
context 'with a admin-clouds.yaml present' do
it 'provides a hash' do
File.expects(:exists?).with('/etc/openstack/puppet/clouds.yaml').returns(false)
File.expects(:exists?).with('/etc/openstack/puppet/admin-clouds.yaml').returns(true)
response = klass.get_os_vars_from_cloudsfile('project')
expect(response).to eq({
'OS_CLOUD' => 'project',
'OS_CLIENT_CONFIG_FILE' => '/etc/openstack/puppet/admin-clouds.yaml'
})
end
end
context 'with a clouds.yaml not present' do
it 'provides an empty hash' do
File.expects(:exists?).with('/etc/openstack/puppet/clouds.yaml').returns(false)
File.expects(:exists?).with('/etc/openstack/puppet/admin-clouds.yaml').returns(false)
response = klass.get_os_vars_from_cloudsfile('project')
expect(response).to eq({})
end
end
end
describe '#get_os_vars_from_rcfile' do describe '#get_os_vars_from_rcfile' do
context 'with a valid RC file' do context 'with a valid RC file' do
it 'provides a hash' do it 'provides a hash' do
@@ -209,6 +246,28 @@ describe Puppet::Provider::Openstack::Auth do
end end
end end
context 'with clouds.yaml file' do
it 'is successful' do
# return incomplete creds from env
klass.expects(:get_os_vars_from_env)
.returns({ 'OS_USERNAME' => 'incompleteusername',
'OS_AUTH_URL' => 'incompleteauthurl' })
File.expects(:exists?).with('/etc/openstack/puppet/clouds.yaml').returns(true)
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")
expect(klass.instance_variable_get(:@credentials).to_env).to eq({
'OS_IDENTITY_API_VERSION' => '3',
'OS_CLOUD' => 'project',
'OS_CLIENT_CONFIG_FILE' => '/etc/openstack/puppet/clouds.yaml',
})
end
end
context 'with a RC file containing user credentials' do context 'with a RC file containing user credentials' do
it 'is successful' do it 'is successful' do
# return incomplete creds from env # return incomplete creds from env
@@ -216,6 +275,8 @@ describe Puppet::Provider::Openstack::Auth do
.returns({ 'OS_USERNAME' => 'incompleteusername', .returns({ 'OS_USERNAME' => 'incompleteusername',
'OS_AUTH_URL' => 'incompleteauthurl' }) 'OS_AUTH_URL' => 'incompleteauthurl' })
mock = "export OS_USERNAME='test'\nexport OS_PASSWORD='abc123'\nexport OS_PROJECT_NAME='test'\nexport OS_AUTH_URL='http://127.0.0.1:5000'\nexport OS_NOT_VALID='notvalid'" mock = "export OS_USERNAME='test'\nexport OS_PASSWORD='abc123'\nexport OS_PROJECT_NAME='test'\nexport OS_AUTH_URL='http://127.0.0.1:5000'\nexport OS_NOT_VALID='notvalid'"
File.expects(:exists?).with("/etc/openstack/puppet/clouds.yaml").returns(false)
File.expects(:exists?).with("/etc/openstack/puppet/admin-clouds.yaml").returns(false)
File.expects(:exists?).with("#{ENV['HOME']}/openrc").returns(true) File.expects(:exists?).with("#{ENV['HOME']}/openrc").returns(true)
File.expects(:open).with("#{ENV['HOME']}/openrc").returns(StringIO.new(mock)) File.expects(:open).with("#{ENV['HOME']}/openrc").returns(StringIO.new(mock))
klass.expects(:openstack) klass.expects(:openstack)
@@ -241,6 +302,8 @@ describe Puppet::Provider::Openstack::Auth do
klass.expects(:get_os_vars_from_env) klass.expects(:get_os_vars_from_env)
.returns({ 'OS_TOKEN' => 'incomplete' }) .returns({ 'OS_TOKEN' => 'incomplete' })
mock = "export OS_TOKEN='test'\nexport OS_ENDPOINT='abc123'\nexport OS_NOT_VALID='notvalid'\n" mock = "export OS_TOKEN='test'\nexport OS_ENDPOINT='abc123'\nexport OS_NOT_VALID='notvalid'\n"
File.expects(:exists?).with("/etc/openstack/puppet/clouds.yaml").returns(false)
File.expects(:exists?).with("/etc/openstack/puppet/admin-clouds.yaml").returns(false)
File.expects(:exists?).with("#{ENV['HOME']}/openrc").returns(true) File.expects(:exists?).with("#{ENV['HOME']}/openrc").returns(true)
File.expects(:open).with("#{ENV['HOME']}/openrc").returns(StringIO.new(mock)) File.expects(:open).with("#{ENV['HOME']}/openrc").returns(StringIO.new(mock))
klass.expects(:openstack) klass.expects(:openstack)

View File

@@ -120,18 +120,18 @@ describe Puppet::Provider::Openstack::Credentials do
creds.cloud = 'openstack' creds.cloud = 'openstack'
creds.client_config_file = '/etc/openstack/clouds.yaml' creds.client_config_file = '/etc/openstack/clouds.yaml'
creds.unset creds.unset
expect(creds.auth_url).to eq('') expect(creds.auth_url).to eq(nil)
expect(creds.password).to eq('') expect(creds.password).to eq(nil)
expect(creds.project_name).to eq('') expect(creds.project_name).to eq(nil)
expect(creds.domain_name).to eq('') expect(creds.domain_name).to eq(nil)
expect(creds.system_scope).to eq('') expect(creds.system_scope).to eq(nil)
expect(creds.username).to eq('') expect(creds.username).to eq(nil)
expect(creds.token).to eq('') expect(creds.token).to eq(nil)
expect(creds.endpoint).to eq('') expect(creds.endpoint).to eq(nil)
expect(creds.region_name).to eq('') expect(creds.region_name).to eq(nil)
expect(creds.identity_api_version).to eq('identity_api_version') expect(creds.identity_api_version).to eq('identity_api_version')
expect(creds.cloud).to eq('') expect(creds.cloud).to eq(nil)
expect(creds.client_config_file).to eq('') expect(creds.client_config_file).to eq(nil)
newcreds = Puppet::Provider::Openstack::CredentialsV3.new newcreds = Puppet::Provider::Openstack::CredentialsV3.new
expect(newcreds.identity_api_version).to eq('3') expect(newcreds.identity_api_version).to eq('3')
end end