miasma-azure 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.
@@ -0,0 +1,461 @@
1
+ require 'securerandom'
2
+ require 'miasma'
3
+
4
+ module Miasma
5
+ module Models
6
+ class Orchestration
7
+ class Azure < Orchestration
8
+
9
+ include Contrib::AzureApiCore::ApiCommon
10
+
11
+ # Resource status to state mapping
12
+ STATUS_MAP = Smash.new(
13
+ 'Failed' => :create_failed,
14
+ 'Canceled' => :create_failed,
15
+ 'Succeeded' => :create_complete,
16
+ 'Deleting' => :delete_in_progress,
17
+ 'Deleted' => :delete_complete
18
+ )
19
+
20
+ # @return [String] supported API version
21
+ def api_version
22
+ '2015-01-01'
23
+ end
24
+
25
+ # Generate the URL path required for given stack
26
+ #
27
+ # @param stack [Models::Orchestration::Stack]
28
+ # @return [String] generated path
29
+ def generate_path(stack=nil)
30
+ path = "/subscriptions/#{azure_subscription_id}/resourcegroups"
31
+ path << "/#{stack.name}/providers/microsoft.resources/deployments/miasma-stack" if stack
32
+ path
33
+ end
34
+
35
+ # Convert given status value to correct state value
36
+ #
37
+ # @param val [String] Resource status
38
+ # @param modifier [String, Symbol] optional state prefix modifier
39
+ # @return [Symbol] resource state
40
+ def status_to_state(val, modifier=nil)
41
+ val = STATUS_MAP.fetch(val, :create_in_progress)
42
+ if(modifier && modifier.to_s != 'create' && val.to_s.start_with?('create'))
43
+ val = val.to_s.sub('create', modifier).to_sym
44
+ end
45
+ val
46
+ end
47
+
48
+ # Fetch stacks or update provided stack data
49
+ #
50
+ # @param stack [Models::Orchestration::Stack]
51
+ # @return [Array<Models::Orchestration::Stack>]
52
+ def load_stack_data(stack=nil)
53
+ if(stack)
54
+ fetch_single_stack(stack)
55
+ else
56
+ fetch_all_stacks.map do |n_stack|
57
+ fetch_single_stack(n_stack)
58
+ end
59
+ end
60
+ end
61
+
62
+ # Populate stack model data
63
+ #
64
+ # @param stack [Models::Orchestration::Stack]
65
+ # @return [Models::Orchestration::Stack]
66
+ def fetch_single_stack(stack)
67
+ unless(stack.custom[:base_load])
68
+ n_stack = fetch_all_stacks.detect do |s|
69
+ s.name == stack.name ||
70
+ s.id == stack.id
71
+ end
72
+ if(n_stack)
73
+ stack.data.deep_merge!(n_stack.attributes)
74
+ else
75
+ stack.state = :delete_complete
76
+ stack.status = 'Deleted'
77
+ return stack
78
+ end
79
+ end
80
+ stack.custom.delete(:base_load)
81
+ result = request(
82
+ :path => generate_path(stack),
83
+ :expects => [200, 404]
84
+ )
85
+ if(result[:response].code == 404)
86
+ if(stack.tags && state = stack.tags[:state])
87
+ case state
88
+ when 'create'
89
+ stack.status = 'Creating'
90
+ stack.state = :create_in_progress
91
+ else
92
+ stack.status = 'Deleting'
93
+ stack.state = :delete_in_progress
94
+ end
95
+ else
96
+ stack.data.merge!(
97
+ :state => :unknown,
98
+ :status => 'Unknown'
99
+ )
100
+ end
101
+ stack.valid_state
102
+ else
103
+ item = result[:body]
104
+ deployment_id = item[:id]
105
+ stack_id = deployment_id.sub(/\/providers\/microsoft.resources.+/i, '')
106
+ stack_name = File.basename(stack_id)
107
+ stack.data.merge!(
108
+ :id => stack_id,
109
+ :name => stack_name,
110
+ :parameters => Smash[
111
+ item.fetch(:properties, :parameters, {}).map do |p_name, p_value|
112
+ [p_name, p_value[:value]]
113
+ end
114
+ ],
115
+ :outputs => item.fetch(:outputs, {}).map{ |o_name, o_value|
116
+ Smash.new(:key => o_name, :value => o_value[:value])
117
+ },
118
+ :template_url => item.get(:properties, :templateLink, :uri),
119
+ :state => status_to_state(
120
+ item.get(:properties, :provisioningState),
121
+ stack.tags[:state]
122
+ ),
123
+ :status => item.get(:properties, :provisioningState),
124
+ :updated => Time.parse(item.get(:properties, :timestamp)),
125
+ :custom => item
126
+ )
127
+ stack.valid_state
128
+ end
129
+ end
130
+
131
+ # Fetch all available stacks
132
+ #
133
+ # @return [Array<Models::Orchestration::Stack>]
134
+ def fetch_all_stacks
135
+ result = request(
136
+ :path => generate_path
137
+ )
138
+ result.fetch(:body, :value, []).map do |item|
139
+ new_stack = Stack.new(self)
140
+ new_stack.load_data(
141
+ :id => item[:id],
142
+ :name => item[:name],
143
+ :state => status_to_state(
144
+ item.get(:properties, :provisioningState),
145
+ item.get(:tags, :state)
146
+ ),
147
+ :status => item.get(:properties, :provisioningState),
148
+ :tags => item.fetch(:tags, Smash.new),
149
+ :created => item.get(:tags, :created) ? Time.at(item.get(:tags, :created).to_i).utc : nil,
150
+ :custom => Smash.new(:base_load => true)
151
+ ).valid_state
152
+ end
153
+ end
154
+
155
+ # Save the stack
156
+ #
157
+ # @param stack [Models::Orchestration::Stack]
158
+ # @return [Models::Orchestration::Stack]
159
+ def stack_save(stack)
160
+ store_template!(stack)
161
+ unless(stack.persisted?)
162
+ request(
163
+ :path => [generate_path, stack.name].join('/'),
164
+ :method => :put,
165
+ :json => {
166
+ :location => azure_region,
167
+ :tags => {
168
+ :created => Time.now.to_i,
169
+ :state => 'create'
170
+ }
171
+ },
172
+ :expects => [200, 201]
173
+ )
174
+ else
175
+ request(
176
+ :path => [generate_path, stack.name].join('/'),
177
+ :method => :patch,
178
+ :json => {
179
+ :tags => stack.tags.merge(
180
+ :updated => Time.now.to_i,
181
+ :state => 'update'
182
+ )
183
+ }
184
+ )
185
+ end
186
+ result = request(
187
+ :path => generate_path(stack),
188
+ :method => :put,
189
+ :expects => [200, 201],
190
+ :json => {
191
+ :properties => {
192
+ :templateLink => {
193
+ :uri => stack.template_url,
194
+ :contentVersion => '1.0.0.0'
195
+ },
196
+ :parameters => Smash[
197
+ stack.parameters.map do |p_key, p_value|
198
+ [p_key, :value => p_value]
199
+ end
200
+ ],
201
+ :mode => 'Complete'
202
+ }
203
+ }
204
+ )
205
+ deployment_id = result.get(:body, :id)
206
+ stack_id = deployment_id.sub(/\/providers\/microsoft.resources.+/i, '')
207
+ stack_name = File.basename(stack_id)
208
+ stack.id = stack_id
209
+ stack.name = stack_name
210
+ stack.valid_state
211
+ stack
212
+ end
213
+
214
+ # Store the stack template in the object store for future
215
+ # reference
216
+ #
217
+ # @param stack [Models::Orchestration::Stack]
218
+ # @return [Models::Orchestration::Stack]
219
+ def store_template!(stack)
220
+ storage = api_for(:storage)
221
+ bucket = storage.buckets.get(azure_root_orchestration_container)
222
+ unless(bucket)
223
+ bucket = storage.buckets.build
224
+ bucket.name = azure_root_orchestration_container
225
+ bucket.save
226
+ end
227
+ file = bucket.files.build
228
+ file.name = "#{stack.name}-#{attributes.checksum}.json"
229
+ file.body = MultiJson.dump(stack.template)
230
+ file.save
231
+ stack.template_url = file.url
232
+ stack.template = nil
233
+ stack
234
+ end
235
+
236
+ # Delete the stack template persisted in the object store
237
+ #
238
+ # @param stack [Models::Orchestration::Stack]
239
+ # @return [TrueClass, NilClass]
240
+ def delete_template!(stack)
241
+ storage = api_for(:storage)
242
+ bucket = storage.buckets.get(azure_root_orchestration_container)
243
+ if(bucket)
244
+ t_file = bucket.files.get("#{stack.name}-#{attributes.checksum}.json")
245
+ if(t_file)
246
+ t_file.destroy
247
+ true
248
+ end
249
+ end
250
+ end
251
+
252
+ # Reload the stack data from the API
253
+ #
254
+ # @param stack [Models::Orchestration::Stack]
255
+ # @return [Models::Orchestration::Stack]
256
+ def stack_reload(stack)
257
+ if(stack.persisted?)
258
+ load_stack_data(stack)
259
+ end
260
+ stack
261
+ end
262
+
263
+ # Delete the stack
264
+ #
265
+ # @param stack [Models::Orchestration::Stack]
266
+ # @return [TrueClass, FalseClass]
267
+ def stack_destroy(stack)
268
+ if(stack.persisted?)
269
+ request(
270
+ :path => [generate_path, stack.name].join('/'),
271
+ :method => :patch,
272
+ :json => {
273
+ :tags => stack.tags.merge(
274
+ :updated => Time.now.to_i,
275
+ :state => 'delete'
276
+ )
277
+ }
278
+ )
279
+ delete_template!(stack)
280
+ request(
281
+ :method => :delete,
282
+ :expects => [202, 204],
283
+ :path => generate_path(stack)
284
+ )
285
+ request(
286
+ :path => [generate_path, stack.name].join('/'),
287
+ :method => :delete,
288
+ :expects => 202
289
+ )
290
+ true
291
+ else
292
+ false
293
+ end
294
+ end
295
+
296
+ # Fetch stack template
297
+ #
298
+ # @param stack [Stack]
299
+ # @return [Smash] stack template
300
+ def stack_template_load(stack)
301
+ if(stack.persisted?)
302
+ if(stack.template_url)
303
+ storage = api_for(:storage)
304
+ location = URI.parse(stack.template_url)
305
+ bucket, file = location.path.sub('/', '').split('/', 2)
306
+ file = storage.buckets.get(bucket).files.get(file)
307
+ file.body.rewind
308
+ MultiJson.load(file.body.read).to_smash
309
+ else
310
+ Smash.new
311
+ end
312
+ else
313
+ Smash.new
314
+ end
315
+ end
316
+
317
+ # Validate stack template
318
+ #
319
+ # @param stack [Stack]
320
+ # @return [NilClass, String] nil if valid, string error message if invalid
321
+ def stack_template_validate(stack)
322
+ begin
323
+ result = request(
324
+ :path => [generate_path(stack), 'validate'].join('/'),
325
+ :method => :post,
326
+ :json => {
327
+ :properties => {
328
+ :template => stack.template,
329
+ :parameters => stack.parameters,
330
+ :mode => 'Complete'
331
+ }
332
+ }
333
+ )
334
+ nil
335
+ rescue Error::ApiError::RequestError => e
336
+ begin
337
+ error = MultiJson.load(e.response.body.to_s).to_smash
338
+ "#{error.get(:error, :code)} - #{error.get(:error, :message)}"
339
+ rescue
340
+ "Failed to extract error information! - #{e.response.body.to_s}"
341
+ end
342
+ end
343
+ end
344
+
345
+ # Return single stack
346
+ #
347
+ # @param ident [String] name or ID
348
+ # @return [Stack]
349
+ def stack_get(ident)
350
+ i = Stack.new(self)
351
+ i.id = i.name = ident
352
+ i.reload
353
+ end
354
+
355
+ # Return all stacks
356
+ #
357
+ # @param options [Hash] filter
358
+ # @return [Array<Models::Orchestration::Stack>]
359
+ # @todo check if we need any mappings on state set
360
+ def stack_all
361
+ load_stack_data
362
+ end
363
+
364
+ # Return all resources for stack
365
+ #
366
+ # @param stack [Models::Orchestration::Stack]
367
+ # @return [Array<Models::Orchestration::Stack::Resource>]
368
+ def resource_all(stack)
369
+ if(stack.persisted?)
370
+ result = request(
371
+ :path => [generate_path, stack.name, 'resources'].join('/'),
372
+ )
373
+ result.fetch(:body, :value, []).map do |res|
374
+ info = Smash.new(
375
+ :id => res[:id],
376
+ :type => res[:type],
377
+ :name => res[:name],
378
+ :logical_id => res[:name],
379
+ :state => :unknown,
380
+ :status => 'Unknown'
381
+ )
382
+ evt = stack.events.all.detect do |event|
383
+ event.resource_id == res[:id]
384
+ end
385
+ if(evt)
386
+ info = info.merge(
387
+ Smash.new(
388
+ :state => evt.resource_state,
389
+ :status => evt.resource_status,
390
+ :status_reason => evt.resource_status_reason,
391
+ :updated => evt.time
392
+ )
393
+ )
394
+ end
395
+ Stack::Resource.new(stack, info).valid_state
396
+ end
397
+ else
398
+ []
399
+ end
400
+ end
401
+
402
+ # Reload the stack resource data from the API
403
+ #
404
+ # @param resource [Models::Orchestration::Stack::Resource]
405
+ # @return [Models::Orchestration::Resource]
406
+ def resource_reload(resource)
407
+ resource.stack.resources.reload
408
+ resource.stack.resources.get(resource.id)
409
+ end
410
+
411
+ # Return all events for stack
412
+ #
413
+ # @param stack [Models::Orchestration::Stack]
414
+ # @return [Array<Models::Orchestration::Stack::Event>]
415
+ def event_all(stack, evt_id=nil)
416
+ result = request(
417
+ :path => [generate_path(stack), 'operations'].join('/')
418
+ )
419
+ events = result.get(:body, :value).map do |event|
420
+ Stack::Event.new(
421
+ stack,
422
+ :id => event[:operationId],
423
+ :resource_id => event.get(:properties, :targetResource, :id),
424
+ :resource_name => event.get(:properties, :targetResource, :resourceName),
425
+ :resource_logical_id => event.get(:properties, :targetResource, :resourceName),
426
+ :resource_state => status_to_state(event.get(:properties, :provisioningState)),
427
+ :resource_status => event.get(:properties, :provisioningState),
428
+ :resource_status_reason => event.get(:properties, :statusCode),
429
+ :time => Time.parse(event.get(:properties, :timestamp))
430
+ ).valid_state
431
+ end
432
+ if(evt_id)
433
+ idx = events.index{|d| e.id == evt_id}
434
+ idx = idx ? idx + 1 : 0
435
+ events.slice(idx, events.size)
436
+ else
437
+ events
438
+ end
439
+ end
440
+
441
+ # Return all new events for event collection
442
+ #
443
+ # @param events [Models::Orchestration::Stack::Events]
444
+ # @return [Array<Models::Orchestration::Stack::Event>]
445
+ def event_all_new(events)
446
+ event_all(events.stack, events.all.first.id)
447
+ end
448
+
449
+ # Reload the stack event data from the API
450
+ #
451
+ # @param resource [Models::Orchestration::Stack::Event]
452
+ # @return [Models::Orchestration::Event]
453
+ def event_reload(event)
454
+ event.stack.events.reload
455
+ event.stack.events.get(event.id)
456
+ end
457
+
458
+ end
459
+ end
460
+ end
461
+ end