vmpooler 2.4.0 → 3.0.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.
@@ -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