miasma 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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