Merge "Resource keystone_identity_provider for Keystone."

This commit is contained in:
Jenkins 2016-01-07 18:56:57 +00:00 committed by Gerrit Code Review
commit 021c737e1c
6 changed files with 687 additions and 3 deletions

View File

@ -0,0 +1,157 @@
require 'json'
require 'puppet/provider/keystone'
class Puppet::Error::OpenstackDuplicateRemoteId < Puppet::Error; end
Puppet::Type.type(:keystone_identity_provider).provide(
:openstack,
:parent => Puppet::Provider::Keystone
) do
desc 'Provider to manage keystone identity provider.'
@credentials = Puppet::Provider::Openstack::CredentialsV3.new
mk_resource_methods
def create
properties = []
remote_ids = []
remote_id_file = []
option_enable = '--enable'
remote_ids += resource[:remote_ids] if resource[:remote_ids]
remote_id_file += ['--remote-id-file', resource[:remote_id_file]] if
resource[:remote_id_file]
properties += self.class.remote_ids_cli(remote_ids)
properties += remote_id_file
option_enable = '--disable' if resource[:enabled] == :false
properties << option_enable
properties += ['--description', resource[:description]] if
resource[:description]
properties << resource[:name]
@property_hash = self.class.request('identity provider',
'create',
properties)
rescue Puppet::ExecutionFailure => e
if e.message =~
/openstack Conflict occurred attempting to store identity_provider/
raise(Puppet::Error::OpenstackDuplicateRemoteId,
'One of the remote-id of this resource is already ' \
'registered by another identity provider: ' \
"#{e.message}")
else
raise e
end
else
@property_hash[:ensure] = :present
end
def destroy
self.class.request('identity provider', 'delete', id)
@property_hash.clear
end
def exists?
@property_hash[:ensure] == :present
end
def self.instances
list = request('identity provider', 'list')
list.collect do |identity_provider|
current_resource =
request('identity provider', 'show', identity_provider[:id])
new(
:name => identity_provider[:id],
:id => identity_provider[:id],
:description => identity_provider[:description],
:enabled => identity_provider[:enabled].downcase.chomp == 'true' ? true : false,
:remote_ids => clean_remote_ids(current_resource[:remote_ids]),
:ensure => :present
)
end
end
def self.prefetch(resources)
identity_providers = instances
resources.keys.each do |name|
if provider = identity_providers.find { |existing| existing.name == name }
resources[name].provider = provider
end
end
end
# puppetlabs/PUP-1470: to be removed when puppet 3.5 is no longer supported.
def enabled
if @property_hash[:enabled].nil?
:absent
else
@property_hash[:enabled]
end
end
def enabled=(value)
options = value == :false ? ['--disable'] : ['--enable']
options << id
self.class.request('identity provider', 'set', options)
end
def remote_ids=(value)
options = []
options += self.class.remote_ids_cli(value)
self.class.request('identity provider', 'set', options + [id]) unless
options.empty?
end
def remote_id_file=(value)
options = ['--remote-id-file', value]
self.class.request('identity provider', 'set', options + [id])
end
def remote_id_file
remote_ids
end
# bug/python-openstackclient/1478995: when fixed, parsing will be done by OSC.
def self.clean_remote_ids(remote_ids)
version = request('--version', '').sub(/openstack\s+/i, '').strip
if Gem::Version.new(version) < Gem::Version.new('1.9.0')
clean_remote_ids_old(remote_ids)
else
remote_ids.split(',').map(&:strip)
end
end
def self.clean_remote_ids_old(remote_ids)
remote_ids_clean = []
if remote_ids != '[]'
python_array_of_unicode_string = %r/
u # the u character
(?<delimiter>["']) # followed by a delimiter
(?<value> # which holds the value
.+? # composed of non-delimiter
)
(\k<delimiter>) # ended by the delimiter
/x
remote_ids_clean = JSON.parse(remote_ids.gsub(
python_array_of_unicode_string,
'"\k<value>"'))
end
rescue JSON::ParserError
raise(Puppet::Error,
"Could not parse #{remote_ids} into a valid structure. " \
'Please submit a bug report.')
else
remote_ids_clean
end
def self.remote_ids_cli(remote_ids)
remote_ids.map { |e| ['--remote-id', e.to_s] }.flatten
end
end

View File

@ -0,0 +1,98 @@
# LP#1408531
File.expand_path('../..', File.dirname(__FILE__)).tap { |dir| $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir) }
File.expand_path('../../../../openstacklib/lib', File.dirname(__FILE__)).tap { |dir| $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir) }
require 'puppet/provider/keystone/util'
Puppet::Type.newtype(:keystone_identity_provider) do
desc 'Type for managing identity provider.'
ensurable
newparam(:name, :namevar => true) do
newvalues(/\S+/)
end
newproperty(:enabled) do
newvalues(/^(t|T)rue$/, /^(f|F)alse$/, true, false)
def insync?(is)
is.to_s.downcase.to_sym == should.to_s.downcase.to_sym
end
defaultto(true)
munge do |value|
value.to_s.downcase.to_sym
end
end
newproperty(:description) do
desc 'Description of the identity server.'
newvalues(nil, /\S+/)
def insync?(is)
if should != is
raise(Puppet::Error,
'The description cannot be changed ' \
"from #{should} to #{is}")
end
true
end
end
newproperty(:remote_ids, :array_matching => :all) do
def insync?(is)
# remote_ids and remote_id_file are mutually exclusive.
return true unless resource.parameters[:remote_id_file].nil?
is.map(&:to_s).sort == should.map(&:to_s).sort
end
defaultto([])
validate do |v|
if idx = v.to_s.index('"')
raise(Puppet::ResourceError,
'rfc3986#section-2: remote id cannot have a double quote' \
": #{v} at position #{idx}"
)
end
if v.to_s.match(/\s/)
raise(Puppet::ResourceError,
"Remote id cannot have space in it: '#{v}'"
)
end
end
munge(&:to_s)
end
newproperty(:remote_id_file) do
validate do |v|
unless resource.parameters[:remote_ids].nil?
raise(Puppet::ResourceError,
'Cannot have both remote_ids and remote_id_file')
end
unless Pathname.new(v).absolute?
raise(Puppet::ResourceError,
"You must specify an absolute path name not '#{v}'.")
end
end
def insync?(is)
ids_in_file = File.readlines(should).map(&:strip).delete_if(&:empty?)
ids_in_file.sort == is.sort
end
end
newproperty(:id) do
validate do
raise(Puppet::Error, 'This is a read only property')
end
end
autorequire(:file) do
if self[:remote_id_file] && Pathname.new(self[:remote_id_file]).absolute?
self[:remote_id_file]
end
end
autorequire(:anchor) do
['keystone_started']
end
end

View File

@ -68,7 +68,6 @@ describe 'keystone server running with Apache/WSGI with resources' do
}
EOS
# Run it twice and test for idempotency
apply_manifest(pp, :catch_failures => true)
apply_manifest(pp, :catch_changes => true)
@ -168,11 +167,14 @@ describe 'keystone server running with Apache/WSGI with resources' do
end
describe 'with v3 admin with v3 credentials' do
include_examples 'keystone user/tenant/service/role/endpoint resources using v3 API',
'--os-username adminv3 --os-password a_big_secret --os-project-name openstackv3 --os-user-domain-name admin_domain --os-project-domain-name admin_domain'
'--os-username adminv3 --os-password a_big_secret --os-project-name openstackv3' \
' --os-user-domain-name admin_domain --os-project-domain-name admin_domain'
end
describe "with v3 service with v3 credentials" do
include_examples 'keystone user/tenant/service/role/endpoint resources using v3 API',
'--os-username beaker-civ3 --os-password secret --os-project-name servicesv3 --os-user-domain-name service_domain --os-project-domain-name service_domain'
'--os-username beaker-civ3 --os-password secret --os-project-name servicesv3 --os-user-domain-name service_domain --os-project-domain-name service_domain'
end
end
describe 'composite namevar quick test' do

View File

@ -12,6 +12,12 @@ RSpec.configure do |c|
end
end
RSpec::Matchers.define :be_absent do
match do |actual|
actual == :absent
end
end
at_exit { RSpec::Puppet::Coverage.report! }
def setup_provider_tests

View File

@ -0,0 +1,298 @@
require 'puppet'
require 'spec_helper'
require 'puppet/provider/keystone_identity_provider/openstack'
describe Puppet::Type.type(:keystone_identity_provider).provider(:openstack) do
let(:set_env) do
ENV['OS_USERNAME'] = 'test'
ENV['OS_PASSWORD'] = 'abc123'
ENV['OS_PROJECT_NAME'] = 'test'
ENV['OS_AUTH_URL'] = 'http://127.0.0.1:5000/v3'
end
let(:id_provider_attrs) do
{
:name => 'idp_one',
:enabled => true,
:description => 'Nice id provider',
:remote_ids => ['entityid_idp1', 'http://entityid_idp2/saml/meta', 3],
:ensure => :present
}
end
let(:resource) do
Puppet::Type::Keystone_identity_provider.new(id_provider_attrs)
end
let(:provider) { described_class.new(resource) }
before(:example) { set_env }
describe '#create success' do
it 'creates an identity provider' do
described_class.expects(:openstack)
.with(
'identity provider', 'create',
'--format', 'shell', [
'--remote-id', 'entityid_idp1',
'--remote-id', 'http://entityid_idp2/saml/meta',
'--remote-id', '3',
'--enable',
'--description', 'Nice id provider',
'idp_one'
]
)
.once
.returns(
<<-EOR
description="Nice id provider"
enabled="True"
id="idp_one"
remote_ids="[u'entityid_idp1', u'http://entityid_idp2/saml/meta', u'3']"
EOR
)
provider.create
expect(provider.exists?).to be_truthy
end
end
describe '#create failure' do
it 'fails with an helpfull message when hitting remote-id duplicate.' do
described_class.expects(:openstack)
.with(
'identity provider', 'create',
'--format', 'shell', [
'--remote-id', 'entityid_idp1',
'--remote-id', 'http://entityid_idp2/saml/meta',
'--remote-id', '3',
'--enable',
'--description', 'Nice id provider',
'idp_one'
]
)
.once
.raises(Puppet::ExecutionFailure,
'openstack Conflict occurred attempting to' \
' store identity_provider')
expect { provider.create }
.to raise_error(Puppet::Error::OpenstackDuplicateRemoteId)
end
end
describe '#create with a remote-id-file' do
let(:id_provider_attrs) do
{
:name => 'idp_one',
:enabled => true,
:description => 'Nice id provider',
:remote_id_file => '/tmp/remoteids',
:ensure => :present
}
end
it 'create a resource whit remote id in a file' do
described_class.expects(:openstack)
.with(
'identity provider', 'create',
'--format', 'shell', [
'--remote-id-file', '/tmp/remoteids',
'--enable',
'--description', 'Nice id provider',
'idp_one'
]
)
.once
.returns(
<<-EOR
description="Nice id provider"
enabled="True"
id="idp_one"
remote_ids="[u'entityid_idp1', u'http://entityid_idp2/saml/meta', u'3']"
EOR
)
provider.create
expect(provider.exists?).to be_truthy
end
end
describe '#destroy' do
it 'destroy an identity provider' do
provider.instance_variable_get('@property_hash')[:id] = 'idp_one'
described_class.expects(:openstack)
.with(
'identity provider', 'delete', 'idp_one'
)
provider.destroy
expect(provider.exists?).to be_falsy
end
end
describe '#instances' do
it 'finds every identity provider' do
described_class.expects(:openstack)
.with(
'identity provider', 'list',
'--quiet', '--format', 'csv', []
)
.once
.returns(
<<-EOR
"ID","Enabled","Description"
"idp_one",True,""
"idp_two",False,"Idp two description"
EOR
)
described_class.expects(:openstack)
.with(
'identity provider', 'show',
'--format', 'shell', 'idp_one'
)
.once
.returns(
<<-EOR
description="None"
enabled="True"
id="idp_one"
remote_ids="[u'entityid_idp1', u'http://entityid_idp2/saml/meta', u'3']"
EOR
)
described_class.expects(:openstack)
.with(
'identity provider', 'show',
'--format', 'shell', 'idp_two'
)
.once
.returns(
<<-EOR
description="Idp two description"
enabled="False"
id="idp_two"
remote_ids="[]"
EOR
)
described_class.expects(:openstack)
.with('--version', '', [])
.twice
.returns("openstack 1.7.0\n")
instances =
Puppet::Type::Keystone_identity_provider::ProviderOpenstack.instances
expect(instances.count).to eq(2)
expect(instances[0].description).to be_empty
expect(instances[1].enabled).to be_falsy
end
end
describe '#update' do
context 'remote_ids' do
it 'changes the remote_ids' do
provider.expects(:id).returns('1234')
described_class.expects(:openstack)
.with(
'identity provider', 'set',
[
'--remote-id', 'entityid_idp1',
'--remote-id', 'http://entityid_idp2/saml/meta',
'1234'
]
)
.once
provider.remote_ids = ['entityid_idp1', 'http://entityid_idp2/saml/meta']
end
end
context 'with remote_id_file' do
it 'changes the remote_id_file' do
provider.expects(:id).returns('1234')
described_class.expects(:openstack)
.with(
'identity provider', 'set',
['--remote-id-file', '/tmp/new_file', '1234']
)
.once
provider.remote_id_file = '/tmp/new_file'
end
end
context 'enabled' do
it 'changes the enable to true' do
provider.expects(:id).returns('1234')
described_class.expects(:openstack)
.with(
'identity provider', 'set',
['--enable', '1234']
)
.once
provider.enabled = :true
end
it 'changes the enable to false' do
provider.expects(:id).returns('1234')
described_class.expects(:openstack)
.with(
'identity provider', 'set',
['--disable', '1234']
)
.once
provider.enabled = :false
end
end
end
describe '#prefetch' do
let(:resources_catalog) { { 'idp_one' => provider } }
let(:found_resource) do
existing = described_class.new
existing.instance_variable_set('@property_hash',
:name => 'idp_one',
:id => 'idp_one',
:description => '',
:enabled => true,
:remote_ids => [
'entityid_idp1',
'http://entityid_idp2/saml/meta',
'3'],
:ensure => :present
)
existing
end
it 'fill the resource with the right provider' do
described_class.expects(:instances)
.once
.returns([found_resource])
expect(resources_catalog['idp_one'].provider).to be_absent
described_class.prefetch(resources_catalog)
expect(resources_catalog['idp_one'].provider).not_to be_absent
end
end
describe '#clean_remote_ids' do
context 'before python-openstackclient/+bug/1478995' do
let(:edge_cases_remote_ids) do
{
%q|[u'http://remoteid?id=idp_one&name=ldap', u"http://remoteid_2?id='idp'"]| =>
['http://remoteid?id=idp_one&name=ldap', "http://remoteid_2?id='idp'"],
%q|[u'http://remoteid?id=idp_one&name=ldap']| => ['http://remoteid?id=idp_one&name=ldap']
}
end
it 'should handle tricky cases' do
described_class.expects(:openstack)
.with('--version', '', [])
.twice
.returns("openstack 1.7.0\n")
edge_cases_remote_ids.each do |edge_case, solution|
expect(described_class.clean_remote_ids(edge_case)).to eq(solution)
end
end
end
context 'after python-openstackclient/+bug/1478995' do
let(:remote_ids) do
[
"http://remoteid?id=idp_one&name=ldap, http://remoteid_2?id='idp'",
['http://remoteid?id=idp_one&name=ldap', "http://remoteid_2?id='idp'"]
]
end
it 'should handle the new output' do
described_class.expects(:openstack)
.with('--version', '', [])
.once
.returns("openstack 1.9.0\n")
expect(described_class.clean_remote_ids(remote_ids[0])).to eq(remote_ids[1])
end
end
end
end

View File

@ -0,0 +1,123 @@
require 'spec_helper'
require 'puppet'
require 'puppet/type/keystone_identity_provider'
describe Puppet::Type.type(:keystone_identity_provider) do
let(:service_provider) do
Puppet::Type.type(:keystone_identity_provider).new(
:name => 'foo',
:remote_ids => ['remoteid_idp1', 'http://remoteid_idp2/saml/meta', 3],
:description => 'Original description'
)
end
describe '#remote-ids property' do
it 'should be in sync with unsorted array of remote-ids' do
remote_ids = service_provider.parameter('remote_ids')
expect(remote_ids.insync?(
['http://remoteid_idp2/saml/meta', 3, 'remoteid_idp1'])).to be_truthy
end
it 'should not allow an id with space in it' do
expect do
@service_provider = Puppet::Type.type(:keystone_identity_provider).new(
:name => 'foo',
:remote_ids => ['remote id one']
)
end.to raise_error(Puppet::ResourceError,
/Remote id cannot have space in it/)
end
it 'should not allow an id with double quote in it' do
expect do
@service_provider = Puppet::Type.type(:keystone_identity_provider).new(
:name => 'foo',
:remote_ids => ['http://remoteone?id="foo"']
)
end.to raise_error(Puppet::ResourceError,
/double quote: http:\/\/remoteone\?id="foo" at position 20/)
end
end
describe '#description property' do
it "Can't be modified" do
description = service_provider.parameter('description')
expect do
description.insync?('New description')
end.to raise_error(
Puppet::Error,
/^The description cannot be changed from Original description to New description$/
)
end
end
describe '#remote_id_file' do
context 'remote_id_file and remote_ids are both set' do
it 'must fail' do
expect do
@service_provider = Puppet::Type.type(:keystone_identity_provider).new(
:name => 'foo',
:remote_ids => ['remoteone'],
:remote_id_file => '/tmp/remote_ids'
)
end.to raise_error(Puppet::ResourceError,
/Cannot have both remote_ids and remote_id_file/)
end
end
context 'remote_id_file is not an absolute path' do
it 'must raise a error' do
expect do
@service_provider = Puppet::Type.type(:keystone_identity_provider).new(
:name => 'foo',
:remote_id_file => 'tmp/remote_ids'
)
end.to raise_error(Puppet::ResourceError,
/You must specify an absolute path name not 'tmp\/remote_ids'/)
end
end
context 'remote_id_file is in sync relative to the ids in the file' do
let(:service_provider) do
Puppet::Type.type(:keystone_identity_provider).new(
:name => 'foo',
:remote_id_file => '/tmp/remote_ids'
)
end
it 'must be in sync' do
File.expects(:readlines).with('/tmp/remote_ids').once
.returns([' remoteids', '', 'http://secondids ', ' '])
remote_id_file = service_provider.parameter('remote_id_file')
expect(remote_id_file.insync?(
['http://secondids', 'remoteids'])).to be_truthy
end
end
end
describe '#autorequire' do
let(:file_good) do
Puppet::Type.type(:file).new(
:name => '/tmp/remote-ids',
:ensure => :present
)
end
let(:file_bad) do
Puppet::Type.type(:file).new(
:name => '/tmp/another-file',
:ensure => :present
)
end
let(:service_provider) do
Puppet::Type.type(:keystone_identity_provider).new(
:name => 'foo',
:remote_id_file => '/tmp/remote-ids',
:description => 'Original description'
)
end
describe 'should autorequire the correct file' do
let(:resources) { [service_provider, file_good, file_bad] }
include_examples 'autorequire the correct resources'
end
end
end