Neutron Nova Interactions Support

Recent versions of Neutron are able to provide active notifications to
Nova about changes to port status and data.  This change adds the
neutron::server::notifications class to allow Puppet to configure this
feature when setting up a Neutron server.

neutron.conf requires a tenant UUID for communicating with Nova.  This
changes adds adds a custom type and provider to translate an admin
tenant name to the required UUID.

Co-Authored-By: emilienm <emilien.macchi@enovance.com>
Change-Id: I57af7cb47a12adabed9b1ee65e1c352b3e5fc2de
Closes-bug: 1306694
This commit is contained in:
Lars Kellogg-Stedman 2014-04-17 15:15:31 -04:00
parent 71bd36760d
commit f9b3d184a0
10 changed files with 678 additions and 2 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@ spec/fixtures/modules/*
spec/fixtures/manifests/site.pp
Gemfile.lock
.vendor
.bundle/
vendor/

View File

@ -1,9 +1,12 @@
source 'https://rubygems.org'
gem 'json'
group :development, :test do
gem 'puppetlabs_spec_helper', :require => false
gem 'puppet-lint', '~> 0.3.2'
gem 'rake', '10.1.1'
gem 'webmock'
end
if puppetversion = ENV['PUPPET_GEM_VERSION']

View File

@ -17,6 +17,12 @@ class { 'neutron::server':
connection => 'mysql://neutron:password@192.168.1.1/neutron',
}
# Configure nova notifications system
class { 'neutron::server::notifications':
nova_admin_tenant_name => 'admin',
nova_admin_password => 'secrete',
}
# Various agents
class { 'neutron::agents::dhcp': }
class { 'neutron::agents::l3': }

View File

@ -0,0 +1,183 @@
## NB: This must work with Ruby 1.8!
# This providers permits the nova_admin_tenant_id paramter in neutron.conf
# to be set by providing a nova_admin_tenant_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_v2_tenants(auth_url,
token)
url = URI.parse("#{auth_url}/tenants")
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['tenants']
end
Puppet::Type.type(:nova_admin_tenant_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_tenant_by_name (token)
tenants = keystone_v2_tenants(
@resource[:auth_url],
token)
tenants.select{|tenant| tenant['name'] == @resource[:tenant_name]}
end
def exists?
false
end
def create
config
end
# This looks for the tenant specified by the 'tenant_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_tenant_id
token = authenticate
tenants = find_tenant_by_name(token)
if tenants.length == 1
return tenants[0]['id']
elsif tenants.length > 1
raise KeystoneAPIError, 'Found multiple matches for tenant name'
else
raise KeystoneAPIError, 'Unable to find matching tenant'
end
end
def config
Puppet::Type.type(:neutron_config).new(
{:name => 'DEFAULT/nova_admin_tenant_id', :value => "#{get_tenant_id}"}
).create
end
end

View File

@ -15,4 +15,9 @@ Puppet::Type.newtype(:neutron_config) do
value
end
end
def create
provider.create
end
end

View File

@ -0,0 +1,32 @@
Puppet::Type.newtype(:nova_admin_tenant_id_setter) do
ensurable
newparam(:name, :namevar => true) do
desc 'The name of the setting to update'
end
newparam(:tenant_name) do
desc 'The nova admin tenant 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,112 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# == Class: neutron::server::notifications
#
# Configure Notification System Options
#
# === Parameters
#
# [*notify_nova_on_port_status_changes*]
# (optional) Send notification to nova when port status is active.
# Defaults to true
#
# [*notify_nova_on_port_data_changes*]
# (optional) Send notifications to nova when port data (fixed_ips/floatingips)
# change so nova can update its cache.
# Defaults to true
#
# [*send_events_interval*]
# (optional) Number of seconds between sending events to nova if there are
# any events to send.
# Defaults to '2'
#
# [*nova_url*]
# (optional) URL for connection to nova (Only supports one nova region
# currently).
# Defaults to 'http://127.0.0.1:8774/v2'
#
# [*nova_admin_auth_url*]
# (optional) Authorization URL for connection to nova in admin context.
# Defaults to 'http://127.0.0.1:35357/v2.0'
#
# [*nova_admin_username*]
# (optional) Username for connection to nova in admin context
# Defaults to 'nova'
#
# [*nova_admin_tenant_name*]
# (optional) The name of the admin nova tenant
# Defaults to 'services'
#
# [*nova_admin_tenant_id*]
# (optional) The UUID of the admin nova tenant. If provided this takes
# precedence over nova_admin_tenant_name.
#
# [*nova_admin_password*]
# (required) Password for connection to nova in admin context.
#
# [*nova_region_name*]
# (optional) Name of nova region to use. Useful if keystone manages more than
# one region.
# Defaults to 'RegionOne'
#
class neutron::server::notifications (
$notify_nova_on_port_status_changes = true,
$notify_nova_on_port_data_changes = true,
$send_events_interval = '2',
$nova_url = 'http://127.0.0.1:8774/v2',
$nova_admin_auth_url = 'http://127.0.0.1:35357/v2.0',
$nova_admin_username = 'nova',
$nova_admin_tenant_name = 'services',
$nova_admin_tenant_id = undef,
$nova_admin_password = false,
$nova_region_name = 'RegionOne',
) {
# Depend on the specified keystone_user resource, if it exists.
Keystone_user <| title == 'nova' |> -> Class[neutron::server::notifications]
if ! $nova_admin_password {
fail('nova_admin_password must be set.')
}
if ! ( $nova_admin_tenant_id or $nova_admin_tenant_name ) {
fail('You must provide either nova_admin_tenant_name or nova_admin_tenant_id.')
}
neutron_config {
'DEFAULT/notify_nova_on_port_status_changes': value => $notify_nova_on_port_status_changes;
'DEFAULT/notify_nova_on_port_data_changes': value => $notify_nova_on_port_data_changes;
'DEFAULT/send_events_interval': value => $send_events_interval;
'DEFAULT/nova_url': value => $nova_url;
'DEFAULT/nova_admin_auth_url': value => $nova_admin_auth_url;
'DEFAULT/nova_admin_username': value => $nova_admin_username;
'DEFAULT/nova_admin_password': value => $nova_admin_password;
'DEFAULT/nova_region_name': value => $nova_region_name;
}
if $nova_admin_tenant_id {
neutron_config {
'DEFAULT/nova_admin_tenant_id': value => $nova_admin_tenant_id;
}
} else {
nova_admin_tenant_id_setter {'nova_admin_tenant_id':
ensure => present,
tenant_name => $nova_admin_tenant_name,
auth_url => $nova_admin_auth_url,
auth_username => $nova_admin_username,
auth_password => $nova_admin_password,
auth_tenant_name => $nova_admin_tenant_name,
}
}
}

View File

@ -0,0 +1,150 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Unit tests for neutron::server::notifications class
#
require 'spec_helper'
describe 'neutron::server::notifications' do
let :pre_condition do
'define keystone_user ($name) {}'
end
let :default_params do
{
:notify_nova_on_port_status_changes => true,
:notify_nova_on_port_data_changes => true,
:send_events_interval => '2',
:nova_url => 'http://127.0.0.1:8774/v2',
:nova_admin_auth_url => 'http://127.0.0.1:35357/v2.0',
:nova_admin_username => 'nova',
:nova_admin_tenant_name => 'services',
:nova_region_name => 'RegionOne'
}
end
let :params do
{
:nova_admin_password => 'secrete',
:nova_admin_tenant_id => 'UUID'
}
end
shared_examples_for 'neutron server notifications' do
let :p do
default_params.merge(params)
end
it 'configure neutron.conf' do
should contain_neutron_config('DEFAULT/notify_nova_on_port_status_changes').with_value(true)
should contain_neutron_config('DEFAULT/notify_nova_on_port_data_changes').with_value(true)
should contain_neutron_config('DEFAULT/send_events_interval').with_value('2')
should contain_neutron_config('DEFAULT/nova_url').with_value('http://127.0.0.1:8774/v2')
should contain_neutron_config('DEFAULT/nova_admin_auth_url').with_value('http://127.0.0.1:35357/v2.0')
should contain_neutron_config('DEFAULT/nova_admin_username').with_value('nova')
should contain_neutron_config('DEFAULT/nova_admin_password').with_value('secrete')
should contain_neutron_config('DEFAULT/nova_region_name').with_value('RegionOne')
should contain_neutron_config('DEFAULT/nova_admin_tenant_id').with_value('UUID')
end
context 'when overriding parameters' do
before :each do
params.merge!(
:notify_nova_on_port_status_changes => false,
:notify_nova_on_port_data_changes => false,
:send_events_interval => '10',
:nova_url => 'http://nova:8774/v3',
:nova_admin_auth_url => 'http://keystone:35357/v2.0',
:nova_admin_username => 'joe',
:nova_region_name => 'MyRegion',
:nova_admin_tenant_id => 'UUID2'
)
end
it 'should configure neutron server with overrided parameters' do
should contain_neutron_config('DEFAULT/notify_nova_on_port_status_changes').with_value(false)
should contain_neutron_config('DEFAULT/notify_nova_on_port_data_changes').with_value(false)
should contain_neutron_config('DEFAULT/send_events_interval').with_value('10')
should contain_neutron_config('DEFAULT/nova_url').with_value('http://nova:8774/v3')
should contain_neutron_config('DEFAULT/nova_admin_auth_url').with_value('http://keystone:35357/v2.0')
should contain_neutron_config('DEFAULT/nova_admin_username').with_value('joe')
should contain_neutron_config('DEFAULT/nova_admin_password').with_value('secrete')
should contain_neutron_config('DEFAULT/nova_region_name').with_value('MyRegion')
should contain_neutron_config('DEFAULT/nova_admin_tenant_id').with_value('UUID2')
end
end
context 'when no nova_admin_password is specified' do
before :each do
params.merge!(:nova_admin_password => '')
end
it 'should fail to configure neutron server' do
expect { subject }.to raise_error(Puppet::Error, /nova_admin_password must be set./)
end
end
context 'when no nova_admin_tenant_id and nova_admin_tenant_name specified' do
before :each do
params.merge!(
:nova_admin_tenant_id => '',
:nova_admin_tenant_name => ''
)
end
it 'should fail to configure neutron server' do
expect { subject }.to raise_error(Puppet::Error, /You must provide either nova_admin_tenant_name or nova_admin_tenant_id./)
end
end
context 'when providing a tenant name' do
before :each do
params.merge!(
:nova_admin_tenant_id => '',
:nova_admin_tenant_name => 'services'
)
end
it 'should configure nova admin tenant id' do
should contain_nova_admin_tenant_id_setter('nova_admin_tenant_id').with(
:ensure => 'present',
:tenant_name => 'services',
:auth_url => 'http://127.0.0.1:35357/v2.0',
:auth_password => 'secrete',
:auth_tenant_name => 'services'
)
end
end
end
context 'on Debian platforms' do
let :facts do
{ :osfamily => 'Debian' }
end
let :platform_params do
{}
end
it_configures 'neutron server notifications'
end
context 'on RedHat platforms' do
let :facts do
{ :osfamily => 'RedHat' }
end
let :platform_params do
{}
end
it_configures 'neutron server notifications'
end
end

View File

@ -1,7 +1,13 @@
require 'puppetlabs_spec_helper/module_spec_helper'
require 'shared_examples'
require 'webmock/rspec'
require 'json'
fixture_path = File.expand_path(File.join(__FILE__, '..', 'fixtures'))
RSpec.configure do |c|
c.alias_it_should_behave_like_to :it_configures, 'configures'
c.alias_it_should_behave_like_to :it_raises, 'raises'
c.alias_it_should_behave_like_to :it_configures, 'configures'
c.alias_it_should_behave_like_to :it_raises, 'raises'
c.module_path = File.join(fixture_path, 'modules')
c.manifest_dir = File.join(fixture_path, 'manifests')
end

View File

@ -0,0 +1,177 @@
require 'spec_helper'
require 'puppet'
require 'puppet/type/nova_admin_tenant_id_setter'
provider_class = Puppet::Type.type(:nova_admin_tenant_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 v2.0/tenants
tenants_response = {
'tenants' => [
{
'name' => 'services',
'id' => 'UUID_SERVICES'
},
{
'name' => 'multiple_matches_tenant',
'id' => 'UUID1'
},
{
'name' => 'multiple_matches_tenant',
'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(:nova_admin_tenant_id_setter)' do
let :params do
{
:name => 'nova_admin_tenant_id',
:tenant_name => 'services',
:auth_username => 'nova',
:auth_password => 'secret',
:auth_tenant_name => 'admin',
:auth_url => 'http://127.0.0.1:35357/v2.0',
}
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/v2.0/tenants").
with(:headers => {'X-Auth-Token'=>'TOKEN'}).
to_return(:status => 200,
:body => tenants_response.to_json,
:headers => {})
end
it 'should create a resource' do
resource = Puppet::Type::Nova_admin_tenant_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 tenant that does not exist?
context 'when tenant 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/v2.0/tenants").
with(:headers => {'X-Auth-Token'=>'TOKEN'}).
to_return(:status => 200,
:body => tenants_response.to_json,
:headers => {})
params.merge!(:tenant_name => 'bad_tenant_name')
end
it 'should receive an api error' do
resource = Puppet::Type::Nova_admin_tenant_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 tenant/
end
end
# What happens if we ask for a tenant name that results in multiple
# matches?
context 'when there are multiple matching tenants' 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/v2.0/tenants").
with(:headers => {'X-Auth-Token'=>'TOKEN'}).
to_return(:status => 200,
:body => tenants_response.to_json,
:headers => {})
params.merge!(:tenant_name => 'multiple_matches_tenant')
end
it 'should receive an api error' do
resource = Puppet::Type::Nova_admin_tenant_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 tenant 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::Nova_admin_tenant_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::Nova_admin_tenant_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::Nova_admin_tenant_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