1
0
Fork 0
mirror of https://github.com/fog/fog.git synced 2022-11-09 13:51:43 -05:00

Merge pull request #1146 from juliohr/rackspace-block-storage

Rackspace Block Storage
This commit is contained in:
Brad Gignac 2012-09-14 08:30:29 -07:00
commit c1e2676ae2
36 changed files with 973 additions and 8 deletions

View file

@ -3,6 +3,8 @@ class Rackspace < Fog::Bin
def class_for(key)
case key
when :block_storage
Fog::Rackspace::BlockStorage
when :cdn
Fog::CDN::Rackspace
when :compute

View file

@ -42,14 +42,15 @@ module Fog
end
end
service(:cdn, 'rackspace/cdn', 'CDN')
service(:compute, 'rackspace/compute', 'Compute')
service(:compute_v2, 'rackspace/compute_v2', 'Compute v2')
service(:dns, 'rackspace/dns', 'DNS')
service(:storage, 'rackspace/storage', 'Storage')
service(:load_balancers, 'rackspace/load_balancers', 'LoadBalancers')
service(:identity, 'rackspace/identity', 'Identity')
service(:databases, 'rackspace/databases', 'Databases')
service(:block_storage, 'rackspace/block_storage', 'BlockStorage')
service(:cdn, 'rackspace/cdn', 'CDN')
service(:compute, 'rackspace/compute', 'Compute')
service(:compute_v2, 'rackspace/compute_v2', 'Compute v2')
service(:dns, 'rackspace/dns', 'DNS')
service(:storage, 'rackspace/storage', 'Storage')
service(:load_balancers, 'rackspace/load_balancers', 'LoadBalancers')
service(:identity, 'rackspace/identity', 'Identity')
service(:databases, 'rackspace/databases', 'Databases')
def self.authenticate(options, connection_options = {})
rackspace_auth_url = options[:rackspace_auth_url] || "auth.api.rackspacecloud.com"

View file

@ -0,0 +1,114 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'rackspace'))
module Fog
module Rackspace
class BlockStorage < Fog::Service
class ServiceError < Fog::Rackspace::Errors::ServiceError; end
class InternalServerError < Fog::Rackspace::Errors::InternalServerError; end
class BadRequest < Fog::Rackspace::Errors::BadRequest; end
DFW_ENDPOINT = 'https://dfw.blockstorage.api.rackspacecloud.com/v1'
LON_ENDPOINT = 'https://lon.blockstorage.api.rackspacecloud.com/v1'
ORD_ENDPOINT = 'https://ord.blockstorage.api.rackspacecloud.com/v1'
requires :rackspace_api_key, :rackspace_username
recognizes :rackspace_auth_url
recognizes :rackspace_endpoint
model_path 'fog/rackspace/models/block_storage'
model :volume
collection :volumes
model :volume_type
collection :volume_types
model :snapshot
collection :snapshots
model :snapshot
collection :snapshots
request_path 'fog/rackspace/requests/block_storage'
request :create_volume
request :delete_volume
request :get_volume
request :list_volumes
request :get_volume_type
request :list_volume_types
request :create_snapshot
request :delete_snapshot
request :get_snapshot
request :list_snapshots
class Mock
def request(params)
Fog::Mock.not_implemented
end
end
class Real
def initialize(options = {})
@rackspace_api_key = options[:rackspace_api_key]
@rackspace_username = options[:rackspace_username]
@rackspace_auth_url = options[:rackspace_auth_url]
@rackspace_must_reauthenticate = false
@connection_options = options[:connection_options] || {}
endpoint = options[:rackspace_endpoint] || DFW_ENDPOINT
uri = URI.parse(endpoint)
@host = uri.host
@persistent = options[:persistent] || false
@path = uri.path
@port = uri.port
@scheme = uri.scheme
authenticate
@connection = Fog::Connection.new(uri.to_s, @persistent, @connection_options)
end
def request(params)
begin
response = @connection.request(params.merge!({
:headers => {
'Content-Type' => 'application/json',
'X-Auth-Token' => @auth_token
}.merge!(params[:headers] || {}),
:host => @host,
:path => "#{@path}/#{params[:path]}"
}))
rescue Excon::Errors::NotFound => error
raise NotFound.slurp error
rescue Excon::Errors::BadRequest => error
raise BadRequest.slurp error
rescue Excon::Errors::InternalServerError => error
raise InternalServerError.slurp error
rescue Excon::Errors::HTTPStatusError => error
raise ServiceError.slurp error
end
unless response.body.empty?
response.body = Fog::JSON.decode(response.body)
end
response
end
private
def authenticate
options = {
:rackspace_api_key => @rackspace_api_key,
:rackspace_username => @rackspace_username,
:rackspace_auth_url => @rackspace_auth_url
}
credentials = Fog::Rackspace.authenticate(options, @connection_options)
@auth_token = credentials['X-Auth-Token']
account_id = credentials['X-Server-Management-Url'].match(/.*\/([\d]+)$/)[1]
@path = "#{@path}/#{account_id}"
end
end
end
end
end

View file

@ -24,6 +24,8 @@ module Fog
collection :flavors
model :image
collection :images
model :attachments
collection :attachments
request_path 'fog/rackspace/requests/compute_v2'
request :list_servers
@ -44,6 +46,11 @@ module Fog
request :list_flavors
request :get_flavor
request :attach_volume
request :get_attachment
request :list_attachments
request :delete_attachment
class Mock
def request(params)
Fog::Mock.not_implemented

View file

@ -0,0 +1,46 @@
require 'fog/core/model'
module Fog
module Rackspace
class BlockStorage
class Snapshot < Fog::Model
AVAILABLE = 'available'
CREATING = 'creating'
DELETING = 'deleting'
ERROR = 'error'
ERROR_DELETING = 'error_deleting'
identity :id
attribute :created_at, :aliases => 'createdAt'
attribute :state, :aliases => 'status'
attribute :display_name
attribute :display_description
attribute :size
attribute :volume_id
attribute :availability_zone
def ready?
state == AVAILABLE
end
def save(force = false)
requires :volume_id
data = connection.create_snapshot(volume_id, {
:display_name => display_name,
:display_description => display_description,
:force => force
})
merge_attributes(data.body['snapshot'])
true
end
def destroy
requires :identity
connection.delete_snapshot(identity)
true
end
end
end
end
end

View file

@ -0,0 +1,25 @@
require 'fog/core/collection'
require 'fog/rackspace/models/block_storage/snapshot'
module Fog
module Rackspace
class BlockStorage
class Snapshots < Fog::Collection
model Fog::Rackspace::BlockStorage::Snapshot
def all
data = connection.list_snapshots.body['snapshots']
load(data)
end
def get(snapshot_id)
data = connection.get_snapshot(snapshot_id).body['snapshot']
new(data)
rescue Fog::Rackspace::BlockStorage::NotFound
nil
end
end
end
end
end

View file

@ -0,0 +1,58 @@
require 'fog/core/model'
module Fog
module Rackspace
class BlockStorage
class Volume < Fog::Model
AVAILABLE = 'available'
ATTACHING = 'attaching'
CREATING = 'creating'
DELETING = 'deleting'
ERROR = 'error'
ERROR_DELETING = 'error_deleting'
IN_USE = 'in-use'
identity :id
attribute :created_at, :aliases => 'createdAt'
attribute :state, :aliases => 'status'
attribute :display_name
attribute :display_description
attribute :size
attribute :attachments
attribute :volume_type
attribute :availability_zone
def ready?
state == AVAILABLE
end
def attached?
state == IN_USE
end
def snapshots
connection.snapshots.select { |s| s.volume_id == identity }
end
def save
requires :size
data = connection.create_volume(size, {
:display_name => display_name,
:display_description => display_description,
:volume_type => volume_type,
:availability_zone => availability_zone
})
merge_attributes(data.body['volume'])
true
end
def destroy
requires :identity
connection.delete_volume(identity)
true
end
end
end
end
end

View file

@ -0,0 +1,14 @@
require 'fog/core/model'
module Fog
module Rackspace
class BlockStorage
class VolumeType < Fog::Model
identity :id
attribute :name
attribute :extra_specs
end
end
end
end

View file

@ -0,0 +1,25 @@
require 'fog/core/collection'
require 'fog/rackspace/models/block_storage/volume_type'
module Fog
module Rackspace
class BlockStorage
class VolumeTypes < Fog::Collection
model Fog::Rackspace::BlockStorage::VolumeType
def all
data = connection.list_volume_types.body['volume_types']
load(data)
end
def get(volume_type_id)
data = connection.get_volume_type(volume_type_id).body['volume_type']
new(data)
rescue Fog::Rackspace::BlockStorage::NotFound
nil
end
end
end
end
end

View file

@ -0,0 +1,25 @@
require 'fog/core/collection'
require 'fog/rackspace/models/block_storage/volume'
module Fog
module Rackspace
class BlockStorage
class Volumes < Fog::Collection
model Fog::Rackspace::BlockStorage::Volume
def all
data = connection.list_volumes.body['volumes']
load(data)
end
def get(volume_id)
data = connection.get_volume(volume_id).body['volume']
new(data)
rescue Fog::Rackspace::BlockStorage::NotFound
nil
end
end
end
end
end

View file

@ -0,0 +1,34 @@
require 'fog/core/model'
module Fog
module Compute
class RackspaceV2
class Attachment < Fog::Model
identity :id
attribute :server_id, :aliases => 'serverId'
attribute :volume_id, :aliases => 'volumeId'
attribute :device
def save
requires :server, :identity, :device
data = connection.attach_volume(server.identity, identity, device)
merge_attributes(data.body['volumeAttachment'])
true
end
def destroy
requires :server, :identity
connection.delete_attachment(server.identity, identity)
true
end
private
def server
collection.server
end
end
end
end
end

View file

@ -0,0 +1,25 @@
require 'fog/core/collection'
require 'fog/rackspace/models/compute_v2/attachment'
module Fog
module Compute
class RackspaceV2
class Attachments < Fog::Collection
model Fog::Compute::RackspaceV2::Attachment
attr_accessor :server
def all
data = connection.list_attachments(server.id).body['volumeAttachments']
load(data)
end
def get(volume_id)
data = connection.get_attachment(server.id, volume_id).body['volumeAttachment']
data && new(data)
end
end
end
end
end

View file

@ -82,6 +82,15 @@ module Fog
@image ||= connection.images.get(image_id)
end
def attachments
@attachments ||= begin
Fog::Compute::RackspaceV2::Attachments.new({
:connection => connection,
:server => self
})
end
end
def ready?
state == ACTIVE
end

View file

@ -0,0 +1,26 @@
module Fog
module Rackspace
class BlockStorage
class Real
def create_snapshot(volume_id, options = {})
data = {
'snapshot' => {
'volume_id' => volume_id
}
}
data['snapshot']['display_name'] = options[:display_name] unless options[:display_name].nil?
data['snapshot']['display_description'] = options[:display_description] unless options[:display_description].nil?
data['snapshot']['force'] = options[:force] unless options[:force].nil?
request(
:body => Fog::JSON.encode(data),
:expects => [200],
:method => 'POST',
:path => "snapshots"
)
end
end
end
end
end

View file

@ -0,0 +1,27 @@
module Fog
module Rackspace
class BlockStorage
class Real
def create_volume(size, options = {})
data = {
'volume' => {
'size' => size
}
}
data['volume']['display_name'] = options[:display_name] unless options[:display_name].nil?
data['volume']['display_description'] = options[:display_description] unless options[:display_description].nil?
data['volume']['volume_type'] = options[:volume_type] unless options[:volume_type].nil?
data['volume']['availability_zone'] = options[:availability_zone] unless options[:availability_zone].nil?
request(
:body => Fog::JSON.encode(data),
:expects => [200],
:method => 'POST',
:path => "volumes"
)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module Fog
module Rackspace
class BlockStorage
class Real
def delete_snapshot(snapshot_id)
request(
:expects => [202],
:method => 'DELETE',
:path => "snapshots/#{snapshot_id}"
)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module Fog
module Rackspace
class BlockStorage
class Real
def delete_volume(volume_id)
request(
:expects => [202],
:method => 'DELETE',
:path => "volumes/#{volume_id}"
)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module Fog
module Rackspace
class BlockStorage
class Real
def get_snapshot(snapshot_id)
request(
:expects => [200],
:method => 'GET',
:path => "snapshots/#{snapshot_id}"
)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module Fog
module Rackspace
class BlockStorage
class Real
def get_volume(volume_id)
request(
:expects => [200],
:method => 'GET',
:path => "volumes/#{volume_id}"
)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module Fog
module Rackspace
class BlockStorage
class Real
def get_volume_type(volume_type_id)
request(
:expects => [200],
:method => 'GET',
:path => "types/#{volume_type_id}"
)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module Fog
module Rackspace
class BlockStorage
class Real
def list_snapshots
request(
:expects => [200],
:method => 'GET',
:path => 'snapshots'
)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module Fog
module Rackspace
class BlockStorage
class Real
def list_volume_types
request(
:expects => [200],
:method => 'GET',
:path => 'types'
)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module Fog
module Rackspace
class BlockStorage
class Real
def list_volumes
request(
:expects => [200],
:method => 'GET',
:path => 'volumes'
)
end
end
end
end
end

View file

@ -0,0 +1,23 @@
module Fog
module Compute
class RackspaceV2
class Real
def attach_volume(server_id, volume_id, device)
data = {
'volumeAttachment' => {
'volumeId' => volume_id,
'device' => device
}
}
request(
:body => Fog::JSON.encode(data),
:expects => [200],
:method => 'POST',
:path => "servers/#{server_id}/os-volume_attachments"
)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module Fog
module Compute
class RackspaceV2
class Real
def delete_attachment(server_id, volume_id)
request(
:expects => [202],
:method => 'DELETE',
:path => "servers/#{server_id}/os-volume_attachments/#{volume_id}"
)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module Fog
module Compute
class RackspaceV2
class Real
def get_attachment(server_id, volume_id)
request(
:expects => [200, 203, 300],
:method => 'GET',
:path => "servers/#{server_id}/os-volume_attachments/#{volume_id}"
)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module Fog
module Compute
class RackspaceV2
class Real
def list_attachments(server_id)
request(
:expects => [200, 203, 300],
:method => 'GET',
:path => "servers/#{server_id}/os-volume_attachments"
)
end
end
end
end
end

View file

@ -0,0 +1,20 @@
Shindo.tests('Fog::Rackspace::BlockStorage | snapshot', ['rackspace']) do
pending if Fog.mocking?
service = Fog::Rackspace::BlockStorage.new
volume = service.volumes.create({
:display_name => "fog_#{Time.now.to_i.to_s}",
:size => 100
})
volume.wait_for { ready? }
options = { :display_name => "fog_#{Time.now.to_i.to_s}", :volume_id => volume.id }
model_tests(service.snapshots, options, false) do
@instance.wait_for { ready? }
end
volume.wait_for { snapshots.empty? }
volume.destroy
end

View file

@ -0,0 +1,20 @@
Shindo.tests('Fog::Rackspace::BlockStorage | snapshots', ['rackspace']) do
pending if Fog.mocking?
service = Fog::Rackspace::BlockStorage.new
volume = service.volumes.create({
:display_name => "fog_#{Time.now.to_i.to_s}",
:size => 100
})
volume.wait_for { ready? }
options = { :display_name => "fog_#{Time.now.to_i.to_s}", :volume_id => volume.id }
collection_tests(service.snapshots, options, false) do
@instance.wait_for { ready? }
end
volume.wait_for { snapshots.empty? }
volume.destroy
end

View file

@ -0,0 +1,27 @@
Shindo.tests('Fog::Rackspace::BlockStorage | volume', ['rackspace']) do
pending if Fog.mocking?
service = Fog::Rackspace::BlockStorage.new
options = { :display_name => "fog_#{Time.now.to_i.to_s}", :size => 100 }
model_tests(service.volumes, options, false) do
@instance.wait_for { ready? }
tests('#attached?').succeeds do
@instance.state = 'in-use'
returns(true) { @instance.attached? }
end
tests('#snapshots').succeeds do
snapshot = service.snapshots.create({ :volume_id => @instance.id })
snapshot.wait_for { ready? }
returns(true) { @instance.snapshots.first.id == snapshot.id }
snapshot.destroy
end
@instance.wait_for { snapshots.empty? }
end
end

View file

@ -0,0 +1,20 @@
Shindo.tests('Fog::Rackspace::BlockStorage | volume_types', ['rackspace']) do
pending if Fog.mocking?
service = Fog::Rackspace::BlockStorage.new
tests("success") do
tests("#all").succeeds do
service.volume_types.all
end
tests("#get").succeeds do
service.volume_types.get(1)
end
end
tests("failure").returns(nil) do
service.volume_types.get('some_random_identity')
end
end

View file

@ -0,0 +1,11 @@
Shindo.tests('Fog::Rackspace::BlockStorage | volumes', ['rackspace']) do
pending if Fog.mocking?
service = Fog::Rackspace::BlockStorage.new
options = { :display_name => "fog_#{Time.now.to_i.to_s}", :size => 100 }
collection_tests(service.volumes, options, false) do
@instance.wait_for { ready? }
end
end

View file

@ -0,0 +1,81 @@
Shindo.tests('Fog::Rackspace::BlockStorage | snapshot_tests', ['rackspace']) do
pending if Fog.mocking?
SNAPSHOT_FORMAT = {
'id' => String,
'status' => String,
'display_name' => Fog::Nullable::String,
'display_description' => Fog::Nullable::String,
'volume_id' => String,
'size' => Integer,
'created_at' => String
}
GET_SNAPSHOT_FORMAT = {
'snapshot' => SNAPSHOT_FORMAT
}
LIST_SNAPSHOT_FORMAT = {
'snapshots' => [SNAPSHOT_FORMAT]
}
def snapshot_deleted?(service, snapshot_id)
begin
service.get_snapshot(snapshot_id)
false
rescue
true
end
end
service = Fog::Rackspace::BlockStorage.new
tests('success') do
volume = service.create_volume(10).body['volume']
volume_id = volume['id']
snapshot_id = nil
until service.get_volume(volume_id).body['volume']['status'] == 'available'
sleep 10
end
tests("#create_snapshot(#{volume_id})").formats(GET_SNAPSHOT_FORMAT) do
service.create_snapshot(volume_id).body.tap do |b|
snapshot_id = b['snapshot']['id']
end
end
tests("#list_snapshots").formats(LIST_SNAPSHOT_FORMAT) do
service.list_snapshots.body
end
tests("#get_snapshot(#{snapshot_id})").formats(GET_SNAPSHOT_FORMAT) do
service.get_snapshot(snapshot_id).body
end
until service.get_snapshot(snapshot_id).body['snapshot']['status'] == 'available' do
sleep 10
end
tests("#delete_snapshot(#{snapshot_id})").succeeds do
service.delete_snapshot(snapshot_id)
end
until snapshot_deleted?(service, snapshot_id)
sleep 10
end
service.delete_volume(volume_id)
end
tests('failure') do
tests("#create_snapshot('invalid')").raises(Fog::Rackspace::BlockStorage::NotFound) do
service.create_snapshot('invalid')
end
tests("#get_snapshot('invalid')").raises(Fog::Rackspace::BlockStorage::NotFound) do
service.get_snapshot('invalid')
end
end
end

View file

@ -0,0 +1,61 @@
Shindo.tests('Fog::Rackspace::BlockStorage | volume_tests', ['rackspace']) do
pending if Fog.mocking?
VOLUME_FORMAT = {
'id' => String,
'status' => String,
'display_name' => Fog::Nullable::String,
'display_description' => Fog::Nullable::String,
'size' => Integer,
'created_at' => String,
'volume_type' => String,
'availability_zone' => String,
'snapshot_id' => Fog::Nullable::String,
'attachments' => Array,
'metadata' => Hash
}
GET_VOLUME_FORMAT = {
'volume' => VOLUME_FORMAT
}
LIST_VOLUME_FORMAT = {
'volumes' => [VOLUME_FORMAT]
}
service = Fog::Rackspace::BlockStorage.new
tests('success') do
id = nil
size = 10
tests("#create_volume(#{size})").formats(GET_VOLUME_FORMAT) do
data = service.create_volume(size).body
id = data['volume']['id']
data
end
tests("#list_volumes").formats(LIST_VOLUME_FORMAT) do
service.list_volumes.body
end
tests("#get_volume(#{id})").formats(GET_VOLUME_FORMAT) do
service.get_volume(id).body
end
tests("#delete_volume(#{id})").succeeds do
service.delete_volume(id)
end
end
tests('failure') do
tests("#create_volume(-1)").raises(Fog::Rackspace::BlockStorage::BadRequest) do
service.create_volume(-1)
end
tests("#get_volume(-1)").raises(Fog::Rackspace::BlockStorage::NotFound) do
service.get_volume(-1)
end
end
end

View file

@ -0,0 +1,31 @@
Shindo.tests('Fog::Rackspace::BlockStorage | volume_type_tests', ['rackspace']) do
pending if Fog.mocking?
VOLUME_TYPE_FORMAT = {
'name' => String,
'extra_specs' => Hash
}
LIST_VOLUME_TYPE_FORMAT = {
'volume_types' => [VOLUME_TYPE_FORMAT.merge({ 'id' => Integer })]
}
GET_VOLUME_TYPE_FORMAT = {
'volume_type' => VOLUME_TYPE_FORMAT.merge({ 'id' => String })
}
service = Fog::Rackspace::BlockStorage.new
tests('success') do
volume_type_id = 1
tests("#list_volume_types").formats(LIST_VOLUME_TYPE_FORMAT) do
service.list_volume_types.body
end
tests("#get_volume_type(#{volume_type_id})").formats(GET_VOLUME_TYPE_FORMAT) do
service.get_volume_type(volume_type_id).body
end
end
end

View file

@ -0,0 +1,68 @@
Shindo.tests('Fog::Compute::RackspaceV2 | attachment_tests', ['rackspace']) do
pending if Fog.mocking?
ATTACHMENT_FORMAT = {
'volumeAttachment' => {
'id' => String,
'serverId' => String,
'volumeId' => String,
'device' => Fog::Nullable::String
}
}
LIST_ATTACHMENTS_FORMAT = {
'volumeAttachments' => [ATTACHMENT_FORMAT]
}
compute_service = Fog::Compute.new(:provider => 'Rackspace', :version => 'V2')
block_storage_service = Fog::Rackspace::BlockStorage.new
name = 'fog' + Time.now.to_i.to_s
image_id = '3afe97b2-26dc-49c5-a2cc-a2fc8d80c001' # Ubuntu 11.10
flavor_id = '2' # 512 MB
server_id = compute_service.create_server(name, image_id, flavor_id, 1, 1).body['server']['id']
volume_id = block_storage_service.create_volume(1).body['volume']['id']
device_id = '/dev/xvde'
tests('success') do
until compute_service.get_server(server_id).body['server']['status'] == 'ACTIVE'
sleep 10
end
until block_storage_service.get_volume(volume_id).body['volume']['status'] == 'available'
sleep 10
end
tests("#attach_volume(#{server_id}, #{volume_id}, #{device_id})").formats(ATTACHMENT_FORMAT) do
compute_service.attach_volume(server_id, volume_id, device_id).body
end
tests("#list_attachments(#{server_id})").formats(LIST_ATTACHMENTS_FORMAT) do
compute_service.list_attachments(server_id).body
end
until block_storage_service.get_volume(volume_id).body['volume']['status'] == 'in-use'
sleep 10
end
tests("#get_attachment(#{server_id}, #{volume_id})").formats(ATTACHMENT_FORMAT) do
compute_service.get_attachment(server_id, volume_id).body
end
tests("#delete_attachment(#{server_id}, #{volume_id})").succeeds do
compute_service.delete_attachment(server_id, volume_id)
end
end
tests('failure') do
tests("#attach_volume('', #{volume_id}, #{device_id})").raises(Fog::Compute::RackspaceV2::NotFound) do
compute_service.attach_volume('', volume_id, device_id)
end
tests("#delete_attachment('', #{volume_id})").raises(Fog::Compute::RackspaceV2::NotFound) do
compute_service.delete_attachment('', volume_id)
end
end
end