Merge "Keystone_endpoint match service by name/type."

This commit is contained in:
Jenkins 2015-11-24 15:29:02 +00:00 committed by Gerrit Code Review
commit 8f10d5dfdf
8 changed files with 523 additions and 61 deletions

View File

@ -97,8 +97,31 @@ keystone_service { 'nova':
description => 'Openstack Compute Service',
}
```
Services can also be written with the type as a suffix:
```puppet
keystone_service { 'nova::type':
ensure => present,
description => 'Openstack Compute Service',
}
# Setup nova keystone endpoint
keystone_endpoint { 'example-1-west/nova':
ensure => present,
type => 'compute',
public_url => "http://127.0.0.1:8774/v2/%(tenant_id)s",
admin_url => "http://127.0.0.1:8774/v2/%(tenant_id)s",
internal_url => "http://127.0.0.1:8774/v2/%(tenant_id)s",
}
```
Endpoints can also be written with the type as a suffix:
```puppet
keystone_endpoint { 'example-1-west/nova::compute':
ensure => present,
public_url => "http://127.0.0.1:8774/v2/%(tenant_id)s",
admin_url => "http://127.0.0.1:8774/v2/%(tenant_id)s",
@ -106,6 +129,11 @@ keystone_endpoint { 'example-1-west/nova':
}
```
Defining a endpoint without the type is supported in Liberty release
for backward compatibility, but will be dropped in Mitaka, as this can
lead to corruption of the endpoint database if omitted. See (this
bug)[https://bugs.launchpad.net/puppet-keystone/+bug/1506996]
**Setting up a database for keystone**
A keystone database can be configured separately from the keystone services.

View File

@ -7,6 +7,10 @@ Puppet::Type.type(:keystone_endpoint).provide(
desc "Provider to manage keystone endpoints."
include PuppetX::Keystone::CompositeNamevar::Helpers
@endpoints = nil
@services = nil
@credentials = Puppet::Provider::Openstack::CredentialsV3.new
def initialize(value={})
@ -15,16 +19,42 @@ Puppet::Type.type(:keystone_endpoint).provide(
end
def create
region, name = resource[:name].split('/')
# Reset the cache.
self.class.services = nil
name = resource[:name]
region = resource[:region]
type = resource[:type]
type = self.class.type_from_service(name) unless set?(:type)
@property_hash[:type] = type
services = self.class.services.find_all { |s| s[:name] == name }
service = services.find { |s| s[:type] == type }
if service.nil? && services.count == 1
# For backward comptatibility, match the service by name only.
name = services[0][:id]
else
# Math the service by id.
name = service[:id] if service
end
ids = []
created = false
[:admin_url, :internal_url, :public_url].each do |scope|
if resource[scope]
ids << endpoint_create(name, region, scope.to_s.sub(/_url$/,''),
resource[scope])[:id]
created = true
ids << endpoint_create(name, region, scope.to_s.sub(/_url$/, ''),
resource[scope])[:id]
end
end
@property_hash[:id] = ids.join(',')
@property_hash[:ensure] = :present
if created
@property_hash[:id] = ids.join(',')
@property_hash[:ensure] = :present
else
warning('Specifying a keystone_endpoint without an ' \
'admin_url/public_url/internal_url ' \
"won't create the endpoint at all, despite what Puppet is saying.")
@property_hash[:ensure] = :absent
end
end
def destroy
@ -53,21 +83,22 @@ Puppet::Type.type(:keystone_endpoint).provide(
@property_flush[:admin_url] = value
end
def region=(value)
raise(Puppet::Error, "Updating the endpoint's region is not currently supported.")
def region=(_)
fail(Puppet::Error, "Updating the endpoint's region is not currently supported.")
end
def self.instances
names=[]
list=[]
endpoints = request('endpoint', 'list')
names = []
list = []
endpoints.each do |current|
name = "#{current[:region]}/#{current[:service_name]}"
name = transform_name(current[:region], current[:service_name], current[:service_type])
unless names.include?(name)
names << name
endpoint = { :name => name, current[:interface].to_sym => current }
endpoints.each do |ep_osc|
if (ep_osc[:id] != current[:id]) && (ep_osc[:service_name] == current[:service_name])
if (ep_osc[:id] != current[:id]) &&
(ep_osc[:service_name] == current[:service_name]) &&
(ep_osc[:service_type] == current[:service_type])
endpoint.merge!(ep_osc[:interface].to_sym => ep_osc)
end
end
@ -88,11 +119,11 @@ Puppet::Type.type(:keystone_endpoint).provide(
end
def self.prefetch(resources)
endpoints = instances
resources.keys.each do |name|
if provider = endpoints.find{ |endpoint| endpoint.name == name }
resources[name].provider = provider
end
prefetch_composite(resources) do |sorted_namevars|
name = sorted_namevars[0]
region = sorted_namevars[1]
type = sorted_namevars[2]
transform_name(region, name, type)
end
end
@ -118,4 +149,74 @@ Puppet::Type.type(:keystone_endpoint).provide(
properties = [name, interface, url, '--region', region]
self.class.request('endpoint', 'create', properties)
end
private
def self.endpoints
return @endpoints unless @endpoints.nil?
@endpoints = request('endpoint', 'list')
end
def self.endpoints=(value)
@endpoints = value
end
def self.services
return @services unless @services.nil?
@services = request('service', 'list')
end
def self.services=(value)
@services = value
end
def self.endpoint_from_region_name(region, name)
endpoints.find_all { |e| e[:region] == region && e[:service_name] == name }
.map { |e| e[:service_type] }.uniq
end
def self.type_from_service(name)
types = services.find_all { |s| s[:name] == name }.map { |e| e[:type] }.uniq
if types.count == 1
types[0]
else
# We don't fail here as it can happen during a ensure => absent.
PuppetX::Keystone::CompositeNamevar::Unset
end
end
def self.service_type(services, region, name)
nbr_of_services = services.count
err_msg = ["endpoint matching #{region}/#{name}:"]
type = nil
case
when nbr_of_services == 1
type = services[0]
when nbr_of_services > 1
err_msg += [endpoint_from_region_name(region, name).join(' ')]
when nbr_of_services < 1
# Then we try to get the type by service name.
type = type_from_service(name)
end
if !type.nil?
type
else
fail(Puppet::Error, 'Cannot get the correct endpoint type: ' \
"#{err_msg.join(' ')}")
end
end
def self.transform_name(region, name, type)
if type == PuppetX::Keystone::CompositeNamevar::Unset
type = service_type(endpoint_from_region_name(region, name), region, name)
end
if type == PuppetX::Keystone::CompositeNamevar::Unset
Puppet.debug("Could not find the type for endpoint #{region}/#{name}")
"#{region}/#{name}"
else
"#{region}/#{name}::#{type}"
end
end
end

View File

@ -1,34 +1,43 @@
# 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_x/keystone/composite_namevar'
require 'puppet_x/keystone/type'
Puppet::Type.newtype(:keystone_endpoint) do
desc 'Type for managing keystone endpoints.'
include PuppetX::Keystone::CompositeNamevar::Helpers
ensurable
newparam(:name, :namevar => true) do
newvalues(/\S+\/\S+/)
end
newparam(:name, :namevar => true)
newproperty(:id) do
validate do |v|
raise(Puppet::Error, 'This is a read only property')
include PuppetX::Keystone::Type::ReadOnly
end
newparam(:region) do
isnamevar
include PuppetX::Keystone::Type::Required
end
newparam(:type) do
isnamevar
defaultto do
deprecation_msg = 'Support for a endpoint without the type ' \
'set is deprecated in Liberty. ' \
'It will be dropped in Mitaka.'
warning(deprecation_msg)
PuppetX::Keystone::CompositeNamevar::Unset
end
end
newproperty(:region) do
end
newproperty(:public_url)
newproperty(:public_url) do
end
newproperty(:internal_url)
newproperty(:internal_url) do
end
newproperty(:admin_url) do
end
newproperty(:admin_url)
# we should not do anything until the keystone service is started
autorequire(:anchor) do
@ -36,7 +45,54 @@ Puppet::Type.newtype(:keystone_endpoint) do
end
autorequire(:keystone_service) do
(region, service_name) = self[:name].split('/')
[service_name]
if parameter_set?(:type)
"#{name}::#{self[:type]}"
else
title = catalog.resources
.find_all { |e| e.type == :keystone_service && e[:name] == name }
.map { |e| e.title }.uniq
if title.count == 1
title
else
warning("Couldn't find the type of the domain to require using #{name}")
name
end
end
end
def self.title_patterns
name = PuppetX::Keystone::CompositeNamevar.not_two_colon_regex
type = Regexp.new(/.+/)
region = Regexp.new(/[^\/]+/)
[
[
/^(#{region})\/(#{name})::(#{type})$/,
[
[:region],
[:name],
[:type]
]
],
[
/^(#{region})\/(#{name})$/,
[
[:region],
[:name]
]
],
[
/^(#{name})::(#{type})$/,
[
[:name],
[:type]
]
],
[
/^(#{name})$/,
[
[:name]
]
]
]
end
end

View File

@ -5,8 +5,13 @@ module PuppetX
def self.included(klass)
klass.class_eval do
defaultto do
custom = ''
if respond_to?(:required_custom_message)
custom = send(:required_custom_message)
end
fail(Puppet::ResourceError,
"Parameter #{name} failed on " \
"#{custom}" \
"Parameter #{name} failed on " \
"#{resource.class.to_s.split('::')[-1]}[#{resource.name}]: " \
'Required parameter.')
end

View File

@ -234,16 +234,49 @@ describe 'basic keystone server with resources' do
end
end
end
describe 'composite namevar for keystone_service' do
describe 'composite namevar for keystone_service and keystone_endpoint' do
let(:pp) do
<<-EOM
keystone_service { 'service_1::type_1': ensure => present }
keystone_service { 'service_1': type => 'type_2', ensure => present }
keystone_endpoint { 'RegionOne/service_1::type_2':
ensure => present,
public_url => 'http://public_service1_type2',
internal_url => 'http://internal_service1_type2',
admin_url => 'http://admin_service1_type2'
}
keystone_endpoint { 'service_1':
ensure => present,
region => 'RegionOne',
type => 'type_1',
public_url => 'http://public_url/',
internal_url => 'http://public_url/',
admin_url => 'http://public_url/'
}
EOM
end
it 'should be possible to create two services different only by their type' do
apply_manifest(pp, :catch_failures => true)
apply_manifest(pp, :catch_changes => true)
end
describe 'puppet service are created' do
it 'for service' do
shell('puppet resource keystone_service') do |result|
expect(result.stdout)
.to include_regexp([/keystone_service { 'service_1::type_1':/,
/keystone_service { 'service_1::type_2':/])
end
end
end
describe 'puppet endpoints are created' do
it 'for service' do
shell('puppet resource keystone_endpoint') do |result|
expect(result.stdout)
.to include_regexp([/keystone_endpoint { 'RegionOne\/service_1::type_1':/,
/keystone_endpoint { 'RegionOne\/service_1::type_2':/])
end
end
end
end
end

View File

@ -215,4 +215,48 @@ describe 'keystone server running with Apache/WSGI with resources' do
apply_manifest(pp, :catch_changes => true)
end
end
describe 'composite namevar for keystone_service and keystone_endpoint' do
let(:pp) do
<<-EOM
keystone_service { 'service_1::type_1': ensure => present }
keystone_service { 'service_1': type => 'type_2', ensure => present }
keystone_endpoint { 'RegionOne/service_1::type_2':
ensure => present,
public_url => 'http://public_service1_type2',
internal_url => 'http://internal_service1_type2',
admin_url => 'http://admin_service1_type2'
}
keystone_endpoint { 'service_1':
ensure => present,
region => 'RegionOne',
type => 'type_1',
public_url => 'http://public_url/',
internal_url => 'http://public_url/',
admin_url => 'http://public_url/'
}
EOM
end
it 'should be possible to create two services different only by their type' do
apply_manifest(pp, :catch_failures => true)
apply_manifest(pp, :catch_changes => true)
end
describe 'puppet service are created' do
it 'for service' do
shell('puppet resource keystone_service') do |result|
expect(result.stdout)
.to include_regexp([/keystone_service { 'service_1::type_1':/,
/keystone_service { 'service_1::type_2':/])
end
end
end
describe 'puppet endpoints are created' do
it 'for service' do
shell('puppet resource keystone_endpoint') do |result|
expect(result.stdout)
.to include_regexp([/keystone_endpoint { 'RegionOne\/service_1::type_1':/,
/keystone_endpoint { 'RegionOne\/service_1::type_2':/])
end
end
end
end
end

View File

@ -2,9 +2,7 @@ require 'puppet'
require 'spec_helper'
require 'puppet/provider/keystone_endpoint/openstack'
provider_class = Puppet::Type.type(:keystone_endpoint).provider(:openstack)
describe provider_class do
describe Puppet::Type.type(:keystone_endpoint).provider(:openstack) do
let(:set_env) do
ENV['OS_USERNAME'] = 'test'
@ -17,11 +15,11 @@ describe provider_class do
let(:endpoint_attrs) do
{
:name => 'region/endpoint',
:title => 'region/endpoint',
:ensure => 'present',
:public_url => 'http://127.0.0.1:5000',
:internal_url => 'http://127.0.0.1:5001',
:admin_url => 'http://127.0.0.1:5002',
:admin_url => 'http://127.0.0.1:5002'
}
end
@ -30,47 +28,79 @@ describe provider_class do
end
let(:provider) do
provider_class.new(resource)
described_class.new(resource)
end
before(:each) do
set_env
described_class.endpoints = nil
described_class.services = nil
end
describe '#create' do
it 'creates an endpoint' do
provider.class.expects(:openstack)
.with('endpoint', 'create', '--format', 'shell', ['endpoint', 'admin', 'http://127.0.0.1:5002', '--region', 'region'])
before(:each) do
described_class.expects(:openstack)
.with('endpoint', 'create', '--format', 'shell',
['service_id1', 'admin', 'http://127.0.0.1:5002', '--region', 'region'])
.returns('admin_url="http://127.0.0.1:5002"
id="endpoint1_id"
region="region"
')
provider.class.expects(:openstack)
.with('endpoint', 'create', '--format', 'shell', ['endpoint', 'internal', 'http://127.0.0.1:5001', '--region', 'region'])
described_class.expects(:openstack)
.with('endpoint', 'create', '--format', 'shell',
['service_id1', 'internal', 'http://127.0.0.1:5001', '--region', 'region'])
.returns('internal_url="http://127.0.0.1:5001"
id="endpoint2_id"
region="region"
')
provider.class.expects(:openstack)
.with('endpoint', 'create', '--format', 'shell', ['endpoint', 'public', 'http://127.0.0.1:5000', '--region', 'region'])
described_class.expects(:openstack)
.with('endpoint', 'create', '--format', 'shell',
['service_id1', 'public', 'http://127.0.0.1:5000', '--region', 'region'])
.returns('public_url="http://127.0.0.1:5000"
id="endpoint3_id"
region="region"
')
provider.create
expect(provider.exists?).to be_truthy
expect(provider.id).to eq('endpoint1_id,endpoint2_id,endpoint3_id')
described_class.expects(:openstack)
.with('service', 'list', '--quiet', '--format', 'csv', [])
.returns('"ID","Name","Type"
"service_id1","endpoint","type_one"
')
end
context 'without the type' do
it 'creates an endpoint' do
provider.create
expect(provider.exists?).to be_truthy
expect(provider.id).to eq('endpoint1_id,endpoint2_id,endpoint3_id')
end
end
context 'with the type' do
let(:endpoint_attrs) do
{
:title => 'region/endpoint',
:ensure => 'present',
:public_url => 'http://127.0.0.1:5000',
:internal_url => 'http://127.0.0.1:5001',
:admin_url => 'http://127.0.0.1:5002',
:type => 'type_one'
}
end
it 'creates an endpoint' do
provider.create
expect(provider.exists?).to be_truthy
expect(provider.id).to eq('endpoint1_id,endpoint2_id,endpoint3_id')
end
end
end
describe '#destroy' do
it 'destroys an endpoint' do
provider.instance_variable_get('@property_hash')[:id] = 'endpoint1_id,endpoint2_id,endpoint3_id'
provider.class.expects(:openstack)
described_class.expects(:openstack)
.with('endpoint', 'delete', 'endpoint1_id')
provider.class.expects(:openstack)
described_class.expects(:openstack)
.with('endpoint', 'delete', 'endpoint2_id')
provider.class.expects(:openstack)
described_class.expects(:openstack)
.with('endpoint', 'delete', 'endpoint3_id')
provider.destroy
expect(provider.exists?).to be_falsey
@ -80,7 +110,7 @@ region="region"
describe '#exists' do
context 'when tenant does not exist' do
subject(:response) do
response = provider.exists?
provider.exists?
end
it { is_expected.to be_falsey }
@ -89,16 +119,130 @@ region="region"
describe '#instances' do
it 'finds every tenant' do
provider.class.expects(:openstack)
described_class.expects(:openstack)
.with('endpoint', 'list', '--quiet', '--format', 'csv', [])
.returns('"ID","Region","Service Name","Service Type","Enabled","Interface","URL"
"endpoint1_id","RegionOne","keystone","identity",True,"admin","http://127.0.0.1:5002"
"endpoint2_id","RegionOne","keystone","identity",True,"internal","https://127.0.0.1:5001"
"endpoint3_id","RegionOne","keystone","identity",True,"public","https://127.0.0.1:5000"
')
instances = Puppet::Type::Keystone_endpoint::ProviderOpenstack.instances
instances = described_class.instances
expect(instances.count).to eq(1)
end
end
describe '#prefetch' do
context 'working: fq or nfq and matching resource' do
before(:each) do
described_class.expects(:openstack)
.with('endpoint', 'list', '--quiet', '--format', 'csv', [])
.returns('"ID","Region","Service Name","Service Type","Enabled","Interface","URL"
"endpoint1_id","RegionOne","keystone","identity",True,"admin","http://127.0.0.1:5002"
"endpoint2_id","RegionOne","keystone","identity",True,"internal","https://127.0.0.1:5001"
"endpoint3_id","RegionOne","keystone","identity",True,"public","https://127.0.0.1:5000"
')
end
context '#fq resource in title' do
let(:resources) do
[Puppet::Type.type(:keystone_endpoint).new(:title => 'RegionOne/keystone::identity', :ensure => :present),
Puppet::Type.type(:keystone_endpoint).new(:title => 'RegionOne/keystone::identityv3', :ensure => :present)]
end
include_examples 'prefetch the resources'
end
context '#fq resource' do
let(:resources) do
[Puppet::Type.type(:keystone_endpoint).new(:title => 'keystone', :region => 'RegionOne', :type => 'identity', :ensure => :present),
Puppet::Type.type(:keystone_endpoint).new(:title => 'RegionOne/keystone::identityv3', :ensure => :present)]
end
include_examples 'prefetch the resources'
end
context '#nfq resource in title matching existing endpoint' do
let(:resources) do
[Puppet::Type.type(:keystone_endpoint).new(:title => 'RegionOne/keystone', :ensure => :present),
Puppet::Type.type(:keystone_endpoint).new(:title => 'RegionOne/keystone::identityv3', :ensure => :present)]
end
include_examples 'prefetch the resources'
end
context '#nfq resource matching existing endpoint' do
let(:resources) do
[Puppet::Type.type(:keystone_endpoint).new(:title => 'keystone', :region => 'RegionOne', :ensure => :present),
Puppet::Type.type(:keystone_endpoint).new(:title => 'RegionOne/keystone::identityv3', :ensure => :present)]
end
include_examples 'prefetch the resources'
end
end
context 'not working' do
context 'too many type' do
before(:each) do
described_class.expects(:openstack)
.with('endpoint', 'list', '--quiet', '--format', 'csv', [])
.returns('"ID","Region","Service Name","Service Type","Enabled","Interface","URL"
"endpoint1_id","RegionOne","keystone","identity",True,"admin","http://127.0.0.1:5002"
"endpoint2_id","RegionOne","keystone","identity",True,"internal","https://127.0.0.1:5001"
"endpoint3_id","RegionOne","keystone","identity",True,"public","https://127.0.0.1:5000"
"endpoint4_id","RegionOne","keystone","identityv3",True,"admin","http://127.0.0.1:5002"
"endpoint5_id","RegionOne","keystone","identityv3",True,"internal","https://127.0.0.1:5001"
"endpoint6_id","RegionOne","keystone","identityv3",True,"public","https://127.0.0.1:5000"
')
end
it "should fail as it's not possible to get the right type here" do
existing = Puppet::Type.type(:keystone_endpoint)
.new(:title => 'RegionOne/keystone', :ensure => :present)
resource = mock
r = []
r << existing
catalog = Puppet::Resource::Catalog.new
r.each { |res| catalog.add_resource(res) }
m_value = mock
m_first = mock
resource.expects(:values).returns(m_value)
m_value.expects(:first).returns(m_first)
m_first.expects(:catalog).returns(catalog)
m_first.expects(:class).returns(described_class.resource_type)
expect { described_class.prefetch(resource) }
.to raise_error(Puppet::Error,
/endpoint matching RegionOne\/keystone: identity identityv3/)
end
end
end
context 'not any type but existing service' do
before(:each) do
described_class.expects(:openstack)
.with('endpoint', 'list', '--quiet', '--format', 'csv', [])
.returns('"ID","Region","Service Name","Service Type","Enabled","Interface","URL"
"endpoint1_id","RegionOne","keystone","identity",True,"admin","http://127.0.0.1:5002"
"endpoint2_id","RegionOne","keystone","identity",True,"internal","https://127.0.0.1:5001"
"endpoint3_id","RegionOne","keystone","identity",True,"public","https://127.0.0.1:5000"
')
described_class.expects(:openstack)
.with('service', 'list', '--quiet', '--format', 'csv', [])
.returns('"ID","Name","Type"
"service1_id","keystonev3","identity"
')
end
it 'should be successful' do
existing = Puppet::Type.type(:keystone_endpoint)
.new(:title => 'RegionOne/keystonev3', :ensure => :present)
resource = mock
r = []
r << existing
catalog = Puppet::Resource::Catalog.new
r.each { |res| catalog.add_resource(res) }
m_value = mock
m_first = mock
resource.expects(:values).returns(m_value)
m_value.expects(:first).returns(m_first)
m_first.expects(:catalog).returns(catalog)
m_first.expects(:class).returns(described_class.resource_type)
expect { described_class.prefetch(resource) }.not_to raise_error
expect(existing.provider.ensure).to eq(:absent)
end
end
end
end
end

View File

@ -1,9 +1,60 @@
require 'spec_helper'
require 'puppet'
require 'puppet/type/keystone_endpoint'
describe Puppet::Type.type(:keystone_endpoint) do
it 'should fail when the namevar does not contain a region' do
expect do
Puppet::Type.type(:keystone_endpoint).new(:name => 'foo')
end.to raise_error(Puppet::Error, /Invalid value/)
describe 'region_one/endpoint_name::type_one' do
include_examples 'parse title correctly',
:name => 'endpoint_name',
:region => 'region_one',
:type => 'type_one'
end
describe 'new_endpoint_without_region::type' do
include_examples 'croak on the required parameter',
'Parameter region failed on Keystone_endpoint[new_endpoint_without_region]:'
end
describe '#autorequire' do
let(:service_one) do
Puppet::Type.type(:keystone_service).new(:title => 'service_one', :type => 'type_one')
end
let(:service_two) do
Puppet::Type.type(:keystone_service).new(:title => 'service_one::type_two')
end
let(:service_three) do
Puppet::Type.type(:keystone_service).new(:title => 'service_two::type_one')
end
context 'domain autorequire from title' do
let(:endpoint) do
Puppet::Type.type(:keystone_endpoint).new(:title => 'region_one/service_one::type_one')
end
describe 'should require the correct domain' do
let(:resources) { [endpoint, service_one, service_two] }
include_examples 'autorequire the correct resources'
end
end
context 'domain autorequire from title without type (to be removed at Mitaka)' do
let(:endpoint) do
Puppet::Type.type(:keystone_endpoint).new(:title => 'region_one/service_one')
end
describe 'should require the correct domain' do
let(:resources) { [endpoint, service_one, service_two] }
include_examples 'autorequire the correct resources'
end
end
context 'domain autorequire from title without type on fq service name (to be removed at Mitaka)' do
let(:endpoint) do
Puppet::Type.type(:keystone_endpoint).new(:title => 'region_one/service_two')
end
describe 'should require the correct domain' do
let(:resources) { [endpoint, service_three, service_one] }
include_examples 'autorequire the correct resources'
end
end
end
end