Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added mocks for Fog::Storage::OpenStack #262

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions lib/fog/storage/openstack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,201 @@ class OpenStack < Fog::Service
request :public_url

class Mock
class MockContainer
attr_reader :objects, :meta, :service

# Create a new container. Generally, you should call
# {Fog::Storage::OpenStack#add_container} instead.
def initialize(service)
@service = service
@objects, @meta = {}, {}
end

# Determine if this container contains any MockObjects or not.
#
# @return [Boolean]
def empty?
@objects.empty?
end

# Total sizes of all objects added to this container.
#
# @return [Integer] The number of bytes occupied by each contained
# object.
def bytes_used
@objects.values.map { |o| o.bytes_used }.reduce(0) { |a, b| a + b }
end

# Render the HTTP headers that would be associated with this
# container.
#
# @return [Hash<String, String>] Any metadata supplied to this
# container, plus additional headers indicating the container's
# size.
def to_headers
@meta.merge({
'X-Container-Object-Count' => @objects.size,
'X-Container-Bytes-Used' => bytes_used
})
end

# Access a MockObject within this container by (unescaped) name.
#
# @return [MockObject, nil] Return the MockObject at this name if
# one exists; otherwise, `nil`.
def mock_object(name)
@objects[(name)]
end

# Access a MockObject with a specific name, raising a
# ` Fog::Storage::OpenStack::NotFound` exception if none are present.
#
# @param name [String] (Unescaped) object name.
# @return [MockObject] The object within this container with the
# specified name.
def mock_object!(name)
mock_object(name) or raise Fog::Storage::OpenStack::NotFound.new
end

# Add a new MockObject to this container. An existing object with
# the same name will be overwritten.
#
# @param name [String] The object's name, unescaped.
# @param data [String, #read] The contents of the object.
def add_object(name, data)
@objects[(name)] = MockObject.new(data, service)
end

# Remove a MockObject from the container by name. No effect if the
# object is not present beforehand.
#
# @param name [String] The (unescaped) object name to remove.
def remove_object(name)
@objects.delete (name)
end
end

class MockObject
attr_reader :hash, :bytes_used, :content_type, :last_modified
attr_reader :body, :meta, :service
attr_accessor :static_manifest

# Construct a new object. Generally, you should call
# {MockContainer#add_object} instead of instantiating these directly.
def initialize(data, service)
data = Fog::Storage.parse_data(data)
@service = service

@bytes_used = data[:headers]['Content-Length']
@content_type = data[:headers]['Content-Type']
if data[:body].respond_to? :read
@body = data[:body].read
elsif data[:body].respond_to? :body
@body = data[:body].body
else
@body = data[:body]
end
@last_modified = Time.now.utc
@hash = Digest::MD5.hexdigest(@body)
@meta = {}
@static_manifest = false
end

# Determine if this object was created as a static large object
# manifest.
#
# @return [Boolean]
def static_manifest?
@static_manifest
end

# Determine if this object has the metadata header that marks it as a
# dynamic large object manifest.
#
# @return [Boolean]
def dynamic_manifest?
! large_object_prefix.nil?
end

# Iterate through each MockObject that contains a part of the data for
# this logical object. In the normal case, this will only yield the
# receiver directly. For dynamic and static large object manifests,
# however, this call will yield each MockObject that contains a part
# of the whole, in sequence.
#
# Manifests that refer to containers or objects that don't exist will
# skip those sections and log a warning, instead.
#
# @yield [MockObject] Each object that holds a part of this logical
# object.
def each_part
case
when dynamic_manifest?
# Concatenate the contents and sizes of each matching object.
# Note that cname and oprefix are already escaped.
cname, oprefix = large_object_prefix.split('/', 2)

target_container = service.data[cname]
if target_container
all = target_container.objects.keys
matching = all.select { |name| name.start_with? oprefix }
keys = matching.sort

keys.each do |name|
yield target_container.objects[name]
end
else
Fog::Logger.warning "Invalid container in dynamic object manifest: #{cname}"
yield self
end
when static_manifest?
Fog::JSON.decode(body).each do |segment|
cname, oname = segment['path'].split('/', 2)

cont = service.mock_container cname
unless cont
Fog::Logger.warning "Invalid container in static object manifest: #{cname}"
next
end

obj = cont.mock_object oname
unless obj
Fog::Logger.warning "Invalid object in static object manifest: #{oname}"
next
end

yield obj
end
else
yield self
end
end

# Access the object name prefix that controls which other objects
# comprise a dynamic large object.
#
# @return [String, nil] The object name prefix, or `nil` if none is
# present.
def large_object_prefix
@meta['X-Object-Manifest']
end

# Construct the fake HTTP headers that should be returned on requests
# targetting this object. Includes computed `Content-Type`,
# `Content-Length`, `Last-Modified` and `ETag` headers in addition to
# whatever metadata has been associated with this object manually.
#
# @return [Hash<String, String>] Header values stored in a Hash.
def to_headers
{
'Content-Type' => @content_type,
'Content-Length' => @bytes_used,
'Last-Modified' => @last_modified.strftime('%a, %b %d %Y %H:%M:%S %Z'),
'ETag' => @hash
}.merge(@meta)
end
end

def self.data
@data ||= Hash.new do |hash, key|
hash[key] = {}
Expand All @@ -57,6 +252,18 @@ def self.reset
def initialize(options = {})
@openstack_api_key = options[:openstack_api_key]
@openstack_username = options[:openstack_username]
@openstack_project_id = options[:openstack_project_id]
@openstack_domain_name = options[:openstack_domain_name]
@openstack_temp_url_key = options[:openstack_temp_url_key]

uri = URI.parse(options[:openstack_management_url])
@host = uri.host
@port = uri.port
@path = uri.path
@scheme = uri.scheme
rescue URI::InvalidURIError => _ex
@scheme = 'https'
@host = 'www.opensctak.url'
@path = '/v1/AUTH_1234'
end

Expand All @@ -77,6 +284,31 @@ def change_account(account)
def reset_account_name
@path = @original_path
end

# Access a MockContainer with the specified name, if one exists.
#
# @param cname [String] The (unescaped) container name.
# @return [MockContainer, nil] The named MockContainer, or `nil` if
# none exist.
def mock_container(cname)
data[(cname)]
end

# Access a MockContainer with the specified name, raising a
# { Fog::Storage::OpenStack::NotFound} exception if none exist.
#
# @param cname [String] The (unescaped) container name.
# @throws [ Fog::Storage::OpenStack::NotFound] If no container with the
# given name exists.
# @return [MockContainer] The existing MockContainer.
def mock_container!(cname)
mock_container(cname) or raise Fog::Storage::OpenStack::NotFound.new
end

def add_container(name)
data[(name)] = MockContainer.new(self)
end

end

class Real
Expand Down
21 changes: 21 additions & 0 deletions lib/fog/storage/openstack/requests/copy_object.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
module Fog
module Storage
class OpenStack
class Mock
def copy_object(source_container_name, source_object_name, target_container_name, target_object_name, options = {})
source_container = mock_container!(source_container_name)
source_object = source_container.mock_object!(source_object_name)

target_container = mock_container!(target_container_name)
target_object = target_container.add_object(target_object_name, source_object.dup)

response = Excon::Response.new

if source_object && target_container
response.status = 200
else
response.status = 404
raise(Excon::Errors.status_error({:expects => 200}, response))
end

response
end
end

class Real
# Copy object
#
Expand Down
6 changes: 6 additions & 0 deletions lib/fog/storage/openstack/requests/delete_container.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
module Fog
module Storage
class OpenStack
class Mock
def delete_container(name)
fail "Mock Not Implemented (#delete_container) in: #{__FILE__}:#{__LINE__}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you did a copy and paste but you dont need to define this. Once It is not implemented fog will throw the error automatically. =)

So you can skip this change.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I added this myself to see where was the exception from. Fog only showed sometinhg like Not yet implemented and I wanted to get the exact location of the not implemented thing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. Maybe you might want to add this to fog-core instead so all providers will have this feature?

Also, does this work with all active maintained ruby versions?

end
end

class Real
# Delete an existing container
#
Expand Down
6 changes: 6 additions & 0 deletions lib/fog/storage/openstack/requests/delete_multiple_objects.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
module Fog
module Storage
class OpenStack
class Mock
def delete_multiple_objects(container, object_names, options = {})
fail "Mock Not Implemented (#delete_multiple_objects) in #{__FILE__}:#{__LINE__}"
end
end

class Real
# Deletes multiple objects or containers with a single request.
#
Expand Down
12 changes: 12 additions & 0 deletions lib/fog/storage/openstack/requests/delete_object.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
module Fog
module Storage
class OpenStack
class Mock
def delete_object(container, object)
cc = mock_container!(container)
cc.mock_object!(object)
cc.remove_object(object)

response = Excon::Response.new
response.status = 204
response
end
end

class Real
# Delete an existing object
#
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
module Fog
module Storage
class OpenStack
class Mock
def delete_static_large_object(container, object, options = {})
fail "Mock Not Implemented (#delete_static_large_object) in: #{__FILE__}:#{__LINE__}"
end
end

class Real
# Delete a static large object.
#
Expand Down
23 changes: 23 additions & 0 deletions lib/fog/storage/openstack/requests/get_container.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
module Fog
module Storage
class OpenStack
class Mock
def get_container(container, options = {})
c = mock_container! container

results = []
c.objects.each do |key, mock_file|
results << {
"hash" => mock_file.hash,
"last_modified" => mock_file.last_modified.strftime('%Y-%m-%dT%H:%M:%S.%L'),
"bytes" => mock_file.bytes_used,
"name" => key,
"content_type" => mock_file.content_type
}
end

response = Excon::Response.new
response.status = 200
response.headers = c.to_headers
response.body = results
response
end
end

class Real
# Get details for container and total bytes stored
#
Expand Down
16 changes: 16 additions & 0 deletions lib/fog/storage/openstack/requests/get_containers.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
module Fog
module Storage
class OpenStack
class Mock
def get_containers(options = {})
results = data.map do |name, container|
{
"name" => name,
"count" => container.objects.size,
"bytes" => container.bytes_used
}
end
response = Excon::Response.new
response.status = 200
response.body = results
response
end
end

class Real
# List existing storage containers
#
Expand Down
Loading