vmpooler 2.4.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,1757 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'vmpooler/util/parsing'
4
-
5
- module Vmpooler
6
- class API
7
- class V1 < Sinatra::Base
8
- api_version = '1'
9
- api_prefix = "/api/v#{api_version}"
10
-
11
- helpers do
12
- include Vmpooler::API::Helpers
13
- end
14
-
15
- def backend
16
- Vmpooler::API.settings.redis
17
- end
18
-
19
- def metrics
20
- Vmpooler::API.settings.metrics
21
- end
22
-
23
- def config
24
- Vmpooler::API.settings.config[:config]
25
- end
26
-
27
- def full_config
28
- Vmpooler::API.settings.config
29
- end
30
-
31
- def pools
32
- Vmpooler::API.settings.config[:pools]
33
- end
34
-
35
- def pools_at_startup
36
- Vmpooler::API.settings.config[:pools_at_startup]
37
- end
38
-
39
- def pool_exists?(template)
40
- Vmpooler::API.settings.config[:pool_names].include?(template)
41
- end
42
-
43
- def need_auth!
44
- validate_auth(backend)
45
- end
46
-
47
- def need_token!
48
- validate_token(backend)
49
- end
50
-
51
- def checkoutlock
52
- Vmpooler::API.settings.checkoutlock
53
- end
54
-
55
- def get_template_aliases(template)
56
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
57
- result = []
58
- aliases = Vmpooler::API.settings.config[:alias]
59
- if aliases
60
- result += aliases[template] if aliases[template].is_a?(Array)
61
- template_backends << aliases[template] if aliases[template].is_a?(String)
62
- end
63
- result
64
- end
65
- end
66
-
67
- def get_pool_weights(template_backends)
68
- pool_index = pool_index(pools)
69
- weighted_pools = {}
70
- template_backends.each do |t|
71
- next unless pool_index.key? t
72
-
73
- index = pool_index[t]
74
- clone_target = pools[index]['clone_target'] || config['clone_target']
75
- next unless config.key?('backend_weight')
76
-
77
- weight = config['backend_weight'][clone_target]
78
- if weight
79
- weighted_pools[t] = weight
80
- end
81
- end
82
- weighted_pools
83
- end
84
-
85
- def count_selection(selection)
86
- result = {}
87
- selection.uniq.each do |poolname|
88
- result[poolname] = selection.count(poolname)
89
- end
90
- result
91
- end
92
-
93
- def evaluate_template_aliases(template, count)
94
- template_backends = []
95
- template_backends << template if backend.sismember('vmpooler__pools', template)
96
- selection = []
97
- aliases = get_template_aliases(template)
98
- if aliases
99
- template_backends += aliases
100
- weighted_pools = get_pool_weights(template_backends)
101
-
102
- if weighted_pools.count > 1 && weighted_pools.count == template_backends.count
103
- pickup = Pickup.new(weighted_pools)
104
- count.to_i.times do
105
- selection << pickup.pick
106
- end
107
- else
108
- count.to_i.times do
109
- selection << template_backends.sample
110
- end
111
- end
112
- end
113
-
114
- count_selection(selection)
115
- end
116
-
117
- def fetch_single_vm(template)
118
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
119
- template_backends = [template]
120
- aliases = Vmpooler::API.settings.config[:alias]
121
- if aliases
122
- template_backends += aliases[template] if aliases[template].is_a?(Array)
123
- template_backends << aliases[template] if aliases[template].is_a?(String)
124
- pool_index = pool_index(pools)
125
- weighted_pools = {}
126
- template_backends.each do |t|
127
- next unless pool_index.key? t
128
-
129
- index = pool_index[t]
130
- clone_target = pools[index]['clone_target'] || config['clone_target']
131
- next unless config.key?('backend_weight')
132
-
133
- weight = config['backend_weight'][clone_target]
134
- if weight
135
- weighted_pools[t] = weight
136
- end
137
- end
138
-
139
- if weighted_pools.count == template_backends.count
140
- pickup = Pickup.new(weighted_pools)
141
- selection = pickup.pick
142
- template_backends.delete(selection)
143
- template_backends.unshift(selection)
144
- else
145
- first = template_backends.sample
146
- template_backends.delete(first)
147
- template_backends.unshift(first)
148
- end
149
- end
150
-
151
- checkoutlock.synchronize do
152
- template_backends.each do |template_backend|
153
- vms = backend.smembers("vmpooler__ready__#{template_backend}")
154
- next if vms.empty?
155
-
156
- vms.reverse.each do |vm|
157
- ready = vm_ready?(vm, config['domain'])
158
- if ready
159
- smoved = backend.smove("vmpooler__ready__#{template_backend}", "vmpooler__running__#{template_backend}", vm)
160
- if smoved
161
- return [vm, template_backend, template]
162
- else
163
- metrics.increment("checkout.smove.failed.#{template_backend}")
164
- return [nil, nil, nil]
165
- end
166
- else
167
- backend.smove("vmpooler__ready__#{template_backend}", "vmpooler__completed__#{template_backend}", vm)
168
- metrics.increment("checkout.nonresponsive.#{template_backend}")
169
- end
170
- end
171
- end
172
- [nil, nil, nil]
173
- end
174
- end
175
- end
176
-
177
- def return_vm_to_ready_state(template, vm)
178
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
179
- backend.srem("vmpooler__migrating__#{template}", vm)
180
- backend.hdel("vmpooler__active__#{template}", vm)
181
- backend.hdel("vmpooler__vm__#{vm}", 'checkout', 'token:token', 'token:user')
182
- backend.smove("vmpooler__running__#{template}", "vmpooler__ready__#{template}", vm)
183
- end
184
- end
185
-
186
- def account_for_starting_vm(template, vm)
187
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do |span|
188
- user = backend.hget("vmpooler__token__#{request.env['HTTP_X_AUTH_TOKEN']}", 'user')
189
- span.set_attribute('enduser.id', user)
190
- has_token_result = has_token?
191
- backend.sadd("vmpooler__migrating__#{template}", vm)
192
- backend.hset("vmpooler__active__#{template}", vm, Time.now)
193
- backend.hset("vmpooler__vm__#{vm}", 'checkout', Time.now)
194
-
195
- if Vmpooler::API.settings.config[:auth] and has_token_result
196
- backend.hset("vmpooler__vm__#{vm}", 'token:token', request.env['HTTP_X_AUTH_TOKEN'])
197
- backend.hset("vmpooler__vm__#{vm}", 'token:user', user)
198
-
199
- if config['vm_lifetime_auth'].to_i > 0
200
- backend.hset("vmpooler__vm__#{vm}", 'lifetime', config['vm_lifetime_auth'].to_i)
201
- end
202
- end
203
- end
204
- end
205
-
206
- def update_result_hosts(result, template, vm)
207
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
208
- result[template] ||= {}
209
- if result[template]['hostname']
210
- result[template]['hostname'] = Array(result[template]['hostname'])
211
- result[template]['hostname'].push(vm)
212
- else
213
- result[template]['hostname'] = vm
214
- end
215
- end
216
- end
217
-
218
- def atomically_allocate_vms(payload)
219
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do |span|
220
- result = { 'ok' => false }
221
- failed = false
222
- vms = []
223
-
224
- validate_token(backend) if Vmpooler::API.settings.config[:auth] and has_token?
225
-
226
- payload.each do |requested, count|
227
- count.to_i.times do |_i|
228
- vmname, vmpool, vmtemplate = fetch_single_vm(requested)
229
- if vmname
230
- account_for_starting_vm(vmpool, vmname)
231
- vms << [vmpool, vmname, vmtemplate]
232
- metrics.increment("checkout.success.#{vmpool}")
233
- update_user_metrics('allocate', vmname) if Vmpooler::API.settings.config[:config]['usage_stats']
234
- else
235
- failed = true
236
- metrics.increment("checkout.empty.#{requested}")
237
- break
238
- end
239
- end
240
- end
241
-
242
- if failed
243
- vms.each do |(vmpool, vmname, _vmtemplate)|
244
- return_vm_to_ready_state(vmpool, vmname)
245
- end
246
- span.add_event('error', attributes: {
247
- 'error.type' => 'Vmpooler::API::V1.atomically_allocate_vms',
248
- 'error.message' => '503 due to failing to allocate one or more vms'
249
- })
250
- status 503
251
- else
252
- vm_names = []
253
- vms.each do |(_vmpool, vmname, vmtemplate)|
254
- update_result_hosts(result, vmtemplate, vmname)
255
- vm_names.append(vmname)
256
- end
257
-
258
- span.set_attribute('vmpooler.vm_names', vm_names.join(',')) unless vm_names.empty?
259
-
260
- result['ok'] = true
261
- result['domain'] = config['domain'] if config['domain']
262
- end
263
-
264
- result
265
- end
266
- end
267
-
268
- def component_to_test(match, labels_string)
269
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
270
- return if labels_string.nil?
271
-
272
- labels_string_parts = labels_string.split(',')
273
- labels_string_parts.each do |part|
274
- key, value = part.split('=')
275
- next if value.nil?
276
- return value if key == match
277
- end
278
- 'none'
279
- end
280
- end
281
-
282
- def update_user_metrics(operation, vmname)
283
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do |span|
284
- begin
285
- backend.multi
286
- backend.hget("vmpooler__vm__#{vmname}", 'tag:jenkins_build_url')
287
- backend.hget("vmpooler__vm__#{vmname}", 'token:user')
288
- backend.hget("vmpooler__vm__#{vmname}", 'template')
289
- jenkins_build_url, user, poolname = backend.exec
290
- poolname = poolname.gsub('.', '_')
291
-
292
- if user
293
- user = user.gsub('.', '_')
294
- else
295
- user = 'unauthenticated'
296
- end
297
- metrics.increment("user.#{user}.#{operation}.#{poolname}")
298
-
299
- if jenkins_build_url
300
- if jenkins_build_url.include? 'litmus'
301
- # Very simple filter for Litmus jobs - just count them coming through for the moment.
302
- metrics.increment("usage_litmus.#{user}.#{operation}.#{poolname}")
303
- else
304
- url_parts = jenkins_build_url.split('/')[2..-1]
305
- jenkins_instance = url_parts[0].gsub('.', '_')
306
- value_stream_parts = url_parts[2].split('_')
307
- value_stream_parts = value_stream_parts.map { |s| s.gsub('.', '_') }
308
- value_stream = value_stream_parts.shift
309
- branch = value_stream_parts.pop
310
- project = value_stream_parts.shift
311
- job_name = value_stream_parts.join('_')
312
- build_metadata_parts = url_parts[3]
313
- component_to_test = component_to_test('RMM_COMPONENT_TO_TEST_NAME', build_metadata_parts)
314
-
315
- metrics.increment("usage_jenkins_instance.#{jenkins_instance}.#{value_stream}.#{operation}.#{poolname}")
316
- metrics.increment("usage_branch_project.#{branch}.#{project}.#{operation}.#{poolname}")
317
- metrics.increment("usage_job_component.#{job_name}.#{component_to_test}.#{operation}.#{poolname}")
318
- end
319
- end
320
- rescue StandardError => e
321
- puts 'd', "[!] [#{poolname}] failed while evaluating usage labels on '#{vmname}' with an error: #{e}"
322
- span.record_exception(e)
323
- span.status = OpenTelemetry::Trace::Status.error(e.to_s)
324
- span.add_event('log', attributes: {
325
- 'log.severity' => 'debug',
326
- 'log.message' => "[#{poolname}] failed while evaluating usage labels on '#{vmname}' with an error: #{e}"
327
- })
328
- end
329
- end
330
- end
331
-
332
- def reset_pool_size(poolname)
333
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
334
- result = { 'ok' => false }
335
-
336
- pool_index = pool_index(pools)
337
-
338
- pools_updated = 0
339
- sync_pool_sizes
340
-
341
- pool_size_now = pools[pool_index[poolname]]['size'].to_i
342
- pool_size_original = pools_at_startup[pool_index[poolname]]['size'].to_i
343
- result['pool_size_before_reset'] = pool_size_now
344
- result['pool_size_before_overrides'] = pool_size_original
345
-
346
- unless pool_size_now == pool_size_original
347
- pools[pool_index[poolname]]['size'] = pool_size_original
348
- backend.hdel('vmpooler__config__poolsize', poolname)
349
- backend.sadd('vmpooler__pool__undo_size_override', poolname)
350
- pools_updated += 1
351
- status 201
352
- end
353
-
354
- status 200 unless pools_updated > 0
355
- result['ok'] = true
356
- result
357
- end
358
- end
359
-
360
- def update_pool_size(payload)
361
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
362
- result = { 'ok' => false }
363
-
364
- pool_index = pool_index(pools)
365
- pools_updated = 0
366
- sync_pool_sizes
367
-
368
- payload.each do |poolname, size|
369
- unless pools[pool_index[poolname]]['size'] == size.to_i
370
- pools[pool_index[poolname]]['size'] = size.to_i
371
- backend.hset('vmpooler__config__poolsize', poolname, size)
372
- pools_updated += 1
373
- status 201
374
- end
375
- end
376
- status 200 unless pools_updated > 0
377
- result['ok'] = true
378
- result
379
- end
380
- end
381
-
382
- def reset_pool_template(poolname)
383
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
384
- result = { 'ok' => false }
385
-
386
- pool_index_live = pool_index(pools)
387
- pool_index_original = pool_index(pools_at_startup)
388
-
389
- pools_updated = 0
390
- sync_pool_templates
391
-
392
- template_now = pools[pool_index_live[poolname]]['template']
393
- template_original = pools_at_startup[pool_index_original[poolname]]['template']
394
- result['template_before_reset'] = template_now
395
- result['template_before_overrides'] = template_original
396
-
397
- unless template_now == template_original
398
- pools[pool_index_live[poolname]]['template'] = template_original
399
- backend.hdel('vmpooler__config__template', poolname)
400
- backend.sadd('vmpooler__pool__undo_template_override', poolname)
401
- pools_updated += 1
402
- status 201
403
- end
404
-
405
- status 200 unless pools_updated > 0
406
- result['ok'] = true
407
- result
408
- end
409
- end
410
-
411
- def update_pool_template(payload)
412
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
413
- result = { 'ok' => false }
414
-
415
- pool_index = pool_index(pools)
416
- pools_updated = 0
417
- sync_pool_templates
418
-
419
- payload.each do |poolname, template|
420
- unless pools[pool_index[poolname]]['template'] == template
421
- pools[pool_index[poolname]]['template'] = template
422
- backend.hset('vmpooler__config__template', poolname, template)
423
- pools_updated += 1
424
- status 201
425
- end
426
- end
427
- status 200 unless pools_updated > 0
428
- result['ok'] = true
429
- result
430
- end
431
- end
432
-
433
- def reset_pool(payload)
434
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
435
- result = { 'ok' => false }
436
-
437
- payload.each do |poolname, _count|
438
- backend.sadd('vmpooler__poolreset', poolname)
439
- end
440
- status 201
441
- result['ok'] = true
442
- result
443
- end
444
- end
445
-
446
- def update_clone_target(payload)
447
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
448
- result = { 'ok' => false }
449
-
450
- pool_index = pool_index(pools)
451
- pools_updated = 0
452
- sync_clone_targets
453
-
454
- payload.each do |poolname, clone_target|
455
- unless pools[pool_index[poolname]]['clone_target'] == clone_target
456
- pools[pool_index[poolname]]['clone_target'] = clone_target
457
- backend.hset('vmpooler__config__clone_target', poolname, clone_target)
458
- pools_updated += 1
459
- status 201
460
- end
461
- end
462
- status 200 unless pools_updated > 0
463
- result['ok'] = true
464
- result
465
- end
466
- end
467
-
468
- def sync_pool_templates
469
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
470
- pool_index = pool_index(pools)
471
- template_configs = backend.hgetall('vmpooler__config__template')
472
- template_configs&.each do |poolname, template|
473
- next unless pool_index.include? poolname
474
-
475
- pools[pool_index[poolname]]['template'] = template
476
- end
477
- end
478
- end
479
-
480
- def sync_pool_sizes
481
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
482
- pool_index = pool_index(pools)
483
- poolsize_configs = backend.hgetall('vmpooler__config__poolsize')
484
- poolsize_configs&.each do |poolname, size|
485
- next unless pool_index.include? poolname
486
-
487
- pools[pool_index[poolname]]['size'] = size.to_i
488
- end
489
- end
490
- end
491
-
492
- def sync_clone_targets
493
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
494
- pool_index = pool_index(pools)
495
- clone_target_configs = backend.hgetall('vmpooler__config__clone_target')
496
- clone_target_configs&.each do |poolname, clone_target|
497
- next unless pool_index.include? poolname
498
-
499
- pools[pool_index[poolname]]['clone_target'] = clone_target
500
- end
501
- end
502
- end
503
-
504
- def too_many_requested?(payload)
505
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
506
- payload&.each do |poolname, count|
507
- next unless count.to_i > config['max_ondemand_instances_per_request']
508
-
509
- metrics.increment("ondemandrequest_fail.toomanyrequests.#{poolname}")
510
- return true
511
- end
512
- false
513
- end
514
- end
515
-
516
- def generate_ondemand_request(payload)
517
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do |span|
518
- result = { 'ok': false }
519
-
520
- requested_instances = payload.reject { |k, _v| k == 'request_id' }
521
- if too_many_requested?(requested_instances)
522
- e_message = "requested amount of instances exceeds the maximum #{config['max_ondemand_instances_per_request']}"
523
- result['message'] = e_message
524
- status 403
525
- span.add_event('error', attributes: {
526
- 'error.type' => 'Vmpooler::API::V1.generate_ondemand_request',
527
- 'error.message' => "403 due to #{e_message}"
528
- })
529
- return result
530
- end
531
-
532
- score = Time.now.to_i
533
- request_id = payload['request_id']
534
- request_id ||= generate_request_id
535
- result['request_id'] = request_id
536
- span.set_attribute('vmpooler.request_id', request_id)
537
-
538
- if backend.exists?("vmpooler__odrequest__#{request_id}")
539
- e_message = "request_id '#{request_id}' has already been created"
540
- result['message'] = e_message
541
- status 409
542
- span.add_event('error', attributes: {
543
- 'error.type' => 'Vmpooler::API::V1.generate_ondemand_request',
544
- 'error.message' => "409 due to #{e_message}"
545
- })
546
- metrics.increment('ondemandrequest_generate.duplicaterequests')
547
- return result
548
- end
549
-
550
- status 201
551
-
552
- platforms_with_aliases = []
553
- requested_instances.each do |poolname, count|
554
- selection = evaluate_template_aliases(poolname, count)
555
- selection.map { |selected_pool, selected_pool_count| platforms_with_aliases << "#{poolname}:#{selected_pool}:#{selected_pool_count}" }
556
- end
557
- platforms_string = platforms_with_aliases.join(',')
558
-
559
- return result unless backend.zadd('vmpooler__provisioning__request', score, request_id)
560
-
561
- backend.hset("vmpooler__odrequest__#{request_id}", 'requested', platforms_string)
562
- if Vmpooler::API.settings.config[:auth] and has_token?
563
- token_token = request.env['HTTP_X_AUTH_TOKEN']
564
- token_user = backend.hget("vmpooler__token__#{token_token}", 'user')
565
- backend.hset("vmpooler__odrequest__#{request_id}", 'token:token', token_token)
566
- backend.hset("vmpooler__odrequest__#{request_id}", 'token:user', token_user)
567
- span.set_attribute('enduser.id', token_user)
568
- end
569
-
570
- result['domain'] = config['domain'] if config['domain']
571
- result[:ok] = true
572
- metrics.increment('ondemandrequest_generate.success')
573
- result
574
- end
575
- end
576
-
577
- def generate_request_id
578
- SecureRandom.uuid
579
- end
580
-
581
- get '/' do
582
- sync_pool_sizes
583
- redirect to('/dashboard/')
584
- end
585
-
586
- # Provide run-time statistics
587
- #
588
- # Example:
589
- #
590
- # {
591
- # "boot": {
592
- # "duration": {
593
- # "average": 163.6,
594
- # "min": 65.49,
595
- # "max": 830.07,
596
- # "total": 247744.71000000002
597
- # },
598
- # "count": {
599
- # "total": 1514
600
- # }
601
- # },
602
- # "capacity": {
603
- # "current": 968,
604
- # "total": 975,
605
- # "percent": 99.3
606
- # },
607
- # "clone": {
608
- # "duration": {
609
- # "average": 17.0,
610
- # "min": 4.66,
611
- # "max": 637.96,
612
- # "total": 25634.15
613
- # },
614
- # "count": {
615
- # "total": 1507
616
- # }
617
- # },
618
- # "queue": {
619
- # "pending": 12,
620
- # "cloning": 0,
621
- # "booting": 12,
622
- # "ready": 968,
623
- # "running": 367,
624
- # "completed": 0,
625
- # "total": 1347
626
- # },
627
- # "pools": {
628
- # "ready": 100,
629
- # "running": 120,
630
- # "pending": 5,
631
- # "max": 250,
632
- # }
633
- # "status": {
634
- # "ok": true,
635
- # "message": "Battle station fully armed and operational.",
636
- # "empty": [ # NOTE: would not have 'ok: true' w/ "empty" pools
637
- # "redhat-7-x86_64",
638
- # "ubuntu-1404-i386"
639
- # ],
640
- # "uptime": 179585.9
641
- # }
642
- #
643
- # If the query parameter 'view' is provided, it will be used to select which top level
644
- # element to compute and return. Select them by specifying them in a comma separated list.
645
- # For example /status?view=capacity,boot
646
- # would return only the "capacity" and "boot" statistics. "status" is always returned
647
-
648
- get "#{api_prefix}/status/?" do
649
- content_type :json
650
-
651
- if params[:view]
652
- views = params[:view].split(",")
653
- end
654
-
655
- result = {
656
- status: {
657
- ok: true,
658
- message: 'Battle station fully armed and operational.'
659
- }
660
- }
661
-
662
- sync_pool_sizes
663
-
664
- result[:capacity] = get_capacity_metrics(pools, backend) unless views and not views.include?("capacity")
665
- result[:queue] = get_queue_metrics(pools, backend) unless views and not views.include?("queue")
666
- result[:clone] = get_task_metrics(backend, 'clone', Date.today.to_s) unless views and not views.include?("clone")
667
- result[:boot] = get_task_metrics(backend, 'boot', Date.today.to_s) unless views and not views.include?("boot")
668
-
669
- # Check for empty pools
670
- result[:pools] = {} unless views and not views.include?("pools")
671
- ready_hash = get_list_across_pools_redis_scard(pools, 'vmpooler__ready__', backend)
672
- running_hash = get_list_across_pools_redis_scard(pools, 'vmpooler__running__', backend)
673
- pending_hash = get_list_across_pools_redis_scard(pools, 'vmpooler__pending__', backend)
674
- lastBoot_hash = get_list_across_pools_redis_hget(pools, 'vmpooler__lastboot', backend)
675
-
676
- unless views and not views.include?("pools")
677
- pools.each do |pool|
678
- # REMIND: move this out of the API and into the back-end
679
- ready = ready_hash[pool['name']]
680
- running = running_hash[pool['name']]
681
- pending = pending_hash[pool['name']]
682
- max = pool['size']
683
- lastBoot = lastBoot_hash[pool['name']]
684
- aka = pool['alias']
685
-
686
- result[:pools][pool['name']] = {
687
- ready: ready,
688
- running: running,
689
- pending: pending,
690
- max: max,
691
- lastBoot: lastBoot
692
- }
693
-
694
- if aka
695
- result[:pools][pool['name']][:alias] = aka
696
- end
697
-
698
- # for backwards compatibility, include separate "empty" stats in "status" block
699
- if ready == 0 && max != 0
700
- result[:status][:empty] ||= []
701
- result[:status][:empty].push(pool['name'])
702
-
703
- result[:status][:ok] = false
704
- result[:status][:message] = "Found #{result[:status][:empty].length} empty pools."
705
- end
706
- end
707
- end
708
-
709
- result[:status][:uptime] = (Time.now - Vmpooler::API.settings.config[:uptime]).round(1) if Vmpooler::API.settings.config[:uptime]
710
-
711
- JSON.pretty_generate(Hash[result.sort_by { |k, _v| k }])
712
- end
713
-
714
- # request statistics for specific pools by passing parameter 'pool'
715
- # with a coma separated list of pools we want to query ?pool=ABC,DEF
716
- # returns the ready, max numbers and the aliases (if set)
717
- get "#{api_prefix}/poolstat/?" do
718
- content_type :json
719
-
720
- result = {}
721
-
722
- poolscopy = []
723
-
724
- if params[:pool]
725
- subpool = params[:pool].split(",")
726
- poolscopy = pools.select do |p|
727
- if subpool.include?(p['name'])
728
- true
729
- elsif !p['alias'].nil?
730
- if p['alias'].instance_of?(Array)
731
- (p['alias'] & subpool).any?
732
- elsif p['alias'].instance_of?(String)
733
- subpool.include?(p['alias'])
734
- end
735
- end
736
- end
737
- end
738
-
739
- result[:pools] = {}
740
-
741
- poolscopy.each do |pool|
742
- result[:pools][pool['name']] = {}
743
-
744
- max = pool['size']
745
- aka = pool['alias']
746
-
747
- result[:pools][pool['name']][:max] = max
748
-
749
- if aka
750
- result[:pools][pool['name']][:alias] = aka
751
- end
752
- end
753
-
754
- ready_hash = get_list_across_pools_redis_scard(poolscopy, 'vmpooler__ready__', backend)
755
-
756
- ready_hash.each { |k, v| result[:pools][k][:ready] = v }
757
-
758
- JSON.pretty_generate(Hash[result.sort_by { |k, _v| k }])
759
- end
760
-
761
- # requests the total number of running VMs
762
- get "#{api_prefix}/totalrunning/?" do
763
- content_type :json
764
- queue = {
765
- running: 0
766
- }
767
-
768
- queue[:running] = get_total_across_pools_redis_scard(pools, 'vmpooler__running__', backend)
769
-
770
- JSON.pretty_generate(queue)
771
- end
772
-
773
- get "#{api_prefix}/summary/?" do
774
- content_type :json
775
-
776
- result = {
777
- daily: []
778
- }
779
-
780
- from_param = params[:from] || Date.today.to_s
781
- to_param = params[:to] || Date.today.to_s
782
-
783
- # Validate date formats
784
- [from_param, to_param].each do |param|
785
- if !validate_date_str(param.to_s)
786
- halt 400, "Invalid date format '#{param}', must match YYYY-MM-DD."
787
- end
788
- end
789
-
790
- from_date, to_date = Date.parse(from_param), Date.parse(to_param)
791
-
792
- if to_date < from_date
793
- halt 400, 'Date range is invalid, \'to\' cannot come before \'from\'.'
794
- elsif from_date > Date.today
795
- halt 400, 'Date range is invalid, \'from\' must be in the past.'
796
- end
797
-
798
- boot = get_task_summary(backend, 'boot', from_date, to_date, :bypool => true)
799
- clone = get_task_summary(backend, 'clone', from_date, to_date, :bypool => true)
800
- tag = get_tag_summary(backend, from_date, to_date)
801
-
802
- result[:boot] = boot[:boot]
803
- result[:clone] = clone[:clone]
804
- result[:tag] = tag[:tag]
805
-
806
- daily = {}
807
-
808
- boot[:daily].each do |day|
809
- daily[day[:date]] ||= {}
810
- daily[day[:date]][:boot] = day[:boot]
811
- end
812
-
813
- clone[:daily].each do |day|
814
- daily[day[:date]] ||= {}
815
- daily[day[:date]][:clone] = day[:clone]
816
- end
817
-
818
- tag[:daily].each do |day|
819
- daily[day[:date]] ||= {}
820
- daily[day[:date]][:tag] = day[:tag]
821
- end
822
-
823
- daily.each_key do |day|
824
- result[:daily].push({
825
- date: day,
826
- boot: daily[day][:boot],
827
- clone: daily[day][:clone],
828
- tag: daily[day][:tag]
829
- })
830
- end
831
-
832
- JSON.pretty_generate(result)
833
- end
834
-
835
- get "#{api_prefix}/summary/:route/?:key?/?" do
836
- content_type :json
837
-
838
- result = {}
839
-
840
- from_param = params[:from] || Date.today.to_s
841
- to_param = params[:to] || Date.today.to_s
842
-
843
- # Validate date formats
844
- [from_param, to_param].each do |param|
845
- if !validate_date_str(param.to_s)
846
- halt 400, "Invalid date format '#{param}', must match YYYY-MM-DD."
847
- end
848
- end
849
-
850
- from_date, to_date = Date.parse(from_param), Date.parse(to_param)
851
-
852
- if to_date < from_date
853
- halt 400, 'Date range is invalid, \'to\' cannot come before \'from\'.'
854
- elsif from_date > Date.today
855
- halt 400, 'Date range is invalid, \'from\' must be in the past.'
856
- end
857
-
858
- case params[:route]
859
- when 'boot'
860
- result = get_task_summary(backend, 'boot', from_date, to_date, :bypool => true, :only => params[:key])
861
- when 'clone'
862
- result = get_task_summary(backend, 'clone', from_date, to_date, :bypool => true, :only => params[:key])
863
- when 'tag'
864
- result = get_tag_summary(backend, from_date, to_date, :only => params[:key])
865
- else
866
- halt 404, JSON.pretty_generate({ 'ok' => false })
867
- end
868
-
869
- JSON.pretty_generate(result)
870
- end
871
-
872
- get "#{api_prefix}/token/?" do
873
- content_type :json
874
-
875
- status 404
876
- result = { 'ok' => false }
877
-
878
- if Vmpooler::API.settings.config[:auth]
879
- status 401
880
-
881
- need_auth!
882
-
883
- backend.keys('vmpooler__token__*').each do |key|
884
- data = backend.hgetall(key)
885
-
886
- if data['user'] == Rack::Auth::Basic::Request.new(request.env).username
887
- span = OpenTelemetry::Trace.current_span
888
- span.set_attribute('enduser.id', data['user'])
889
- token = key.split('__').last
890
-
891
- result[token] ||= {}
892
-
893
- result[token]['created'] = data['created']
894
- result[token]['last'] = data['last'] || 'never'
895
-
896
- result['ok'] = true
897
- end
898
- end
899
-
900
- if result['ok']
901
- status 200
902
- else
903
- status 404
904
- end
905
- end
906
-
907
- JSON.pretty_generate(result)
908
- end
909
-
910
- get "#{api_prefix}/token/:token/?" do
911
- content_type :json
912
-
913
- status 404
914
- result = { 'ok' => false }
915
-
916
- if Vmpooler::API.settings.config[:auth]
917
- token = backend.hgetall("vmpooler__token__#{params[:token]}")
918
-
919
- if not token.nil? and not token.empty?
920
- status 200
921
-
922
- pools.each do |pool|
923
- backend.smembers("vmpooler__running__#{pool['name']}").each do |vm|
924
- if backend.hget("vmpooler__vm__#{vm}", 'token:token') == params[:token]
925
- token['vms'] ||= {}
926
- token['vms']['running'] ||= []
927
- token['vms']['running'].push(vm)
928
- end
929
- end
930
- end
931
-
932
- result = { 'ok' => true, params[:token] => token }
933
- end
934
- end
935
-
936
- JSON.pretty_generate(result)
937
- end
938
-
939
- delete "#{api_prefix}/token/:token/?" do
940
- content_type :json
941
-
942
- status 404
943
- result = { 'ok' => false }
944
-
945
- if Vmpooler::API.settings.config[:auth]
946
- status 401
947
-
948
- need_auth!
949
-
950
- if backend.del("vmpooler__token__#{params[:token]}").to_i > 0
951
- status 200
952
- result['ok'] = true
953
- end
954
- end
955
-
956
- JSON.pretty_generate(result)
957
- end
958
-
959
- post "#{api_prefix}/token" do
960
- content_type :json
961
-
962
- status 404
963
- result = { 'ok' => false }
964
-
965
- if Vmpooler::API.settings.config[:auth]
966
- status 401
967
-
968
- need_auth!
969
-
970
- o = [('a'..'z'), ('0'..'9')].map(&:to_a).flatten
971
- result['token'] = o[rand(25)] + (0...31).map { o[rand(o.length)] }.join
972
-
973
- backend.hset("vmpooler__token__#{result['token']}", 'user', @auth.username)
974
- backend.hset("vmpooler__token__#{result['token']}", 'created', Time.now)
975
- span = OpenTelemetry::Trace.current_span
976
- span.set_attribute('enduser.id', @auth.username)
977
-
978
- status 200
979
- result['ok'] = true
980
- end
981
-
982
- JSON.pretty_generate(result)
983
- end
984
-
985
- get "#{api_prefix}/vm/?" do
986
- content_type :json
987
-
988
- result = []
989
-
990
- pools.each do |pool|
991
- result.push(pool['name'])
992
- end
993
-
994
- JSON.pretty_generate(result)
995
- end
996
-
997
- post "#{api_prefix}/ondemandvm/?" do
998
- content_type :json
999
- metrics.increment('http_requests_vm_total.post.ondemand.requestid')
1000
-
1001
- need_token! if Vmpooler::API.settings.config[:auth]
1002
-
1003
- result = { 'ok' => false }
1004
-
1005
- begin
1006
- payload = JSON.parse(request.body.read)
1007
-
1008
- if payload
1009
- invalid = invalid_templates(payload.reject { |k, _v| k == 'request_id' })
1010
- if invalid.empty?
1011
- result = generate_ondemand_request(payload)
1012
- else
1013
- result[:bad_templates] = invalid
1014
- invalid.each do |bad_template|
1015
- metrics.increment("ondemandrequest_fail.invalid.#{bad_template}")
1016
- end
1017
- status 404
1018
- end
1019
- else
1020
- metrics.increment('ondemandrequest_fail.invalid.unknown')
1021
- status 404
1022
- end
1023
- rescue JSON::ParserError
1024
- span = OpenTelemetry::Trace.current_span
1025
- span.status = OpenTelemetry::Trace::Status.error('JSON payload could not be parsed')
1026
- status 400
1027
- result = {
1028
- 'ok' => false,
1029
- 'message' => 'JSON payload could not be parsed'
1030
- }
1031
- end
1032
-
1033
- JSON.pretty_generate(result)
1034
- end
1035
-
1036
- post "#{api_prefix}/ondemandvm/:template/?" do
1037
- content_type :json
1038
- result = { 'ok' => false }
1039
- metrics.increment('http_requests_vm_total.delete.ondemand.template')
1040
-
1041
- need_token! if Vmpooler::API.settings.config[:auth]
1042
-
1043
- payload = extract_templates_from_query_params(params[:template])
1044
-
1045
- if payload
1046
- invalid = invalid_templates(payload.reject { |k, _v| k == 'request_id' })
1047
- if invalid.empty?
1048
- result = generate_ondemand_request(payload)
1049
- else
1050
- result[:bad_templates] = invalid
1051
- invalid.each do |bad_template|
1052
- metrics.increment("ondemandrequest_fail.invalid.#{bad_template}")
1053
- end
1054
- status 404
1055
- end
1056
- else
1057
- metrics.increment('ondemandrequest_fail.invalid.unknown')
1058
- status 404
1059
- end
1060
-
1061
- JSON.pretty_generate(result)
1062
- end
1063
-
1064
- get "#{api_prefix}/ondemandvm/:requestid/?" do
1065
- content_type :json
1066
- metrics.increment('http_requests_vm_total.get.ondemand.request')
1067
-
1068
- status 404
1069
- result = check_ondemand_request(params[:requestid])
1070
-
1071
- JSON.pretty_generate(result)
1072
- end
1073
-
1074
- delete "#{api_prefix}/ondemandvm/:requestid/?" do
1075
- content_type :json
1076
- need_token! if Vmpooler::API.settings.config[:auth]
1077
- metrics.increment('http_requests_vm_total.delete.ondemand.request')
1078
-
1079
- status 404
1080
- result = delete_ondemand_request(params[:requestid])
1081
-
1082
- JSON.pretty_generate(result)
1083
- end
1084
-
1085
- post "#{api_prefix}/vm/?" do
1086
- content_type :json
1087
- result = { 'ok' => false }
1088
- metrics.increment('http_requests_vm_total.post.vm.checkout')
1089
-
1090
- payload = JSON.parse(request.body.read)
1091
-
1092
- if payload
1093
- invalid = invalid_templates(payload)
1094
- if invalid.empty?
1095
- result = atomically_allocate_vms(payload)
1096
- else
1097
- invalid.each do |bad_template|
1098
- metrics.increment("checkout.invalid.#{bad_template}")
1099
- end
1100
- status 404
1101
- end
1102
- else
1103
- metrics.increment('checkout.invalid.unknown')
1104
- status 404
1105
- end
1106
-
1107
- JSON.pretty_generate(result)
1108
- end
1109
-
1110
- def extract_templates_from_query_params(params)
1111
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
1112
- payload = {}
1113
-
1114
- params.split('+').each do |template|
1115
- payload[template] ||= 0
1116
- payload[template] += 1
1117
- end
1118
-
1119
- payload
1120
- end
1121
- end
1122
-
1123
- def invalid_templates(payload)
1124
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
1125
- invalid = []
1126
- payload.keys.each do |template|
1127
- invalid << template unless pool_exists?(template)
1128
- end
1129
- invalid
1130
- end
1131
- end
1132
-
1133
- def invalid_template_or_size(payload)
1134
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
1135
- invalid = []
1136
- payload.each do |pool, size|
1137
- invalid << pool unless pool_exists?(pool)
1138
- unless is_integer?(size)
1139
- invalid << pool
1140
- next
1141
- end
1142
- invalid << pool unless Integer(size) >= 0
1143
- end
1144
- invalid
1145
- end
1146
- end
1147
-
1148
- def invalid_template_or_path(payload)
1149
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
1150
- invalid = []
1151
- payload.each do |pool, template|
1152
- invalid << pool unless pool_exists?(pool)
1153
- invalid << pool unless template.include? '/'
1154
- invalid << pool if template[0] == '/'
1155
- invalid << pool if template[-1] == '/'
1156
- end
1157
- invalid
1158
- end
1159
- end
1160
-
1161
- def invalid_pool(payload)
1162
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do
1163
- invalid = []
1164
- payload.each do |pool, _clone_target|
1165
- invalid << pool unless pool_exists?(pool)
1166
- end
1167
- invalid
1168
- end
1169
- end
1170
-
1171
- def check_ondemand_request(request_id)
1172
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do |span|
1173
- span.set_attribute('vmpooler.request_id', request_id)
1174
- result = { 'ok' => false }
1175
- request_hash = backend.hgetall("vmpooler__odrequest__#{request_id}")
1176
- if request_hash.empty?
1177
- e_message = "no request found for request_id '#{request_id}'"
1178
- result['message'] = e_message
1179
- span.add_event('error', attributes: {
1180
- 'error.type' => 'Vmpooler::API::V1.check_ondemand_request',
1181
- 'error.message' => e_message
1182
- })
1183
- return result
1184
- end
1185
-
1186
- result['request_id'] = request_id
1187
- result['ready'] = false
1188
- result['ok'] = true
1189
- status 202
1190
-
1191
- case request_hash['status']
1192
- when 'ready'
1193
- result['ready'] = true
1194
- Parsing.get_platform_pool_count(request_hash['requested']) do |platform_alias, pool, _count|
1195
- instances = backend.smembers("vmpooler__#{request_id}__#{platform_alias}__#{pool}")
1196
-
1197
- if result.key?(platform_alias)
1198
- result[platform_alias][:hostname] = result[platform_alias][:hostname] + instances
1199
- else
1200
- result[platform_alias] = { 'hostname': instances }
1201
- end
1202
- end
1203
- result['domain'] = config['domain'] if config['domain']
1204
- status 200
1205
- when 'failed'
1206
- result['message'] = "The request failed to provision instances within the configured ondemand_request_ttl '#{config['ondemand_request_ttl']}'"
1207
- status 200
1208
- when 'deleted'
1209
- result['message'] = 'The request has been deleted'
1210
- status 200
1211
- else
1212
- Parsing.get_platform_pool_count(request_hash['requested']) do |platform_alias, pool, count|
1213
- instance_count = backend.scard("vmpooler__#{request_id}__#{platform_alias}__#{pool}")
1214
- instances_pending = count.to_i - instance_count.to_i
1215
-
1216
- if result.key?(platform_alias) && result[platform_alias].key?(:ready)
1217
- result[platform_alias][:ready] = (result[platform_alias][:ready].to_i + instance_count).to_s
1218
- result[platform_alias][:pending] = (result[platform_alias][:pending].to_i + instances_pending).to_s
1219
- else
1220
- result[platform_alias] = {
1221
- 'ready': instance_count.to_s,
1222
- 'pending': instances_pending.to_s
1223
- }
1224
- end
1225
- end
1226
- end
1227
-
1228
- result
1229
- end
1230
- end
1231
-
1232
- def delete_ondemand_request(request_id)
1233
- tracer.in_span("Vmpooler::API::V1.#{__method__}") do |span|
1234
- span.set_attribute('vmpooler.request_id', request_id)
1235
- result = { 'ok' => false }
1236
-
1237
- platforms = backend.hget("vmpooler__odrequest__#{request_id}", 'requested')
1238
- unless platforms
1239
- e_message = "no request found for request_id '#{request_id}'"
1240
- result['message'] = e_message
1241
- span.add_event('error', attributes: {
1242
- 'error.type' => 'Vmpooler::API::V1.delete_ondemand_request',
1243
- 'error.message' => e_message
1244
- })
1245
- return result
1246
- end
1247
-
1248
- if backend.hget("vmpooler__odrequest__#{request_id}", 'status') == 'deleted'
1249
- result['message'] = 'the request has already been deleted'
1250
- else
1251
- backend.hset("vmpooler__odrequest__#{request_id}", 'status', 'deleted')
1252
-
1253
- Parsing.get_platform_pool_count(platforms) do |platform_alias, pool, _count|
1254
- backend.smembers("vmpooler__#{request_id}__#{platform_alias}__#{pool}")&.each do |vm|
1255
- backend.smove("vmpooler__running__#{pool}", "vmpooler__completed__#{pool}", vm)
1256
- end
1257
- backend.del("vmpooler__#{request_id}__#{platform_alias}__#{pool}")
1258
- end
1259
- backend.expire("vmpooler__odrequest__#{request_id}", 129_600_0)
1260
- end
1261
- status 200
1262
- result['ok'] = true
1263
- result
1264
- end
1265
- end
1266
-
1267
- post "#{api_prefix}/vm/:template/?" do
1268
- content_type :json
1269
- result = { 'ok' => false }
1270
- metrics.increment('http_requests_vm_total.get.vm.template')
1271
-
1272
- payload = extract_templates_from_query_params(params[:template])
1273
-
1274
- if payload
1275
- invalid = invalid_templates(payload)
1276
- if invalid.empty?
1277
- result = atomically_allocate_vms(payload)
1278
- else
1279
- invalid.each do |bad_template|
1280
- metrics.increment("checkout.invalid.#{bad_template}")
1281
- end
1282
- status 404
1283
- end
1284
- else
1285
- metrics.increment('checkout.invalid.unknown')
1286
- status 404
1287
- end
1288
-
1289
- JSON.pretty_generate(result)
1290
- end
1291
-
1292
- get "#{api_prefix}/vm/:hostname/?" do
1293
- content_type :json
1294
- metrics.increment('http_requests_vm_total.get.vm.hostname')
1295
-
1296
- result = {}
1297
-
1298
- status 404
1299
- result['ok'] = false
1300
-
1301
- params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
1302
-
1303
- rdata = backend.hgetall("vmpooler__vm__#{params[:hostname]}")
1304
- unless rdata.empty?
1305
- status 200
1306
- result['ok'] = true
1307
-
1308
- result[params[:hostname]] = {}
1309
-
1310
- result[params[:hostname]]['template'] = rdata['template']
1311
- result[params[:hostname]]['lifetime'] = (rdata['lifetime'] || config['vm_lifetime']).to_i
1312
-
1313
- if rdata['destroy']
1314
- result[params[:hostname]]['running'] = ((Time.parse(rdata['destroy']) - Time.parse(rdata['checkout'])) / 60 / 60).round(2) if rdata['checkout']
1315
- result[params[:hostname]]['state'] = 'destroyed'
1316
- elsif rdata['checkout']
1317
- result[params[:hostname]]['running'] = ((Time.now - Time.parse(rdata['checkout'])) / 60 / 60).round(2)
1318
- result[params[:hostname]]['remaining'] = ((Time.parse(rdata['checkout']) + rdata['lifetime'].to_i*60*60 - Time.now) / 60 / 60).round(2)
1319
- result[params[:hostname]]['start_time'] = Time.parse(rdata['checkout']).to_datetime.rfc3339
1320
- result[params[:hostname]]['end_time'] = (Time.parse(rdata['checkout']) + rdata['lifetime'].to_i*60*60).to_datetime.rfc3339
1321
- result[params[:hostname]]['state'] = 'running'
1322
- elsif rdata['check']
1323
- result[params[:hostname]]['state'] = 'ready'
1324
- else
1325
- result[params[:hostname]]['state'] = 'pending'
1326
- end
1327
-
1328
- rdata.keys.each do |key|
1329
- if key.match('^tag\:(.+?)$')
1330
- result[params[:hostname]]['tags'] ||= {}
1331
- result[params[:hostname]]['tags'][$1] = rdata[key]
1332
- end
1333
-
1334
- if key.match('^snapshot\:(.+?)$')
1335
- result[params[:hostname]]['snapshots'] ||= []
1336
- result[params[:hostname]]['snapshots'].push($1)
1337
- end
1338
- end
1339
-
1340
- if rdata['disk']
1341
- result[params[:hostname]]['disk'] = rdata['disk'].split(':')
1342
- end
1343
-
1344
- # Look up IP address of the hostname
1345
- begin
1346
- ipAddress = TCPSocket.gethostbyname(params[:hostname])[3]
1347
- rescue StandardError
1348
- ipAddress = ""
1349
- end
1350
-
1351
- result[params[:hostname]]['ip'] = ipAddress
1352
-
1353
- if config['domain']
1354
- result[params[:hostname]]['domain'] = config['domain']
1355
- end
1356
-
1357
- result[params[:hostname]]['host'] = rdata['host'] if rdata['host']
1358
- result[params[:hostname]]['migrated'] = rdata['migrated'] if rdata['migrated']
1359
-
1360
- end
1361
-
1362
- JSON.pretty_generate(result)
1363
- end
1364
-
1365
- delete "#{api_prefix}/vm/:hostname/?" do
1366
- content_type :json
1367
- metrics.increment('http_requests_vm_total.delete.vm.hostname')
1368
-
1369
- result = {}
1370
-
1371
- status 404
1372
- result['ok'] = false
1373
-
1374
- params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
1375
-
1376
- rdata = backend.hgetall("vmpooler__vm__#{params[:hostname]}")
1377
- unless rdata.empty?
1378
- need_token! if rdata['token:token']
1379
-
1380
- if backend.srem("vmpooler__running__#{rdata['template']}", params[:hostname])
1381
- backend.sadd("vmpooler__completed__#{rdata['template']}", params[:hostname])
1382
-
1383
- status 200
1384
- result['ok'] = true
1385
- metrics.increment('delete.success')
1386
- update_user_metrics('destroy', params[:hostname]) if Vmpooler::API.settings.config[:config]['usage_stats']
1387
- else
1388
- metrics.increment('delete.failed')
1389
- end
1390
- end
1391
-
1392
- JSON.pretty_generate(result)
1393
- end
1394
-
1395
- put "#{api_prefix}/vm/:hostname/?" do
1396
- content_type :json
1397
- metrics.increment('http_requests_vm_total.put.vm.modify')
1398
-
1399
- status 404
1400
- result = { 'ok' => false }
1401
-
1402
- failure = []
1403
-
1404
- params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
1405
-
1406
- if backend.exists?("vmpooler__vm__#{params[:hostname]}")
1407
- begin
1408
- jdata = JSON.parse(request.body.read)
1409
- rescue StandardError => e
1410
- span = OpenTelemetry::Trace.current_span
1411
- span.record_exception(e)
1412
- span.status = OpenTelemetry::Trace::Status.error(e.to_s)
1413
- halt 400, JSON.pretty_generate(result)
1414
- end
1415
-
1416
- # Validate data payload
1417
- jdata.each do |param, arg|
1418
- case param
1419
- when 'lifetime'
1420
- need_token! if Vmpooler::API.settings.config[:auth]
1421
-
1422
- # in hours, defaults to one week
1423
- max_lifetime_upper_limit = config['max_lifetime_upper_limit']
1424
- if max_lifetime_upper_limit
1425
- max_lifetime_upper_limit = max_lifetime_upper_limit.to_i
1426
- if arg.to_i >= max_lifetime_upper_limit
1427
- failure.push("You provided a lifetime (#{arg}) that exceeds the configured maximum of #{max_lifetime_upper_limit}.")
1428
- end
1429
- end
1430
-
1431
- # validate lifetime is within boundaries
1432
- unless arg.to_i > 0
1433
- failure.push("You provided a lifetime (#{arg}) but you must provide a positive number.")
1434
- end
1435
-
1436
- when 'tags'
1437
- failure.push("You provided tags (#{arg}) as something other than a hash.") unless arg.is_a?(Hash)
1438
- failure.push("You provided unsuppored tags (#{arg}).") if config['allowed_tags'] && !(arg.keys - config['allowed_tags']).empty?
1439
- else
1440
- failure.push("Unknown argument #{arg}.")
1441
- end
1442
- end
1443
-
1444
- if !failure.empty?
1445
- status 400
1446
- result['failure'] = failure
1447
- else
1448
- jdata.each do |param, arg|
1449
- case param
1450
- when 'lifetime'
1451
- need_token! if Vmpooler::API.settings.config[:auth]
1452
-
1453
- arg = arg.to_i
1454
-
1455
- backend.hset("vmpooler__vm__#{params[:hostname]}", param, arg)
1456
- when 'tags'
1457
- filter_tags(arg)
1458
- export_tags(backend, params[:hostname], arg)
1459
- end
1460
- end
1461
-
1462
- status 200
1463
- result['ok'] = true
1464
- end
1465
- end
1466
-
1467
- JSON.pretty_generate(result)
1468
- end
1469
-
1470
- post "#{api_prefix}/vm/:hostname/disk/:size/?" do
1471
- content_type :json
1472
- metrics.increment('http_requests_vm_total.post.vm.disksize')
1473
-
1474
- need_token! if Vmpooler::API.settings.config[:auth]
1475
-
1476
- status 404
1477
- result = { 'ok' => false }
1478
-
1479
- params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
1480
-
1481
- if ((params[:size].to_i > 0 )and (backend.exists?("vmpooler__vm__#{params[:hostname]}")))
1482
- result[params[:hostname]] = {}
1483
- result[params[:hostname]]['disk'] = "+#{params[:size]}gb"
1484
-
1485
- backend.sadd('vmpooler__tasks__disk', "#{params[:hostname]}:#{params[:size]}")
1486
-
1487
- status 202
1488
- result['ok'] = true
1489
- end
1490
-
1491
- JSON.pretty_generate(result)
1492
- end
1493
-
1494
- post "#{api_prefix}/vm/:hostname/snapshot/?" do
1495
- content_type :json
1496
- metrics.increment('http_requests_vm_total.post.vm.snapshot')
1497
-
1498
- need_token! if Vmpooler::API.settings.config[:auth]
1499
-
1500
- status 404
1501
- result = { 'ok' => false }
1502
-
1503
- params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
1504
-
1505
- if backend.exists?("vmpooler__vm__#{params[:hostname]}")
1506
- result[params[:hostname]] = {}
1507
-
1508
- o = [('a'..'z'), ('0'..'9')].map(&:to_a).flatten
1509
- result[params[:hostname]]['snapshot'] = o[rand(25)] + (0...31).map { o[rand(o.length)] }.join
1510
-
1511
- backend.sadd('vmpooler__tasks__snapshot', "#{params[:hostname]}:#{result[params[:hostname]]['snapshot']}")
1512
-
1513
- status 202
1514
- result['ok'] = true
1515
- end
1516
-
1517
- JSON.pretty_generate(result)
1518
- end
1519
-
1520
- post "#{api_prefix}/vm/:hostname/snapshot/:snapshot/?" do
1521
- content_type :json
1522
- metrics.increment('http_requests_vm_total.post.vm.snapshot')
1523
-
1524
- need_token! if Vmpooler::API.settings.config[:auth]
1525
-
1526
- status 404
1527
- result = { 'ok' => false }
1528
-
1529
- params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
1530
-
1531
- unless backend.hget("vmpooler__vm__#{params[:hostname]}", "snapshot:#{params[:snapshot]}").to_i.zero?
1532
- backend.sadd('vmpooler__tasks__snapshot-revert', "#{params[:hostname]}:#{params[:snapshot]}")
1533
-
1534
- status 202
1535
- result['ok'] = true
1536
- end
1537
-
1538
- JSON.pretty_generate(result)
1539
- end
1540
-
1541
- delete "#{api_prefix}/config/poolsize/:pool/?" do
1542
- content_type :json
1543
- result = { 'ok' => false }
1544
-
1545
- if config['experimental_features']
1546
- need_token! if Vmpooler::API.settings.config[:auth]
1547
-
1548
- if pool_exists?(params[:pool])
1549
- result = reset_pool_size(params[:pool])
1550
- else
1551
- metrics.increment('config.invalid.unknown')
1552
- status 404
1553
- end
1554
- else
1555
- status 405
1556
- end
1557
-
1558
- JSON.pretty_generate(result)
1559
- end
1560
-
1561
- post "#{api_prefix}/config/poolsize/?" do
1562
- content_type :json
1563
- result = { 'ok' => false }
1564
-
1565
- if config['experimental_features']
1566
- need_token! if Vmpooler::API.settings.config[:auth]
1567
-
1568
- payload = JSON.parse(request.body.read)
1569
-
1570
- if payload
1571
- invalid = invalid_template_or_size(payload)
1572
- if invalid.empty?
1573
- result = update_pool_size(payload)
1574
- else
1575
- invalid.each do |bad_template|
1576
- metrics.increment("config.invalid.#{bad_template}")
1577
- end
1578
- result[:not_configured] = invalid
1579
- status 400
1580
- end
1581
- else
1582
- metrics.increment('config.invalid.unknown')
1583
- status 404
1584
- end
1585
- else
1586
- status 405
1587
- end
1588
-
1589
- JSON.pretty_generate(result)
1590
- end
1591
-
1592
- delete "#{api_prefix}/config/pooltemplate/:pool/?" do
1593
- content_type :json
1594
- result = { 'ok' => false }
1595
-
1596
- if config['experimental_features']
1597
- need_token! if Vmpooler::API.settings.config[:auth]
1598
-
1599
- if pool_exists?(params[:pool])
1600
- result = reset_pool_template(params[:pool])
1601
- else
1602
- metrics.increment('config.invalid.unknown')
1603
- status 404
1604
- end
1605
- else
1606
- status 405
1607
- end
1608
-
1609
- JSON.pretty_generate(result)
1610
- end
1611
-
1612
- post "#{api_prefix}/config/pooltemplate/?" do
1613
- content_type :json
1614
- result = { 'ok' => false }
1615
-
1616
- if config['experimental_features']
1617
- need_token! if Vmpooler::API.settings.config[:auth]
1618
-
1619
- payload = JSON.parse(request.body.read)
1620
-
1621
- if payload
1622
- invalid = invalid_template_or_path(payload)
1623
- if invalid.empty?
1624
- result = update_pool_template(payload)
1625
- else
1626
- invalid.each do |bad_template|
1627
- metrics.increment("config.invalid.#{bad_template}")
1628
- end
1629
- result[:bad_templates] = invalid
1630
- status 400
1631
- end
1632
- else
1633
- metrics.increment('config.invalid.unknown')
1634
- status 404
1635
- end
1636
- else
1637
- status 405
1638
- end
1639
-
1640
- JSON.pretty_generate(result)
1641
- end
1642
-
1643
- post "#{api_prefix}/poolreset/?" do
1644
- content_type :json
1645
- result = { 'ok' => false }
1646
-
1647
- if config['experimental_features']
1648
- need_token! if Vmpooler::API.settings.config[:auth]
1649
-
1650
- begin
1651
- payload = JSON.parse(request.body.read)
1652
- if payload
1653
- invalid = invalid_templates(payload)
1654
- if invalid.empty?
1655
- result = reset_pool(payload)
1656
- else
1657
- invalid.each do |bad_pool|
1658
- metrics.increment("poolreset.invalid.#{bad_pool}")
1659
- end
1660
- result[:bad_pools] = invalid
1661
- status 400
1662
- end
1663
- else
1664
- metrics.increment('poolreset.invalid.unknown')
1665
- status 404
1666
- end
1667
- rescue JSON::ParserError
1668
- span = OpenTelemetry::Trace.current_span
1669
- span.record_exception(e)
1670
- span.status = OpenTelemetry::Trace::Status.error('JSON payload could not be parsed')
1671
- status 400
1672
- result = {
1673
- 'ok' => false,
1674
- 'message' => 'JSON payload could not be parsed'
1675
- }
1676
- end
1677
- else
1678
- status 405
1679
- end
1680
-
1681
- JSON.pretty_generate(result)
1682
- end
1683
-
1684
- post "#{api_prefix}/config/clonetarget/?" do
1685
- content_type :json
1686
- result = { 'ok' => false }
1687
-
1688
- if config['experimental_features']
1689
- need_token! if Vmpooler::API.settings.config[:auth]
1690
-
1691
- payload = JSON.parse(request.body.read)
1692
-
1693
- if payload
1694
- invalid = invalid_pool(payload)
1695
- if invalid.empty?
1696
- result = update_clone_target(payload)
1697
- else
1698
- invalid.each do |bad_template|
1699
- metrics.increment("config.invalid.#{bad_template}")
1700
- end
1701
- result[:bad_templates] = invalid
1702
- status 400
1703
- end
1704
- else
1705
- metrics.increment('config.invalid.unknown')
1706
- status 404
1707
- end
1708
- else
1709
- status 405
1710
- end
1711
-
1712
- JSON.pretty_generate(result)
1713
- end
1714
-
1715
- get "#{api_prefix}/config/?" do
1716
- content_type :json
1717
- result = { 'ok' => false }
1718
- status 404
1719
-
1720
- if pools
1721
- sync_pool_sizes
1722
- sync_pool_templates
1723
-
1724
- pool_configuration = []
1725
- pools.each do |pool|
1726
- pool['template_ready'] = template_ready?(pool, backend)
1727
- pool_configuration << pool
1728
- end
1729
-
1730
- result = {
1731
- pool_configuration: pool_configuration,
1732
- status: {
1733
- ok: true
1734
- }
1735
- }
1736
-
1737
- status 200
1738
- end
1739
- JSON.pretty_generate(result)
1740
- end
1741
-
1742
- get "#{api_prefix}/full_config/?" do
1743
- content_type :json
1744
-
1745
- result = {
1746
- full_config: full_config,
1747
- status: {
1748
- ok: true
1749
- }
1750
- }
1751
-
1752
- status 200
1753
- JSON.pretty_generate(result)
1754
- end
1755
- end
1756
- end
1757
- end