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 +4 -4
- data/CHANGELOG.md +6 -0
- data/LICENSE +13 -0
- data/README.md +18 -3
- data/lib/miasma/contrib/aws.rb +18 -1
- data/lib/miasma/contrib/aws/orchestration.rb +3 -3
- data/lib/miasma/contrib/aws/storage.rb +330 -0
- data/lib/miasma/contrib/open_stack.rb +334 -0
- data/lib/miasma/contrib/open_stack/compute.rb +105 -0
- data/lib/miasma/contrib/open_stack/orchestration.rb +255 -0
- data/lib/miasma/contrib/rackspace.rb +34 -94
- data/lib/miasma/contrib/rackspace/compute.rb +1 -92
- data/lib/miasma/contrib/rackspace/orchestration.rb +1 -231
- data/lib/miasma/error.rb +7 -5
- data/lib/miasma/models/orchestration/resource.rb +1 -1
- data/lib/miasma/models/orchestration/stack.rb +2 -2
- data/lib/miasma/models/storage.rb +36 -8
- data/lib/miasma/models/storage/bucket.rb +23 -10
- data/lib/miasma/models/storage/file.rb +44 -4
- data/lib/miasma/models/storage/files.rb +6 -1
- data/lib/miasma/types/api.rb +10 -4
- data/lib/miasma/types/model.rb +4 -1
- data/lib/miasma/utils/lazy.rb +7 -2
- data/lib/miasma/version.rb +1 -1
- data/miasma.gemspec +2 -1
- metadata +21 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 70ab81fefcaa4ab8942ca15c53e1ad296a5d5938
|
4
|
+
data.tar.gz: b2042bdf52407d095767388854adad5d8783526a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6cb01229d765fc2f5d66ba690848af6dc1af060641e4ce4d0d3d791d27051d0a35511077a2dcfdc7ef3cc6e74aee3a395cf61703f1a507b38ee130061b409b49
|
7
|
+
data.tar.gz: f83fc149fb9b9bac850ef563a17eb133472d281a65ac6ce3db66c32da4bb4e12b5d367b761e1b040ad4f01305977067d07dc70cde63d9534752a6148f62fbd3f
|
data/CHANGELOG.md
CHANGED
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
|
data/lib/miasma/contrib/aws.rb
CHANGED
@@ -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
|
-
:
|
78
|
-
:
|
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
|
-
:
|
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
|