miasma 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,334 @@
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
+ )
261
+
262
+ # @return [Miasma::Contrib::OpenStackApiCore::Authenticate]
263
+ attr_reader :identity
264
+
265
+ # Create a new api instance
266
+ #
267
+ # @param creds [Smash] credential hash
268
+ # @return [self]
269
+ def initialize(creds)
270
+ @credentials = creds
271
+ if(creds[:open_stack_identity_url].include?('v3'))
272
+ @identity = identity_class('Authenticate::Version3').new(creds)
273
+ elsif(creds[:open_stack_identity_url].include?('v2'))
274
+ @identity = identity_class('Authenticate::Version2').new(creds)
275
+ else
276
+ # @todo allow attribute to override?
277
+ raise ArgumentError.new('Failed to determine Identity service version')
278
+ end
279
+ end
280
+
281
+ # @return [Class] class from instance class, falls back to parent
282
+ def identity_class(i_name)
283
+ [self.class, Miasma::Contrib::OpenStackApiCore].map do |klass|
284
+ i_name.split('::').inject(klass) do |memo, key|
285
+ if(memo.const_defined?(key))
286
+ memo.const_get(key)
287
+ else
288
+ break
289
+ end
290
+ end
291
+ end.compact.first
292
+ end
293
+
294
+ # Provide end point URL for service
295
+ #
296
+ # @param api_name [String] name of api
297
+ # @param region [String] region in use
298
+ # @return [String] public URL
299
+ def endpoint_for(api_name, region)
300
+ api = self.class.const_get(:API_MAP)[api_name]
301
+ srv = identity.service_catalog.detect do |info|
302
+ info[:name] == api
303
+ end
304
+ unless(srv)
305
+ raise NotImplementedError.new("No API mapping found for `#{api_name}`")
306
+ end
307
+ if(region)
308
+ point = srv[:endpoints].detect do |endpoint|
309
+ endpoint[:region].to_s.downcase == region.to_s.downcase
310
+ end
311
+ else
312
+ point = srv[:endpoints].first
313
+ end
314
+ if(point)
315
+ point.fetch(
316
+ :publicURL,
317
+ point[:url]
318
+ )
319
+ else
320
+ raise KeyError.new("Lookup failed for `#{api_name}` within region `#{region}`")
321
+ end
322
+ end
323
+
324
+ # @return [String] API token
325
+ def api_token
326
+ identity.api_token
327
+ end
328
+
329
+ end
330
+ end
331
+
332
+ Models::Compute.autoload :OpenStack, 'miasma/contrib/open_stack/compute'
333
+ Models::Orchestration.autoload :OpenStack, 'miasma/contrib/open_stack/orchestration'
334
+ 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
+ :creation_time => 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