Implement Keystone domain creation
Keystone domain has to be created for Heat. This patch implements this via helper script [1] since we don't have support for Keystone v3 API in puppet-keystone yet. This implementation should be refactored as soon as we will have v3 API available in puppet-keystone. For more info please check [2]. [1] https://github.com/openstack/heat/blob/master/bin/heat-keystone-setup-domain [2] https://bugzilla.redhat.com/show_bug.cgi?id=1076172 Change-Id: I036a84eee6b9d0afa9a9ed96849494324ba4c4db
This commit is contained in:
parent
72eee325ef
commit
41608dc6c8
3
Gemfile
3
Gemfile
@ -4,6 +4,9 @@ group :development, :test do
|
||||
gem 'puppetlabs_spec_helper', :require => false
|
||||
gem 'puppet-lint', '~> 0.3.2'
|
||||
gem 'rake', '10.1.1'
|
||||
gem 'rspec', '< 2.99'
|
||||
gem 'json'
|
||||
gem 'webmock'
|
||||
end
|
||||
|
||||
if puppetversion = ENV['PUPPET_GEM_VERSION']
|
||||
|
183
lib/puppet/provider/heat_domain_id_setter/ruby.rb
Normal file
183
lib/puppet/provider/heat_domain_id_setter/ruby.rb
Normal file
@ -0,0 +1,183 @@
|
||||
## NB: This must work with Ruby 1.8!
|
||||
|
||||
# This provider permits the stack_user_domain parameter in heat.conf
|
||||
# to be set by providing a domain_name to the Puppet module and
|
||||
# using the Keystone REST API to translate the name into the corresponding
|
||||
# UUID.
|
||||
#
|
||||
# This requires that tenant names be unique. If there are multiple matches
|
||||
# for a given tenant name, this provider will raise an exception.
|
||||
|
||||
require 'rubygems'
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
|
||||
class KeystoneError < Puppet::Error
|
||||
end
|
||||
|
||||
class KeystoneConnectionError < KeystoneError
|
||||
end
|
||||
|
||||
class KeystoneAPIError < KeystoneError
|
||||
end
|
||||
|
||||
# Provides common request handling semantics to the other methods in
|
||||
# this module.
|
||||
#
|
||||
# +req+::
|
||||
# An HTTPRequest object
|
||||
# +url+::
|
||||
# A parsed URL (returned from URI.parse)
|
||||
def handle_request(req, url)
|
||||
begin
|
||||
res = Net::HTTP.start(url.host, url.port) {|http|
|
||||
http.request(req)
|
||||
}
|
||||
|
||||
if res.code != '200'
|
||||
raise KeystoneAPIError, "Received error response from Keystone server at #{url}: #{res.message}"
|
||||
end
|
||||
rescue Errno::ECONNREFUSED => detail
|
||||
raise KeystoneConnectionError, "Failed to connect to Keystone server at #{url}: #{detail}"
|
||||
rescue SocketError => detail
|
||||
raise KeystoneConnectionError, "Failed to connect to Keystone server at #{url}: #{detail}"
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
# Authenticates to a Keystone server and obtains an authentication token.
|
||||
# It returns a 2-element +[token, authinfo]+, where +token+ is a token
|
||||
# suitable for passing to openstack apis in the +X-Auth-Token+ header, and
|
||||
# +authinfo+ is the complete response from Keystone, including the service
|
||||
# catalog (if available).
|
||||
#
|
||||
# +auth_url+::
|
||||
# Keystone endpoint URL. This function assumes API version
|
||||
# 2.0 and an administrative endpoint, so this will typically look like
|
||||
# +http://somehost:35357/v2.0+.
|
||||
#
|
||||
# +username+::
|
||||
# Username for authentication.
|
||||
#
|
||||
# +password+::
|
||||
# Password for authentication
|
||||
#
|
||||
# +tenantID+::
|
||||
# Tenant UUID
|
||||
#
|
||||
# +tenantName+::
|
||||
# Tenant name
|
||||
#
|
||||
def keystone_v2_authenticate(auth_url,
|
||||
username,
|
||||
password,
|
||||
tenantId=nil,
|
||||
tenantName=nil)
|
||||
|
||||
post_args = {
|
||||
'auth' => {
|
||||
'passwordCredentials' => {
|
||||
'username' => username,
|
||||
'password' => password
|
||||
},
|
||||
}}
|
||||
|
||||
if tenantId
|
||||
post_args['auth']['tenantId'] = tenantId
|
||||
end
|
||||
|
||||
if tenantName
|
||||
post_args['auth']['tenantName'] = tenantName
|
||||
end
|
||||
|
||||
url = URI.parse("#{auth_url}/tokens")
|
||||
req = Net::HTTP::Post.new url.path
|
||||
req['content-type'] = 'application/json'
|
||||
req.body = post_args.to_json
|
||||
|
||||
res = handle_request(req, url)
|
||||
data = JSON.parse res.body
|
||||
return data['access']['token']['id'], data
|
||||
end
|
||||
|
||||
# Queries a Keystone server to a list of all tenants.
|
||||
#
|
||||
# +auth_url+::
|
||||
# Keystone endpoint. See the notes for +auth_url+ in
|
||||
# +keystone_v2_authenticate+.
|
||||
#
|
||||
# +token+::
|
||||
# A Keystone token that will be passed in requests as the value of the
|
||||
# +X-Auth-Token+ header.
|
||||
#
|
||||
def keystone_v3_domains(auth_url,
|
||||
token)
|
||||
|
||||
auth_url.sub!('v2.0', 'v3')
|
||||
url = URI.parse("#{auth_url}/domains")
|
||||
req = Net::HTTP::Get.new url.path
|
||||
req['content-type'] = 'application/json'
|
||||
req['x-auth-token'] = token
|
||||
|
||||
res = handle_request(req, url)
|
||||
data = JSON.parse res.body
|
||||
data['domains']
|
||||
end
|
||||
|
||||
Puppet::Type.type(:heat_domain_id_setter).provide(:ruby) do
|
||||
def authenticate
|
||||
token, authinfo = keystone_v2_authenticate(
|
||||
@resource[:auth_url],
|
||||
@resource[:auth_username],
|
||||
@resource[:auth_password],
|
||||
nil,
|
||||
@resource[:auth_tenant_name])
|
||||
|
||||
return token
|
||||
end
|
||||
|
||||
def find_domain_by_name(token)
|
||||
domains = keystone_v3_domains(
|
||||
@resource[:auth_url],
|
||||
token)
|
||||
domains.select{|domain| domain['name'] == @resource[:domain_name]}
|
||||
end
|
||||
|
||||
def exists?
|
||||
false
|
||||
end
|
||||
|
||||
def create
|
||||
config
|
||||
end
|
||||
|
||||
# This looks for the domain specified by the 'domain_name' parameter to
|
||||
# the resource and returns the corresponding UUID if there is a single
|
||||
# match.
|
||||
#
|
||||
# Raises a KeystoneAPIError if:
|
||||
#
|
||||
# - There are multiple matches, or
|
||||
# - There are zero matches
|
||||
def get_domain_id
|
||||
token = authenticate
|
||||
domains = find_domain_by_name(token)
|
||||
|
||||
if domains.length == 1
|
||||
return domains[0]['id']
|
||||
elsif domains.length > 1
|
||||
name = domains[0]['name']
|
||||
raise KeystoneAPIError, 'Found multiple matches for domain name "#{name}"'
|
||||
else
|
||||
raise KeystoneAPIError, 'Unable to find matching domain'
|
||||
end
|
||||
end
|
||||
|
||||
def config
|
||||
Puppet::Type.type(:heat_config).new(
|
||||
{:name => 'DEFAULT/stack_user_domain', :value => "#{get_domain_id}"}
|
||||
).create
|
||||
end
|
||||
|
||||
end
|
@ -40,4 +40,8 @@ Puppet::Type.newtype(:heat_config) do
|
||||
defaultto false
|
||||
end
|
||||
|
||||
def create
|
||||
provider.create
|
||||
end
|
||||
|
||||
end
|
||||
|
31
lib/puppet/type/heat_domain_id_setter.rb
Normal file
31
lib/puppet/type/heat_domain_id_setter.rb
Normal file
@ -0,0 +1,31 @@
|
||||
Puppet::Type.newtype(:heat_domain_id_setter) do
|
||||
|
||||
ensurable
|
||||
|
||||
newparam(:name, :namevar => true) do
|
||||
desc 'The name of the setting to update'
|
||||
end
|
||||
|
||||
newparam(:domain_name) do
|
||||
desc 'The heat domain name'
|
||||
end
|
||||
|
||||
newparam(:auth_url) do
|
||||
desc 'The Keystone endpoint URL'
|
||||
defaultto 'http://localhost:35357/v2.0'
|
||||
end
|
||||
|
||||
newparam(:auth_username) do
|
||||
desc 'Username with which to authenticate'
|
||||
defaultto 'admin'
|
||||
end
|
||||
|
||||
newparam(:auth_password) do
|
||||
desc 'Password with which to authenticate'
|
||||
end
|
||||
|
||||
newparam(:auth_tenant_name) do
|
||||
desc 'Tenant name with which to authenticate'
|
||||
defaultto 'admin'
|
||||
end
|
||||
end
|
73
manifests/keystone/domain.pp
Normal file
73
manifests/keystone/domain.pp
Normal file
@ -0,0 +1,73 @@
|
||||
# == Class: heat::keystone::domain
|
||||
#
|
||||
# Configures heat domain in Keystone.
|
||||
#
|
||||
# Note: Implementation is done by heat-keystone-setup-domain script temporarily
|
||||
# because currently puppet-keystone does not support v3 API
|
||||
#
|
||||
# === Parameters
|
||||
#
|
||||
# [*auth_url*]
|
||||
# Keystone auth url
|
||||
#
|
||||
# [*keystone_admin*]
|
||||
# Keystone admin user
|
||||
#
|
||||
# [*keystone_password*]
|
||||
# Keystone admin password
|
||||
#
|
||||
# [*keystone_tenant*]
|
||||
# Keystone admin tenant name
|
||||
#
|
||||
# [*domain_name*]
|
||||
# Heat domain name. Defaults to 'heat'.
|
||||
#
|
||||
# [*domain_admin*]
|
||||
# Keystone domain admin user which will be created. Defaults to 'heat_admin'.
|
||||
#
|
||||
# [*domain_password*]
|
||||
# Keystone domain admin user password. Defaults to 'changeme'.
|
||||
#
|
||||
class heat::keystone::domain (
|
||||
$auth_url = undef,
|
||||
$keystone_admin = undef,
|
||||
$keystone_password = undef,
|
||||
$keystone_tenant = undef,
|
||||
$domain_name = 'heat',
|
||||
$domain_admin = 'heat_admin',
|
||||
$domain_password = 'changeme',
|
||||
) {
|
||||
|
||||
include heat::params
|
||||
|
||||
$cmd_evn = [
|
||||
"OS_USERNAME=${keystone_admin}",
|
||||
"OS_PASSWORD=${keystone_password}",
|
||||
"OS_AUTH_URL=${auth_url}",
|
||||
"HEAT_DOMAIN=${domain_name}",
|
||||
"HEAT_DOMAIN_ADMIN=${domain_admin}",
|
||||
"HEAT_DOMAIN_PASSWORD=${domain_password}"
|
||||
]
|
||||
exec { 'heat_domain_create':
|
||||
path => '/usr/bin',
|
||||
command => 'heat-keystone-setup-domain &>/dev/null',
|
||||
environment => $cmd_evn,
|
||||
require => Package['heat-common'],
|
||||
}
|
||||
|
||||
heat_domain_id_setter { 'heat_domain_id':
|
||||
ensure => present,
|
||||
domain_name => $domain_name,
|
||||
auth_url => $auth_url,
|
||||
auth_username => $keystone_admin,
|
||||
auth_password => $keystone_password,
|
||||
auth_tenant_name => $keystone_tenant,
|
||||
require => Exec['heat_domain_create'],
|
||||
}
|
||||
|
||||
heat_config {
|
||||
'DEFAULT/stack_domain_admin': value => $domain_admin;
|
||||
'DEFAULT/stack_domain_admin_password': value => $domain_password;
|
||||
}
|
||||
|
||||
}
|
66
spec/classes/heat_keystone_domain_spec.rb
Normal file
66
spec/classes/heat_keystone_domain_spec.rb
Normal file
@ -0,0 +1,66 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'heat::keystone::domain' do
|
||||
|
||||
let :params do {
|
||||
:auth_url => 'http://127.0.0.1:35357/v2.0',
|
||||
:keystone_admin => 'admin',
|
||||
:keystone_password => 'admin_passwd',
|
||||
:keystone_tenant => 'admin',
|
||||
:domain_name => 'heat',
|
||||
:domain_admin => 'heat_admin',
|
||||
:domain_password => 'domain_passwd'
|
||||
}
|
||||
end
|
||||
|
||||
shared_examples_for 'heat keystone domain' do
|
||||
it 'configure heat.conf' do
|
||||
should contain_heat_config('DEFAULT/stack_domain_admin').with_value(params[:domain_admin])
|
||||
should contain_heat_config('DEFAULT/stack_domain_admin_password').with_value(params[:domain_password])
|
||||
end
|
||||
|
||||
it 'should configure heat domain id' do
|
||||
should contain_heat_domain_id_setter('heat_domain_id').with(
|
||||
:ensure => 'present',
|
||||
:domain_name => params[:domain_name],
|
||||
:auth_url => params[:auth_url],
|
||||
:auth_username => params[:keystone_admin],
|
||||
:auth_password => params[:keystone_password],
|
||||
:auth_tenant_name => params[:keystone_tenant]
|
||||
)
|
||||
end
|
||||
|
||||
it 'should exec helper script' do
|
||||
should contain_exec('heat_domain_create').with(
|
||||
:command => 'heat-keystone-setup-domain &>/dev/null',
|
||||
:path => '/usr/bin',
|
||||
:require => 'Package[heat-common]',
|
||||
:environment => [
|
||||
"OS_USERNAME=#{params[:keystone_admin]}",
|
||||
"OS_PASSWORD=#{params[:keystone_password]}",
|
||||
"OS_AUTH_URL=#{params[:auth_url]}",
|
||||
"HEAT_DOMAIN=#{params[:domain_name]}",
|
||||
"HEAT_DOMAIN_ADMIN=#{params[:domain_admin]}",
|
||||
"HEAT_DOMAIN_PASSWORD=#{params[:domain_password]}"
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context 'on Debian platforms' do
|
||||
let :facts do
|
||||
{ :osfamily => 'Debian' }
|
||||
end
|
||||
|
||||
it_configures 'heat keystone domain'
|
||||
end
|
||||
|
||||
context 'on RedHat platforms' do
|
||||
let :facts do
|
||||
{ :osfamily => 'RedHat' }
|
||||
end
|
||||
|
||||
it_configures 'heat keystone domain'
|
||||
end
|
||||
end
|
@ -1,5 +1,7 @@
|
||||
require 'puppetlabs_spec_helper/module_spec_helper'
|
||||
require 'shared_examples'
|
||||
require 'webmock/rspec'
|
||||
require 'json'
|
||||
|
||||
RSpec.configure do |c|
|
||||
c.alias_it_should_behave_like_to :it_configures, 'configures'
|
||||
|
177
spec/unit/provider/heat_domain_id_setter/heat_spec.rb
Normal file
177
spec/unit/provider/heat_domain_id_setter/heat_spec.rb
Normal file
@ -0,0 +1,177 @@
|
||||
require 'spec_helper'
|
||||
require 'puppet'
|
||||
require 'puppet/type/heat_domain_id_setter'
|
||||
|
||||
provider_class = Puppet::Type.type(:heat_domain_id_setter).provider(:ruby)
|
||||
|
||||
# used to simulate an authentication response from Keystone
|
||||
# (POST v2.0/tokens)
|
||||
auth_response = {
|
||||
'access' => {
|
||||
'token' => {
|
||||
'id' => 'TOKEN',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# used to simulate a response to GET v3/domains
|
||||
domains_response = {
|
||||
'domains' => [
|
||||
{
|
||||
'name' => 'heat',
|
||||
'id' => 'UUID_HEAT'
|
||||
},
|
||||
{
|
||||
'name' => 'multiple_matches_domain',
|
||||
'id' => 'UUID1'
|
||||
},
|
||||
{
|
||||
'name' => 'multiple_matches_domain',
|
||||
'id' => 'UUID2'
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
# Stub for ini_setting resource
|
||||
Puppet::Type.newtype(:ini_setting) do
|
||||
end
|
||||
|
||||
# Stub for ini_setting provider
|
||||
Puppet::Type.newtype(:ini_setting).provide(:ruby) do
|
||||
def create
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Puppet::Type.type(:heat_keystone_domain_id_setter)' do
|
||||
let :params do
|
||||
{
|
||||
:name => 'heat_domain_id',
|
||||
:ensure => 'present',
|
||||
:domain_name => 'heat',
|
||||
:auth_url => 'http://127.0.0.1:35357/v2.0',
|
||||
:auth_username => 'admin',
|
||||
:auth_password => 'admin_passwd',
|
||||
:auth_tenant_name => 'admin',
|
||||
}
|
||||
end
|
||||
|
||||
it 'should have a non-nil provider' do
|
||||
expect(provider_class).not_to be_nil
|
||||
end
|
||||
|
||||
context 'when url is correct' do
|
||||
before :each do
|
||||
stub_request(:post, "http://127.0.0.1:35357/v2.0/tokens").
|
||||
to_return(:status => 200,
|
||||
:body => auth_response.to_json,
|
||||
:headers => {})
|
||||
stub_request(:get, "http://127.0.0.1:35357/v3/domains").
|
||||
with(:headers => {'X-Auth-Token'=>'TOKEN'}).
|
||||
to_return(:status => 200,
|
||||
:body => domains_response.to_json,
|
||||
:headers => {})
|
||||
end
|
||||
|
||||
it 'should create a resource' do
|
||||
resource = Puppet::Type::Heat_domain_id_setter.new(params)
|
||||
provider = provider_class.new(resource)
|
||||
expect(provider.exists?).to be_false
|
||||
expect(provider.create).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
# What happens if we ask for a domain that does not exist?
|
||||
context 'when domain cannot be found' do
|
||||
before :each do
|
||||
stub_request(:post, "http://127.0.0.1:35357/v2.0/tokens").
|
||||
to_return(:status => 200,
|
||||
:body => auth_response.to_json,
|
||||
:headers => {})
|
||||
stub_request(:get, "http://127.0.0.1:35357/v3/domains").
|
||||
with(:headers => {'X-Auth-Token'=>'TOKEN'}).
|
||||
to_return(:status => 200,
|
||||
:body => domains_response.to_json,
|
||||
:headers => {})
|
||||
|
||||
params.merge!(:domain_name => 'bad_domain_name')
|
||||
end
|
||||
|
||||
it 'should receive an api error' do
|
||||
resource = Puppet::Type::Heat_domain_id_setter.new(params)
|
||||
provider = provider_class.new(resource)
|
||||
expect(provider.exists?).to be_false
|
||||
expect { provider.create }.to raise_error KeystoneAPIError, /Unable to find matching domain/
|
||||
end
|
||||
end
|
||||
|
||||
# What happens if we ask for a domain name that results in multiple
|
||||
# matches?
|
||||
context 'when there are multiple matching domains' do
|
||||
before :each do
|
||||
stub_request(:post, "http://127.0.0.1:35357/v2.0/tokens").
|
||||
to_return(:status => 200,
|
||||
:body => auth_response.to_json,
|
||||
:headers => {})
|
||||
stub_request(:get, "http://127.0.0.1:35357/v3/domains").
|
||||
with(:headers => {'X-Auth-Token'=>'TOKEN'}).
|
||||
to_return(:status => 200,
|
||||
:body => domains_response.to_json,
|
||||
:headers => {})
|
||||
|
||||
params.merge!(:domain_name => 'multiple_matches_domain')
|
||||
end
|
||||
|
||||
it 'should receive an api error' do
|
||||
resource = Puppet::Type::Heat_domain_id_setter.new(params)
|
||||
provider = provider_class.new(resource)
|
||||
expect(provider.exists?).to be_false
|
||||
expect { provider.create }.to raise_error KeystoneAPIError, /Found multiple matches for domain name/
|
||||
end
|
||||
end
|
||||
|
||||
# What happens if we pass a bad password?
|
||||
context 'when password is incorrect' do
|
||||
before :each do
|
||||
stub_request(:post, "http://127.0.0.1:35357/v2.0/tokens").
|
||||
to_return(:status => 401,
|
||||
:body => auth_response.to_json,
|
||||
:headers => {})
|
||||
end
|
||||
|
||||
it 'should receive an authentication error' do
|
||||
resource = Puppet::Type::Heat_domain_id_setter.new(params)
|
||||
provider = provider_class.new(resource)
|
||||
expect(provider.exists?).to be_false
|
||||
expect { provider.create }.to raise_error KeystoneAPIError
|
||||
end
|
||||
end
|
||||
|
||||
# What happens if the server is not listening?
|
||||
context 'when keystone server is unavailable' do
|
||||
before :each do
|
||||
stub_request(:post, "http://127.0.0.1:35357/v2.0/tokens").to_raise Errno::ECONNREFUSED
|
||||
end
|
||||
|
||||
it 'should receive a connection error' do
|
||||
resource = Puppet::Type::Heat_domain_id_setter.new(params)
|
||||
provider = provider_class.new(resource)
|
||||
expect(provider.exists?).to be_false
|
||||
expect { provider.create }.to raise_error KeystoneConnectionError
|
||||
end
|
||||
end
|
||||
|
||||
# What happens if we mistype the hostname?
|
||||
context 'when keystone server is unknown' do
|
||||
before :each do
|
||||
stub_request(:post, "http://127.0.0.1:35357/v2.0/tokens").to_raise SocketError, 'getaddrinfo: Name or service not known'
|
||||
end
|
||||
|
||||
it 'should receive a connection error' do
|
||||
resource = Puppet::Type::Heat_domain_id_setter.new(params)
|
||||
provider = provider_class.new(resource)
|
||||
expect(provider.exists?).to be_false
|
||||
expect { provider.create }.to raise_error KeystoneConnectionError
|
||||
end
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue
Block a user