miasma-open-stack 0.1.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 +7 -0
- data/CHANGELOG.md +2 -0
- data/LICENSE +13 -0
- data/README.md +20 -0
- data/lib/miasma-open-stack.rb +2 -0
- data/lib/miasma-open-stack/version.rb +4 -0
- data/lib/miasma/contrib/open_stack.rb +343 -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/open_stack/storage.rb +342 -0
- data/miasma-open-stack.gemspec +15 -0
- metadata +68 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d37df368308f78f1e686ef8c27307a5a41691204
|
4
|
+
data.tar.gz: 9f35dcfd89dcc20fa7fdad3e219709fc665d0ed2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 21bbe599db191aa0f0d85645f708ca83aa172c07c97139a8563bb9a6db1025886a099cd13b0ed196b3bb44f49edbf005a5c3ad01a5758e0909dda5aeff1df613
|
7
|
+
data.tar.gz: e888e1aecd8bedcb02b63d581965b8bec77a40c222b1d713a13fe7587e2db3c48116e8bb11ae4b342210d3997eb594989b6cafc954aebc337c2e0b796aa40cb9
|
data/CHANGELOG.md
ADDED
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
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Miasma OpenStack
|
2
|
+
|
3
|
+
OpenStack API plugin for the miasma cloud library
|
4
|
+
|
5
|
+
## Current support matrix
|
6
|
+
|
7
|
+
|Model |Create|Read|Update|Delete|
|
8
|
+
|--------------|------|----|------|------|
|
9
|
+
|AutoScale | | | | |
|
10
|
+
|BlockStorage | | | | |
|
11
|
+
|Compute | X | X | | X |
|
12
|
+
|DNS | | | | |
|
13
|
+
|LoadBalancer | | | | |
|
14
|
+
|Network | | | | |
|
15
|
+
|Orchestration | X | X | X | X |
|
16
|
+
|Queues | | | | |
|
17
|
+
|Storage | X | X | X | X |
|
18
|
+
|
19
|
+
## Info
|
20
|
+
* Repository: https://github.com/miasma-rb/miasma-open-stack
|
@@ -0,0 +1,343 @@
|
|
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
|
+
'storage' => 'swift'
|
261
|
+
)
|
262
|
+
|
263
|
+
include Miasma::Utils::Memoization
|
264
|
+
|
265
|
+
# @return [Miasma::Contrib::OpenStackApiCore::Authenticate]
|
266
|
+
attr_reader :identity
|
267
|
+
|
268
|
+
# Create a new api instance
|
269
|
+
#
|
270
|
+
# @param creds [Smash] credential hash
|
271
|
+
# @return [self]
|
272
|
+
def initialize(creds)
|
273
|
+
@credentials = creds
|
274
|
+
memo_key = "miasma_open_stack_identity_#{creds.checksum}"
|
275
|
+
if(creds[:open_stack_identity_url].include?('v3'))
|
276
|
+
@identity = memoize(memo_key, :direct) do
|
277
|
+
identity_class('Authenticate::Version3').new(creds)
|
278
|
+
end
|
279
|
+
elsif(creds[:open_stack_identity_url].include?('v2'))
|
280
|
+
@identity = memoize(memo_key, :direct) do
|
281
|
+
identity_class('Authenticate::Version2').new(creds)
|
282
|
+
end
|
283
|
+
else
|
284
|
+
# @todo allow attribute to override?
|
285
|
+
raise ArgumentError.new('Failed to determine Identity service version')
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
# @return [Class] class from instance class, falls back to parent
|
290
|
+
def identity_class(i_name)
|
291
|
+
[self.class, Miasma::Contrib::OpenStackApiCore].map do |klass|
|
292
|
+
i_name.split('::').inject(klass) do |memo, key|
|
293
|
+
if(memo.const_defined?(key))
|
294
|
+
memo.const_get(key)
|
295
|
+
else
|
296
|
+
break
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end.compact.first
|
300
|
+
end
|
301
|
+
|
302
|
+
# Provide end point URL for service
|
303
|
+
#
|
304
|
+
# @param api_name [String] name of api
|
305
|
+
# @param region [String] region in use
|
306
|
+
# @return [String] public URL
|
307
|
+
def endpoint_for(api_name, region)
|
308
|
+
api = self.class.const_get(:API_MAP)[api_name]
|
309
|
+
srv = identity.service_catalog.detect do |info|
|
310
|
+
info[:name] == api
|
311
|
+
end
|
312
|
+
unless(srv)
|
313
|
+
raise NotImplementedError.new("No API mapping found for `#{api_name}`")
|
314
|
+
end
|
315
|
+
if(region)
|
316
|
+
point = srv[:endpoints].detect do |endpoint|
|
317
|
+
endpoint[:region].to_s.downcase == region.to_s.downcase
|
318
|
+
end
|
319
|
+
else
|
320
|
+
point = srv[:endpoints].first
|
321
|
+
end
|
322
|
+
if(point)
|
323
|
+
point.fetch(
|
324
|
+
:publicURL,
|
325
|
+
point[:url]
|
326
|
+
)
|
327
|
+
else
|
328
|
+
raise KeyError.new("Lookup failed for `#{api_name}` within region `#{region}`")
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
# @return [String] API token
|
333
|
+
def api_token
|
334
|
+
identity.api_token
|
335
|
+
end
|
336
|
+
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
Models::Compute.autoload :OpenStack, 'miasma/contrib/open_stack/compute'
|
341
|
+
Models::Orchestration.autoload :OpenStack, 'miasma/contrib/open_stack/orchestration'
|
342
|
+
Models::Storage.autoload :OpenStack, 'miasma/contrib/open_stack/storage'
|
343
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
|
3
|
+
module Miasma
|
4
|
+
module Models
|
5
|
+
class Compute
|
6
|
+
class OpenStack < Compute
|
7
|
+
|
8
|
+
include Contrib::OpenStackApiCore::ApiCommon
|
9
|
+
|
10
|
+
# @return [Smash] map state to valid internal values
|
11
|
+
SERVER_STATE_MAP = Smash.new(
|
12
|
+
'ACTIVE' => :running,
|
13
|
+
'DELETED' => :terminated,
|
14
|
+
'SUSPENDED' => :stopped,
|
15
|
+
'PASSWORD' => :running
|
16
|
+
)
|
17
|
+
|
18
|
+
def server_save(server)
|
19
|
+
unless(server.persisted?)
|
20
|
+
server.load_data(server.attributes)
|
21
|
+
result = request(
|
22
|
+
:expects => 202,
|
23
|
+
:method => :post,
|
24
|
+
:path => '/servers',
|
25
|
+
:json => {
|
26
|
+
:server => {
|
27
|
+
:flavorRef => server.flavor_id,
|
28
|
+
:name => server.name,
|
29
|
+
:imageRef => server.image_id,
|
30
|
+
:metadata => server.metadata,
|
31
|
+
:personality => server.personality,
|
32
|
+
:key_pair => server.key_name
|
33
|
+
}
|
34
|
+
}
|
35
|
+
)
|
36
|
+
server.id = result.get(:body, :server, :id)
|
37
|
+
else
|
38
|
+
raise "WAT DO I DO!?"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def server_destroy(server)
|
43
|
+
if(server.persisted?)
|
44
|
+
result = request(
|
45
|
+
:expects => 204,
|
46
|
+
:method => :delete,
|
47
|
+
:path => "/servers/#{server.id}"
|
48
|
+
)
|
49
|
+
true
|
50
|
+
else
|
51
|
+
false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def server_change_state(server, state)
|
56
|
+
end
|
57
|
+
|
58
|
+
def server_reload(server)
|
59
|
+
res = servers.reload.all
|
60
|
+
node = res.detect do |s|
|
61
|
+
s.id == server.id
|
62
|
+
end
|
63
|
+
if(node)
|
64
|
+
server.load_data(node.data.dup)
|
65
|
+
server.valid_state
|
66
|
+
else
|
67
|
+
server.data[:state] = :terminated
|
68
|
+
server.dirty.clear
|
69
|
+
server
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def server_all
|
74
|
+
result = request(
|
75
|
+
:method => :get,
|
76
|
+
:path => '/servers/detail'
|
77
|
+
)
|
78
|
+
result[:body].fetch(:servers, []).map do |srv|
|
79
|
+
Server.new(
|
80
|
+
self,
|
81
|
+
:id => srv[:id],
|
82
|
+
:name => srv[:name],
|
83
|
+
:image_id => srv.get(:image, :id),
|
84
|
+
:flavor_id => srv.get(:flavor, :id),
|
85
|
+
:state => SERVER_STATE_MAP.fetch(srv[:status], :pending),
|
86
|
+
:addresses_private => srv.fetch(:addresses, :private, []).map{|a|
|
87
|
+
Server::Address.new(
|
88
|
+
:version => a[:version].to_i, :address => a[:addr]
|
89
|
+
)
|
90
|
+
},
|
91
|
+
:addresses_public => srv.fetch(:addresses, :public, []).map{|a|
|
92
|
+
Server::Address.new(
|
93
|
+
:version => a[:version].to_i, :address => a[:addr]
|
94
|
+
)
|
95
|
+
},
|
96
|
+
:status => srv[:status],
|
97
|
+
:key_name => srv[:key_name]
|
98
|
+
).valid_state
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,255 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
|
3
|
+
module Miasma
|
4
|
+
module Models
|
5
|
+
class Orchestration
|
6
|
+
class OpenStack < Orchestration
|
7
|
+
|
8
|
+
include Contrib::OpenStackApiCore::ApiCommon
|
9
|
+
|
10
|
+
# @return [Smash] external to internal resource mapping
|
11
|
+
RESOURCE_MAPPING = Smash.new(
|
12
|
+
'OS::Nova::Server' => Smash.new(
|
13
|
+
:api => :compute,
|
14
|
+
:collection => :servers
|
15
|
+
)
|
16
|
+
# 'OS::Heat::AutoScalingGroup' => Smash.new(
|
17
|
+
# :api => :auto_scale,
|
18
|
+
# :collection => :groups
|
19
|
+
# )
|
20
|
+
)
|
21
|
+
|
22
|
+
# Save the stack
|
23
|
+
#
|
24
|
+
# @param stack [Models::Orchestration::Stack]
|
25
|
+
# @return [Models::Orchestration::Stack]
|
26
|
+
def stack_save(stack)
|
27
|
+
if(stack.persisted?)
|
28
|
+
stack.load_data(stack.attributes)
|
29
|
+
result = request(
|
30
|
+
:expects => 202,
|
31
|
+
:method => :put,
|
32
|
+
:path => "/stacks/#{stack.name}/#{stack.id}",
|
33
|
+
:json => {
|
34
|
+
:stack_name => stack.name,
|
35
|
+
:template => MultiJson.dump(stack.template),
|
36
|
+
:parameters => stack.parameters || {}
|
37
|
+
}
|
38
|
+
)
|
39
|
+
stack.valid_state
|
40
|
+
else
|
41
|
+
stack.load_data(stack.attributes)
|
42
|
+
result = request(
|
43
|
+
:expects => 201,
|
44
|
+
:method => :post,
|
45
|
+
:path => '/stacks',
|
46
|
+
:json => {
|
47
|
+
:stack_name => stack.name,
|
48
|
+
:template => MultiJson.dump(stack.template),
|
49
|
+
:parameters => stack.parameters || {},
|
50
|
+
:disable_rollback => (!!stack.disable_rollback).to_s
|
51
|
+
}
|
52
|
+
)
|
53
|
+
stack.id = result.get(:body, :stack, :id)
|
54
|
+
stack.valid_state
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Reload the stack data from the API
|
59
|
+
#
|
60
|
+
# @param stack [Models::Orchestration::Stack]
|
61
|
+
# @return [Models::Orchestration::Stack]
|
62
|
+
def stack_reload(stack)
|
63
|
+
if(stack.persisted?)
|
64
|
+
result = request(
|
65
|
+
:method => :get,
|
66
|
+
:path => "/stacks/#{stack.name}/#{stack.id}",
|
67
|
+
:expects => 200
|
68
|
+
)
|
69
|
+
stk = result.get(:body, :stack)
|
70
|
+
stack.load_data(
|
71
|
+
:id => stk[:id],
|
72
|
+
:capabilities => stk[:capabilities],
|
73
|
+
:created => Time.parse(stk[:creation_time]),
|
74
|
+
:description => stk[:description],
|
75
|
+
:disable_rollback => stk[:disable_rollback].to_s.downcase == 'true',
|
76
|
+
:notification_topics => stk[:notification_topics],
|
77
|
+
:name => stk[:stack_name],
|
78
|
+
:state => stk[:stack_status].downcase.to_sym,
|
79
|
+
:status => stk[:stack_status],
|
80
|
+
:status_reason => stk[:stack_status_reason],
|
81
|
+
:template_description => stk[:template_description],
|
82
|
+
:timeout_in_minutes => stk[:timeout_mins].to_s.empty? ? nil : stk[:timeout_mins].to_i,
|
83
|
+
:updated => stk[:updated_time].to_s.empty? ? nil : Time.parse(stk[:updated_time]),
|
84
|
+
:parameters => stk.fetch(:parameters, Smash.new),
|
85
|
+
:outputs => stk.fetch(:outputs, []).map{ |output|
|
86
|
+
Smash.new(
|
87
|
+
:key => output[:output_key],
|
88
|
+
:value => output[:output_value],
|
89
|
+
:description => output[:description]
|
90
|
+
)
|
91
|
+
}
|
92
|
+
).valid_state
|
93
|
+
end
|
94
|
+
stack
|
95
|
+
end
|
96
|
+
|
97
|
+
# Delete the stack
|
98
|
+
#
|
99
|
+
# @param stack [Models::Orchestration::Stack]
|
100
|
+
# @return [TrueClass, FalseClass]
|
101
|
+
def stack_destroy(stack)
|
102
|
+
if(stack.persisted?)
|
103
|
+
request(
|
104
|
+
:method => :delete,
|
105
|
+
:path => "/stacks/#{stack.name}/#{stack.id}",
|
106
|
+
:expects => 204
|
107
|
+
)
|
108
|
+
true
|
109
|
+
else
|
110
|
+
false
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Fetch stack template
|
115
|
+
#
|
116
|
+
# @param stack [Stack]
|
117
|
+
# @return [Smash] stack template
|
118
|
+
def stack_template_load(stack)
|
119
|
+
if(stack.persisted?)
|
120
|
+
result = request(
|
121
|
+
:method => :get,
|
122
|
+
:path => "/stacks/#{stack.name}/#{stack.id}/template"
|
123
|
+
)
|
124
|
+
result.fetch(:body, Smash.new)
|
125
|
+
else
|
126
|
+
Smash.new
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Validate stack template
|
131
|
+
#
|
132
|
+
# @param stack [Stack]
|
133
|
+
# @return [NilClass, String] nil if valid, string error message if invalid
|
134
|
+
def stack_template_validate(stack)
|
135
|
+
begin
|
136
|
+
result = request(
|
137
|
+
:method => :post,
|
138
|
+
:path => '/validate',
|
139
|
+
:json => Smash.new(
|
140
|
+
:template => stack.template
|
141
|
+
)
|
142
|
+
)
|
143
|
+
nil
|
144
|
+
rescue Error::ApiError::RequestError => e
|
145
|
+
MultiJson.load(e.response.body.to_s).to_smash.get(:error, :message)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Return all stacks
|
150
|
+
#
|
151
|
+
# @param options [Hash] filter
|
152
|
+
# @return [Array<Models::Orchestration::Stack>]
|
153
|
+
# @todo check if we need any mappings on state set
|
154
|
+
def stack_all(options={})
|
155
|
+
result = request(
|
156
|
+
:method => :get,
|
157
|
+
:path => '/stacks'
|
158
|
+
)
|
159
|
+
result.fetch(:body, :stacks, []).map do |s|
|
160
|
+
Stack.new(
|
161
|
+
self,
|
162
|
+
:id => s[:id],
|
163
|
+
:created => Time.parse(s[:creation_time]),
|
164
|
+
:description => s[:description],
|
165
|
+
:name => s[:stack_name],
|
166
|
+
:state => s[:stack_status].downcase.to_sym,
|
167
|
+
:status => s[:stack_status],
|
168
|
+
:status_reason => s[:stack_status_reason],
|
169
|
+
:updated => s[:updated_time].to_s.empty? ? nil : Time.parse(s[:updated_time])
|
170
|
+
).valid_state
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Return all resources for stack
|
175
|
+
#
|
176
|
+
# @param stack [Models::Orchestration::Stack]
|
177
|
+
# @return [Array<Models::Orchestration::Stack::Resource>]
|
178
|
+
def resource_all(stack)
|
179
|
+
result = request(
|
180
|
+
:method => :get,
|
181
|
+
:path => "/stacks/#{stack.name}/#{stack.id}/resources",
|
182
|
+
:expects => 200
|
183
|
+
)
|
184
|
+
result.fetch(:body, :resources, []).map do |resource|
|
185
|
+
Stack::Resource.new(
|
186
|
+
stack,
|
187
|
+
:id => resource[:physical_resource_id],
|
188
|
+
:name => resource[:resource_name],
|
189
|
+
:type => resource[:resource_type],
|
190
|
+
:logical_id => resource[:logical_resource_id],
|
191
|
+
:state => resource[:resource_status].downcase.to_sym,
|
192
|
+
:status => resource[:resource_status],
|
193
|
+
:status_reason => resource[:resource_status_reason],
|
194
|
+
:updated => Time.parse(resource[:updated_time])
|
195
|
+
).valid_state
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Reload the stack resource data from the API
|
200
|
+
#
|
201
|
+
# @param resource [Models::Orchestration::Stack::Resource]
|
202
|
+
# @return [Models::Orchestration::Resource]
|
203
|
+
def resource_reload(resource)
|
204
|
+
resource.stack.resources.reload
|
205
|
+
resource.stack.resources.get(resource.id)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Return all events for stack
|
209
|
+
#
|
210
|
+
# @param stack [Models::Orchestration::Stack]
|
211
|
+
# @return [Array<Models::Orchestration::Stack::Event>]
|
212
|
+
def event_all(stack, marker = nil)
|
213
|
+
params = marker ? {:marker => marker} : {}
|
214
|
+
result = request(
|
215
|
+
:path => "/stacks/#{stack.name}/#{stack.id}/events",
|
216
|
+
:method => :get,
|
217
|
+
:expects => 200,
|
218
|
+
:params => params
|
219
|
+
)
|
220
|
+
result.fetch(:body, :events, []).map do |event|
|
221
|
+
Stack::Event.new(
|
222
|
+
stack,
|
223
|
+
:id => event[:id],
|
224
|
+
:resource_id => event[:physical_resource_id],
|
225
|
+
:resource_name => event[:resource_name],
|
226
|
+
:resource_logical_id => event[:logical_resource_id],
|
227
|
+
:resource_state => event[:resource_status].downcase.to_sym,
|
228
|
+
:resource_status => event[:resource_status],
|
229
|
+
:resource_status_reason => event[:resource_status_reason],
|
230
|
+
:time => Time.parse(event[:event_time])
|
231
|
+
).valid_state
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Return all new events for event collection
|
236
|
+
#
|
237
|
+
# @param events [Models::Orchestration::Stack::Events]
|
238
|
+
# @return [Array<Models::Orchestration::Stack::Event>]
|
239
|
+
def event_all_new(events)
|
240
|
+
event_all(events.stack, events.all.first.id)
|
241
|
+
end
|
242
|
+
|
243
|
+
# Reload the stack event data from the API
|
244
|
+
#
|
245
|
+
# @param resource [Models::Orchestration::Stack::Event]
|
246
|
+
# @return [Models::Orchestration::Event]
|
247
|
+
def event_reload(event)
|
248
|
+
event.stack.events.reload
|
249
|
+
event.stack.events.get(event.id)
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
@@ -0,0 +1,342 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module Miasma
|
5
|
+
module Models
|
6
|
+
class Storage
|
7
|
+
class OpenStack < Storage
|
8
|
+
|
9
|
+
include Contrib::OpenStackApiCore::ApiCommon
|
10
|
+
|
11
|
+
# Save bucket
|
12
|
+
#
|
13
|
+
# @param bucket [Models::Storage::Bucket]
|
14
|
+
# @return [Models::Storage::Bucket]
|
15
|
+
def bucket_save(bucket)
|
16
|
+
unless(bucket.persisted?)
|
17
|
+
request(
|
18
|
+
:path => full_path(bucket),
|
19
|
+
:method => :put,
|
20
|
+
:expects => [201, 204]
|
21
|
+
)
|
22
|
+
bucket.id = bucket.name
|
23
|
+
bucket.valid_state
|
24
|
+
end
|
25
|
+
bucket
|
26
|
+
end
|
27
|
+
|
28
|
+
# Destroy bucket
|
29
|
+
#
|
30
|
+
# @param bucket [Models::Storage::Bucket]
|
31
|
+
# @return [TrueClass, FalseClass]
|
32
|
+
def bucket_destroy(bucket)
|
33
|
+
if(bucket.persisted?)
|
34
|
+
request(
|
35
|
+
:path => full_path(bucket),
|
36
|
+
:method => :delete,
|
37
|
+
:expects => 204
|
38
|
+
)
|
39
|
+
true
|
40
|
+
else
|
41
|
+
false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Reload the bucket
|
46
|
+
#
|
47
|
+
# @param bucket [Models::Storage::Bucket]
|
48
|
+
# @return [Models::Storage::Bucket]
|
49
|
+
def bucket_reload(bucket)
|
50
|
+
if(bucket.persisted?)
|
51
|
+
begin
|
52
|
+
result = request(
|
53
|
+
:path => full_path(bucket),
|
54
|
+
:method => :head,
|
55
|
+
:expects => 204,
|
56
|
+
:params => {
|
57
|
+
:format => 'json'
|
58
|
+
}
|
59
|
+
)
|
60
|
+
meta = Smash.new.tap do |m|
|
61
|
+
result[:response].headers.each do |k,v|
|
62
|
+
if(k.to_s.start_with?('X-Container-Meta-'))
|
63
|
+
m[k.sub('X-Container-Meta-', '')] = v
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
bucket.metadata = meta unless meta.empty?
|
68
|
+
bucket.valid_state
|
69
|
+
rescue Error::ApiError::RequestError => e
|
70
|
+
if(e.response.status == 404)
|
71
|
+
bucket.data.clear
|
72
|
+
bucket.dirty.clear
|
73
|
+
else
|
74
|
+
raise
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
bucket
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return all buckets
|
82
|
+
#
|
83
|
+
# @return [Array<Models::Storage::Bucket>]
|
84
|
+
def bucket_all
|
85
|
+
result = request(
|
86
|
+
:path => '/',
|
87
|
+
:expects => [200, 204],
|
88
|
+
:params => {
|
89
|
+
:format => 'json'
|
90
|
+
}
|
91
|
+
)
|
92
|
+
[result[:body]].flatten.compact.map do |bkt|
|
93
|
+
Bucket.new(
|
94
|
+
self,
|
95
|
+
:id => bkt['name'],
|
96
|
+
:name => bkt['name']
|
97
|
+
).valid_state
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Return filtered files
|
102
|
+
#
|
103
|
+
# @param args [Hash] filter options
|
104
|
+
# @return [Array<Models::Storage::File>]
|
105
|
+
def file_filter(bucket, args)
|
106
|
+
result = request(
|
107
|
+
:path => full_path(bucket),
|
108
|
+
:expects => [200, 204],
|
109
|
+
:params => {
|
110
|
+
:prefix => args[:prefix],
|
111
|
+
:format => :json
|
112
|
+
}
|
113
|
+
)
|
114
|
+
[result[:body]].flatten.compact.map do |file|
|
115
|
+
File.new(
|
116
|
+
bucket,
|
117
|
+
:id => ::File.join(bucket.name, file[:name]),
|
118
|
+
:name => file[:name],
|
119
|
+
:updated => file[:last_modified],
|
120
|
+
:size => file[:bytes].to_i
|
121
|
+
).valid_state
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Return all files within bucket
|
126
|
+
#
|
127
|
+
# @param bucket [Bucket]
|
128
|
+
# @return [Array<File>]
|
129
|
+
# @todo pagination auto-follow
|
130
|
+
def file_all(bucket)
|
131
|
+
result = request(
|
132
|
+
:path => full_path(bucket),
|
133
|
+
:expects => [200, 204],
|
134
|
+
:params => {
|
135
|
+
:format => :json
|
136
|
+
}
|
137
|
+
)
|
138
|
+
[result[:body]].flatten.compact.map do |file|
|
139
|
+
File.new(
|
140
|
+
bucket,
|
141
|
+
:id => ::File.join(bucket.name, file[:name]),
|
142
|
+
:name => file[:name],
|
143
|
+
:updated => file[:last_modified],
|
144
|
+
:size => file[:bytes].to_i
|
145
|
+
).valid_state
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Save file
|
150
|
+
#
|
151
|
+
# @param file [Models::Storage::File]
|
152
|
+
# @return [Models::Storage::File]
|
153
|
+
def file_save(file)
|
154
|
+
if(file.dirty?)
|
155
|
+
file.load_data(file.attributes)
|
156
|
+
args = Smash.new
|
157
|
+
args[:headers] = Smash[
|
158
|
+
Smash.new(
|
159
|
+
:content_type => 'Content-Type',
|
160
|
+
:content_disposition => 'Content-Disposition',
|
161
|
+
:content_encoding => 'Content-Encoding'
|
162
|
+
).map do |attr, key|
|
163
|
+
if(file.attributes[attr])
|
164
|
+
[key, file.attributes[attr]]
|
165
|
+
end
|
166
|
+
end.compact
|
167
|
+
]
|
168
|
+
if(file.attributes[:body].is_a?(IO) && file.body.size >= Storage::MAX_BODY_SIZE_FOR_STRINGIFY)
|
169
|
+
parts = []
|
170
|
+
file.body.rewind
|
171
|
+
while(content = file.body.read(Storage::READ_BODY_CHUNK_SIZE))
|
172
|
+
data = Smash.new(
|
173
|
+
:path => "segments/#{full_path(file)}-#{SecureRandom.uuid}",
|
174
|
+
:etag => Digest::MD5.hexdigest(content),
|
175
|
+
:size_bytes => content.length
|
176
|
+
)
|
177
|
+
request(
|
178
|
+
:path => data[:path],
|
179
|
+
:method => :put,
|
180
|
+
:expects => 201,
|
181
|
+
:headers => {
|
182
|
+
'Content-Length' => data[:size_bytes],
|
183
|
+
'Etag' => data[:etag]
|
184
|
+
}
|
185
|
+
)
|
186
|
+
parts << data
|
187
|
+
end
|
188
|
+
result = request(
|
189
|
+
:path => full_path(file),
|
190
|
+
:method => :put,
|
191
|
+
:expects => 201,
|
192
|
+
:params => {
|
193
|
+
'multipart-manifest' => :put
|
194
|
+
},
|
195
|
+
:json => parts
|
196
|
+
)
|
197
|
+
else
|
198
|
+
if(file.attributes[:body].is_a?(IO) || file.attributes[:body].is_a?(StringIO))
|
199
|
+
args[:headers]['Content-Length'] = file.body.size.to_s
|
200
|
+
file.body.rewind
|
201
|
+
args[:body] = file.body.read
|
202
|
+
file.body.rewind
|
203
|
+
end
|
204
|
+
result = request(
|
205
|
+
args.merge(
|
206
|
+
:method => :put,
|
207
|
+
:expects => 201,
|
208
|
+
:path => full_path(file)
|
209
|
+
)
|
210
|
+
)
|
211
|
+
end
|
212
|
+
file.id = ::File.join(file.bucket.name, file.name)
|
213
|
+
file.reload
|
214
|
+
end
|
215
|
+
file
|
216
|
+
end
|
217
|
+
|
218
|
+
# Destroy file
|
219
|
+
#
|
220
|
+
# @param file [Models::Storage::File]
|
221
|
+
# @return [TrueClass, FalseClass]
|
222
|
+
def file_destroy(file)
|
223
|
+
if(file.persisted?)
|
224
|
+
request(
|
225
|
+
:path => full_path(file),
|
226
|
+
:method => :delete
|
227
|
+
)
|
228
|
+
true
|
229
|
+
else
|
230
|
+
false
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Reload the file
|
235
|
+
#
|
236
|
+
# @param file [Models::Storage::File]
|
237
|
+
# @return [Models::Storage::File]
|
238
|
+
def file_reload(file)
|
239
|
+
if(file.persisted?)
|
240
|
+
result = request(
|
241
|
+
:path => full_path(file),
|
242
|
+
:method => :head
|
243
|
+
)
|
244
|
+
info = result[:headers]
|
245
|
+
new_info = Smash.new.tap do |data|
|
246
|
+
data[:updated] = info[:last_modified]
|
247
|
+
data[:etag] = info[:etag]
|
248
|
+
data[:size] = info[:content_length].to_i
|
249
|
+
data[:content_type] = info[:content_type]
|
250
|
+
meta = Smash.new.tap do |m|
|
251
|
+
result[:response].headers.each do |k, v|
|
252
|
+
if(k.to_s.start_with?('X-Object-Meta-'))
|
253
|
+
m[k.sub('X-Object-Meta-', '')] = v
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
data[:metadata] = meta unless meta.empty?
|
258
|
+
end
|
259
|
+
file.load_data(file.attributes.deep_merge(new_info))
|
260
|
+
file.valid_state
|
261
|
+
end
|
262
|
+
file
|
263
|
+
end
|
264
|
+
|
265
|
+
# Create publicly accessible URL
|
266
|
+
#
|
267
|
+
# @param timeout_secs [Integer] seconds available
|
268
|
+
# @return [String] URL
|
269
|
+
# @todo where is this in swift?
|
270
|
+
def file_url(file, timeout_secs)
|
271
|
+
if(file.persisted?)
|
272
|
+
raise NotImplementedError
|
273
|
+
else
|
274
|
+
raise Error::ModelPersistError.new "#{file} has not been saved!"
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Fetch the contents of the file
|
279
|
+
#
|
280
|
+
# @param file [Models::Storage::File]
|
281
|
+
# @return [IO, HTTP::Response::Body]
|
282
|
+
def file_body(file)
|
283
|
+
if(file.persisted?)
|
284
|
+
result = request(:path => full_path(file))
|
285
|
+
content = result[:body]
|
286
|
+
begin
|
287
|
+
if(content.is_a?(String))
|
288
|
+
StringIO.new(content)
|
289
|
+
else
|
290
|
+
if(content.respond_to?(:stream!))
|
291
|
+
content.stream!
|
292
|
+
end
|
293
|
+
content
|
294
|
+
end
|
295
|
+
rescue HTTP::StateError
|
296
|
+
StringIO.new(content.to_s)
|
297
|
+
end
|
298
|
+
else
|
299
|
+
StringIO.new('')
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# @return [String] escaped bucket name
|
304
|
+
def bucket_path(bucket)
|
305
|
+
uri_escape(bucket.name)
|
306
|
+
end
|
307
|
+
|
308
|
+
# @return [String] escaped file path
|
309
|
+
def file_path(file)
|
310
|
+
file.name.split('/').map do |part|
|
311
|
+
uri_escape(part)
|
312
|
+
end.join('/')
|
313
|
+
end
|
314
|
+
|
315
|
+
# Provide full path for object
|
316
|
+
#
|
317
|
+
# @param file_or_bucket [File, Bucket]
|
318
|
+
# @return [String]
|
319
|
+
def full_path(file_or_bucket)
|
320
|
+
path = ''
|
321
|
+
if(file_or_bucket.respond_to?(:bucket))
|
322
|
+
path << '/' << bucket_path(file_or_bucket.bucket)
|
323
|
+
end
|
324
|
+
path << '/' << file_path(file_or_bucket)
|
325
|
+
path
|
326
|
+
end
|
327
|
+
|
328
|
+
# URL string escape
|
329
|
+
#
|
330
|
+
# @param string [String] string to escape
|
331
|
+
# @return [String] escaped string
|
332
|
+
# @todo move this to common module
|
333
|
+
def uri_escape(string)
|
334
|
+
string.to_s.gsub(/([^a-zA-Z0-9_.\-~])/) do
|
335
|
+
'%' << $1.unpack('H2' * $1.bytesize).join('%').upcase
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + '/lib/'
|
2
|
+
require 'miasma-open-stack/version'
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'miasma-open-stack'
|
5
|
+
s.version = MiasmaOpenStack::VERSION.version
|
6
|
+
s.summary = 'Smoggy OpenStack API'
|
7
|
+
s.author = 'Chris Roberts'
|
8
|
+
s.email = 'code@chrisroberts.org'
|
9
|
+
s.homepage = 'https://github.com/miasma-rb/miasma-open-stack'
|
10
|
+
s.description = 'Smoggy OpenStack API'
|
11
|
+
s.license = 'Apache 2.0'
|
12
|
+
s.require_path = 'lib'
|
13
|
+
s.add_dependency 'miasma'
|
14
|
+
s.files = Dir['lib/**/*'] + %w(miasma-open-stack.gemspec README.md CHANGELOG.md LICENSE)
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: miasma-open-stack
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Roberts
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-01-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: miasma
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: Smoggy OpenStack API
|
28
|
+
email: code@chrisroberts.org
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- CHANGELOG.md
|
34
|
+
- LICENSE
|
35
|
+
- README.md
|
36
|
+
- lib/miasma-open-stack.rb
|
37
|
+
- lib/miasma-open-stack/version.rb
|
38
|
+
- lib/miasma/contrib/open_stack.rb
|
39
|
+
- lib/miasma/contrib/open_stack/compute.rb
|
40
|
+
- lib/miasma/contrib/open_stack/orchestration.rb
|
41
|
+
- lib/miasma/contrib/open_stack/storage.rb
|
42
|
+
- miasma-open-stack.gemspec
|
43
|
+
homepage: https://github.com/miasma-rb/miasma-open-stack
|
44
|
+
licenses:
|
45
|
+
- Apache 2.0
|
46
|
+
metadata: {}
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 2.2.2
|
64
|
+
signing_key:
|
65
|
+
specification_version: 4
|
66
|
+
summary: Smoggy OpenStack API
|
67
|
+
test_files: []
|
68
|
+
has_rdoc:
|