miasma 0.1.0 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8f4c2fc901adb9940a31de3c3379646e1225eea6
4
- data.tar.gz: c45a82a7de2a1004a51a6c1a3b5fb10c60028088
3
+ metadata.gz: 70ab81fefcaa4ab8942ca15c53e1ad296a5d5938
4
+ data.tar.gz: b2042bdf52407d095767388854adad5d8783526a
5
5
  SHA512:
6
- metadata.gz: 0e0147bc6672e477a407ff627ea6e37839cae93bc16c6f26499cd5f57a4069358a2e6849062d153bfd1b9940b23fe0568f0e5c6cf40b561be0bdfcd4476ff461
7
- data.tar.gz: cdec65223d5ede929aad74fdf520b1a99c9a67a5d63a4559fff2cf414c5bad11eba6342da21fe9a5f27b89ff2f2b71488452e65123da99cac6a7add616902e74
6
+ metadata.gz: 6cb01229d765fc2f5d66ba690848af6dc1af060641e4ce4d0d3d791d27051d0a35511077a2dcfdc7ef3cc6e74aee3a395cf61703f1a507b38ee130061b409b49
7
+ data.tar.gz: f83fc149fb9b9bac850ef563a17eb133472d281a65ac6ce3db66c32da4bb4e12b5d367b761e1b040ad4f01305977067d07dc70cde63d9534752a6148f62fbd3f
@@ -1,2 +1,8 @@
1
+ # v0.2.0
2
+ * Add initial OpenStack provider support
3
+ * Refactor of Rackspace provider support (build off OpenStack)
4
+ * Finish defining storage modeling interfaces
5
+ * Add initial storage provider implementation (AWS)
6
+
1
7
  # v0.1.0
2
8
  * Initial release
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2014 Chris Roberts
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md CHANGED
@@ -143,6 +143,7 @@ model completions.
143
143
 
144
144
  * AWS
145
145
  * Rackspace
146
+ * OpenStack
146
147
 
147
148
  ### Models
148
149
 
@@ -150,7 +151,7 @@ model completions.
150
151
 
151
152
  |Model |Create|Read|Update|Delete|
152
153
  |--------------|------|----|------|------|
153
- |AutoScale | X | | | |
154
+ |AutoScale | X | X | | |
154
155
  |BlockStorage | | | | |
155
156
  |Compute | X | X | | X |
156
157
  |DNS | | | | |
@@ -158,13 +159,13 @@ model completions.
158
159
  |Network | | | | |
159
160
  |Orchestration | X | X | X | X |
160
161
  |Queues | | | | |
161
- |Storage | | | | |
162
+ |Storage | X | X | X | X |
162
163
 
163
164
  #### Rackspace
164
165
 
165
166
  |Model |Create|Read|Update|Delete|
166
167
  |--------------|------|----|------|------|
167
- |AutoScale | X | | | |
168
+ |AutoScale | X | X | | |
168
169
  |BlockStorage | | | | |
169
170
  |Compute | X | X | | X |
170
171
  |DNS | | | | |
@@ -174,6 +175,20 @@ model completions.
174
175
  |Queues | | | | |
175
176
  |Storage | | | | |
176
177
 
178
+ #### OpenStack
179
+
180
+ |Model |Create|Read|Update|Delete|
181
+ |--------------|------|----|------|------|
182
+ |AutoScale | | | | |
183
+ |BlockStorage | | | | |
184
+ |Compute | X | X | | X |
185
+ |DNS | | | | |
186
+ |LoadBalancer | | | | |
187
+ |Network | | | | |
188
+ |Orchestration | X | X | X | X |
189
+ |Queues | | | | |
190
+ |Storage | | | | |
191
+
177
192
  ## Info
178
193
 
179
194
  * Repository: https://github.com/chrisroberts/miasma
@@ -294,6 +294,7 @@ module Miasma
294
294
  attribute :aws_secret_access_key, String, :required => true
295
295
  attribute :aws_region, String, :required => true
296
296
  attribute :aws_host, String
297
+ attribute :aws_bucket_region, String
297
298
 
298
299
  # @return [Contrib::AwsApiCore::SignatureV4]
299
300
  attr_reader :signer
@@ -332,6 +333,11 @@ module Miasma
332
333
  )
333
334
  end
334
335
 
336
+ # @return [String] custom escape for aws compat
337
+ def uri_escape(string)
338
+ signer.safe_escape(string)
339
+ end
340
+
335
341
  # @return [HTTP] connection for requests (forces headers)
336
342
  def connection
337
343
  super.with_headers(
@@ -356,7 +362,7 @@ module Miasma
356
362
  def make_request(connection, http_method, request_args)
357
363
  dest, options = request_args
358
364
  path = URI.parse(dest).path
359
- options = options.to_smash
365
+ options = options ? options.to_smash : Smash.new
360
366
  options[:params] = options.fetch(:params, Smash.new).to_smash.deep_merge('Version' => self.class::API_VERSION)
361
367
  if(http_method.to_sym == :post)
362
368
  if(options[:form])
@@ -365,6 +371,7 @@ module Miasma
365
371
  options[:form] = options.delete(:params)
366
372
  end
367
373
  end
374
+ update_request(connection, options)
368
375
  signature = signer.generate(
369
376
  http_method, path, options.merge(
370
377
  Smash.new(
@@ -378,6 +385,15 @@ module Miasma
378
385
  connection.auth(signature).send(http_method, dest, options)
379
386
  end
380
387
 
388
+ # Simple callback to allow request option adjustments prior to
389
+ # signature calculation
390
+ #
391
+ # @param opts [Smash] request options
392
+ # @return [TrueClass]
393
+ def update_request(con, opts)
394
+ true
395
+ end
396
+
381
397
  end
382
398
 
383
399
  end
@@ -387,4 +403,5 @@ module Miasma
387
403
  Models::LoadBalancer.autoload :Aws, 'miasma/contrib/aws/load_balancer'
388
404
  Models::AutoScale.autoload :Aws, 'miasma/contrib/aws/auto_scale'
389
405
  Models::Orchestration.autoload :Aws, 'miasma/contrib/aws/orchestration'
406
+ Models::Storage.autoload :Aws, 'miasma/contrib/aws/storage'
390
407
  end
@@ -74,8 +74,8 @@ module Miasma
74
74
  :name => stk['StackName'],
75
75
  :capabilities => [stk.get('Capabilities', 'member')].flatten(1).compact,
76
76
  :description => stk['Description'],
77
- :creation_time => stk['CreationTime'],
78
- :updated_time => stk['LastUpdatedTime'],
77
+ :created => stk['CreationTime'],
78
+ :updated => stk['LastUpdatedTime'],
79
79
  :notification_topics => [stk.get('NotificationARNs', 'member')].flatten(1).compact,
80
80
  :timeout_in_minutes => stk['TimeoutInMinutes'],
81
81
  :status => stk['StackStatus'],
@@ -265,7 +265,7 @@ module Miasma
265
265
  :state => res['ResourceStatus'].downcase.to_sym,
266
266
  :status => res['ResourceStatus'],
267
267
  :status_reason => res['ResourceStatusReason'],
268
- :updated_time => res['Timestamp']
268
+ :updated => res['Timestamp']
269
269
  ).valid_state
270
270
  end
271
271
  end
@@ -0,0 +1,330 @@
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].length.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 = request(:path => '/')
121
+ [result.get(:body, 'ListAllMyBucketsResult', 'Buckets', 'Bucket')].flatten.compact.map do |bkt|
122
+ Bucket.new(
123
+ self,
124
+ :id => bkt['Name'],
125
+ :name => bkt['Name'],
126
+ :created => bkt['CreationDate']
127
+ ).valid_state
128
+ end
129
+ end
130
+
131
+ # Return all files within bucket
132
+ #
133
+ # @param bucket [Bucket]
134
+ # @return [Array<File>]
135
+ def file_all(bucket)
136
+ result = request(
137
+ :path => '/',
138
+ :endpoint => bucket_endpoint(bucket)
139
+ )
140
+ [result.get(:body, 'ListBucketResult', 'Contents')].flatten.compact.map do |file|
141
+ File.new(
142
+ bucket,
143
+ :id => ::File.join(bucket.name, file['Key']),
144
+ :name => file['Key'],
145
+ :updated => file['LastModified'],
146
+ :size => file['Size'].to_i
147
+ ).valid_state
148
+ end
149
+ end
150
+
151
+ # Save file
152
+ #
153
+ # @param file [Models::Storage::File]
154
+ # @return [Models::Storage::File]
155
+ def file_save(file)
156
+ if(file.dirty?)
157
+ file.load_data(file.attributes)
158
+ args = Smash.new
159
+ args[:headers] = Smash[
160
+ Smash.new(
161
+ :content_type => 'Content-Type',
162
+ :content_disposition => 'Content-Disposition',
163
+ :content_encoding => 'Content-Encoding'
164
+ ).map do |attr, key|
165
+ if(file.attributes[attr])
166
+ [key, file.attributes[attr]]
167
+ end
168
+ end.compact
169
+ ]
170
+ if(file.attributes[:body].is_a?(IO) && file.body.length >= 102400)
171
+ upload_id = request(
172
+ args.merge(
173
+ Smash.new(
174
+ :path => uri_escape(file.name),
175
+ :endpoint => bucket_endpoint(bucket),
176
+ :params => {
177
+ :uploads => true
178
+ }
179
+ )
180
+ )
181
+ ).get(:body, 'InitiateMultipartUploadResult', 'UploadId')
182
+ count = 1
183
+ parts = []
184
+ file.body.rewind
185
+ while(content = file.body.read(102400))
186
+ parts << [
187
+ count,
188
+ request(
189
+ :method => :put,
190
+ :path => uri_escape(file.name),
191
+ :endpoint => bucket_endpoint(bucket),
192
+ :headers => Smash.new(
193
+ 'Content-Length' => content.size,
194
+ 'Content-MD5' => Digest::MD5.hexdigest(content)
195
+ ),
196
+ :params => Smash.new(
197
+ 'partNumber' => count,
198
+ 'uploadId' => upload_id
199
+ ),
200
+ :body => content
201
+ ).get(:body, :headers, :etag)
202
+ ]
203
+ count += 1
204
+ end
205
+ complete = SimpleXml.xml_out(
206
+ Smash.new(
207
+ 'CompleteMultipartUpload' => {
208
+ 'Part' => parts.map{|part|
209
+ {'PartNumber' => part.first, 'ETag' => part.last}
210
+ }
211
+ }
212
+ ),
213
+ 'AttrPrefix' => true,
214
+ 'KeepRoot' => true
215
+ )
216
+ result = request(
217
+ :method => :post,
218
+ :path => uri_escape(file.name),
219
+ :endpoint => bucket_endpoint(file.bucket),
220
+ :params => Smash.new(
221
+ 'UploadId' => upload_id
222
+ ),
223
+ :headers => Smash.new(
224
+ 'Content-Length' => complete.size
225
+ ),
226
+ :body => complete
227
+ )
228
+ file.etag = result.get(:body, 'CompleteMultipartUploadResult', 'ETag')
229
+ else
230
+ if(file.attributes[:body].is_a?(IO) || file.attributes[:body].is_a?(StringIO))
231
+ args[:headers]['Content-Length'] = file.body.length.to_s
232
+ file.body.rewind
233
+ args[:body] = file.body.read
234
+ file.body.rewind
235
+ end
236
+ result = request(
237
+ args.merge(
238
+ Smash.new(
239
+ :method => :put,
240
+ :path => uri_escape(file.name),
241
+ :endpoint => bucket_endpoint(file.bucket)
242
+ )
243
+ )
244
+ )
245
+ file.etag = result.get(:headers, :etag)
246
+ end
247
+ file.id = ::File.join(file.bucket.name, file.name)
248
+ file.valid_state
249
+ end
250
+ file
251
+ end
252
+
253
+ # Destroy file
254
+ #
255
+ # @param file [Models::Storage::File]
256
+ # @return [TrueClass, FalseClass]
257
+ def file_destroy(file)
258
+ if(file.persisted?)
259
+ request(
260
+ :method => :delete,
261
+ :path => file.name,
262
+ :endpoint => bucket_endpoint(file.bucket),
263
+ :expect => 204
264
+ )
265
+ true
266
+ else
267
+ false
268
+ end
269
+ end
270
+
271
+ # Reload the file
272
+ #
273
+ # @param file [Models::Storage::File]
274
+ # @return [Models::Storage::File]
275
+ def file_reload(file)
276
+ if(file.persisted?)
277
+ name = file.name
278
+ result = request(
279
+ :path => uri_escape(file.name),
280
+ :endpoint => bucket_endpoint(file.bucket)
281
+ )
282
+ file.data.clear && file.dirty.clear
283
+ info = result[:headers]
284
+ file.load_data(
285
+ :id => ::File.join(file.bucket.name, name),
286
+ :name => name,
287
+ :updated => info[:last_modified],
288
+ :etag => info[:etag],
289
+ :size => info[:content_length].to_i,
290
+ :content_type => info[:content_type]
291
+ ).valid_state
292
+ end
293
+ file
294
+ end
295
+
296
+ # Fetch the contents of the file
297
+ #
298
+ # @param file [Models::Storage::File]
299
+ # @return [IO, HTTP::Response::Body]
300
+ def file_body(file)
301
+ if(file.persisted?)
302
+ result = request(
303
+ :path => uri_escape(file.name),
304
+ :endpoint => bucket_endpoint(file.bucket)
305
+ )
306
+ content = result[:body]
307
+ content.is_a?(String) ? StringIO.new(content) : content
308
+ else
309
+ StringIO.new('')
310
+ end
311
+ end
312
+
313
+ # Simple callback to allow request option adjustments prior to
314
+ # signature calculation
315
+ #
316
+ # @param opts [Smash] request options
317
+ # @return [TrueClass]
318
+ # @note this only updates when :body is defined. if a :post is
319
+ # happening (which implicitly forces :form) or :json is used
320
+ # it will not properly checksum. (but that's probably okay)
321
+ def update_request(con, opts)
322
+ con.default_headers['x-amz-content-sha256'] = Digest::SHA256.
323
+ hexdigest(opts.fetch(:body, ''))
324
+ true
325
+ end
326
+
327
+ end
328
+ end
329
+ end
330
+ end