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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2 -0
- data/LICENSE +13 -0
- data/README.md +91 -0
- data/lib/miasma-azure.rb +2 -0
- data/lib/miasma-azure/api.rb +18 -0
- data/lib/miasma-azure/version.rb +4 -0
- data/lib/miasma/contrib/azure.rb +369 -0
- data/lib/miasma/contrib/azure/orchestration.rb +461 -0
- data/lib/miasma/contrib/azure/storage.rb +356 -0
- data/miasma-azure.gemspec +23 -0
- metadata +180 -0
@@ -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
|