vmpooler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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