ruby-atmos 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +8 -0
- data/README +129 -0
- data/Rakefile +94 -0
- data/lib/atmos/attributes.rb +545 -0
- data/lib/atmos/exceptions.rb +28 -0
- data/lib/atmos/object.rb +263 -0
- data/lib/atmos/parser.rb +110 -0
- data/lib/atmos/request.rb +188 -0
- data/lib/atmos/rest.rb +254 -0
- data/lib/atmos/store.rb +209 -0
- data/lib/atmos/util.rb +40 -0
- data/lib/atmos/version.rb +12 -0
- data/test/credentials.rb +141 -0
- data/test/esutest.rb +499 -0
- data/test/files/SmallImageForTest.iso +0 -0
- data/test/files/dragaf-tiny-from-vsphere.ova +0 -0
- data/test/files/something.txt +1 -0
- data/test/request_test.rb +50 -0
- data/test/suite.rb +9 -0
- data/test/suite_noproxy.rb +12 -0
- data/test/suite_proxy.rb +14 -0
- data/test/test_acl.rb +283 -0
- data/test/test_metadata.rb +162 -0
- data/test/test_object_create.rb +152 -0
- data/test/test_object_misc.rb +80 -0
- data/test/test_object_read.rb +114 -0
- data/test/test_object_update.rb +58 -0
- data/test/test_store.rb +125 -0
- data/test/test_util.rb +58 -0
- metadata +164 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module Atmos
|
2
|
+
module Exceptions
|
3
|
+
|
4
|
+
class AtmosException < Exception
|
5
|
+
end
|
6
|
+
|
7
|
+
class ArgumentException < AtmosException
|
8
|
+
end
|
9
|
+
|
10
|
+
class AuthException < AtmosException
|
11
|
+
end
|
12
|
+
|
13
|
+
class InvalidStateException < AtmosException
|
14
|
+
end
|
15
|
+
|
16
|
+
class NoSuchObjectException < AtmosException
|
17
|
+
end
|
18
|
+
|
19
|
+
class ServerException < AtmosException
|
20
|
+
end
|
21
|
+
|
22
|
+
class NotImplementedException < AtmosException
|
23
|
+
end
|
24
|
+
|
25
|
+
class InternalLibraryException < AtmosException
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/atmos/object.rb
ADDED
@@ -0,0 +1,263 @@
|
|
1
|
+
module Atmos
|
2
|
+
|
3
|
+
#
|
4
|
+
# == Object
|
5
|
+
# This class represents an object in an Atmos store.
|
6
|
+
#
|
7
|
+
# === Object Data
|
8
|
+
# When an object is instantiated, it's data is not loaded
|
9
|
+
# due to memory considerations. You can access an object's
|
10
|
+
# data as a ruby String or as a progressive download via a block.
|
11
|
+
#
|
12
|
+
# ==== all at once
|
13
|
+
#
|
14
|
+
# obj.data
|
15
|
+
#
|
16
|
+
# ==== progressive download
|
17
|
+
#
|
18
|
+
# obj.data_as_stream do |chunk|
|
19
|
+
# datafile.write(chunk)
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# ==== partial data
|
23
|
+
#
|
24
|
+
# obj.data_as_stream(0...59) do |chunk|
|
25
|
+
# readin.concat(chunk)
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
#
|
29
|
+
# === System Metadata
|
30
|
+
# Each object has some information about it stored by the system.
|
31
|
+
# This information is read only, and available as a hash on the object:
|
32
|
+
#
|
33
|
+
# obj.system_metadata => Hash
|
34
|
+
#
|
35
|
+
# obj.system_metadata.each do |key,value|
|
36
|
+
# puts "#{key}=#{value}"
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# See Atmos::Metadata for more detailed information.
|
40
|
+
#
|
41
|
+
#
|
42
|
+
# === User Metadata
|
43
|
+
# There are two kinds of user metadata, listable and non-listable.
|
44
|
+
# Each of these is available as a hash
|
45
|
+
# on the object class. These can both be modified.
|
46
|
+
#
|
47
|
+
# obj.listable_metadata => Hash
|
48
|
+
#
|
49
|
+
# obj.listable_metadata.each do |key,value|
|
50
|
+
# puts "#{key}=#{value}"
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# obj.metadata => Hash
|
54
|
+
#
|
55
|
+
# obj.metadata.each do |key,value|
|
56
|
+
# puts "#{key}=#{value}"
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# See Atmos::Metadata for more detailed information.
|
60
|
+
#
|
61
|
+
#
|
62
|
+
# === Access Control Lists (ACLs)
|
63
|
+
#
|
64
|
+
# There are two hashes for access control available as properties
|
65
|
+
# on the object: +user_acl+ and +group_acl+.
|
66
|
+
# The keys are the Atmos usernames and the values are one of
|
67
|
+
# <tt>:none</tt>, <tt>:read</tt>, <tt>:write</tt>, <tt>:full</tt>.
|
68
|
+
#
|
69
|
+
# puts obj.user_acl.inspect => {user => :full}
|
70
|
+
# puts obj.group_acl.inspect => {other => :none}
|
71
|
+
#
|
72
|
+
# See Atmos::ACL for more detailed information.
|
73
|
+
#
|
74
|
+
#
|
75
|
+
class Object
|
76
|
+
attr_reader :aoid, :request, :user # :nodoc:
|
77
|
+
|
78
|
+
# Hash-like object containing user access control properties of the object.
|
79
|
+
attr_reader :user_acl
|
80
|
+
|
81
|
+
# Hash-like object containing group access control properties of the object.
|
82
|
+
attr_reader :group_acl
|
83
|
+
|
84
|
+
# Hash-like object containing non-listable metadata associated with the object.
|
85
|
+
attr_reader :metadata
|
86
|
+
|
87
|
+
# Hash-like object containing listable metadata associated with the object.
|
88
|
+
attr_reader :listable_metadata
|
89
|
+
|
90
|
+
# Hash-like object containing read-only system metadata associated with the object.
|
91
|
+
attr_reader :system_metadata
|
92
|
+
|
93
|
+
@request = nil
|
94
|
+
@checksum = nil
|
95
|
+
|
96
|
+
|
97
|
+
#
|
98
|
+
# This constructor is only meant for internal use. Get or create an object
|
99
|
+
# with an Atmos::Store object:
|
100
|
+
#
|
101
|
+
# obj = store.create
|
102
|
+
# obj = store.get(obj_id)
|
103
|
+
#
|
104
|
+
def initialize(store, aoid, options = {})
|
105
|
+
Atmos::LOG.debug("obj.new options: #{options.inspect}")
|
106
|
+
validate_options(options)
|
107
|
+
|
108
|
+
@deleted = false
|
109
|
+
@aoid = aoid
|
110
|
+
@store = store
|
111
|
+
@request = Atmos::Request.new(:store => @store)
|
112
|
+
@user = @store.user
|
113
|
+
|
114
|
+
# this means we're creating a new object,
|
115
|
+
# not representing an existing one
|
116
|
+
if (@aoid.nil?)
|
117
|
+
response = @request.do(:create_object, options)
|
118
|
+
@aoid = response.id
|
119
|
+
end
|
120
|
+
|
121
|
+
@system_metadata = Atmos::Metadata.new(self, Atmos::Metadata::SYSTEM)
|
122
|
+
|
123
|
+
# These guys need the object to have a valid id, so we do it after the create
|
124
|
+
@user_acl = Atmos::ACL.new(self, Atmos::ACL::USER)
|
125
|
+
@group_acl = Atmos::ACL.new(self, Atmos::ACL::GROUP)
|
126
|
+
@metadata = Atmos::Metadata.new(self, Atmos::Metadata::NON_LISTABLE)
|
127
|
+
@listable_metadata = Atmos::Metadata.new(self, Atmos::Metadata::LISTABLE)
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
#
|
132
|
+
# Truncates the object to size 0 without changing any of the Metadata or ACLs.
|
133
|
+
#
|
134
|
+
def truncate
|
135
|
+
do_delete_check
|
136
|
+
response = @request.do(:trunc_object, :id => @aoid, :data => nil, :length => 0)
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Deletes the object from Atmos and invalidates the object.
|
141
|
+
#
|
142
|
+
# obj = store.create
|
143
|
+
# obj.delete
|
144
|
+
#
|
145
|
+
def delete
|
146
|
+
do_delete_check
|
147
|
+
response = @request.do(:delete_object, :id => @aoid)
|
148
|
+
@deleted = true
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
#
|
153
|
+
# Returns all the object data in a single string. Be judicious about
|
154
|
+
# use of this method, since it can load the entire blob into memory.
|
155
|
+
#
|
156
|
+
# Optional:
|
157
|
+
# * <tt>:range</tt> - range of bytes to retrieve (e.g. 0...10000)
|
158
|
+
#
|
159
|
+
def data(range = nil)
|
160
|
+
do_delete_check
|
161
|
+
response = @request.do(:read_object, :id => @aoid, 'Range' => range)
|
162
|
+
response.http_response.body
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
#
|
167
|
+
# Allows progressive download of the object's data. Takes a block:
|
168
|
+
#
|
169
|
+
# obj.data_as_stream do |chunk|
|
170
|
+
# datafile.write(chunk)
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
# Optional:
|
174
|
+
# * <tt>:range</tt> - range of bytes to retrieve (e.g. 0...10000)
|
175
|
+
#
|
176
|
+
def data_as_stream(range = nil, &block)
|
177
|
+
do_delete_check
|
178
|
+
@request.do(:read_object, :id => @aoid, 'Range' => range) do |response|
|
179
|
+
response.read_body do |chunk|
|
180
|
+
block.call(chunk)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
|
186
|
+
def update(data, range=nil)
|
187
|
+
response = @request.do(:update_object, :id => @aoid, :data => data, 'Range' => range)
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
#
|
192
|
+
# Checks to see if the represented object exists on Atmos
|
193
|
+
# by requesting it's system metadata.
|
194
|
+
#
|
195
|
+
# Returns boolean +true+ or +false+.
|
196
|
+
#
|
197
|
+
def exists?
|
198
|
+
rv = true
|
199
|
+
begin
|
200
|
+
@request.do(:list_system_metadata, :id => @aoid)
|
201
|
+
rescue Atmos::Exceptions::NoSuchObjectException
|
202
|
+
rv = false
|
203
|
+
end
|
204
|
+
rv
|
205
|
+
end
|
206
|
+
|
207
|
+
|
208
|
+
def copy #:nodoc:
|
209
|
+
do_delete_check
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
def headers #:nodoc:
|
214
|
+
do_delete_check
|
215
|
+
headers = {}
|
216
|
+
[@user_acl, @group_acl, @tags, @listable_tags, @metadata, @listable_metadata].each do |attr|
|
217
|
+
val = attr.header_value
|
218
|
+
next if (val.nil? || val.empty?)
|
219
|
+
headers[attr.header_name] = attr.header_value
|
220
|
+
end
|
221
|
+
headers
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
private
|
226
|
+
def do_delete_check
|
227
|
+
raise Atmos::Exceptions::InvalidStateException if (@deleted)
|
228
|
+
end
|
229
|
+
|
230
|
+
def validate_options(options)
|
231
|
+
invalid_values = []
|
232
|
+
|
233
|
+
valid_options = [:checksum, :user_acl, :group_acl, :metadata, :listable_metadata, :mimetype, :length, :data].freeze
|
234
|
+
invalid_options = options.keys - valid_options
|
235
|
+
raise Atmos::Exceptions::ArgumentException, "Unrecognized options: #{invalid_options.inspect}" if (!invalid_options.empty?)
|
236
|
+
|
237
|
+
options.each do |k,v|
|
238
|
+
case k
|
239
|
+
when :checksum
|
240
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(Boolean))
|
241
|
+
when :user_acl
|
242
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(Hash))
|
243
|
+
when :group_acl
|
244
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(Hash))
|
245
|
+
when :metadata
|
246
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(Hash))
|
247
|
+
when :listable_metadata
|
248
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(Hash))
|
249
|
+
when :mimetype
|
250
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(String))
|
251
|
+
when :length
|
252
|
+
invalid_values.push(k) if (options[k].nil? || !options[k].kind_of?(Integer))
|
253
|
+
when :data
|
254
|
+
invalid_values.push(k) if (options[k].nil? || (!options[k].kind_of?(String) && !options[k].class.ancestors.include?(IO)))
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
raise Atmos::Exceptions::ArgumentException, "Options of invalid type: #{invalid_values.inspect}" if (!invalid_values.empty?)
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
data/lib/atmos/parser.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
module Atmos
|
2
|
+
class Parser
|
3
|
+
|
4
|
+
NOKOGIRI = "nokogiri"
|
5
|
+
REXML = "rexml"
|
6
|
+
@@parser = nil
|
7
|
+
|
8
|
+
|
9
|
+
def self.parser
|
10
|
+
@@parser
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def self.parser=(which)
|
15
|
+
|
16
|
+
@@parser = which
|
17
|
+
if (@@parser == NOKOGIRI)
|
18
|
+
require 'nokogiri'
|
19
|
+
elsif (@@parser == REXML)
|
20
|
+
require 'rexml/document'
|
21
|
+
else
|
22
|
+
raise Atmos::Exceptions::ArgumentException, "The XML parser has not been set to a known parser."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def self.response_get_array(response, string)
|
28
|
+
rv = nil
|
29
|
+
|
30
|
+
if (parser == NOKOGIRI)
|
31
|
+
|
32
|
+
doc = Nokogiri::XML(response.body)
|
33
|
+
rv = doc.xpath(string).map do |elt|
|
34
|
+
elt.content
|
35
|
+
end
|
36
|
+
|
37
|
+
elsif (parser == REXML)
|
38
|
+
|
39
|
+
doc = ::REXML::Document.new(response.body)
|
40
|
+
rv = doc.elements.to_a(string).map do |elt|
|
41
|
+
elt.text
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
rv
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def self.response_get_string(response, string)
|
51
|
+
response_get_array(response,string)[0]
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
#
|
56
|
+
# Utility method to check the atmos server XML response for errors.
|
57
|
+
# Throws exception on error.
|
58
|
+
#
|
59
|
+
def self.response_check_action_error(action, response)
|
60
|
+
doc = nil
|
61
|
+
code = nil
|
62
|
+
msg = nil
|
63
|
+
exclass = Atmos::Exceptions::AtmosException
|
64
|
+
|
65
|
+
Atmos::LOG.info("body: #{response.body}")
|
66
|
+
if (!response.body.nil? && response.body.match(/<\?xml/))
|
67
|
+
if (parser == NOKOGIRI)
|
68
|
+
|
69
|
+
doc = Nokogiri::XML(response.body)
|
70
|
+
|
71
|
+
code = doc.xpath('//Error/Code')[0]
|
72
|
+
code = code.content if (!code.nil?)
|
73
|
+
|
74
|
+
msg = doc.xpath('//Error/Message')[0]
|
75
|
+
msg = msg.content if (!msg.nil?)
|
76
|
+
|
77
|
+
elsif (parser == REXML)
|
78
|
+
|
79
|
+
code = response_get_string(response, '//Error/Code')
|
80
|
+
msg = response_get_string(response, '//Error/Message')
|
81
|
+
|
82
|
+
else
|
83
|
+
raise Atmos::Exceptions::InvalidStateException, "The XML parser has not been set to a known parser."
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
if (!code.nil?)
|
88
|
+
|
89
|
+
Atmos::LOG.info("code: #{code}")
|
90
|
+
Atmos::LOG.info("msg: #{msg}")
|
91
|
+
|
92
|
+
if (!REST[action][:errors].nil? && !REST[action][:errors][code].nil?)
|
93
|
+
tmp = REST[action][:errors][code][:message]
|
94
|
+
msg = tmp if (!tmp.nil?)
|
95
|
+
|
96
|
+
tmp = REST[action][:errors][code][:class]
|
97
|
+
exclass = tmp if (!tmp.nil?)
|
98
|
+
end
|
99
|
+
|
100
|
+
raise exclass, msg
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
true
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module Atmos
|
2
|
+
class Request # :nodoc:
|
3
|
+
|
4
|
+
def initialize(options = {})
|
5
|
+
valid = [:store, :default_tag].freeze
|
6
|
+
invalid = options.keys - valid
|
7
|
+
raise Atmos::Exceptions::ArgumentException,
|
8
|
+
"Unrecognized options: #{invalid.inspect}" if (!invalid.empty?)
|
9
|
+
Atmos::LOG.debug("Request.initialize: options: #{options.inspect}")
|
10
|
+
|
11
|
+
@baseurl = options[:store].uri
|
12
|
+
@uid = options[:store].uid
|
13
|
+
@secret = options[:store].secret
|
14
|
+
@http = options[:store].http
|
15
|
+
@tag = options[:default_tag]
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def do(actionname, options = {}, &block)
|
20
|
+
verbs = [:head, :get, :put, :post, :delete].freeze
|
21
|
+
Atmos::LOG.info("do #{actionname} options: #{options.inspect}")
|
22
|
+
raise Atmos::Exceptions::InternalLibraryException,
|
23
|
+
"Invalid REST action: #{actionname}" if (REST[actionname].nil?)
|
24
|
+
|
25
|
+
action = REST[actionname]
|
26
|
+
uri = (options[:id].nil?) ? action[:uri] : action[:uri].sub(/:id/, options[:id])
|
27
|
+
url = URI::join(@baseurl.to_s, uri.to_s)
|
28
|
+
verb = action[:verb]
|
29
|
+
|
30
|
+
if (options[:id].nil? && !action[:uri].index(':id').nil?)
|
31
|
+
raise Atmos::Exceptions::ArgumentException, "An id is required for this action (#{actionname})."
|
32
|
+
end
|
33
|
+
|
34
|
+
request = (verbs.include?(verb)) ? Net::HTTP.const_get(verb.to_s.capitalize).new(uri) : nil
|
35
|
+
raise Atmos::Exceptions::AtmosException,
|
36
|
+
"Couldn't create Net::HTTP request object for #{verb}" if (request.nil?)
|
37
|
+
|
38
|
+
headers = {}
|
39
|
+
headers['Date'] = Time.now().httpdate()
|
40
|
+
headers['x-emc-date'] = Time.now().httpdate()
|
41
|
+
headers['x-emc-uid'] = @uid
|
42
|
+
|
43
|
+
if (!action[:required_headers].nil?)
|
44
|
+
action[:required_headers].each do |header|
|
45
|
+
case header
|
46
|
+
when 'Content-Type' then
|
47
|
+
headers[header] = (!options[:mimetype].nil?) ? options[:mimetype] : 'binary/octet-stream'
|
48
|
+
when 'Content-Length' then
|
49
|
+
headers[header] = (!options[:length].nil?) ? options[:length] : 0
|
50
|
+
when 'x-emc-tags' then
|
51
|
+
headers[header] = (!options[header].nil?) ? options[header] : ""
|
52
|
+
else
|
53
|
+
if (options[header])
|
54
|
+
headers[header] = options[header]
|
55
|
+
else
|
56
|
+
raise "Value not supplied for required header: '#{header}'"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if (!action[:optional_headers].nil?)
|
63
|
+
action[:optional_headers].each do |header|
|
64
|
+
if (header.eql?('Range') && options.has_key?('Range') && !options['Range'].nil?)
|
65
|
+
r = options['Range']
|
66
|
+
options.delete('Range')
|
67
|
+
#headers[header] = (r.kind_of?(Range)) ? "Bytes=#{r.first}-#{r.last}" : "Bytes=#{r.to_s}"
|
68
|
+
if (r.kind_of?(Range))
|
69
|
+
headers[header] = "Bytes=#{r.first}-#{r.last}"
|
70
|
+
Atmos::LOG.info("Request.initialize: given Range object: #{headers[header]}")
|
71
|
+
else
|
72
|
+
Atmos::LOG.info("Request.initialize: given range string: #{headers[header]}")
|
73
|
+
headers[header] = "Bytes=#{r.to_s}"
|
74
|
+
end
|
75
|
+
if ((!options[:data].nil?) and ((r.end-r.begin+1) != options[:data].length))
|
76
|
+
raise Atmos::Exceptions::ArgumentException, "The range length (#{r.end - r.begin + 1}) doesn't match the data length (#{options[:data].length})."
|
77
|
+
end
|
78
|
+
end
|
79
|
+
key = (action[:header2sym] && action[:header2sym][header]) ? action[:header2sym][header] : header
|
80
|
+
|
81
|
+
if (!options[key].nil?)
|
82
|
+
if (options[key].kind_of?(Hash))
|
83
|
+
headers[header] = ""
|
84
|
+
options[key].each do |key,val|
|
85
|
+
headers[header] += "#{key}=#{val}, "
|
86
|
+
end
|
87
|
+
headers[header].chop!
|
88
|
+
headers[header].chop!
|
89
|
+
elsif (options[key].kind_of?(Array))
|
90
|
+
headers[header] = ""
|
91
|
+
options[key].each do |val|
|
92
|
+
headers[header] += "#{val}, "
|
93
|
+
end
|
94
|
+
headers[header].chop!
|
95
|
+
headers[header].chop!
|
96
|
+
elsif (options[key].respond_to?(:to_s))
|
97
|
+
headers[header] = options[key]
|
98
|
+
else
|
99
|
+
raise "value for optional header '#{header}' id bad type: #{options[header].class}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
if (!@tag.nil? && !action[:optional_headers].nil? && action[:optional_headers].include?('x-emc-listable-meta'))
|
106
|
+
if (headers.has_key?('x-emc-listable-meta'))
|
107
|
+
headers['x-emc-listable-meta'] += ", #{@tag}=atmos-ruby-default"
|
108
|
+
else
|
109
|
+
headers['x-emc-listable-meta'] = "#{@tag}=atmos-ruby-default"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
headers['x-emc-signature'] = calculate_signature(action[:verb], @secret, url, headers)
|
114
|
+
|
115
|
+
if (options[:data])
|
116
|
+
Atmos::LOG.info("there is data: [#{options[:data]}]")
|
117
|
+
if (options[:data].respond_to?(:read))
|
118
|
+
request.body_stream = options[:data]
|
119
|
+
else
|
120
|
+
request.body = options[:data]
|
121
|
+
end
|
122
|
+
headers['Content-Length'] =
|
123
|
+
options[:data].respond_to?(:lstat) ? options[:data].stat.size : options[:data].size
|
124
|
+
Atmos::LOG.info("set Content-Length to: #{headers['Content-Length']}\n")
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
headers.each do |key,val|
|
129
|
+
request[key] = val
|
130
|
+
end
|
131
|
+
|
132
|
+
@http.set_debug_output($stdout) if (Atmos::LOG.level == Log4r::DEBUG)
|
133
|
+
|
134
|
+
|
135
|
+
#
|
136
|
+
# so this is weird. because some of these atmos request may be
|
137
|
+
# insanely long, we need to allow a mechanism for our API layer
|
138
|
+
# to read progressively, not only all at once
|
139
|
+
#
|
140
|
+
if (block)
|
141
|
+
@http.request(request) do |response|
|
142
|
+
block.call(response)
|
143
|
+
end
|
144
|
+
return
|
145
|
+
end
|
146
|
+
|
147
|
+
response = @http.request(request)
|
148
|
+
Atmos::Parser::response_check_action_error(actionname, response)
|
149
|
+
|
150
|
+
Atmos::Response.new(response, actionname)
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
def calculate_signature(verb, secret, url, headers)
|
155
|
+
headers_copy = headers.clone
|
156
|
+
|
157
|
+
# normalize emc headers
|
158
|
+
headers_copy.each do |header, value|
|
159
|
+
if (header.start_with?('x-emc-'))
|
160
|
+
headers.delete(header)
|
161
|
+
headers[header.to_s.strip.downcase] = value.to_s.strip.sub(/\s+/, ' ').sub(/\n/, '')
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# string together all emc headers, no newline at end
|
166
|
+
emc = ""
|
167
|
+
headers.keys.sort.each do |header|
|
168
|
+
if (header.start_with?('x-emc-'))
|
169
|
+
emc += "#{header}:#{headers[header]}\n"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
emc.chomp!
|
173
|
+
|
174
|
+
hashstring = verb.to_s.upcase+"\n"
|
175
|
+
['Content-Type', 'Range', 'Date'].each do |key|
|
176
|
+
hashstring += headers[key] if (!headers[key].nil?)
|
177
|
+
hashstring += "\n"
|
178
|
+
end
|
179
|
+
|
180
|
+
hashstring += url.path
|
181
|
+
hashstring += '?'+url.query if (!url.query.nil?)
|
182
|
+
hashstring += "\n"+ emc
|
183
|
+
|
184
|
+
Atmos::LOG.debug("calculate_signature: hashstring: #{hashstring}")
|
185
|
+
Base64.encode64(HMAC::SHA1.digest(Base64.decode64(secret), hashstring)).chomp()
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|