miasma 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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