fog-softlayer 0.0.5 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +8 -8
  2. data/.travis.yml +37 -0
  3. data/CONTRIBUTING.md +30 -0
  4. data/README.md +8 -5
  5. data/docs/cla-corporate.md +133 -0
  6. data/docs/cla-individual.md +84 -0
  7. data/docs/fog-softlayer-CCLA.pdf +0 -0
  8. data/docs/fog-softlayer-CLA.pdf +0 -0
  9. data/examples/storage.md +107 -0
  10. data/fog-softlayer.gemspec +1 -0
  11. data/gemfiles/Gemfile-edge +14 -0
  12. data/gemfiles/Gemfile-ruby-1.8.7 +14 -0
  13. data/lib/fog/softlayer.rb +1 -0
  14. data/lib/fog/softlayer/compute.rb +1 -1
  15. data/lib/fog/softlayer/compute/shared.rb +1 -1
  16. data/lib/fog/softlayer/core.rb +17 -1
  17. data/lib/fog/softlayer/models/storage/directories.rb +40 -0
  18. data/lib/fog/softlayer/models/storage/directory.rb +50 -0
  19. data/lib/fog/softlayer/models/storage/file.rb +166 -0
  20. data/lib/fog/softlayer/models/storage/files.rb +99 -0
  21. data/lib/fog/softlayer/requests/storage/copy_object.rb +42 -0
  22. data/lib/fog/softlayer/requests/storage/delete_container.rb +38 -0
  23. data/lib/fog/softlayer/requests/storage/delete_multiple_objects.rb +67 -0
  24. data/lib/fog/softlayer/requests/storage/delete_object.rb +40 -0
  25. data/lib/fog/softlayer/requests/storage/delete_static_large_object.rb +43 -0
  26. data/lib/fog/softlayer/requests/storage/get_container.rb +68 -0
  27. data/lib/fog/softlayer/requests/storage/get_containers.rb +51 -0
  28. data/lib/fog/softlayer/requests/storage/get_object.rb +45 -0
  29. data/lib/fog/softlayer/requests/storage/get_object_https_url.rb +88 -0
  30. data/lib/fog/softlayer/requests/storage/head_container.rb +28 -0
  31. data/lib/fog/softlayer/requests/storage/head_containers.rb +25 -0
  32. data/lib/fog/softlayer/requests/storage/head_object.rb +23 -0
  33. data/lib/fog/softlayer/requests/storage/post_set_meta_temp_url_key.rb +37 -0
  34. data/lib/fog/softlayer/requests/storage/put_container.rb +32 -0
  35. data/lib/fog/softlayer/requests/storage/put_dynamic_obj_manifest.rb +43 -0
  36. data/lib/fog/softlayer/requests/storage/put_object.rb +61 -0
  37. data/lib/fog/softlayer/requests/storage/put_object_manifest.rb +16 -0
  38. data/lib/fog/softlayer/requests/storage/put_static_obj_manifest.rb +57 -0
  39. data/lib/fog/softlayer/storage.rb +283 -0
  40. data/lib/fog/softlayer/version.rb +1 -1
  41. data/tests/helper.rb +0 -29
  42. data/tests/helpers/mock_helper.rb +4 -98
  43. data/tests/softlayer/models/storage/directory_tests.rb +49 -0
  44. data/tests/softlayer/models/storage/file_tests.rb +56 -0
  45. data/tests/softlayer/requests/storage/auth_tests.rb +17 -0
  46. data/tests/softlayer/requests/storage/container_tests.rb +52 -0
  47. data/tests/softlayer/requests/storage/object_tests.rb +68 -0
  48. metadata +53 -2
@@ -0,0 +1,28 @@
1
+ module Fog
2
+ module Storage
3
+ class Softlayer
4
+ class Real
5
+
6
+ # List number of objects and total bytes stored
7
+ #
8
+ # ==== Parameters
9
+ # * container<~String> - Name of container to retrieve info for
10
+ #
11
+ # ==== Returns
12
+ # * response<~Excon::Response>:
13
+ # * headers<~Hash>:
14
+ # * 'X-Container-Object-Count'<~String> - Count of containers
15
+ # * 'X-Container-Bytes-Used'<~String> - Bytes used
16
+ def head_container(container)
17
+ request(
18
+ :expects => 204,
19
+ :method => 'HEAD',
20
+ :path => Fog::Softlayer.escape(container),
21
+ :query => {'format' => 'json'}
22
+ )
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ module Fog
2
+ module Storage
3
+ class Softlayer
4
+ class Real
5
+
6
+ # List number of containers and total bytes stored
7
+ #
8
+ # ==== Returns
9
+ # * response<~Excon::Response>:
10
+ # * headers<~Hash>:
11
+ # * 'X-Account-Container-Count'<~String> - Count of containers
12
+ # * 'X-Account-Bytes-Used'<~String> - Bytes used
13
+ def head_containers
14
+ request(
15
+ :expects => 204,
16
+ :method => 'HEAD',
17
+ :path => '',
18
+ :query => {'format' => 'json'}
19
+ )
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Fog
2
+ module Storage
3
+ class Softlayer
4
+ class Real
5
+
6
+ # Get headers for object
7
+ #
8
+ # ==== Parameters
9
+ # * container<~String> - Name of container to look in
10
+ # * object<~String> - Name of object to look for
11
+ #
12
+ def head_object(container, object)
13
+ request({
14
+ :expects => 200,
15
+ :method => 'HEAD',
16
+ :path => "#{Fog::Softlayer.escape(container)}/#{Fog::Softlayer.escape(object)}"
17
+ }, false)
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ module Fog
2
+ module Storage
3
+ class Softlayer
4
+
5
+ class Real
6
+
7
+ # Set the account wide Temp URL Key. This is a secret key that's
8
+ # used to generate signed expiring URLs.
9
+ #
10
+ # Once the key has been set with this request you should create new
11
+ # Storage objects with the :temp_url_key option then use
12
+ # the get_object_https_url method to generate expiring URLs.
13
+ #
14
+ # *** CAUTION *** changing this secret key will invalidate any expiring
15
+ # URLS generated with old keys.
16
+ #
17
+ # ==== Parameters
18
+ # * key<~String> - The new Temp URL Key
19
+ #
20
+ # ==== Returns
21
+ # * response<~Excon::Response>
22
+ #
23
+ # ==== See Also
24
+ # http://docs.rackspace.com/files/api/v1/cf-devguide/content/Set_Account_Metadata-d1a4460.html
25
+ def post_set_meta_temp_url_key(key)
26
+ request(
27
+ :expects => [201, 202, 204],
28
+ :method => 'POST',
29
+ :headers => {'X-Account-Meta-Temp-Url-Key' => key}
30
+ )
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,32 @@
1
+ module Fog
2
+ module Storage
3
+ class Softlayer
4
+ class Mock
5
+ def put_container(name)
6
+ @containers[name] = {} unless @containers[name]
7
+ response = Excon::Response.new
8
+ response.body = ''
9
+ response.status = 201
10
+ response
11
+ end
12
+ end
13
+
14
+ class Real
15
+
16
+ # Create a new container
17
+ #
18
+ # ==== Parameters
19
+ # * name<~String> - Name for container, should be < 256 bytes and must not contain '/'
20
+ #
21
+ def put_container(name)
22
+ request(
23
+ :expects => [201, 202],
24
+ :method => 'PUT',
25
+ :path => Fog::Softlayer.escape(name)
26
+ )
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,43 @@
1
+ module Fog
2
+ module Storage
3
+ class Softlayer
4
+ class Real
5
+
6
+ # Create a new dynamic large object manifest
7
+ #
8
+ # Creates an object with a +X-Object-Manifest+ header that specifies the common prefix ("<container>/<prefix>")
9
+ # for all uploaded segments. Retrieving the manifest object streams all segments matching this prefix.
10
+ # Segments must sort in the order they should be concatenated. Note that any future objects stored in the container
11
+ # along with the segments that match the prefix will be included when retrieving the manifest object.
12
+ #
13
+ # All segments must be stored in the same container, but may be in a different container than the manifest object.
14
+ # The default +X-Object-Manifest+ header is set to "+container+/+object+", but may be overridden in +options+
15
+ # to specify the prefix and/or the container where segments were stored.
16
+ # If overridden, names should be CGI escaped (excluding spaces) if needed (see {Fog::Softlayer.escape}).
17
+ #
18
+ # @param container [String] Name for container where +object+ will be stored. Should be < 256 bytes and must not contain '/'
19
+ # @param object [String] Name for manifest object.
20
+ # @param options [Hash] Config headers for +object+.
21
+ # @option options [String] 'X-Object-Manifest' ("container/object") "<container>/<prefix>" for segment objects.
22
+ #
23
+ # @raise [Fog::Storage::Softlayer::NotFound] HTTP 404
24
+ # @raise [Excon::Errors::BadRequest] HTTP 400
25
+ # @raise [Excon::Errors::Unauthorized] HTTP 401
26
+ # @raise [Excon::Errors::HTTPStatusError]
27
+ #
28
+ # @see http://docs.openstack.org/api/openstack-object-storage/1.0/content/dynamic-large-object-creation.html
29
+ def put_dynamic_obj_manifest(container, object, options = {})
30
+ path = "#{Fog::Softlayer.escape(container)}/#{Fog::Softlayer.escape(object)}"
31
+ headers = {'X-Object-Manifest' => path}.merge(options)
32
+ request(
33
+ :expects => 201,
34
+ :headers => headers,
35
+ :method => 'PUT',
36
+ :path => path
37
+ )
38
+ end
39
+
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,61 @@
1
+ module Fog
2
+ module Storage
3
+ class Softlayer
4
+ class Mock
5
+ def put_object(container, object, data, options = {}, &block)
6
+ if @containers[container]
7
+ @containers[container][object] = data
8
+ response = Excon::Response.new
9
+ response.body = ''
10
+ response.status = 201
11
+ response.headers = {"Last-Modified"=>Time.now, "Content-Length"=>0}
12
+ response
13
+ else
14
+ response = Excon::Response.new
15
+ response.body = '<html><h1>Not Found</h1><p>The resource could not be found.</p></html>'
16
+ response.status = 404
17
+ response.headers = {"Content-Length"=>"70", "Content-Type"=>"text/html; charset=UTF-8", "X-Trans-Id"=>"abcdefghijklmnopqrstuvwx-0123456789", "Date"=>Time.now}
18
+ response
19
+ end
20
+ end
21
+ end
22
+
23
+ class Real
24
+
25
+ # Create a new object
26
+ #
27
+ # When passed a block, it will make a chunked request, calling
28
+ # the block for chunks until it returns an empty string.
29
+ # In this case the data parameter is ignored.
30
+ #
31
+ # ==== Parameters
32
+ # * container<~String> - Name for container, should be < 256 bytes and must not contain '/'
33
+ # * object<~String> - Name for object
34
+ # * data<~String|File> - data to upload
35
+ # * options<~Hash> - config headers for object. Defaults to {}.
36
+ # * block<~Proc> - chunker
37
+ #
38
+ def put_object(container, object, data, options = {}, &block)
39
+ if block_given?
40
+ params = { :request_block => block }
41
+ headers = options
42
+ else
43
+ data = Fog::Storage.parse_data(data)
44
+ headers = data[:headers].merge!(options)
45
+ params = { :body => data[:body] }
46
+ end
47
+
48
+ params.merge!(
49
+ :expects => 201,
50
+ :idempotent => !params[:request_block],
51
+ :headers => headers,
52
+ :method => 'PUT',
53
+ :path => "#{Fog::Softlayer.escape(container)}/#{Fog::Softlayer.escape(object)}"
54
+ )
55
+
56
+ request(params)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,16 @@
1
+ module Fog
2
+ module Storage
3
+ class Softlayer
4
+ class Real
5
+
6
+ # Create a new dynamic large object manifest
7
+ #
8
+ # This is an alias for {#put_dynamic_obj_manifest} for backward compatibility.
9
+ def put_object_manifest(container, object, options = {})
10
+ put_dynamic_obj_manifest(container, object, options)
11
+ end
12
+
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,57 @@
1
+ module Fog
2
+ module Storage
3
+ class Softlayer
4
+ class Real
5
+
6
+ # Create a new static large object manifest.
7
+ #
8
+ # A static large object is similar to a dynamic large object. Whereas a GET for a dynamic large object manifest
9
+ # will stream segments based on the manifest's +X-Object-Manifest+ object name prefix, a static large object
10
+ # manifest streams segments which are defined by the user within the manifest. Information about each segment is
11
+ # provided in +segments+ as an Array of Hash objects, ordered in the sequence which the segments should be streamed.
12
+ #
13
+ # When the SLO manifest is received, each segment's +etag+ and +size_bytes+ will be verified.
14
+ # The +etag+ for each segment is returned in the response to {#put_object}, but may also be calculated.
15
+ # e.g. +Digest::MD5.hexdigest(segment_data)+
16
+ #
17
+ # The maximum number of segments for a static large object is 1000, and all segments (except the last) must be
18
+ # at least 1 MiB in size. Unlike a dynamic large object, segments are not required to be in the same container.
19
+ #
20
+ # @example
21
+ # segments = [
22
+ # { :path => 'segments_container/first_segment',
23
+ # :etag => 'md5 for first_segment',
24
+ # :size_bytes => 'byte size of first_segment' },
25
+ # { :path => 'segments_container/second_segment',
26
+ # :etag => 'md5 for second_segment',
27
+ # :size_bytes => 'byte size of second_segment' }
28
+ # ]
29
+ # put_static_obj_manifest('my_container', 'my_large_object', segments)
30
+ #
31
+ # @param container [String] Name for container where +object+ will be stored.
32
+ # Should be < 256 bytes and must not contain '/'
33
+ # @param object [String] Name for manifest object.
34
+ # @param segments [Array<Hash>] Segment data for the object.
35
+ # @param options [Hash] Config headers for +object+.
36
+ #
37
+ # @raise [Fog::Storage::Softlayer::NotFound] HTTP 404
38
+ # @raise [Excon::Errors::BadRequest] HTTP 400
39
+ # @raise [Excon::Errors::Unauthorized] HTTP 401
40
+ # @raise [Excon::Errors::HTTPStatusError]
41
+ #
42
+ # @see http://docs.openstack.org/api/openstack-object-storage/1.0/content/static-large-objects.html
43
+ def put_static_obj_manifest(container, object, segments, options = {})
44
+ request(
45
+ :expects => 201,
46
+ :method => 'PUT',
47
+ :headers => options,
48
+ :body => Fog::JSON.encode(segments),
49
+ :path => "#{Fog::Softlayer.escape(container)}/#{Fog::Softlayer.escape(object)}",
50
+ :query => { 'multipart-manifest' => 'put' }
51
+ )
52
+ end
53
+
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,283 @@
1
+ require 'fog/softlayer/core'
2
+
3
+ module Fog
4
+ module Storage
5
+ class Softlayer < Fog::Service
6
+ requires :softlayer_username, :softlayer_api_key, :softlayer_cluster
7
+ recognizes :persistent, :softlayer_storage_account, :softlayer_temp_url_key
8
+
9
+ model_path 'fog/softlayer/models/storage'
10
+ model :directory
11
+ collection :directories
12
+ model :file
13
+ collection :files
14
+
15
+ request_path 'fog/softlayer/requests/storage'
16
+ request :copy_object
17
+ request :delete_container
18
+ request :delete_object
19
+ request :delete_multiple_objects
20
+ request :delete_static_large_object
21
+ request :get_container
22
+ request :get_containers
23
+ request :get_object
24
+ request :get_object_https_url
25
+ request :head_container
26
+ request :head_containers
27
+ request :head_object
28
+ request :put_container
29
+ request :put_object
30
+ request :put_object_manifest
31
+ request :put_dynamic_obj_manifest
32
+ request :put_static_obj_manifest
33
+ request :post_set_meta_temp_url_key
34
+
35
+ class Mock
36
+
37
+ def self.data
38
+ @data ||= Hash.new do |hash, key|
39
+ hash[key] = {}
40
+ end
41
+ end
42
+
43
+ def self.reset
44
+ @data = nil
45
+ end
46
+
47
+ def initialize(options={})
48
+ @softlayer_api_key = options[:softlayer_api_key]
49
+ @softlayer_username = options[:softlayer_username]
50
+ @path = '/v1/AUTH_1234'
51
+ @containers = {}
52
+ end
53
+
54
+ def data
55
+ self.class.data[@softlayer_username]
56
+ end
57
+
58
+ def reset_data
59
+ self.class.data.delete(@softlayer_username)
60
+ end
61
+
62
+ def change_account(account)
63
+ @original_path ||= @path
64
+ version_string = @original_path.split('/')[1]
65
+ @path = "/#{version_string}/#{account}"
66
+ end
67
+
68
+ def reset_account_name
69
+ @path = @original_path
70
+ end
71
+
72
+ end
73
+
74
+ class Real
75
+ attr_reader :auth_url
76
+ attr_accessor :auth_token, :auth_expires
77
+
78
+ def initialize(options={})
79
+ @api_key = options[:softlayer_api_key]
80
+ @username = options[:softlayer_username]
81
+ @cluster = options[:softlayer_cluster]
82
+ @storage_account = options[:softlayer_storage_account] || default_storage_account
83
+ @connection_options = options[:connection_options] || {}
84
+ authenticate
85
+ @persistent = options[:persistent] || false
86
+ @connection = Fog::Core::Connection.new("#{@scheme}://#{@host}:#{@port}", @persistent, @connection_options)
87
+ @temp_url_key = options[:softlayer_temp_url_key] || get_temp_url_key_for_account
88
+ end
89
+
90
+ def auth_url
91
+ "https://#{@cluster}.#{Fog::Softlayer::SL_STORAGE_AUTH_URL}"
92
+ end
93
+
94
+ def reload
95
+ @connection.reset
96
+ end
97
+
98
+ def request(params = {}, parse_json = true)
99
+ begin
100
+ params.is_a?(Hash) or raise ArgumentError, "#{self.class}#request params must be a Hash"
101
+ params = _build_params(params)
102
+ response = @connection.request(params)
103
+
104
+ if response.status == 401 && !!@auth_token
105
+ @auth_token = nil; @auth_expires = nil
106
+ authenticate
107
+ response = @connection.request(params)
108
+ end
109
+
110
+ if !response.body.empty? && parse_json && response.get_header('Content-Type') =~ %r{application/json}
111
+ response.body = Fog::JSON.decode(response.body)
112
+ end
113
+
114
+ response
115
+ rescue Excon::Errors::HTTPStatusError => error
116
+ raise case error
117
+ when Excon::Errors::NotFound
118
+ Fog::Storage::Softlayer::NotFound.slurp(error)
119
+ else
120
+ error
121
+ end
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ def _auth_headers
128
+ {
129
+ :headers => {
130
+ 'User-Agent' => "Fog SoftLayer Adapter #{Fog::Softlayer::VERSION}",
131
+ 'X-Auth-User' => "#{@storage_account}:#{@username}",
132
+ 'X-Auth-Key' => @api_key
133
+ }
134
+ }
135
+ end
136
+
137
+ def _build_params(params)
138
+ output = {
139
+ :method => params.delete(:method) || :get
140
+ }
141
+
142
+ output[:path] = params[:path] ? "#{@path}/#{params.delete(:path)}".sub(/\/$/, '') : @path
143
+
144
+ output = output.deep_merge(params)
145
+ output.deep_merge(_headers)
146
+ end
147
+
148
+ def _headers
149
+ {
150
+ :headers => {
151
+ 'Content-Type' => 'application/json',
152
+ 'Accept' => 'application/json',
153
+ 'X-Auth-Token' => @auth_token
154
+ }
155
+ }
156
+ end
157
+
158
+ def authenticate
159
+ if requires_auth?
160
+ connection = Fog::Core::Connection.new(auth_url, false, _auth_headers)
161
+ response = connection.request(:method => :get)
162
+
163
+ raise Fog::Errors::Error.new("Could not authenticate Object Storage User.") unless response.status.between?(200, 208)
164
+
165
+ @auth_token = response.headers['X-Auth-Token']
166
+ @auth_expires = Time.now + response.headers['X-Auth-Token-Expires'].to_i
167
+ @storage_token = response.headers['X-Storage-Token']
168
+
169
+ uri = URI.parse(response.headers['X-Storage-Url'])
170
+ @host = uri.host
171
+ @path = uri.path
172
+ @path.sub!(/\/$/, '')
173
+ @port = uri.port
174
+ @scheme = uri.scheme
175
+ end
176
+ true
177
+ end
178
+
179
+ def default_storage_account
180
+ slapi = Fog::Compute[:softlayer].request(:account, :get_hub_network_storage)
181
+ slapi.body.map { |store| store['username'] }.first if slapi.body and slapi.body.instance_of? Array
182
+ end
183
+
184
+ def get_temp_url_key_for_account
185
+ request.headers['X-Account-Meta-Temp-Url-Key']
186
+ end
187
+
188
+ def requires_auth?
189
+ !@auth_token || !@auth_expires || (@auth_expires.to_i - Time.now.to_i) < 30
190
+ end
191
+
192
+ end
193
+
194
+
195
+
196
+ # Thanks to @camertron! https://gist.github.com/camertron/2939093
197
+ module Memory
198
+ # sizes are a guess, close enough for Mocks
199
+ REF_SIZE = 4 # ?
200
+ OBJ_OVERHEAD = 4 # ?
201
+ FIXNUM_SIZE = 4 # ?
202
+
203
+ # informational output from analysis
204
+ MemoryInfo = Struct.new :roots, :objects, :bytes, :loops
205
+
206
+ def self.analyze(*roots)
207
+ an = Analyzer.new
208
+ an.roots = roots
209
+ an.analyze
210
+ end
211
+
212
+ class Analyzer
213
+ attr_accessor :roots
214
+ attr_reader :result
215
+
216
+ def analyze
217
+ @result = MemoryInfo.new roots, 0, 0, 0
218
+ @objs = {}
219
+
220
+ queue = roots.dup
221
+
222
+ until queue.empty?
223
+ obj = queue.shift
224
+
225
+ case obj
226
+ when IO
227
+ visit(obj)
228
+ when String
229
+ visit(obj) { @result.bytes += obj.size }
230
+ when Fixnum
231
+ @result.bytes += FIXNUM_SIZE
232
+ when Array
233
+ visit(obj) do
234
+ @result.bytes += obj.size * REF_SIZE
235
+ queue.concat(obj)
236
+ end
237
+ when Hash
238
+ visit(obj) do
239
+ @result.bytes += obj.size * REF_SIZE * 2
240
+ obj.each {|k,v| queue.push(k).push(v)}
241
+ end
242
+ when Enumerable
243
+ visit(obj) do
244
+ obj.each do |o|
245
+ @result.bytes += REF_SIZE
246
+ queue.push(o)
247
+ end
248
+ end
249
+ else
250
+ visit(obj) do
251
+ obj.instance_variables.each do |var|
252
+ @result.bytes += REF_SIZE
253
+ queue.push(obj.instance_variable_get(var))
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ @result
260
+ end
261
+
262
+ private
263
+ def visit(obj)
264
+ id = obj.object_id
265
+
266
+ if @objs.has_key? id
267
+ @result.loops += 1
268
+ false
269
+ else
270
+ @objs[id] = true
271
+ @result.bytes += OBJ_OVERHEAD
272
+ @result.objects += 1
273
+ yield obj if block_given?
274
+ true
275
+ end
276
+ end
277
+ end
278
+ end
279
+
280
+ end
281
+ end
282
+ end
283
+