mirror of
https://github.com/puppetlabs/vmpooler.git
synced 2026-01-26 10:08:40 -05:00
Before this change if the smove returned false, we would continue handing out the VM which presumably could still be in the 'ready' state. Upon 'delete' that ready VM would not be picked up and return a 404 which is consistent with the behavior seen. Adding a metric to keep track of the smove failures since this is not expected. I think some API logging would be good to add in the future.
1192 lines
33 KiB
Ruby
1192 lines
33 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Vmpooler
|
|
class API
|
|
class V1 < Sinatra::Base
|
|
api_version = '1'
|
|
api_prefix = "/api/v#{api_version}"
|
|
|
|
helpers do
|
|
include Vmpooler::API::Helpers
|
|
end
|
|
|
|
def backend
|
|
Vmpooler::API.settings.redis
|
|
end
|
|
|
|
def metrics
|
|
Vmpooler::API.settings.metrics
|
|
end
|
|
|
|
def config
|
|
Vmpooler::API.settings.config[:config]
|
|
end
|
|
|
|
def pools
|
|
Vmpooler::API.settings.config[:pools]
|
|
end
|
|
|
|
def pool_exists?(template)
|
|
Vmpooler::API.settings.config[:pool_names].include?(template)
|
|
end
|
|
|
|
def need_auth!
|
|
validate_auth(backend)
|
|
end
|
|
|
|
def need_token!
|
|
validate_token(backend)
|
|
end
|
|
|
|
def checkoutlock
|
|
Vmpooler::API.settings.checkoutlock
|
|
end
|
|
|
|
def fetch_single_vm(template)
|
|
template_backends = [template]
|
|
aliases = Vmpooler::API.settings.config[:alias]
|
|
if aliases
|
|
template_backends += aliases[template] if aliases[template].is_a?(Array)
|
|
template_backends << aliases[template] if aliases[template].is_a?(String)
|
|
pool_index = pool_index(pools)
|
|
weighted_pools = {}
|
|
template_backends.each do |t|
|
|
next unless pool_index.key? t
|
|
|
|
index = pool_index[t]
|
|
clone_target = pools[index]['clone_target'] || config['clone_target']
|
|
next unless config.key?('backend_weight')
|
|
|
|
weight = config['backend_weight'][clone_target]
|
|
if weight
|
|
weighted_pools[t] = weight
|
|
end
|
|
end
|
|
|
|
if weighted_pools.count == template_backends.count
|
|
pickup = Pickup.new(weighted_pools)
|
|
selection = pickup.pick
|
|
template_backends.delete(selection)
|
|
template_backends.unshift(selection)
|
|
else
|
|
first = template_backends.sample
|
|
template_backends.delete(first)
|
|
template_backends.unshift(first)
|
|
end
|
|
end
|
|
|
|
checkoutlock.synchronize do
|
|
template_backends.each do |template_backend|
|
|
vms = backend.smembers("vmpooler__ready__#{template_backend}")
|
|
next if vms.empty?
|
|
|
|
vms.reverse.each do |vm|
|
|
ready = vm_ready?(vm, config['domain'])
|
|
if ready
|
|
smoved = backend.smove("vmpooler__ready__#{template_backend}", "vmpooler__running__#{template_backend}", vm)
|
|
if smoved
|
|
return [vm, template_backend, template]
|
|
else
|
|
metrics.increment("checkout.smove.failed.#{template_backend}")
|
|
return [nil, nil, nil]
|
|
end
|
|
else
|
|
backend.smove("vmpooler__ready__#{template_backend}", "vmpooler__completed__#{template_backend}", vm)
|
|
metrics.increment("checkout.nonresponsive.#{template_backend}")
|
|
end
|
|
end
|
|
end
|
|
[nil, nil, nil]
|
|
end
|
|
end
|
|
|
|
def return_vm_to_ready_state(template, vm)
|
|
backend.smove("vmpooler__running__#{template}", "vmpooler__ready__#{template}", vm)
|
|
end
|
|
|
|
def account_for_starting_vm(template, vm)
|
|
backend.sadd("vmpooler__migrating__#{template}", vm)
|
|
backend.hset("vmpooler__active__#{template}", vm, Time.now)
|
|
backend.hset("vmpooler__vm__#{vm}", 'checkout', Time.now)
|
|
|
|
if Vmpooler::API.settings.config[:auth] and has_token?
|
|
validate_token(backend)
|
|
|
|
backend.hset('vmpooler__vm__' + vm, 'token:token', request.env['HTTP_X_AUTH_TOKEN'])
|
|
backend.hset('vmpooler__vm__' + vm, 'token:user',
|
|
backend.hget('vmpooler__token__' + request.env['HTTP_X_AUTH_TOKEN'], 'user')
|
|
)
|
|
|
|
if config['vm_lifetime_auth'].to_i > 0
|
|
backend.hset('vmpooler__vm__' + vm, 'lifetime', config['vm_lifetime_auth'].to_i)
|
|
end
|
|
end
|
|
end
|
|
|
|
def update_result_hosts(result, template, vm)
|
|
result[template] ||= {}
|
|
if result[template]['hostname']
|
|
result[template]['hostname'] = Array(result[template]['hostname'])
|
|
result[template]['hostname'].push(vm)
|
|
else
|
|
result[template]['hostname'] = vm
|
|
end
|
|
end
|
|
|
|
def atomically_allocate_vms(payload)
|
|
result = { 'ok' => false }
|
|
failed = false
|
|
vms = []
|
|
|
|
payload.each do |requested, count|
|
|
count.to_i.times do |_i|
|
|
vmname, vmpool, vmtemplate = fetch_single_vm(requested)
|
|
if !vmname
|
|
failed = true
|
|
metrics.increment('checkout.empty.' + requested)
|
|
break
|
|
else
|
|
vms << [vmpool, vmname, vmtemplate]
|
|
metrics.increment('checkout.success.' + vmtemplate)
|
|
end
|
|
end
|
|
end
|
|
|
|
if failed
|
|
vms.each do |(vmpool, vmname, _vmtemplate)|
|
|
return_vm_to_ready_state(vmpool, vmname)
|
|
end
|
|
status 503
|
|
else
|
|
vms.each do |(vmpool, vmname, vmtemplate)|
|
|
account_for_starting_vm(vmpool, vmname)
|
|
update_result_hosts(result, vmtemplate, vmname)
|
|
end
|
|
|
|
result['ok'] = true
|
|
result['domain'] = config['domain'] if config['domain']
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def update_pool_size(payload)
|
|
result = { 'ok' => false }
|
|
|
|
pool_index = pool_index(pools)
|
|
pools_updated = 0
|
|
sync_pool_sizes
|
|
|
|
payload.each do |poolname, size|
|
|
unless pools[pool_index[poolname]]['size'] == size.to_i
|
|
pools[pool_index[poolname]]['size'] = size.to_i
|
|
backend.hset('vmpooler__config__poolsize', poolname, size)
|
|
pools_updated += 1
|
|
status 201
|
|
end
|
|
end
|
|
status 200 unless pools_updated > 0
|
|
result['ok'] = true
|
|
result
|
|
end
|
|
|
|
def update_pool_template(payload)
|
|
result = { 'ok' => false }
|
|
|
|
pool_index = pool_index(pools)
|
|
pools_updated = 0
|
|
sync_pool_templates
|
|
|
|
payload.each do |poolname, template|
|
|
unless pools[pool_index[poolname]]['template'] == template
|
|
pools[pool_index[poolname]]['template'] = template
|
|
backend.hset('vmpooler__config__template', poolname, template)
|
|
pools_updated += 1
|
|
status 201
|
|
end
|
|
end
|
|
status 200 unless pools_updated > 0
|
|
result['ok'] = true
|
|
result
|
|
end
|
|
|
|
def reset_pool(payload)
|
|
result = { 'ok' => false }
|
|
|
|
payload.each do |poolname, _count|
|
|
backend.sadd('vmpooler__poolreset', poolname)
|
|
end
|
|
status 201
|
|
result['ok'] = true
|
|
result
|
|
end
|
|
|
|
def update_clone_target(payload)
|
|
result = { 'ok' => false }
|
|
|
|
pool_index = pool_index(pools)
|
|
pools_updated = 0
|
|
sync_clone_targets
|
|
|
|
payload.each do |poolname, clone_target|
|
|
unless pools[pool_index[poolname]]['clone_target'] == clone_target
|
|
pools[pool_index[poolname]]['clone_target'] = clone_target
|
|
backend.hset('vmpooler__config__clone_target', poolname, clone_target)
|
|
pools_updated += 1
|
|
status 201
|
|
end
|
|
end
|
|
status 200 unless pools_updated > 0
|
|
result['ok'] = true
|
|
result
|
|
end
|
|
|
|
def sync_pool_templates
|
|
pool_index = pool_index(pools)
|
|
template_configs = backend.hgetall('vmpooler__config__template')
|
|
template_configs&.each do |poolname, template|
|
|
if pool_index.include? poolname
|
|
unless pools[pool_index[poolname]]['template'] == template
|
|
pools[pool_index[poolname]]['template'] = template
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def sync_pool_sizes
|
|
pool_index = pool_index(pools)
|
|
poolsize_configs = backend.hgetall('vmpooler__config__poolsize')
|
|
poolsize_configs&.each do |poolname, size|
|
|
if pool_index.include? poolname
|
|
unless pools[pool_index[poolname]]['size'] == size.to_i
|
|
pools[pool_index[poolname]]['size'] == size.to_i
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def sync_clone_targets
|
|
pool_index = pool_index(pools)
|
|
clone_target_configs = backend.hgetall('vmpooler__config__clone_target')
|
|
clone_target_configs&.each do |poolname, clone_target|
|
|
if pool_index.include? poolname
|
|
unless pools[pool_index[poolname]]['clone_target'] == clone_target
|
|
pools[pool_index[poolname]]['clone_target'] == clone_target
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
get '/' do
|
|
sync_pool_sizes
|
|
redirect to('/dashboard/')
|
|
end
|
|
|
|
# Provide run-time statistics
|
|
#
|
|
# Example:
|
|
#
|
|
# {
|
|
# "boot": {
|
|
# "duration": {
|
|
# "average": 163.6,
|
|
# "min": 65.49,
|
|
# "max": 830.07,
|
|
# "total": 247744.71000000002
|
|
# },
|
|
# "count": {
|
|
# "total": 1514
|
|
# }
|
|
# },
|
|
# "capacity": {
|
|
# "current": 968,
|
|
# "total": 975,
|
|
# "percent": 99.3
|
|
# },
|
|
# "clone": {
|
|
# "duration": {
|
|
# "average": 17.0,
|
|
# "min": 4.66,
|
|
# "max": 637.96,
|
|
# "total": 25634.15
|
|
# },
|
|
# "count": {
|
|
# "total": 1507
|
|
# }
|
|
# },
|
|
# "queue": {
|
|
# "pending": 12,
|
|
# "cloning": 0,
|
|
# "booting": 12,
|
|
# "ready": 968,
|
|
# "running": 367,
|
|
# "completed": 0,
|
|
# "total": 1347
|
|
# },
|
|
# "pools": {
|
|
# "ready": 100,
|
|
# "running": 120,
|
|
# "pending": 5,
|
|
# "max": 250,
|
|
# }
|
|
# "status": {
|
|
# "ok": true,
|
|
# "message": "Battle station fully armed and operational.",
|
|
# "empty": [ # NOTE: would not have 'ok: true' w/ "empty" pools
|
|
# "redhat-7-x86_64",
|
|
# "ubuntu-1404-i386"
|
|
# ],
|
|
# "uptime": 179585.9
|
|
# }
|
|
#
|
|
# If the query parameter 'view' is provided, it will be used to select which top level
|
|
# element to compute and return. Select them by specifying them in a comma separated list.
|
|
# For example /status?view=capacity,boot
|
|
# would return only the "capacity" and "boot" statistics. "status" is always returned
|
|
|
|
get "#{api_prefix}/status/?" do
|
|
content_type :json
|
|
|
|
if params[:view]
|
|
views = params[:view].split(",")
|
|
end
|
|
|
|
result = {
|
|
status: {
|
|
ok: true,
|
|
message: 'Battle station fully armed and operational.'
|
|
}
|
|
}
|
|
|
|
sync_pool_sizes
|
|
|
|
result[:capacity] = get_capacity_metrics(pools, backend) unless views and not views.include?("capacity")
|
|
result[:queue] = get_queue_metrics(pools, backend) unless views and not views.include?("queue")
|
|
result[:clone] = get_task_metrics(backend, 'clone', Date.today.to_s) unless views and not views.include?("clone")
|
|
result[:boot] = get_task_metrics(backend, 'boot', Date.today.to_s) unless views and not views.include?("boot")
|
|
|
|
# Check for empty pools
|
|
result[:pools] = {} unless views and not views.include?("pools")
|
|
ready_hash = get_list_across_pools_redis_scard(pools, 'vmpooler__ready__', backend)
|
|
running_hash = get_list_across_pools_redis_scard(pools, 'vmpooler__running__', backend)
|
|
pending_hash = get_list_across_pools_redis_scard(pools, 'vmpooler__pending__', backend)
|
|
lastBoot_hash = get_list_across_pools_redis_hget(pools, 'vmpooler__lastboot', backend)
|
|
|
|
unless views and not views.include?("pools")
|
|
pools.each do |pool|
|
|
# REMIND: move this out of the API and into the back-end
|
|
ready = ready_hash[pool['name']]
|
|
running = running_hash[pool['name']]
|
|
pending = pending_hash[pool['name']]
|
|
max = pool['size']
|
|
lastBoot = lastBoot_hash[pool['name']]
|
|
aka = pool['alias']
|
|
|
|
result[:pools][pool['name']] = {
|
|
ready: ready,
|
|
running: running,
|
|
pending: pending,
|
|
max: max,
|
|
lastBoot: lastBoot
|
|
}
|
|
|
|
if aka
|
|
result[:pools][pool['name']][:alias] = aka
|
|
end
|
|
|
|
# for backwards compatibility, include separate "empty" stats in "status" block
|
|
if ready == 0
|
|
result[:status][:empty] ||= []
|
|
result[:status][:empty].push(pool['name'])
|
|
|
|
result[:status][:ok] = false
|
|
result[:status][:message] = "Found #{result[:status][:empty].length} empty pools."
|
|
end
|
|
end
|
|
end
|
|
|
|
result[:status][:uptime] = (Time.now - Vmpooler::API.settings.config[:uptime]).round(1) if Vmpooler::API.settings.config[:uptime]
|
|
|
|
JSON.pretty_generate(Hash[result.sort_by { |k, _v| k }])
|
|
end
|
|
|
|
# request statistics for specific pools by passing parameter 'pool'
|
|
# with a coma separated list of pools we want to query ?pool=ABC,DEF
|
|
# returns the ready, max numbers and the aliases (if set)
|
|
get "#{api_prefix}/poolstat/?" do
|
|
content_type :json
|
|
|
|
result = {}
|
|
|
|
poolscopy = []
|
|
|
|
if params[:pool]
|
|
subpool = params[:pool].split(",")
|
|
poolscopy = pools.select do |p|
|
|
if subpool.include?(p['name'])
|
|
true
|
|
elsif !p['alias'].nil?
|
|
if p['alias'].is_a?(Array)
|
|
(p['alias'] & subpool).any?
|
|
elsif p['alias'].is_a?(String)
|
|
subpool.include?(p['alias'])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
result[:pools] = {}
|
|
|
|
poolscopy.each do |pool|
|
|
result[:pools][pool['name']] = {}
|
|
|
|
max = pool['size']
|
|
aka = pool['alias']
|
|
|
|
result[:pools][pool['name']][:max] = max
|
|
|
|
if aka
|
|
result[:pools][pool['name']][:alias] = aka
|
|
end
|
|
end
|
|
|
|
ready_hash = get_list_across_pools_redis_scard(poolscopy, 'vmpooler__ready__', backend)
|
|
|
|
ready_hash.each { |k, v| result[:pools][k][:ready] = v }
|
|
|
|
JSON.pretty_generate(Hash[result.sort_by { |k, _v| k }])
|
|
end
|
|
|
|
# requests the total number of running VMs
|
|
get "#{api_prefix}/totalrunning/?" do
|
|
content_type :json
|
|
queue = {
|
|
running: 0
|
|
}
|
|
|
|
queue[:running] = get_total_across_pools_redis_scard(pools, 'vmpooler__running__', backend)
|
|
|
|
JSON.pretty_generate(queue)
|
|
end
|
|
|
|
get "#{api_prefix}/summary/?" do
|
|
content_type :json
|
|
|
|
result = {
|
|
daily: []
|
|
}
|
|
|
|
from_param = params[:from] || Date.today.to_s
|
|
to_param = params[:to] || Date.today.to_s
|
|
|
|
# Validate date formats
|
|
[from_param, to_param].each do |param|
|
|
if !validate_date_str(param.to_s)
|
|
halt 400, "Invalid date format '#{param}', must match YYYY-MM-DD."
|
|
end
|
|
end
|
|
|
|
from_date, to_date = Date.parse(from_param), Date.parse(to_param)
|
|
|
|
if to_date < from_date
|
|
halt 400, 'Date range is invalid, \'to\' cannot come before \'from\'.'
|
|
elsif from_date > Date.today
|
|
halt 400, 'Date range is invalid, \'from\' must be in the past.'
|
|
end
|
|
|
|
boot = get_task_summary(backend, 'boot', from_date, to_date, :bypool => true)
|
|
clone = get_task_summary(backend, 'clone', from_date, to_date, :bypool => true)
|
|
tag = get_tag_summary(backend, from_date, to_date)
|
|
|
|
result[:boot] = boot[:boot]
|
|
result[:clone] = clone[:clone]
|
|
result[:tag] = tag[:tag]
|
|
|
|
daily = {}
|
|
|
|
boot[:daily].each do |day|
|
|
daily[day[:date]] ||= {}
|
|
daily[day[:date]][:boot] = day[:boot]
|
|
end
|
|
|
|
clone[:daily].each do |day|
|
|
daily[day[:date]] ||= {}
|
|
daily[day[:date]][:clone] = day[:clone]
|
|
end
|
|
|
|
tag[:daily].each do |day|
|
|
daily[day[:date]] ||= {}
|
|
daily[day[:date]][:tag] = day[:tag]
|
|
end
|
|
|
|
daily.each_key do |day|
|
|
result[:daily].push({
|
|
date: day,
|
|
boot: daily[day][:boot],
|
|
clone: daily[day][:clone],
|
|
tag: daily[day][:tag]
|
|
})
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
get "#{api_prefix}/summary/:route/?:key?/?" do
|
|
content_type :json
|
|
|
|
result = {}
|
|
|
|
from_param = params[:from] || Date.today.to_s
|
|
to_param = params[:to] || Date.today.to_s
|
|
|
|
# Validate date formats
|
|
[from_param, to_param].each do |param|
|
|
if !validate_date_str(param.to_s)
|
|
halt 400, "Invalid date format '#{param}', must match YYYY-MM-DD."
|
|
end
|
|
end
|
|
|
|
from_date, to_date = Date.parse(from_param), Date.parse(to_param)
|
|
|
|
if to_date < from_date
|
|
halt 400, 'Date range is invalid, \'to\' cannot come before \'from\'.'
|
|
elsif from_date > Date.today
|
|
halt 400, 'Date range is invalid, \'from\' must be in the past.'
|
|
end
|
|
|
|
case params[:route]
|
|
when 'boot'
|
|
result = get_task_summary(backend, 'boot', from_date, to_date, :bypool => true, :only => params[:key])
|
|
when 'clone'
|
|
result = get_task_summary(backend, 'clone', from_date, to_date, :bypool => true, :only => params[:key])
|
|
when 'tag'
|
|
result = get_tag_summary(backend, from_date, to_date, :only => params[:key])
|
|
else
|
|
halt 404, JSON.pretty_generate({ 'ok' => false })
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
get "#{api_prefix}/token/?" do
|
|
content_type :json
|
|
|
|
status 404
|
|
result = { 'ok' => false }
|
|
|
|
if Vmpooler::API.settings.config[:auth]
|
|
status 401
|
|
|
|
need_auth!
|
|
|
|
backend.keys('vmpooler__token__*').each do |key|
|
|
data = backend.hgetall(key)
|
|
|
|
if data['user'] == Rack::Auth::Basic::Request.new(request.env).username
|
|
token = key.split('__').last
|
|
|
|
result[token] ||= {}
|
|
|
|
result[token]['created'] = data['created']
|
|
result[token]['last'] = data['last'] || 'never'
|
|
|
|
result['ok'] = true
|
|
end
|
|
end
|
|
|
|
if result['ok']
|
|
status 200
|
|
else
|
|
status 404
|
|
end
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
get "#{api_prefix}/token/:token/?" do
|
|
content_type :json
|
|
|
|
status 404
|
|
result = { 'ok' => false }
|
|
|
|
if Vmpooler::API.settings.config[:auth]
|
|
token = backend.hgetall('vmpooler__token__' + params[:token])
|
|
|
|
if not token.nil? and not token.empty?
|
|
status 200
|
|
|
|
pools.each do |pool|
|
|
backend.smembers('vmpooler__running__' + pool['name']).each do |vm|
|
|
if backend.hget('vmpooler__vm__' + vm, 'token:token') == params[:token]
|
|
token['vms'] ||= {}
|
|
token['vms']['running'] ||= []
|
|
token['vms']['running'].push(vm)
|
|
end
|
|
end
|
|
end
|
|
|
|
result = { 'ok' => true, params[:token] => token }
|
|
end
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
delete "#{api_prefix}/token/:token/?" do
|
|
content_type :json
|
|
|
|
status 404
|
|
result = { 'ok' => false }
|
|
|
|
if Vmpooler::API.settings.config[:auth]
|
|
status 401
|
|
|
|
need_auth!
|
|
|
|
if backend.del('vmpooler__token__' + params[:token]).to_i > 0
|
|
status 200
|
|
result['ok'] = true
|
|
end
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
post "#{api_prefix}/token" do
|
|
content_type :json
|
|
|
|
status 404
|
|
result = { 'ok' => false }
|
|
|
|
if Vmpooler::API.settings.config[:auth]
|
|
status 401
|
|
|
|
need_auth!
|
|
|
|
o = [('a'..'z'), ('0'..'9')].map(&:to_a).flatten
|
|
result['token'] = o[rand(25)] + (0...31).map { o[rand(o.length)] }.join
|
|
|
|
backend.hset('vmpooler__token__' + result['token'], 'user', @auth.username)
|
|
backend.hset('vmpooler__token__' + result['token'], 'created', Time.now)
|
|
|
|
status 200
|
|
result['ok'] = true
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
get "#{api_prefix}/vm/?" do
|
|
content_type :json
|
|
|
|
result = []
|
|
|
|
pools.each do |pool|
|
|
result.push(pool['name'])
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
post "#{api_prefix}/vm/?" do
|
|
content_type :json
|
|
result = { 'ok' => false }
|
|
|
|
payload = JSON.parse(request.body.read)
|
|
|
|
if payload
|
|
invalid = invalid_templates(payload)
|
|
if invalid.empty?
|
|
result = atomically_allocate_vms(payload)
|
|
else
|
|
invalid.each do |bad_template|
|
|
metrics.increment('checkout.invalid.' + bad_template)
|
|
end
|
|
status 404
|
|
end
|
|
else
|
|
metrics.increment('checkout.invalid.unknown')
|
|
status 404
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
def extract_templates_from_query_params(params)
|
|
payload = {}
|
|
|
|
params.split('+').each do |template|
|
|
payload[template] ||= 0
|
|
payload[template] += 1
|
|
end
|
|
|
|
payload
|
|
end
|
|
|
|
def invalid_templates(payload)
|
|
invalid = []
|
|
payload.keys.each do |template|
|
|
invalid << template unless pool_exists?(template)
|
|
end
|
|
invalid
|
|
end
|
|
|
|
def invalid_template_or_size(payload)
|
|
invalid = []
|
|
payload.each do |pool, size|
|
|
invalid << pool unless pool_exists?(pool)
|
|
unless is_integer?(size)
|
|
invalid << pool
|
|
next
|
|
end
|
|
invalid << pool unless Integer(size) >= 0
|
|
end
|
|
invalid
|
|
end
|
|
|
|
def invalid_template_or_path(payload)
|
|
invalid = []
|
|
payload.each do |pool, template|
|
|
invalid << pool unless pool_exists?(pool)
|
|
invalid << pool unless template.include? '/'
|
|
invalid << pool if template[0] == '/'
|
|
invalid << pool if template[-1] == '/'
|
|
end
|
|
invalid
|
|
end
|
|
|
|
def invalid_pool(payload)
|
|
invalid = []
|
|
payload.each do |pool, _clone_target|
|
|
invalid << pool unless pool_exists?(pool)
|
|
end
|
|
invalid
|
|
end
|
|
|
|
post "#{api_prefix}/vm/:template/?" do
|
|
content_type :json
|
|
result = { 'ok' => false }
|
|
|
|
payload = extract_templates_from_query_params(params[:template])
|
|
|
|
if payload
|
|
invalid = invalid_templates(payload)
|
|
if invalid.empty?
|
|
result = atomically_allocate_vms(payload)
|
|
else
|
|
invalid.each do |bad_template|
|
|
metrics.increment('checkout.invalid.' + bad_template)
|
|
end
|
|
status 404
|
|
end
|
|
else
|
|
metrics.increment('checkout.invalid.unknown')
|
|
status 404
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
get "#{api_prefix}/vm/:hostname/?" do
|
|
content_type :json
|
|
|
|
result = {}
|
|
|
|
status 404
|
|
result['ok'] = false
|
|
|
|
params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
|
|
|
|
rdata = backend.hgetall('vmpooler__vm__' + params[:hostname])
|
|
unless rdata.empty?
|
|
status 200
|
|
result['ok'] = true
|
|
|
|
result[params[:hostname]] = {}
|
|
|
|
result[params[:hostname]]['template'] = rdata['template']
|
|
result[params[:hostname]]['lifetime'] = (rdata['lifetime'] || config['vm_lifetime']).to_i
|
|
|
|
if rdata['destroy']
|
|
result[params[:hostname]]['running'] = ((Time.parse(rdata['destroy']) - Time.parse(rdata['checkout'])) / 60 / 60).round(2)
|
|
result[params[:hostname]]['state'] = 'destroyed'
|
|
elsif rdata['checkout']
|
|
result[params[:hostname]]['running'] = ((Time.now - Time.parse(rdata['checkout'])) / 60 / 60).round(2)
|
|
result[params[:hostname]]['remaining'] = ((Time.parse(rdata['checkout']) + rdata['lifetime'].to_i*60*60 - Time.now) / 60 / 60).round(2)
|
|
result[params[:hostname]]['start_time'] = Time.parse(rdata['checkout']).to_datetime.rfc3339
|
|
result[params[:hostname]]['end_time'] = (Time.parse(rdata['checkout']) + rdata['lifetime'].to_i*60*60).to_datetime.rfc3339
|
|
result[params[:hostname]]['state'] = 'running'
|
|
elsif rdata['check']
|
|
result[params[:hostname]]['state'] = 'ready'
|
|
else
|
|
result[params[:hostname]]['state'] = 'pending'
|
|
end
|
|
|
|
rdata.keys.each do |key|
|
|
if key.match('^tag\:(.+?)$')
|
|
result[params[:hostname]]['tags'] ||= {}
|
|
result[params[:hostname]]['tags'][$1] = rdata[key]
|
|
end
|
|
|
|
if key.match('^snapshot\:(.+?)$')
|
|
result[params[:hostname]]['snapshots'] ||= []
|
|
result[params[:hostname]]['snapshots'].push($1)
|
|
end
|
|
end
|
|
|
|
if rdata['disk']
|
|
result[params[:hostname]]['disk'] = rdata['disk'].split(':')
|
|
end
|
|
|
|
# Look up IP address of the hostname
|
|
begin
|
|
ipAddress = TCPSocket.gethostbyname(params[:hostname])[3]
|
|
rescue StandardError
|
|
ipAddress = ""
|
|
end
|
|
|
|
result[params[:hostname]]['ip'] = ipAddress
|
|
|
|
if config['domain']
|
|
result[params[:hostname]]['domain'] = config['domain']
|
|
end
|
|
|
|
result[params[:hostname]]['host'] = rdata['host'] if rdata['host']
|
|
result[params[:hostname]]['migrated'] = rdata['migrated'] if rdata['migrated']
|
|
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
delete "#{api_prefix}/vm/:hostname/?" do
|
|
content_type :json
|
|
|
|
result = {}
|
|
|
|
status 404
|
|
result['ok'] = false
|
|
|
|
params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
|
|
|
|
rdata = backend.hgetall('vmpooler__vm__' + params[:hostname])
|
|
unless rdata.empty?
|
|
need_token! if rdata['token:token']
|
|
|
|
if backend.srem('vmpooler__running__' + rdata['template'], params[:hostname])
|
|
backend.sadd('vmpooler__completed__' + rdata['template'], params[:hostname])
|
|
|
|
status 200
|
|
result['ok'] = true
|
|
else
|
|
metrics.increment('delete.srem.failed')
|
|
end
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
put "#{api_prefix}/vm/:hostname/?" do
|
|
content_type :json
|
|
|
|
status 404
|
|
result = { 'ok' => false }
|
|
|
|
failure = []
|
|
|
|
params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
|
|
|
|
if backend.exists('vmpooler__vm__' + params[:hostname])
|
|
begin
|
|
jdata = JSON.parse(request.body.read)
|
|
rescue StandardError
|
|
halt 400, JSON.pretty_generate(result)
|
|
end
|
|
|
|
# Validate data payload
|
|
jdata.each do |param, arg|
|
|
case param
|
|
when 'lifetime'
|
|
need_token! if Vmpooler::API.settings.config[:auth]
|
|
|
|
# in hours, defaults to one week
|
|
max_lifetime_upper_limit = config['max_lifetime_upper_limit']
|
|
if max_lifetime_upper_limit
|
|
max_lifetime_upper_limit = max_lifetime_upper_limit.to_i
|
|
if arg.to_i >= max_lifetime_upper_limit
|
|
failure.push("You provided a lifetime (#{arg}) that exceeds the configured maximum of #{max_lifetime_upper_limit}.")
|
|
end
|
|
end
|
|
|
|
# validate lifetime is within boundaries
|
|
unless arg.to_i > 0
|
|
failure.push("You provided a lifetime (#{arg}) but you must provide a positive number.")
|
|
end
|
|
when 'tags'
|
|
unless arg.is_a?(Hash)
|
|
failure.push("You provided tags (#{arg}) as something other than a hash.")
|
|
end
|
|
|
|
if config['allowed_tags']
|
|
failure.push("You provided unsuppored tags (#{arg}).") if not (arg.keys - config['allowed_tags']).empty?
|
|
end
|
|
else
|
|
failure.push("Unknown argument #{arg}.")
|
|
end
|
|
end
|
|
|
|
if !failure.empty?
|
|
status 400
|
|
result['failure'] = failure
|
|
else
|
|
jdata.each do |param, arg|
|
|
case param
|
|
when 'lifetime'
|
|
need_token! if Vmpooler::API.settings.config[:auth]
|
|
|
|
arg = arg.to_i
|
|
|
|
backend.hset('vmpooler__vm__' + params[:hostname], param, arg)
|
|
when 'tags'
|
|
filter_tags(arg)
|
|
export_tags(backend, params[:hostname], arg)
|
|
end
|
|
end
|
|
|
|
status 200
|
|
result['ok'] = true
|
|
end
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
post "#{api_prefix}/vm/:hostname/disk/:size/?" do
|
|
content_type :json
|
|
|
|
need_token! if Vmpooler::API.settings.config[:auth]
|
|
|
|
status 404
|
|
result = { 'ok' => false }
|
|
|
|
params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
|
|
|
|
if ((params[:size].to_i > 0 )and (backend.exists('vmpooler__vm__' + params[:hostname])))
|
|
result[params[:hostname]] = {}
|
|
result[params[:hostname]]['disk'] = "+#{params[:size]}gb"
|
|
|
|
backend.sadd('vmpooler__tasks__disk', params[:hostname] + ':' + params[:size])
|
|
|
|
status 202
|
|
result['ok'] = true
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
post "#{api_prefix}/vm/:hostname/snapshot/?" do
|
|
content_type :json
|
|
|
|
need_token! if Vmpooler::API.settings.config[:auth]
|
|
|
|
status 404
|
|
result = { 'ok' => false }
|
|
|
|
params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
|
|
|
|
if backend.exists('vmpooler__vm__' + params[:hostname])
|
|
result[params[:hostname]] = {}
|
|
|
|
o = [('a'..'z'), ('0'..'9')].map(&:to_a).flatten
|
|
result[params[:hostname]]['snapshot'] = o[rand(25)] + (0...31).map { o[rand(o.length)] }.join
|
|
|
|
backend.sadd('vmpooler__tasks__snapshot', params[:hostname] + ':' + result[params[:hostname]]['snapshot'])
|
|
|
|
status 202
|
|
result['ok'] = true
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
post "#{api_prefix}/vm/:hostname/snapshot/:snapshot/?" do
|
|
content_type :json
|
|
|
|
need_token! if Vmpooler::API.settings.config[:auth]
|
|
|
|
status 404
|
|
result = { 'ok' => false }
|
|
|
|
params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
|
|
|
|
unless backend.hget('vmpooler__vm__' + params[:hostname], 'snapshot:' + params[:snapshot]).to_i.zero?
|
|
backend.sadd('vmpooler__tasks__snapshot-revert', params[:hostname] + ':' + params[:snapshot])
|
|
|
|
status 202
|
|
result['ok'] = true
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
post "#{api_prefix}/config/poolsize/?" do
|
|
content_type :json
|
|
result = { 'ok' => false }
|
|
|
|
if config['experimental_features']
|
|
need_token! if Vmpooler::API.settings.config[:auth]
|
|
|
|
payload = JSON.parse(request.body.read)
|
|
|
|
if payload
|
|
invalid = invalid_template_or_size(payload)
|
|
if invalid.empty?
|
|
result = update_pool_size(payload)
|
|
else
|
|
invalid.each do |bad_template|
|
|
metrics.increment("config.invalid.#{bad_template}")
|
|
end
|
|
result[:bad_templates] = invalid
|
|
status 400
|
|
end
|
|
else
|
|
metrics.increment('config.invalid.unknown')
|
|
status 404
|
|
end
|
|
else
|
|
status 405
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
post "#{api_prefix}/config/pooltemplate/?" do
|
|
content_type :json
|
|
result = { 'ok' => false }
|
|
|
|
if config['experimental_features']
|
|
need_token! if Vmpooler::API.settings.config[:auth]
|
|
|
|
payload = JSON.parse(request.body.read)
|
|
|
|
if payload
|
|
invalid = invalid_template_or_path(payload)
|
|
if invalid.empty?
|
|
result = update_pool_template(payload)
|
|
else
|
|
invalid.each do |bad_template|
|
|
metrics.increment("config.invalid.#{bad_template}")
|
|
end
|
|
result[:bad_templates] = invalid
|
|
status 400
|
|
end
|
|
else
|
|
metrics.increment('config.invalid.unknown')
|
|
status 404
|
|
end
|
|
else
|
|
status 405
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
post "#{api_prefix}/poolreset/?" do
|
|
content_type :json
|
|
result = { 'ok' => false }
|
|
|
|
if config['experimental_features']
|
|
need_token! if Vmpooler::API.settings.config[:auth]
|
|
|
|
begin
|
|
payload = JSON.parse(request.body.read)
|
|
if payload
|
|
invalid = invalid_templates(payload)
|
|
if invalid.empty?
|
|
result = reset_pool(payload)
|
|
else
|
|
invalid.each do |bad_pool|
|
|
metrics.increment("poolreset.invalid.#{bad_pool}")
|
|
end
|
|
result[:bad_pools] = invalid
|
|
status 400
|
|
end
|
|
else
|
|
metrics.increment('poolreset.invalid.unknown')
|
|
status 404
|
|
end
|
|
rescue JSON::ParserError
|
|
status 400
|
|
result = {
|
|
'ok' => false,
|
|
'message' => 'JSON payload could not be parsed'
|
|
}
|
|
end
|
|
else
|
|
status 405
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
post "#{api_prefix}/config/clonetarget/?" do
|
|
content_type :json
|
|
result = { 'ok' => false }
|
|
|
|
if config['experimental_features']
|
|
need_token! if Vmpooler::API.settings.config[:auth]
|
|
|
|
payload = JSON.parse(request.body.read)
|
|
|
|
if payload
|
|
invalid = invalid_pool(payload)
|
|
if invalid.empty?
|
|
result = update_clone_target(payload)
|
|
else
|
|
invalid.each do |bad_template|
|
|
metrics.increment("config.invalid.#{bad_template}")
|
|
end
|
|
result[:bad_templates] = invalid
|
|
status 400
|
|
end
|
|
else
|
|
metrics.increment('config.invalid.unknown')
|
|
status 404
|
|
end
|
|
else
|
|
status 405
|
|
end
|
|
|
|
JSON.pretty_generate(result)
|
|
end
|
|
|
|
get "#{api_prefix}/config/?" do
|
|
content_type :json
|
|
result = { 'ok' => false }
|
|
status 404
|
|
|
|
if pools
|
|
sync_pool_sizes
|
|
sync_pool_templates
|
|
|
|
pool_configuration = []
|
|
pools.each do |pool|
|
|
pool['template_ready'] = template_ready?(pool, backend)
|
|
pool_configuration << pool
|
|
end
|
|
|
|
result = {
|
|
pool_configuration: pool_configuration,
|
|
status: {
|
|
ok: true
|
|
}
|
|
}
|
|
|
|
status 200
|
|
end
|
|
JSON.pretty_generate(result)
|
|
end
|
|
end
|
|
end
|
|
end
|