miasma 0.2.10 → 0.2.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,405 +0,0 @@
1
- require 'stringio'
2
- require 'xmlsimple'
3
- require 'miasma'
4
-
5
- module Miasma
6
- module Models
7
- class Storage
8
- class Aws < Storage
9
-
10
- # Service name of the API
11
- API_SERVICE = 's3'
12
- # Supported version of the AutoScaling API
13
- API_VERSION = '2006-03-01'
14
-
15
- include Contrib::AwsApiCore::ApiCommon
16
- include Contrib::AwsApiCore::RequestUtils
17
-
18
- # Simple init override to force HOST and adjust region for
19
- # signatures if required
20
- def initialize(args)
21
- args = args.to_smash
22
- cache_region = args[:aws_region]
23
- args[:aws_region] = args.fetch(:aws_bucket_region, 'us-east-1')
24
- super(args)
25
- aws_region = cache_region
26
- if(aws_bucket_region)
27
- self.aws_host = "s3-#{aws_bucket_region}.amazonaws.com"
28
- else
29
- self.aws_host = 's3.amazonaws.com'
30
- end
31
- end
32
-
33
- # Save bucket
34
- #
35
- # @param bucket [Models::Storage::Bucket]
36
- # @return [Models::Storage::Bucket]
37
- def bucket_save(bucket)
38
- unless(bucket.persisted?)
39
- req_args = Smash.new(
40
- :method => :put,
41
- :path => '/',
42
- :endpoint => bucket_endpoint(bucket)
43
- )
44
- if(aws_bucket_region)
45
- req_args[:body] = XmlSimple.xml_out(
46
- Smash.new(
47
- 'CreateBucketConfiguration' => {
48
- 'LocationConstraint' => aws_bucket_region
49
- }
50
- ),
51
- 'AttrPrefix' => true,
52
- 'KeepRoot' => true
53
- )
54
- req_args[:headers] = Smash.new(
55
- 'Content-Length' => req_args[:body].size.to_s
56
- )
57
- end
58
- request(req_args)
59
- bucket.id = bucket.name
60
- bucket.valid_state
61
- end
62
- bucket
63
- end
64
-
65
- # Destroy bucket
66
- #
67
- # @param bucket [Models::Storage::Bucket]
68
- # @return [TrueClass, FalseClass]
69
- def bucket_destroy(bucket)
70
- if(bucket.persisted?)
71
- request(
72
- :path => '/',
73
- :method => :delete,
74
- :endpoint => bucket_endpoint(bucket),
75
- :expects => 204
76
- )
77
- true
78
- else
79
- false
80
- end
81
- end
82
-
83
- # Reload the bucket
84
- #
85
- # @param bucket [Models::Storage::Bucket]
86
- # @return [Models::Storage::Bucket]
87
- def bucket_reload(bucket)
88
- if(bucket.persisted?)
89
- begin
90
- result = request(
91
- :path => '/',
92
- :method => :head,
93
- :endpoint => bucket_endpoint(bucket)
94
- )
95
- rescue Error::ApiError::RequestError => e
96
- if(e.response.status == 404)
97
- bucket.data.clear
98
- bucket.dirty.clear
99
- else
100
- raise
101
- end
102
- end
103
- end
104
- bucket
105
- end
106
-
107
- # Custom bucket endpoint
108
- #
109
- # @param bucket [Models::Storage::Bucket]
110
- # @return [String]
111
- # @todo properly escape bucket name
112
- def bucket_endpoint(bucket)
113
- ::File.join(endpoint, bucket.name)
114
- end
115
-
116
- # Return all buckets
117
- #
118
- # @return [Array<Models::Storage::Bucket>]
119
- def bucket_all
120
- result = all_result_pages(nil, :body, 'ListAllMyBucketsResult', 'Buckets', 'Bucket') do |options|
121
- request(
122
- :path => '/',
123
- :params => options
124
- )
125
- end
126
- result.map do |bkt|
127
- Bucket.new(
128
- self,
129
- :id => bkt['Name'],
130
- :name => bkt['Name'],
131
- :created => bkt['CreationDate']
132
- ).valid_state
133
- end
134
- end
135
-
136
- # Return filtered files
137
- #
138
- # @param args [Hash] filter options
139
- # @return [Array<Models::Storage::File>]
140
- def file_filter(bucket, args)
141
- if(args[:prefix])
142
- result = request(
143
- :path => '/',
144
- :endpoint => bucket_endpoint(bucket),
145
- :params => Smash.new(
146
- :prefix => args[:prefix]
147
- )
148
- )
149
- [result.get(:body, 'ListBucketResult', 'Contents')].flatten.compact.map do |file|
150
- File.new(
151
- bucket,
152
- :id => ::File.join(bucket.name, file['Key']),
153
- :name => file['Key'],
154
- :updated => file['LastModified'],
155
- :size => file['Size'].to_i
156
- ).valid_state
157
- end
158
- else
159
- bucket_all
160
- end
161
- end
162
-
163
- # Return all files within bucket
164
- #
165
- # @param bucket [Bucket]
166
- # @return [Array<File>]
167
- def file_all(bucket)
168
- result = all_result_pages(nil, :body, 'ListBucketResult', 'Contents') do |options|
169
- request(
170
- :path => '/',
171
- :params => options,
172
- :endpoint => bucket_endpoint(bucket)
173
- )
174
- end
175
- result.map do |file|
176
- File.new(
177
- bucket,
178
- :id => ::File.join(bucket.name, file['Key']),
179
- :name => file['Key'],
180
- :updated => file['LastModified'],
181
- :size => file['Size'].to_i
182
- ).valid_state
183
- end
184
- end
185
-
186
- # Save file
187
- #
188
- # @param file [Models::Storage::File]
189
- # @return [Models::Storage::File]
190
- def file_save(file)
191
- if(file.dirty?)
192
- file.load_data(file.attributes)
193
- args = Smash.new
194
- args[:headers] = Smash[
195
- Smash.new(
196
- :content_type => 'Content-Type',
197
- :content_disposition => 'Content-Disposition',
198
- :content_encoding => 'Content-Encoding'
199
- ).map do |attr, key|
200
- if(file.attributes[attr])
201
- [key, file.attributes[attr]]
202
- end
203
- end.compact
204
- ]
205
- if(file.attributes[:body].is_a?(IO) && file.body.size >= Storage::MAX_BODY_SIZE_FOR_STRINGIFY)
206
- upload_id = request(
207
- args.merge(
208
- Smash.new(
209
- :path => file_path(file),
210
- :endpoint => bucket_endpoint(bucket),
211
- :params => {
212
- :uploads => true
213
- }
214
- )
215
- )
216
- ).get(:body, 'InitiateMultipartUploadResult', 'UploadId')
217
- count = 1
218
- parts = []
219
- file.body.rewind
220
- while(content = file.body.read(Storage::READ_BODY_CHUNK_SIZE))
221
- parts << [
222
- count,
223
- request(
224
- :method => :put,
225
- :path => file_path(file),
226
- :endpoint => bucket_endpoint(bucket),
227
- :headers => Smash.new(
228
- 'Content-Length' => content.size,
229
- 'Content-MD5' => Digest::MD5.hexdigest(content)
230
- ),
231
- :params => Smash.new(
232
- 'partNumber' => count,
233
- 'uploadId' => upload_id
234
- ),
235
- :body => content
236
- ).get(:body, :headers, :etag)
237
- ]
238
- count += 1
239
- end
240
- complete = SimpleXml.xml_out(
241
- Smash.new(
242
- 'CompleteMultipartUpload' => {
243
- 'Part' => parts.map{|part|
244
- {'PartNumber' => part.first, 'ETag' => part.last}
245
- }
246
- }
247
- ),
248
- 'AttrPrefix' => true,
249
- 'KeepRoot' => true
250
- )
251
- result = request(
252
- :method => :post,
253
- :path => file_path(file),
254
- :endpoint => bucket_endpoint(file.bucket),
255
- :params => Smash.new(
256
- 'UploadId' => upload_id
257
- ),
258
- :headers => Smash.new(
259
- 'Content-Length' => complete.size
260
- ),
261
- :body => complete
262
- )
263
- file.etag = result.get(:body, 'CompleteMultipartUploadResult', 'ETag')
264
- else
265
- if(file.attributes[:body].is_a?(IO) || file.attributes[:body].is_a?(StringIO))
266
- args[:headers]['Content-Length'] = file.body.size.to_s
267
- file.body.rewind
268
- args[:body] = file.body.read
269
- file.body.rewind
270
- end
271
- result = request(
272
- args.merge(
273
- Smash.new(
274
- :method => :put,
275
- :path => file_path(file),
276
- :endpoint => bucket_endpoint(file.bucket)
277
- )
278
- )
279
- )
280
- file.etag = result.get(:headers, :etag)
281
- end
282
- file.id = ::File.join(file.bucket.name, file.name)
283
- file.valid_state
284
- end
285
- file
286
- end
287
-
288
- # Destroy file
289
- #
290
- # @param file [Models::Storage::File]
291
- # @return [TrueClass, FalseClass]
292
- def file_destroy(file)
293
- if(file.persisted?)
294
- request(
295
- :method => :delete,
296
- :path => file_path(file),
297
- :endpoint => bucket_endpoint(file.bucket),
298
- :expects => 204
299
- )
300
- true
301
- else
302
- false
303
- end
304
- end
305
-
306
- # Reload the file
307
- #
308
- # @param file [Models::Storage::File]
309
- # @return [Models::Storage::File]
310
- def file_reload(file)
311
- if(file.persisted?)
312
- name = file.name
313
- result = request(
314
- :path => file_path(file),
315
- :endpoint => bucket_endpoint(file.bucket)
316
- )
317
- file.data.clear && file.dirty.clear
318
- info = result[:headers]
319
- file.load_data(
320
- :id => ::File.join(file.bucket.name, name),
321
- :name => name,
322
- :updated => info[:last_modified],
323
- :etag => info[:etag],
324
- :size => info[:content_length].to_i,
325
- :content_type => info[:content_type]
326
- ).valid_state
327
- end
328
- file
329
- end
330
-
331
- # Create publicly accessible URL
332
- #
333
- # @param timeout_secs [Integer] seconds available
334
- # @return [String] URL
335
- def file_url(file, timeout_secs)
336
- if(file.persisted?)
337
- signer.generate_url(
338
- :get, ::File.join(uri_escape(file.bucket.name), file_path(file)),
339
- :headers => Smash.new(
340
- 'Host' => aws_host
341
- ),
342
- :params => Smash.new(
343
- 'X-Amz-Date' => Contrib::AwsApiCore.time_iso8601,
344
- 'X-Amz-Expires' => timeout_secs
345
- )
346
- )
347
- else
348
- raise Error::ModelPersistError.new "#{file} has not been saved!"
349
- end
350
- end
351
-
352
- # Fetch the contents of the file
353
- #
354
- # @param file [Models::Storage::File]
355
- # @return [IO, HTTP::Response::Body]
356
- def file_body(file)
357
- if(file.persisted?)
358
- result = request(
359
- :path => file_path(file),
360
- :endpoint => bucket_endpoint(file.bucket),
361
- :disable_body_extraction => true
362
- )
363
- content = result[:body]
364
- begin
365
- if(content.is_a?(String))
366
- StringIO.new(content)
367
- else
368
- if(content.respond_to?(:stream!))
369
- content.stream!
370
- end
371
- content
372
- end
373
- rescue HTTP::StateError
374
- StringIO.new(content.to_s)
375
- end
376
- else
377
- StringIO.new('')
378
- end
379
- end
380
-
381
- # Simple callback to allow request option adjustments prior to
382
- # signature calculation
383
- #
384
- # @param opts [Smash] request options
385
- # @return [TrueClass]
386
- # @note this only updates when :body is defined. if a :post is
387
- # happening (which implicitly forces :form) or :json is used
388
- # it will not properly checksum. (but that's probably okay)
389
- def update_request(con, opts)
390
- con.default_headers['x-amz-content-sha256'] = Digest::SHA256.
391
- hexdigest(opts.fetch(:body, ''))
392
- true
393
- end
394
-
395
- # @return [String] escaped file path
396
- def file_path(file)
397
- file.name.split('/').map do |part|
398
- uri_escape(part)
399
- end.join('/')
400
- end
401
-
402
- end
403
- end
404
- end
405
- end
@@ -1,341 +0,0 @@
1
- require 'miasma'
2
- require 'miasma/utils/smash'
3
- require 'time'
4
-
5
- module Miasma
6
- module Contrib
7
-
8
- # OpenStack API core helper
9
- class OpenStackApiCore
10
-
11
- # Authentication helper class
12
- class Authenticate
13
-
14
- # @return [Smash] token info
15
- attr_reader :token
16
- # @return [Smash] credentials in use
17
- attr_reader :credentials
18
-
19
- # Create new instance
20
- #
21
- # @return [self]
22
- def initialize(credentials)
23
- @credentials = credentials.to_smash
24
- end
25
-
26
- # @return [String] username
27
- def user
28
- load!
29
- @user
30
- end
31
-
32
- # @return [Smash] remote service catalog
33
- def service_catalog
34
- load!
35
- @service_catalog
36
- end
37
-
38
- # @return [String] current API token
39
- def api_token
40
- if(token.nil? || Time.now > token[:expires])
41
- identify_and_load
42
- end
43
- token[:id]
44
- end
45
-
46
- # Identify with authentication endpoint
47
- # and load the service catalog
48
- #
49
- # @return [self]
50
- def identity_and_load
51
- raise NotImplementedError
52
- end
53
-
54
- # @return [Smash] authentication request body
55
- def authentication_request
56
- raise NotImplementedError
57
- end
58
-
59
- protected
60
-
61
- # @return [TrueClass] load authenticator
62
- def load!
63
- !!api_token
64
- end
65
-
66
- # Authentication implementation compatible for v2
67
- class Version2 < Authenticate
68
-
69
- # @return [Smash] authentication request body
70
- def authentication_request
71
- if(credentials[:open_stack_token])
72
- auth = Smash.new(
73
- :token => Smash.new(
74
- :id => credentials[:open_stack_token]
75
- )
76
- )
77
- else
78
- auth = Smash.new(
79
- 'passwordCredentials' => Smash.new(
80
- 'username' => credentials[:open_stack_username],
81
- 'password' => credentials[:open_stack_password]
82
- )
83
- )
84
- end
85
- if(credentials[:open_stack_tenant_name])
86
- auth['tenantName'] = credentials[:open_stack_tenant_name]
87
- end
88
- auth
89
- end
90
-
91
- # Identify with authentication service and load
92
- # token information and service catalog
93
- #
94
- # @return [TrueClass]
95
- def identify_and_load
96
- result = HTTP.post(
97
- File.join(
98
- credentials[:open_stack_identity_url],
99
- 'tokens'
100
- ),
101
- :json => Smash.new(
102
- :auth => authentication_request
103
- )
104
- )
105
- unless(result.status == 200)
106
- raise Error::ApiError::AuthenticationError.new('Failed to authenticate', :response => result)
107
- end
108
- info = MultiJson.load(result.body.to_s).to_smash
109
- info = info[:access]
110
- @user = info[:user]
111
- @service_catalog = info[:serviceCatalog]
112
- @token = info[:token]
113
- token[:expires] = Time.parse(token[:expires])
114
- true
115
- end
116
-
117
- end
118
-
119
- # Authentication implementation compatible for v2
120
- class Version3 < Authenticate
121
-
122
- # @return [Smash] authentication request body
123
- def authentication_request
124
- ident = Smash.new(:methods => [])
125
- if(credentials[:open_stack_password])
126
- ident[:methods] << 'password'
127
- ident[:password] = Smash.new(
128
- :user => Smash.new(
129
- :password => credentials[:open_stack_password]
130
- )
131
- )
132
- if(credentials[:open_stack_user_id])
133
- ident[:password][:user][:id] = credentials[:open_stack_user_id]
134
- else
135
- ident[:password][:user][:name] = credentials[:open_stack_username]
136
- end
137
- if(credentials[:open_stack_domain])
138
- ident[:password][:user][:domain] = Smash.new(
139
- :name => credentials[:open_stack_domain]
140
- )
141
- end
142
- end
143
- if(credentials[:open_stack_token])
144
- ident[:methods] << 'token'
145
- ident[:token] = Smash.new(
146
- :token => Smash.new(
147
- :id => credentials[:open_stack_token]
148
- )
149
- )
150
- end
151
- if(credentials[:open_stack_project_id])
152
- scope = Smash.new(
153
- :project => Smash.new(
154
- :id => credentials[:open_stack_project_id]
155
- )
156
- )
157
- else
158
- if(credentials[:open_stack_domain])
159
- scope = Smash.new(
160
- :domain => Smash.new(
161
- :name => credentials[:open_stack_domain]
162
- )
163
- )
164
- if(credentials[:open_stack_project])
165
- scope[:project] = Smash.new(
166
- :name => credentials[:open_stack_project]
167
- )
168
- end
169
- end
170
- end
171
- auth = Smash.new(:identity => ident)
172
- if(scope)
173
- auth[:scope] = scope
174
- end
175
- auth
176
- end
177
-
178
- # Identify with authentication service and load
179
- # token information and service catalog
180
- #
181
- # @return [TrueClass]
182
- def identify_and_load
183
- result = HTTP.post(
184
- File.join(credentials[:open_stack_identity_url], 'tokens'),
185
- :json => Smash.new(
186
- :auth => authentication_request
187
- )
188
- )
189
- unless(result.status == 200)
190
- raise Error::ApiError::AuthenticationError.new('Failed to authenticate!', result)
191
- end
192
- info = MultiJson.load(result.body.to_s).to_smash[:token]
193
- @service_catalog = info.delete(:catalog)
194
- @token = Smash.new(
195
- :expires => Time.parse(info[:expires_at]),
196
- :id => result.headers['X-Subject-Token']
197
- )
198
- @user = info[:user][:name]
199
- true
200
- end
201
-
202
- end
203
-
204
- end
205
-
206
- # Common API methods
207
- module ApiCommon
208
-
209
- # Set attributes into model
210
- #
211
- # @param klass [Class]
212
- def self.included(klass)
213
- klass.class_eval do
214
- attribute :open_stack_identity_url, String, :required => true
215
- attribute :open_stack_username, String
216
- attribute :open_stack_user_id, String
217
- attribute :open_stack_password, String
218
- attribute :open_stack_token, String
219
- attribute :open_stack_region, String
220
- attribute :open_stack_tenant_name, String
221
- attribute :open_stack_domain, String
222
- attribute :open_stack_project, String
223
- end
224
- end
225
-
226
- # @return [HTTP] with auth token provided
227
- def connection
228
- super.with_headers('X-Auth-Token' => token)
229
- end
230
-
231
- # @return [String] endpoint URL
232
- def endpoint
233
- open_stack_api.endpoint_for(
234
- Utils.snake(self.class.to_s.split('::')[-2]).to_sym,
235
- open_stack_region
236
- )
237
- end
238
-
239
- # @return [String] valid API token
240
- def token
241
- open_stack_api.api_token
242
- end
243
-
244
- # @return [Miasma::Contrib::OpenStackApiCore]
245
- def open_stack_api
246
- key = "miasma_open_stack_api_#{attributes.checksum}".to_sym
247
- memoize(key, :direct) do
248
- Miasma::Contrib::OpenStackApiCore.new(attributes)
249
- end
250
- end
251
-
252
- end
253
-
254
- # @return [Smash] Mapping to external service name
255
- API_MAP = Smash.new(
256
- 'compute' => 'nova',
257
- 'orchestration' => 'heat',
258
- 'network' => 'neutron',
259
- 'identity' => 'keystone'
260
- )
261
-
262
- include Miasma::Utils::Memoization
263
-
264
- # @return [Miasma::Contrib::OpenStackApiCore::Authenticate]
265
- attr_reader :identity
266
-
267
- # Create a new api instance
268
- #
269
- # @param creds [Smash] credential hash
270
- # @return [self]
271
- def initialize(creds)
272
- @credentials = creds
273
- memo_key = "miasma_open_stack_identity_#{creds.checksum}"
274
- if(creds[:open_stack_identity_url].include?('v3'))
275
- @identity = memoize(memo_key, :direct) do
276
- identity_class('Authenticate::Version3').new(creds)
277
- end
278
- elsif(creds[:open_stack_identity_url].include?('v2'))
279
- @identity = memoize(memo_key, :direct) do
280
- identity_class('Authenticate::Version2').new(creds)
281
- end
282
- else
283
- # @todo allow attribute to override?
284
- raise ArgumentError.new('Failed to determine Identity service version')
285
- end
286
- end
287
-
288
- # @return [Class] class from instance class, falls back to parent
289
- def identity_class(i_name)
290
- [self.class, Miasma::Contrib::OpenStackApiCore].map do |klass|
291
- i_name.split('::').inject(klass) do |memo, key|
292
- if(memo.const_defined?(key))
293
- memo.const_get(key)
294
- else
295
- break
296
- end
297
- end
298
- end.compact.first
299
- end
300
-
301
- # Provide end point URL for service
302
- #
303
- # @param api_name [String] name of api
304
- # @param region [String] region in use
305
- # @return [String] public URL
306
- def endpoint_for(api_name, region)
307
- api = self.class.const_get(:API_MAP)[api_name]
308
- srv = identity.service_catalog.detect do |info|
309
- info[:name] == api
310
- end
311
- unless(srv)
312
- raise NotImplementedError.new("No API mapping found for `#{api_name}`")
313
- end
314
- if(region)
315
- point = srv[:endpoints].detect do |endpoint|
316
- endpoint[:region].to_s.downcase == region.to_s.downcase
317
- end
318
- else
319
- point = srv[:endpoints].first
320
- end
321
- if(point)
322
- point.fetch(
323
- :publicURL,
324
- point[:url]
325
- )
326
- else
327
- raise KeyError.new("Lookup failed for `#{api_name}` within region `#{region}`")
328
- end
329
- end
330
-
331
- # @return [String] API token
332
- def api_token
333
- identity.api_token
334
- end
335
-
336
- end
337
- end
338
-
339
- Models::Compute.autoload :OpenStack, 'miasma/contrib/open_stack/compute'
340
- Models::Orchestration.autoload :OpenStack, 'miasma/contrib/open_stack/orchestration'
341
- end