miasma-azure 0.1.0

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