vmpooler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,71 @@
1
+ module Vmpooler
2
+ class API
3
+ class Reroute < Sinatra::Base
4
+ api_version = '1'
5
+
6
+ get '/status/?' do
7
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/status")
8
+ end
9
+
10
+ get '/summary/?' do
11
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/summary")
12
+ end
13
+
14
+ get '/summary/:route/?:key?/?' do
15
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/summary/#{params[:route]}/#{params[:key]}")
16
+ end
17
+
18
+ get '/token/?' do
19
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/token")
20
+ end
21
+
22
+ post '/token/?' do
23
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/token")
24
+ end
25
+
26
+ get '/token/:token/?' do
27
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/token/#{params[:token]}")
28
+ end
29
+
30
+ delete '/token/:token/?' do
31
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/token/#{params[:token]}")
32
+ end
33
+
34
+ get '/vm/?' do
35
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/vm")
36
+ end
37
+
38
+ post '/vm/?' do
39
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/vm")
40
+ end
41
+
42
+ post '/vm/:template/?' do
43
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/vm/#{params[:template]}")
44
+ end
45
+
46
+ get '/vm/:hostname/?' do
47
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/vm/#{params[:hostname]}")
48
+ end
49
+
50
+ delete '/vm/:hostname/?' do
51
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/vm/#{params[:hostname]}")
52
+ end
53
+
54
+ put '/vm/:hostname/?' do
55
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/vm/#{params[:hostname]}")
56
+ end
57
+
58
+ post '/vm/:hostname/snapshot/?' do
59
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/vm/#{params[:hostname]}/snapshot")
60
+ end
61
+
62
+ post '/vm/:hostname/snapshot/:snapshot/?' do
63
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/vm/#{params[:hostname]}/snapshot/#{params[:snapshot]}")
64
+ end
65
+
66
+ put '/vm/:hostname/disk/:size/?' do
67
+ call env.merge('PATH_INFO' => "/api/v#{api_version}/vm/#{params[:hostname]}/disk/#{params[:size]}")
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,938 @@
1
+ module Vmpooler
2
+ class API
3
+ class V1 < Sinatra::Base
4
+ api_version = '1'
5
+ api_prefix = "/api/v#{api_version}"
6
+
7
+ helpers do
8
+ include Vmpooler::API::Helpers
9
+ end
10
+
11
+ def backend
12
+ Vmpooler::API.settings.redis
13
+ end
14
+
15
+ def metrics
16
+ Vmpooler::API.settings.metrics
17
+ end
18
+
19
+ def config
20
+ Vmpooler::API.settings.config[:config]
21
+ end
22
+
23
+ def pools
24
+ Vmpooler::API.settings.config[:pools]
25
+ end
26
+
27
+ def pool_exists?(template)
28
+ Vmpooler::API.settings.config[:pool_names].include?(template)
29
+ end
30
+
31
+ def need_auth!
32
+ validate_auth(backend)
33
+ end
34
+
35
+ def need_token!
36
+ validate_token(backend)
37
+ end
38
+
39
+ def fetch_single_vm(template)
40
+ vm = backend.spop('vmpooler__ready__' + template)
41
+ return [vm, template] if vm
42
+
43
+ aliases = Vmpooler::API.settings.config[:alias]
44
+ if aliases && aliased_template = aliases[template]
45
+ vm = backend.spop('vmpooler__ready__' + aliased_template)
46
+ return [vm, aliased_template] if vm
47
+ end
48
+
49
+ [nil, nil]
50
+ end
51
+
52
+ def return_vm_to_ready_state(template, vm)
53
+ backend.sadd('vmpooler__ready__' + template, vm)
54
+ end
55
+
56
+ def account_for_starting_vm(template, vm)
57
+ backend.sadd('vmpooler__running__' + template, vm)
58
+ backend.sadd('vmpooler__migrating__' + template, vm)
59
+ backend.hset('vmpooler__active__' + template, vm, Time.now)
60
+ backend.hset('vmpooler__vm__' + vm, 'checkout', Time.now)
61
+
62
+ if Vmpooler::API.settings.config[:auth] and has_token?
63
+ validate_token(backend)
64
+
65
+ backend.hset('vmpooler__vm__' + vm, 'token:token', request.env['HTTP_X_AUTH_TOKEN'])
66
+ backend.hset('vmpooler__vm__' + vm, 'token:user',
67
+ backend.hget('vmpooler__token__' + request.env['HTTP_X_AUTH_TOKEN'], 'user')
68
+ )
69
+
70
+ if config['vm_lifetime_auth'].to_i > 0
71
+ backend.hset('vmpooler__vm__' + vm, 'lifetime', config['vm_lifetime_auth'].to_i)
72
+ end
73
+ end
74
+ end
75
+
76
+ def update_result_hosts(result, template, vm)
77
+ result[template] ||= {}
78
+ if result[template]['hostname']
79
+ result[template]['hostname'] = Array(result[template]['hostname'])
80
+ result[template]['hostname'].push(vm)
81
+ else
82
+ result[template]['hostname'] = vm
83
+ end
84
+ end
85
+
86
+ def atomically_allocate_vms(payload)
87
+ result = { 'ok' => false }
88
+ failed = false
89
+ vms = []
90
+
91
+ payload.each do |requested, count|
92
+ count.to_i.times do |_i|
93
+ vm, name = fetch_single_vm(requested)
94
+ if !vm
95
+ failed = true
96
+ metrics.increment('checkout.empty.' + requested)
97
+ break
98
+ else
99
+ vms << [ name, vm ]
100
+ metrics.increment('checkout.success.' + name)
101
+ end
102
+ end
103
+ end
104
+
105
+ if failed
106
+ vms.each do |(name, vm)|
107
+ return_vm_to_ready_state(name, vm)
108
+ end
109
+ status 503
110
+ else
111
+ vms.each do |(name, vm)|
112
+ account_for_starting_vm(name, vm)
113
+ update_result_hosts(result, name, vm)
114
+ end
115
+
116
+ result['ok'] = true
117
+ result['domain'] = config['domain'] if config['domain']
118
+ end
119
+
120
+ result
121
+ end
122
+
123
+ def update_pool_size(payload)
124
+ result = { 'ok' => false }
125
+
126
+ pool_index = pool_index(pools)
127
+ pools_updated = 0
128
+ sync_pool_sizes
129
+
130
+ payload.each do |poolname, size|
131
+ unless pools[pool_index[poolname]]['size'] == size.to_i
132
+ pools[pool_index[poolname]]['size'] = size.to_i
133
+ backend.hset('vmpooler__config__poolsize', poolname, size)
134
+ pools_updated += 1
135
+ status 201
136
+ end
137
+ end
138
+ status 200 unless pools_updated > 0
139
+ result['ok'] = true
140
+ result
141
+ end
142
+
143
+ def update_pool_template(payload)
144
+ result = { 'ok' => false }
145
+
146
+ pool_index = pool_index(pools)
147
+ pools_updated = 0
148
+ sync_pool_templates
149
+
150
+ payload.each do |poolname, template|
151
+ unless pools[pool_index[poolname]]['template'] == template
152
+ pools[pool_index[poolname]]['template'] = template
153
+ backend.hset('vmpooler__config__template', poolname, template)
154
+ pools_updated += 1
155
+ status 201
156
+ end
157
+ end
158
+ status 200 unless pools_updated > 0
159
+ result['ok'] = true
160
+ result
161
+ end
162
+
163
+ def sync_pool_templates
164
+ pool_index = pool_index(pools)
165
+ template_configs = backend.hgetall('vmpooler__config__template')
166
+ unless template_configs.nil?
167
+ template_configs.each do |poolname, template|
168
+ if pool_index.include? poolname
169
+ unless pools[pool_index[poolname]]['template'] == template
170
+ pools[pool_index[poolname]]['template'] = template
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ def sync_pool_sizes
178
+ pool_index = pool_index(pools)
179
+ poolsize_configs = backend.hgetall('vmpooler__config__poolsize')
180
+ unless poolsize_configs.nil?
181
+ poolsize_configs.each do |poolname, size|
182
+ if pool_index.include? poolname
183
+ unless pools[pool_index[poolname]]['size'] == size.to_i
184
+ pools[pool_index[poolname]]['size'] == size.to_i
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ # Provide run-time statistics
192
+ #
193
+ # Example:
194
+ #
195
+ # {
196
+ # "boot": {
197
+ # "duration": {
198
+ # "average": 163.6,
199
+ # "min": 65.49,
200
+ # "max": 830.07,
201
+ # "total": 247744.71000000002
202
+ # },
203
+ # "count": {
204
+ # "total": 1514
205
+ # }
206
+ # },
207
+ # "capacity": {
208
+ # "current": 968,
209
+ # "total": 975,
210
+ # "percent": 99.3
211
+ # },
212
+ # "clone": {
213
+ # "duration": {
214
+ # "average": 17.0,
215
+ # "min": 4.66,
216
+ # "max": 637.96,
217
+ # "total": 25634.15
218
+ # },
219
+ # "count": {
220
+ # "total": 1507
221
+ # }
222
+ # },
223
+ # "queue": {
224
+ # "pending": 12,
225
+ # "cloning": 0,
226
+ # "booting": 12,
227
+ # "ready": 968,
228
+ # "running": 367,
229
+ # "completed": 0,
230
+ # "total": 1347
231
+ # },
232
+ # "pools": {
233
+ # "ready": 100,
234
+ # "running": 120,
235
+ # "pending": 5,
236
+ # "max": 250,
237
+ # }
238
+ # "status": {
239
+ # "ok": true,
240
+ # "message": "Battle station fully armed and operational.",
241
+ # "empty": [ # NOTE: would not have 'ok: true' w/ "empty" pools
242
+ # "redhat-7-x86_64",
243
+ # "ubuntu-1404-i386"
244
+ # ],
245
+ # "uptime": 179585.9
246
+ # }
247
+ #
248
+ # If the query parameter 'view' is provided, it will be used to select which top level
249
+ # element to compute and return. Select them by specifying them in a comma separated list.
250
+ # For example /status?view=capacity,boot
251
+ # would return only the "capacity" and "boot" statistics. "status" is always returned
252
+
253
+ get "#{api_prefix}/status/?" do
254
+ content_type :json
255
+
256
+ if params[:view]
257
+ views = params[:view].split(",")
258
+ end
259
+
260
+ result = {
261
+ status: {
262
+ ok: true,
263
+ message: 'Battle station fully armed and operational.'
264
+ }
265
+ }
266
+
267
+ sync_pool_sizes
268
+
269
+ result[:capacity] = get_capacity_metrics(pools, backend) unless views and not views.include?("capacity")
270
+ result[:queue] = get_queue_metrics(pools, backend) unless views and not views.include?("queue")
271
+ result[:clone] = get_task_metrics(backend, 'clone', Date.today.to_s) unless views and not views.include?("clone")
272
+ result[:boot] = get_task_metrics(backend, 'boot', Date.today.to_s) unless views and not views.include?("boot")
273
+
274
+ # Check for empty pools
275
+ result[:pools] = {} unless views and not views.include?("pools")
276
+ pools.each do |pool|
277
+ # REMIND: move this out of the API and into the back-end
278
+ ready = backend.scard('vmpooler__ready__' + pool['name']).to_i
279
+ running = backend.scard('vmpooler__running__' + pool['name']).to_i
280
+ pending = backend.scard('vmpooler__pending__' + pool['name']).to_i
281
+ max = pool['size']
282
+ lastBoot = backend.hget('vmpooler__lastboot',pool['name']).to_s
283
+ aka = pool['alias']
284
+
285
+ result[:pools][pool['name']] = {
286
+ ready: ready,
287
+ running: running,
288
+ pending: pending,
289
+ max: max,
290
+ lastBoot: lastBoot
291
+ }
292
+
293
+ if aka
294
+ result[:pools][pool['name']][:alias] = aka
295
+ end
296
+
297
+ # for backwards compatibility, include separate "empty" stats in "status" block
298
+ if ready == 0
299
+ result[:status][:empty] ||= []
300
+ result[:status][:empty].push(pool['name'])
301
+
302
+ result[:status][:ok] = false
303
+ result[:status][:message] = "Found #{result[:status][:empty].length} empty pools."
304
+ end
305
+ end unless views and not views.include?("pools")
306
+
307
+ result[:status][:uptime] = (Time.now - Vmpooler::API.settings.config[:uptime]).round(1) if Vmpooler::API.settings.config[:uptime]
308
+
309
+ JSON.pretty_generate(Hash[result.sort_by { |k, _v| k }])
310
+ end
311
+
312
+ get "#{api_prefix}/summary/?" do
313
+ content_type :json
314
+
315
+ result = {
316
+ daily: []
317
+ }
318
+
319
+ from_param = params[:from] || Date.today.to_s
320
+ to_param = params[:to] || Date.today.to_s
321
+
322
+ # Validate date formats
323
+ [from_param, to_param].each do |param|
324
+ if !validate_date_str(param.to_s)
325
+ halt 400, "Invalid date format '#{param}', must match YYYY-MM-DD."
326
+ end
327
+ end
328
+
329
+ from_date, to_date = Date.parse(from_param), Date.parse(to_param)
330
+
331
+ if to_date < from_date
332
+ halt 400, 'Date range is invalid, \'to\' cannot come before \'from\'.'
333
+ elsif from_date > Date.today
334
+ halt 400, 'Date range is invalid, \'from\' must be in the past.'
335
+ end
336
+
337
+ boot = get_task_summary(backend, 'boot', from_date, to_date, :bypool => true)
338
+ clone = get_task_summary(backend, 'clone', from_date, to_date, :bypool => true)
339
+ tag = get_tag_summary(backend, from_date, to_date)
340
+
341
+ result[:boot] = boot[:boot]
342
+ result[:clone] = clone[:clone]
343
+ result[:tag] = tag[:tag]
344
+
345
+ daily = {}
346
+
347
+ boot[:daily].each do |day|
348
+ daily[day[:date]] ||= {}
349
+ daily[day[:date]][:boot] = day[:boot]
350
+ end
351
+
352
+ clone[:daily].each do |day|
353
+ daily[day[:date]] ||= {}
354
+ daily[day[:date]][:clone] = day[:clone]
355
+ end
356
+
357
+ tag[:daily].each do |day|
358
+ daily[day[:date]] ||= {}
359
+ daily[day[:date]][:tag] = day[:tag]
360
+ end
361
+
362
+ daily.each_key do |day|
363
+ result[:daily].push({
364
+ date: day,
365
+ boot: daily[day][:boot],
366
+ clone: daily[day][:clone],
367
+ tag: daily[day][:tag]
368
+ })
369
+ end
370
+
371
+ JSON.pretty_generate(result)
372
+ end
373
+
374
+ get "#{api_prefix}/summary/:route/?:key?/?" do
375
+ content_type :json
376
+
377
+ result = {}
378
+
379
+ from_param = params[:from] || Date.today.to_s
380
+ to_param = params[:to] || Date.today.to_s
381
+
382
+ # Validate date formats
383
+ [from_param, to_param].each do |param|
384
+ if !validate_date_str(param.to_s)
385
+ halt 400, "Invalid date format '#{param}', must match YYYY-MM-DD."
386
+ end
387
+ end
388
+
389
+ from_date, to_date = Date.parse(from_param), Date.parse(to_param)
390
+
391
+ if to_date < from_date
392
+ halt 400, 'Date range is invalid, \'to\' cannot come before \'from\'.'
393
+ elsif from_date > Date.today
394
+ halt 400, 'Date range is invalid, \'from\' must be in the past.'
395
+ end
396
+
397
+ case params[:route]
398
+ when 'boot'
399
+ result = get_task_summary(backend, 'boot', from_date, to_date, :bypool => true, :only => params[:key])
400
+ when 'clone'
401
+ result = get_task_summary(backend, 'clone', from_date, to_date, :bypool => true, :only => params[:key])
402
+ when 'tag'
403
+ result = get_tag_summary(backend, from_date, to_date, :only => params[:key])
404
+ else
405
+ halt 404, JSON.pretty_generate({ 'ok' => false })
406
+ end
407
+
408
+ JSON.pretty_generate(result)
409
+ end
410
+
411
+ get "#{api_prefix}/token/?" do
412
+ content_type :json
413
+
414
+ status 404
415
+ result = { 'ok' => false }
416
+
417
+ if Vmpooler::API.settings.config[:auth]
418
+ status 401
419
+
420
+ need_auth!
421
+
422
+ backend.keys('vmpooler__token__*').each do |key|
423
+ data = backend.hgetall(key)
424
+
425
+ if data['user'] == Rack::Auth::Basic::Request.new(request.env).username
426
+ token = key.split('__').last
427
+
428
+ result[token] ||= {}
429
+
430
+ result[token]['created'] = data['created']
431
+ result[token]['last'] = data['last'] || 'never'
432
+
433
+ result['ok'] = true
434
+ end
435
+ end
436
+
437
+ if result['ok']
438
+ status 200
439
+ else
440
+ status 404
441
+ end
442
+ end
443
+
444
+ JSON.pretty_generate(result)
445
+ end
446
+
447
+ get "#{api_prefix}/token/:token/?" do
448
+ content_type :json
449
+
450
+ status 404
451
+ result = { 'ok' => false }
452
+
453
+ if Vmpooler::API.settings.config[:auth]
454
+ token = backend.hgetall('vmpooler__token__' + params[:token])
455
+
456
+ if not token.nil? and not token.empty?
457
+ status 200
458
+
459
+ pools.each do |pool|
460
+ backend.smembers('vmpooler__running__' + pool['name']).each do |vm|
461
+ if backend.hget('vmpooler__vm__' + vm, 'token:token') == params[:token]
462
+ token['vms'] ||= {}
463
+ token['vms']['running'] ||= []
464
+ token['vms']['running'].push(vm)
465
+ end
466
+ end
467
+ end
468
+
469
+ result = { 'ok' => true, params[:token] => token }
470
+ end
471
+ end
472
+
473
+ JSON.pretty_generate(result)
474
+ end
475
+
476
+ delete "#{api_prefix}/token/:token/?" do
477
+ content_type :json
478
+
479
+ status 404
480
+ result = { 'ok' => false }
481
+
482
+ if Vmpooler::API.settings.config[:auth]
483
+ status 401
484
+
485
+ need_auth!
486
+
487
+ if backend.del('vmpooler__token__' + params[:token]).to_i > 0
488
+ status 200
489
+ result['ok'] = true
490
+ end
491
+ end
492
+
493
+ JSON.pretty_generate(result)
494
+ end
495
+
496
+ post "#{api_prefix}/token" do
497
+ content_type :json
498
+
499
+ status 404
500
+ result = { 'ok' => false }
501
+
502
+ if Vmpooler::API.settings.config[:auth]
503
+ status 401
504
+
505
+ need_auth!
506
+
507
+ o = [('a'..'z'), ('0'..'9')].map(&:to_a).flatten
508
+ result['token'] = o[rand(25)] + (0...31).map { o[rand(o.length)] }.join
509
+
510
+ backend.hset('vmpooler__token__' + result['token'], 'user', @auth.username)
511
+ backend.hset('vmpooler__token__' + result['token'], 'created', Time.now)
512
+
513
+ status 200
514
+ result['ok'] = true
515
+ end
516
+
517
+ JSON.pretty_generate(result)
518
+ end
519
+
520
+ get "#{api_prefix}/vm/?" do
521
+ content_type :json
522
+
523
+ result = []
524
+
525
+ pools.each do |pool|
526
+ result.push(pool['name'])
527
+ end
528
+
529
+ JSON.pretty_generate(result)
530
+ end
531
+
532
+ post "#{api_prefix}/vm/?" do
533
+ content_type :json
534
+ result = { 'ok' => false }
535
+
536
+ payload = JSON.parse(request.body.read)
537
+
538
+ if payload
539
+ invalid = invalid_templates(payload)
540
+ if invalid.empty?
541
+ result = atomically_allocate_vms(payload)
542
+ else
543
+ invalid.each do |bad_template|
544
+ metrics.increment('checkout.invalid.' + bad_template)
545
+ end
546
+ status 404
547
+ end
548
+ else
549
+ metrics.increment('checkout.invalid.unknown')
550
+ status 404
551
+ end
552
+
553
+ JSON.pretty_generate(result)
554
+ end
555
+
556
+ def extract_templates_from_query_params(params)
557
+ payload = {}
558
+
559
+ params.split('+').each do |template|
560
+ payload[template] ||= 0
561
+ payload[template] += 1
562
+ end
563
+
564
+ payload
565
+ end
566
+
567
+ def invalid_templates(payload)
568
+ invalid = []
569
+ payload.keys.each do |template|
570
+ invalid << template unless pool_exists?(template)
571
+ end
572
+ invalid
573
+ end
574
+
575
+ def invalid_template_or_size(payload)
576
+ invalid = []
577
+ payload.each do |pool, size|
578
+ invalid << pool unless pool_exists?(pool)
579
+ unless is_integer?(size)
580
+ invalid << pool
581
+ next
582
+ end
583
+ invalid << pool unless Integer(size) >= 0
584
+ end
585
+ invalid
586
+ end
587
+
588
+ def invalid_template_or_path(payload)
589
+ invalid = []
590
+ payload.each do |pool, template|
591
+ invalid << pool unless pool_exists?(pool)
592
+ invalid << pool unless template.include? '/'
593
+ invalid << pool if template[0] == '/'
594
+ invalid << pool if template[-1] == '/'
595
+ end
596
+ invalid
597
+ end
598
+
599
+ post "#{api_prefix}/vm/:template/?" do
600
+ content_type :json
601
+ result = { 'ok' => false }
602
+
603
+ payload = extract_templates_from_query_params(params[:template])
604
+
605
+ if payload
606
+ invalid = invalid_templates(payload)
607
+ if invalid.empty?
608
+ result = atomically_allocate_vms(payload)
609
+ else
610
+ invalid.each do |bad_template|
611
+ metrics.increment('checkout.invalid.' + bad_template)
612
+ end
613
+ status 404
614
+ end
615
+ else
616
+ metrics.increment('checkout.invalid.unknown')
617
+ status 404
618
+ end
619
+
620
+ JSON.pretty_generate(result)
621
+ end
622
+
623
+ get "#{api_prefix}/vm/:hostname/?" do
624
+ content_type :json
625
+
626
+ result = {}
627
+
628
+ status 404
629
+ result['ok'] = false
630
+
631
+ params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
632
+
633
+ rdata = backend.hgetall('vmpooler__vm__' + params[:hostname])
634
+ unless rdata.empty?
635
+ status 200
636
+ result['ok'] = true
637
+
638
+ result[params[:hostname]] = {}
639
+
640
+ result[params[:hostname]]['template'] = rdata['template']
641
+ result[params[:hostname]]['lifetime'] = (rdata['lifetime'] || config['vm_lifetime']).to_i
642
+
643
+ if rdata['destroy']
644
+ result[params[:hostname]]['running'] = ((Time.parse(rdata['destroy']) - Time.parse(rdata['checkout'])) / 60 / 60).round(2)
645
+ result[params[:hostname]]['state'] = 'destroyed'
646
+ elsif rdata['checkout']
647
+ result[params[:hostname]]['running'] = ((Time.now - Time.parse(rdata['checkout'])) / 60 / 60).round(2)
648
+ result[params[:hostname]]['remaining'] = ((Time.parse(rdata['checkout']) + rdata['lifetime'].to_i*60*60 - Time.now) / 60 / 60).round(2)
649
+ result[params[:hostname]]['start_time'] = Time.parse(rdata['checkout']).to_datetime.rfc3339
650
+ result[params[:hostname]]['end_time'] = (Time.parse(rdata['checkout']) + rdata['lifetime'].to_i*60*60).to_datetime.rfc3339
651
+ result[params[:hostname]]['state'] = 'running'
652
+ elsif rdata['check']
653
+ result[params[:hostname]]['state'] = 'ready'
654
+ else
655
+ result[params[:hostname]]['state'] = 'pending'
656
+ end
657
+
658
+ rdata.keys.each do |key|
659
+ if key.match('^tag\:(.+?)$')
660
+ result[params[:hostname]]['tags'] ||= {}
661
+ result[params[:hostname]]['tags'][$1] = rdata[key]
662
+ end
663
+
664
+ if key.match('^snapshot\:(.+?)$')
665
+ result[params[:hostname]]['snapshots'] ||= []
666
+ result[params[:hostname]]['snapshots'].push($1)
667
+ end
668
+ end
669
+
670
+ if rdata['disk']
671
+ result[params[:hostname]]['disk'] = rdata['disk'].split(':')
672
+ end
673
+
674
+ # Look up IP address of the hostname
675
+ begin
676
+ ipAddress = TCPSocket.gethostbyname(params[:hostname])[3]
677
+ rescue
678
+ ipAddress = ""
679
+ end
680
+
681
+ result[params[:hostname]]['ip'] = ipAddress
682
+
683
+ if config['domain']
684
+ result[params[:hostname]]['domain'] = config['domain']
685
+ end
686
+ end
687
+
688
+ JSON.pretty_generate(result)
689
+ end
690
+
691
+ delete "#{api_prefix}/vm/:hostname/?" do
692
+ content_type :json
693
+
694
+ result = {}
695
+
696
+ status 404
697
+ result['ok'] = false
698
+
699
+ params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
700
+
701
+ rdata = backend.hgetall('vmpooler__vm__' + params[:hostname])
702
+ unless rdata.empty?
703
+ need_token! if rdata['token:token']
704
+
705
+ if backend.srem('vmpooler__running__' + rdata['template'], params[:hostname])
706
+ backend.sadd('vmpooler__completed__' + rdata['template'], params[:hostname])
707
+
708
+ status 200
709
+ result['ok'] = true
710
+ end
711
+ end
712
+
713
+ JSON.pretty_generate(result)
714
+ end
715
+
716
+ put "#{api_prefix}/vm/:hostname/?" do
717
+ content_type :json
718
+
719
+ status 404
720
+ result = { 'ok' => false }
721
+
722
+ failure = false
723
+
724
+ params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
725
+
726
+ if backend.exists('vmpooler__vm__' + params[:hostname])
727
+ begin
728
+ jdata = JSON.parse(request.body.read)
729
+ rescue
730
+ halt 400, JSON.pretty_generate(result)
731
+ end
732
+
733
+ # Validate data payload
734
+ jdata.each do |param, arg|
735
+ case param
736
+ when 'lifetime'
737
+ need_token! if Vmpooler::API.settings.config[:auth]
738
+
739
+ unless arg.to_i > 0
740
+ failure = true
741
+ end
742
+ when 'tags'
743
+ unless arg.is_a?(Hash)
744
+ failure = true
745
+ end
746
+
747
+ if config['allowed_tags']
748
+ failure = true if not (arg.keys - config['allowed_tags']).empty?
749
+ end
750
+ else
751
+ failure = true
752
+ end
753
+ end
754
+
755
+ if failure
756
+ status 400
757
+ else
758
+ jdata.each do |param, arg|
759
+ case param
760
+ when 'lifetime'
761
+ need_token! if Vmpooler::API.settings.config[:auth]
762
+
763
+ arg = arg.to_i
764
+
765
+ backend.hset('vmpooler__vm__' + params[:hostname], param, arg)
766
+ when 'tags'
767
+ filter_tags(arg)
768
+ export_tags(backend, params[:hostname], arg)
769
+ end
770
+ end
771
+
772
+ status 200
773
+ result['ok'] = true
774
+ end
775
+ end
776
+
777
+ JSON.pretty_generate(result)
778
+ end
779
+
780
+ post "#{api_prefix}/vm/:hostname/disk/:size/?" do
781
+ content_type :json
782
+
783
+ need_token! if Vmpooler::API.settings.config[:auth]
784
+
785
+ status 404
786
+ result = { 'ok' => false }
787
+
788
+ params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
789
+
790
+ if ((params[:size].to_i > 0 )and (backend.exists('vmpooler__vm__' + params[:hostname])))
791
+ result[params[:hostname]] = {}
792
+ result[params[:hostname]]['disk'] = "+#{params[:size]}gb"
793
+
794
+ backend.sadd('vmpooler__tasks__disk', params[:hostname] + ':' + params[:size])
795
+
796
+ status 202
797
+ result['ok'] = true
798
+ end
799
+
800
+ JSON.pretty_generate(result)
801
+ end
802
+
803
+ post "#{api_prefix}/vm/:hostname/snapshot/?" do
804
+ content_type :json
805
+
806
+ need_token! if Vmpooler::API.settings.config[:auth]
807
+
808
+ status 404
809
+ result = { 'ok' => false }
810
+
811
+ params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
812
+
813
+ if backend.exists('vmpooler__vm__' + params[:hostname])
814
+ result[params[:hostname]] = {}
815
+
816
+ o = [('a'..'z'), ('0'..'9')].map(&:to_a).flatten
817
+ result[params[:hostname]]['snapshot'] = o[rand(25)] + (0...31).map { o[rand(o.length)] }.join
818
+
819
+ backend.sadd('vmpooler__tasks__snapshot', params[:hostname] + ':' + result[params[:hostname]]['snapshot'])
820
+
821
+ status 202
822
+ result['ok'] = true
823
+ end
824
+
825
+ JSON.pretty_generate(result)
826
+ end
827
+
828
+ post "#{api_prefix}/vm/:hostname/snapshot/:snapshot/?" do
829
+ content_type :json
830
+
831
+ need_token! if Vmpooler::API.settings.config[:auth]
832
+
833
+ status 404
834
+ result = { 'ok' => false }
835
+
836
+ params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
837
+
838
+ unless backend.hget('vmpooler__vm__' + params[:hostname], 'snapshot:' + params[:snapshot]).to_i.zero?
839
+ backend.sadd('vmpooler__tasks__snapshot-revert', params[:hostname] + ':' + params[:snapshot])
840
+
841
+ status 202
842
+ result['ok'] = true
843
+ end
844
+
845
+ JSON.pretty_generate(result)
846
+ end
847
+
848
+ post "#{api_prefix}/config/poolsize/?" do
849
+ content_type :json
850
+ result = { 'ok' => false }
851
+
852
+ if config['experimental_features']
853
+ need_token! if Vmpooler::API.settings.config[:auth]
854
+
855
+ payload = JSON.parse(request.body.read)
856
+
857
+ if payload
858
+ invalid = invalid_template_or_size(payload)
859
+ if invalid.empty?
860
+ result = update_pool_size(payload)
861
+ else
862
+ invalid.each do |bad_template|
863
+ metrics.increment("config.invalid.#{bad_template}")
864
+ end
865
+ result[:bad_templates] = invalid
866
+ status 400
867
+ end
868
+ else
869
+ metrics.increment('config.invalid.unknown')
870
+ status 404
871
+ end
872
+ else
873
+ status 405
874
+ end
875
+
876
+ JSON.pretty_generate(result)
877
+ end
878
+
879
+ post "#{api_prefix}/config/pooltemplate/?" do
880
+ content_type :json
881
+ result = { 'ok' => false }
882
+
883
+ if config['experimental_features']
884
+ need_token! if Vmpooler::API.settings.config[:auth]
885
+
886
+ payload = JSON.parse(request.body.read)
887
+
888
+ if payload
889
+ invalid = invalid_template_or_path(payload)
890
+ if invalid.empty?
891
+ result = update_pool_template(payload)
892
+ else
893
+ invalid.each do |bad_template|
894
+ metrics.increment("config.invalid.#{bad_template}")
895
+ end
896
+ result[:bad_templates] = invalid
897
+ status 400
898
+ end
899
+ else
900
+ metrics.increment('config.invalid.unknown')
901
+ status 404
902
+ end
903
+ else
904
+ status 405
905
+ end
906
+
907
+ JSON.pretty_generate(result)
908
+ end
909
+
910
+ get "#{api_prefix}/config/?" do
911
+ content_type :json
912
+ result = { 'ok' => false }
913
+ status 404
914
+
915
+ if pools
916
+ sync_pool_sizes
917
+ sync_pool_templates
918
+
919
+ pool_configuration = []
920
+ pools.each do |pool|
921
+ pool['template_ready'] = template_ready?(pool, backend)
922
+ pool_configuration << pool
923
+ end
924
+
925
+ result = {
926
+ pool_configuration: pool_configuration,
927
+ status: {
928
+ ok: true
929
+ }
930
+ }
931
+
932
+ status 200
933
+ end
934
+ JSON.pretty_generate(result)
935
+ end
936
+ end
937
+ end
938
+ end