miasma-open-stack 0.1.0

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