fog-softlayer 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+