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:
Martin Magr 2014-06-05 14:24:47 +02:00 committed by Martin Mágr
parent 72eee325ef
commit 41608dc6c8
8 changed files with 539 additions and 0 deletions

View File

@ -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']

View 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

View File

@ -40,4 +40,8 @@ Puppet::Type.newtype(:heat_config) do
defaultto false
end
def create
provider.create
end
end

View 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

View 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;
}
}

View 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

View File

@ -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'

View 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