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 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
@@ -0,0 +1,2 @@
1
+ ## v0.1.0
2
+ * 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 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,2 @@
1
+ require 'miasma'
2
+ require 'miasma-open-stack/version'
@@ -0,0 +1,4 @@
1
+ module MiasmaOpenStack
2
+ # Current library version
3
+ VERSION = Gem::Version.new('0.1.0')
4
+ end
@@ -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: