support for keystone v3 api - v3 domain utility code

This patch implements these parts of the blueprint:

1) Adds some utility code for domain handling.  Puppet code may need
to specify resource titles as "name::domain" e.g. "admin::admin_domain".
The function split_domain is used to split a name in this format into
its ['name', 'domain'] components as an array.
Usually providers will not need to use this, they will use the
name_and_domain method in the keystone.rb provider.  This is resource
aware and will try many different ways to get a domain to use for
the provider:  resource[:domain], then the domain part of
'name::domain' name/title value.
If keystone.conf is available, it will use [identity]
default_domain_id as the default domain id, and look up the domain
name using the 'domain list' operation to create a mapping.
If all else fails, the domain name will be 'Default' which is the
"default" default domain name used by Keystone.

2) Adds the method domain_name_from_id - the providers and other code
will need to map from the domain id to the name, and this method
provides that mapping.

Implements: blueprint api-v3-support

Change-Id: Ifb8171b78904257f8112e1485d5255f0d0f5aca8
Depends-On: Icafc4cb8ed000fd9d3ed6ffde2afe1a1250d90af
This commit is contained in:
Rich Megginson 2015-04-16 09:47:09 -06:00
parent dbf0530c8c
commit a054c503f3
4 changed files with 168 additions and 11 deletions

View File

@ -2,6 +2,7 @@ require 'puppet/util/inifile'
require 'puppet/provider/openstack'
require 'puppet/provider/openstack/auth'
require 'puppet/provider/openstack/credentials'
require 'puppet/provider/keystone/util'
class Puppet::Provider::Keystone < Puppet::Provider::Openstack
@ -30,6 +31,31 @@ class Puppet::Provider::Keystone < Puppet::Provider::Openstack
@admin_endpoint ||= get_admin_endpoint
end
# use the domain in this order:
# 1 - the domain name specified in the resource definition - resource[:domain]
# 2 - the domain name part of the resource name/title e.g. user_name::user_domain
# if passed in by name_and_domain above
# 3 - use the specified default_domain_name
# 4 - lookup the default domain
# 5 - use 'Default' - the "default" default domain if no other one is configured
# Usage: name_and_domain(resource[:name], resource[:domain], default_domain_name)
def self.name_and_domain(namedomstr, domain_from_resource=nil, default_domain_name=nil)
name, domain = Util.split_domain(namedomstr)
ret = [name]
if domain_from_resource
ret << domain_from_resource
elsif domain
ret << domain
elsif default_domain_name
ret << default_domain_name
elsif default_domain
ret << default_domain
else
ret << 'Default'
end
ret
end
def self.admin_token
@admin_token ||= get_admin_token
end
@ -80,8 +106,8 @@ class Puppet::Provider::Keystone < Puppet::Provider::Openstack
def self.request(service, action, properties=nil)
super
rescue Puppet::Error::OpenstackAuthInputError => error
request_by_service_token(service, action, error, properties)
rescue Puppet::Error::OpenstackAuthInputError => error
request_by_service_token(service, action, error, properties)
end
def self.request_by_service_token(service, action, error, properties=nil)
@ -96,6 +122,31 @@ class Puppet::Provider::Keystone < Puppet::Provider::Openstack
INI_FILENAME
end
def self.default_domain
domain_hash[default_domain_id]
end
def self.domain_hash
return @domain_hash if @domain_hash
list = request('domain', 'list')
@domain_hash = Hash[list.collect{|domain| [domain[:id], domain[:name]]}]
@domain_hash
end
def self.domain_name_from_id(id)
domain_hash[id]
end
def self.default_domain_id
return @default_domain_id if @default_domain_id
if keystone_file and keystone_file['identity'] and keystone_file['identity']['default_domain_id']
@default_domain_id = "#{keystone_file['identity']['default_domain_id'].strip}"
else
@default_domain_id = 'default'
end
@default_domain_id
end
def self.keystone_file
return @keystone_file if @keystone_file
if File.exists?(ini_filename)

View File

@ -0,0 +1,25 @@
module Util
# Splits the rightmost part of a string using '::' as delimiter
# Returns an array of both parts or nil if either is empty.
# An empty rightmost part is ignored and converted as 'string::' => 'string'
#
# Examples:
# "foo" -> ["foo", nil]
# "foo::" -> ["foo", nil]
# "foo::bar" -> ["foo", "bar"]
# "foo::bar::" -> ["foo", "bar"]
# "::foo" -> [nil, "foo"]
# "::foo::" -> [nil, "foo"]
# "foo::bar::baz" -> ["foo::bar", "baz"]
# "foo::bar::baz::" -> ["foo::bar", "baz"]
#
def self.split_domain(str)
left, right = nil, nil
unless str.nil?
left, delimiter, right = str.gsub(/::$/, '').rpartition('::')
left, right = right, nil if delimiter.empty?
left = nil if left.empty?
end
return [left, right]
end
end

View File

@ -0,0 +1,29 @@
require 'puppet'
require 'spec_helper'
require 'puppet/provider/keystone'
require 'puppet/provider/keystone/util'
describe "split_domain method" do
it 'should handle nil and empty strings' do
expect(Util.split_domain('')).to eq([nil, nil])
expect(Util.split_domain(nil)).to eq([nil, nil])
end
it 'should return name and no domain' do
expect(Util.split_domain('foo')).to eq(['foo', nil])
expect(Util.split_domain('foo::')).to eq(['foo', nil])
end
it 'should return name and domain' do
expect(Util.split_domain('foo::bar')).to eq(['foo', 'bar'])
expect(Util.split_domain('foo::bar::')).to eq(['foo', 'bar'])
expect(Util.split_domain('::foo::bar')).to eq(['::foo', 'bar'])
expect(Util.split_domain('::foo::bar::')).to eq(['::foo', 'bar'])
expect(Util.split_domain('foo::bar::baz')).to eq(['foo::bar', 'baz'])
expect(Util.split_domain('foo::bar::baz::')).to eq(['foo::bar', 'baz'])
expect(Util.split_domain('::foo::bar::baz')).to eq(['::foo::bar', 'baz'])
expect(Util.split_domain('::foo::bar::baz::')).to eq(['::foo::bar', 'baz'])
end
it 'should return domain only' do
expect(Util.split_domain('::foo')).to eq([nil, 'foo'])
expect(Util.split_domain('::foo::')).to eq([nil, 'foo'])
end
end

View File

@ -6,13 +6,16 @@ require 'tempfile'
klass = Puppet::Provider::Keystone
class Puppet::Provider::Keystone
@credentials = Puppet::Provider::Openstack::CredentialsV2_0.new
@credentials = Puppet::Provider::Openstack::CredentialsV3.new
def self.reset
@admin_endpoint = nil
@tenant_hash = nil
@admin_token = nil
@keystone_file = nil
@domain_id_to_name = nil
@default_domain_id = nil
@domain_hash = nil
end
end
@ -57,7 +60,7 @@ describe Puppet::Provider::Keystone do
File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
Puppet::Util::IniConfig::File.expects(:new).returns(mock)
mock.expects(:read).with('/etc/keystone/keystone.conf')
expect(klass.get_admin_endpoint).to eq('http://192.168.56.210:35357/v2.0/')
expect(klass.get_admin_endpoint).to eq('http://192.168.56.210:35357/v3/')
end
it 'should use localhost in the admin endpoint if bind_host is 0.0.0.0' do
@ -65,7 +68,7 @@ describe Puppet::Provider::Keystone do
File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
Puppet::Util::IniConfig::File.expects(:new).returns(mock)
mock.expects(:read).with('/etc/keystone/keystone.conf')
expect(klass.get_admin_endpoint).to eq('http://127.0.0.1:35357/v2.0/')
expect(klass.get_admin_endpoint).to eq('http://127.0.0.1:35357/v3/')
end
it 'should use [::1] in the admin endpoint if bind_host is ::0' do
@ -73,7 +76,7 @@ describe Puppet::Provider::Keystone do
File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
Puppet::Util::IniConfig::File.expects(:new).returns(mock)
mock.expects(:read).with('/etc/keystone/keystone.conf')
expect(klass.get_admin_endpoint).to eq('http://[::1]:35357/v2.0/')
expect(klass.get_admin_endpoint).to eq('http://[::1]:35357/v3/')
end
it 'should use localhost in the admin endpoint if bind_host is unspecified' do
@ -81,7 +84,7 @@ describe Puppet::Provider::Keystone do
File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
Puppet::Util::IniConfig::File.expects(:new).returns(mock)
mock.expects(:read).with('/etc/keystone/keystone.conf')
expect(klass.get_admin_endpoint).to eq('http://127.0.0.1:35357/v2.0/')
expect(klass.get_admin_endpoint).to eq('http://127.0.0.1:35357/v3/')
end
it 'should use https if ssl is enabled' do
@ -89,7 +92,7 @@ describe Puppet::Provider::Keystone do
File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
Puppet::Util::IniConfig::File.expects(:new).returns(mock)
mock.expects(:read).with('/etc/keystone/keystone.conf')
expect(klass.get_admin_endpoint).to eq('https://192.168.56.210:35357/v2.0/')
expect(klass.get_admin_endpoint).to eq('https://192.168.56.210:35357/v3/')
end
it 'should use http if ssl is disabled' do
@ -97,7 +100,7 @@ describe Puppet::Provider::Keystone do
File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
Puppet::Util::IniConfig::File.expects(:new).returns(mock)
mock.expects(:read).with('/etc/keystone/keystone.conf')
expect(klass.get_admin_endpoint).to eq('http://192.168.56.210:35357/v2.0/')
expect(klass.get_admin_endpoint).to eq('http://192.168.56.210:35357/v3/')
end
it 'should use the defined admin_endpoint if available' do
@ -105,7 +108,7 @@ describe Puppet::Provider::Keystone do
File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
Puppet::Util::IniConfig::File.expects(:new).returns(mock)
mock.expects(:read).with('/etc/keystone/keystone.conf')
expect(klass.get_admin_endpoint).to eq('https://keystone.example.com/v2.0/')
expect(klass.get_admin_endpoint).to eq('https://keystone.example.com/v3/')
end
it 'should handle an admin_endpoint with a trailing slash' do
@ -113,9 +116,58 @@ describe Puppet::Provider::Keystone do
File.expects(:exists?).with("/etc/keystone/keystone.conf").returns(true)
Puppet::Util::IniConfig::File.expects(:new).returns(mock)
mock.expects(:read).with('/etc/keystone/keystone.conf')
expect(klass.get_admin_endpoint).to eq('https://keystone.example.com/v2.0/')
expect(klass.get_admin_endpoint).to eq('https://keystone.example.com/v3/')
end
end
describe 'when using domains' do
it 'name_and_domain should return the resource domain' do
expect(klass.name_and_domain('foo::in_name', 'from_resource', 'default')).to eq(['foo', 'from_resource'])
end
it 'name_and_domain should return the default domain' do
expect(klass.name_and_domain('foo', nil, 'default')).to eq(['foo', 'default'])
end
it 'name_and_domain should return the domain part of the name' do
expect(klass.name_and_domain('foo::in_name', nil, 'default')).to eq(['foo', 'in_name'])
end
it 'should return the default domain name using the default_domain_id from keystone.conf' 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'
mock = {
'DEFAULT' => {
'admin_endpoint' => 'http://127.0.0.1:35357',
'admin_token' => 'admin_token'
},
'identity' => {'default_domain_id' => 'somename'}
}
File.expects(:exists?).with('/etc/keystone/keystone.conf').returns(true)
Puppet::Util::IniConfig::File.expects(:new).returns(mock)
mock.expects(:read).with('/etc/keystone/keystone.conf')
klass.expects(:openstack)
.with('domain', 'list', '--quiet', '--format', 'csv', [])
.returns('"ID","Name","Enabled","Description"
"somename","SomeName",True,"default domain"
')
expect(klass.name_and_domain('foo')).to eq(['foo', 'SomeName'])
end
it 'should return Default if default_domain_id is not configured' 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'
mock = {}
Puppet::Util::IniConfig::File.expects(:new).returns(mock)
File.expects(:exists?).with('/etc/keystone/keystone.conf').returns(true)
mock.expects(:read).with('/etc/keystone/keystone.conf')
klass.expects(:openstack)
.with('domain', 'list', '--quiet', '--format', 'csv', [])
.returns('"ID","Name","Enabled","Description"
"default","Default",True,"default domain"
')
expect(klass.name_and_domain('foo')).to eq(['foo', 'Default'])
end
end
end