From f9b3d184a05978668ee5960c83836b56e02636ce Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Thu, 17 Apr 2014 15:15:31 -0400 Subject: [PATCH] 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 Change-Id: I57af7cb47a12adabed9b1ee65e1c352b3e5fc2de Closes-bug: 1306694 --- .gitignore | 2 + Gemfile | 3 + examples/neutron.pp | 6 + .../ini_setting.rb | 183 ++++++++++++++++++ lib/puppet/type/neutron_config.rb | 5 + .../type/nova_admin_tenant_id_setter.rb | 32 +++ manifests/server/notifications.pp | 112 +++++++++++ .../neutron_server_notifications_spec.rb | 150 ++++++++++++++ spec/spec_helper.rb | 10 +- .../neutron_spec.rb | 177 +++++++++++++++++ 10 files changed, 678 insertions(+), 2 deletions(-) create mode 100644 lib/puppet/provider/nova_admin_tenant_id_setter/ini_setting.rb create mode 100644 lib/puppet/type/nova_admin_tenant_id_setter.rb create mode 100644 manifests/server/notifications.pp create mode 100644 spec/classes/neutron_server_notifications_spec.rb create mode 100644 spec/unit/provider/nova_admin_tenant_id_setter/neutron_spec.rb diff --git a/.gitignore b/.gitignore index 923be4922..da4238187 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ spec/fixtures/modules/* spec/fixtures/manifests/site.pp Gemfile.lock .vendor +.bundle/ +vendor/ diff --git a/Gemfile b/Gemfile index 0d35201b4..972d1200c 100644 --- a/Gemfile +++ b/Gemfile @@ -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'] diff --git a/examples/neutron.pp b/examples/neutron.pp index 2d70c2202..7522e5ef2 100644 --- a/examples/neutron.pp +++ b/examples/neutron.pp @@ -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': } diff --git a/lib/puppet/provider/nova_admin_tenant_id_setter/ini_setting.rb b/lib/puppet/provider/nova_admin_tenant_id_setter/ini_setting.rb new file mode 100644 index 000000000..5f1b127c8 --- /dev/null +++ b/lib/puppet/provider/nova_admin_tenant_id_setter/ini_setting.rb @@ -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 + diff --git a/lib/puppet/type/neutron_config.rb b/lib/puppet/type/neutron_config.rb index 91428be73..b4b0da42a 100644 --- a/lib/puppet/type/neutron_config.rb +++ b/lib/puppet/type/neutron_config.rb @@ -15,4 +15,9 @@ Puppet::Type.newtype(:neutron_config) do value end end + + def create + provider.create + end + end diff --git a/lib/puppet/type/nova_admin_tenant_id_setter.rb b/lib/puppet/type/nova_admin_tenant_id_setter.rb new file mode 100644 index 000000000..d79e86c72 --- /dev/null +++ b/lib/puppet/type/nova_admin_tenant_id_setter.rb @@ -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 + diff --git a/manifests/server/notifications.pp b/manifests/server/notifications.pp new file mode 100644 index 000000000..695ba2124 --- /dev/null +++ b/manifests/server/notifications.pp @@ -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, + } + } +} diff --git a/spec/classes/neutron_server_notifications_spec.rb b/spec/classes/neutron_server_notifications_spec.rb new file mode 100644 index 000000000..0aae43546 --- /dev/null +++ b/spec/classes/neutron_server_notifications_spec.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 53d4dd02d..a21fd4305 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 diff --git a/spec/unit/provider/nova_admin_tenant_id_setter/neutron_spec.rb b/spec/unit/provider/nova_admin_tenant_id_setter/neutron_spec.rb new file mode 100644 index 000000000..69b20795a --- /dev/null +++ b/spec/unit/provider/nova_admin_tenant_id_setter/neutron_spec.rb @@ -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 +