Merge pull request #138 from sschneid/qeng_2807

(QENG-2807) Allow pool 'alias' names
This commit is contained in:
Scott Schneider 2015-11-13 09:24:48 -08:00
commit 20fa7d20be
4 changed files with 274 additions and 82 deletions

View file

@ -33,6 +33,20 @@ module Vmpooler
parsed_config[:config]['vm_checktime'] ||= 15 parsed_config[:config]['vm_checktime'] ||= 15
parsed_config[:config]['vm_lifetime'] ||= 24 parsed_config[:config]['vm_lifetime'] ||= 24
# Create an index of pool aliases
parsed_config[:pools].each do |pool|
if pool['alias']
if pool['alias'].kind_of?(Array)
pool['alias'].each do |a|
parsed_config[:alias] ||= {}
parsed_config[:alias][a] = pool['name']
end
elsif pool['alias'].kind_of?(String)
parsed_config[:alias][pool['alias']] = pool['name']
end
end
end
if parsed_config[:graphite]['server'] if parsed_config[:graphite]['server']
parsed_config[:graphite]['prefix'] ||= 'vmpooler' parsed_config[:graphite]['prefix'] ||= 'vmpooler'
end end

View file

@ -28,6 +28,60 @@ module Vmpooler
validate_token(backend) validate_token(backend)
end end
def alias_deref(hash)
newhash = {}
hash.each do |key, val|
if backend.exists('vmpooler__ready__' + key)
newhash[key] = val
else
if Vmpooler::API.settings.config[:alias][key]
newkey = Vmpooler::API.settings.config[:alias][key]
newhash[newkey] = val
end
end
end
newhash
end
def checkout_vm(template, result)
vm = backend.spop('vmpooler__ready__' + template)
unless vm.nil?
backend.sadd('vmpooler__running__' + 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
result[template] ||= {}
if result[template]['hostname']
result[template]['hostname'] = [result[template]['hostname']] unless result[template]['hostname'].is_a?(Array)
result[template]['hostname'].push(vm)
else
result[template]['hostname'] = vm
end
else
status 503
result['ok'] = false
end
result
end
get "#{api_prefix}/status/?" do get "#{api_prefix}/status/?" do
content_type :json content_type :json
@ -282,11 +336,15 @@ module Vmpooler
post "#{api_prefix}/vm/?" do post "#{api_prefix}/vm/?" do
content_type :json content_type :json
result = {} result = { 'ok' => false }
jdata = alias_deref(JSON.parse(request.body.read))
if not jdata.nil? and not jdata.empty?
available = 1 available = 1
else
jdata = JSON.parse(request.body.read) status 404
end
jdata.each do |key, val| jdata.each do |key, val|
if backend.scard('vmpooler__ready__' + key).to_i < val.to_i if backend.scard('vmpooler__ready__' + key).to_i < val.to_i
@ -298,46 +356,10 @@ module Vmpooler
result['ok'] = true result['ok'] = true
jdata.each do |key, val| jdata.each do |key, val|
result[key] ||= {}
val.to_i.times do |_i| val.to_i.times do |_i|
vm = backend.spop('vmpooler__ready__' + key) result = checkout_vm(key, result)
unless vm.nil?
backend.sadd('vmpooler__running__' + key, vm)
backend.hset('vmpooler__active__' + key, 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 end
result[key] ||= {}
if result[key]['hostname']
result[key]['hostname'] = [result[key]['hostname']] unless result[key]['hostname'].is_a?(Array)
result[key]['hostname'].push(vm)
else
result[key]['hostname'] = vm
end
else
status 503
result['ok'] = false
end
end
end
else
status 503
result['ok'] = false
end end
if result['ok'] && config['domain'] if result['ok'] && config['domain']
@ -350,7 +372,7 @@ module Vmpooler
post "#{api_prefix}/vm/:template/?" do post "#{api_prefix}/vm/:template/?" do
content_type :json content_type :json
result = {} result = { 'ok' => false }
payload = {} payload = {}
params[:template].split('+').each do |template| params[:template].split('+').each do |template|
@ -358,10 +380,16 @@ module Vmpooler
payload[template] = payload[template] + 1 payload[template] = payload[template] + 1
end end
available = 1 payload = alias_deref(payload)
payload.keys.each do |template| if not payload.nil? and not payload.empty?
if backend.scard('vmpooler__ready__' + template) < payload[template] available = 1
else
status 404
end
payload.each do |key, val|
if backend.scard('vmpooler__ready__' + key).to_i < val.to_i
available = 0 available = 0
end end
end end
@ -369,45 +397,11 @@ module Vmpooler
if (available == 1) if (available == 1)
result['ok'] = true result['ok'] = true
params[:template].split('+').each do |template| payload.each do |key, val|
result[template] ||= {} val.to_i.times do |_i|
result = checkout_vm(key, result)
vm = backend.spop('vmpooler__ready__' + template)
unless vm.nil?
backend.sadd('vmpooler__running__' + 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 end
result[template] ||= {}
if result[template]['hostname']
result[template]['hostname'] = [result[template]['hostname']] unless result[template]['hostname'].is_a?(Array)
result[template]['hostname'].push(vm)
else
result[template]['hostname'] = vm
end
else
status 503
result['ok'] = false
end
end
else
status 503
result['ok'] = false
end end
if result['ok'] && config['domain'] if result['ok'] && config['domain']

View file

@ -189,7 +189,8 @@ describe Vmpooler::API::V1 do
pools: [ pools: [
{'name' => 'pool1', 'size' => 5}, {'name' => 'pool1', 'size' => 5},
{'name' => 'pool2', 'size' => 10} {'name' => 'pool2', 'size' => 10}
] ],
alias: { 'poolone' => 'pool1' }
} } } }
before do before do
@ -221,6 +222,31 @@ describe Vmpooler::API::V1 do
expect_json(ok = true, http = 200) expect_json(ok = true, http = 200)
end end
it 'returns a single VM for an alias' do
expect(redis).to receive(:exists).with("vmpooler__ready__poolone").and_return(false)
post "#{prefix}/vm", '{"poolone":"1"}'
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = true, http = 200)
end
it 'fails on nonexistant pools' do
expect(redis).to receive(:exists).with("vmpooler__ready__poolpoolpool").and_return(false)
post "#{prefix}/vm", '{"poolpoolpool":"1"}'
expect_json(ok = false, http = 404)
end
it 'returns multiple VMs' do it 'returns multiple VMs' do
post "#{prefix}/vm", '{"pool1":"1","pool2":"1"}' post "#{prefix}/vm", '{"pool1":"1","pool2":"1"}'
@ -304,6 +330,158 @@ describe Vmpooler::API::V1 do
end end
end end
describe '/vm/:template' do
let(:redis) { double('redis') }
let(:prefix) { '/api/v1' }
let(:config) { {
config: {
'site_name' => 'test pooler',
'vm_lifetime_auth' => 2
},
pools: [
{'name' => 'pool1', 'size' => 5},
{'name' => 'pool2', 'size' => 10}
],
alias: { 'poolone' => 'pool1' }
} }
before do
app.settings.set :config, config
app.settings.set :redis, redis
allow(redis).to receive(:exists).and_return '1'
allow(redis).to receive(:hget).with('vmpooler__token__abcdefghijklmnopqrstuvwxyz012345', 'user').and_return 'jdoe'
allow(redis).to receive(:hset).and_return '1'
allow(redis).to receive(:sadd).and_return '1'
allow(redis).to receive(:scard).and_return '5'
allow(redis).to receive(:spop).with('vmpooler__ready__pool1').and_return 'abcdefghijklmnop'
allow(redis).to receive(:spop).with('vmpooler__ready__pool2').and_return 'qrstuvwxyz012345'
end
describe 'POST /vm/:template' do
it 'returns a single VM' do
post "#{prefix}/vm/pool1", ''
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = true, http = 200)
end
it 'returns a single VM for an alias' do
expect(redis).to receive(:exists).with("vmpooler__ready__poolone").and_return(false)
post "#{prefix}/vm/poolone", ''
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = true, http = 200)
end
it 'fails on nonexistant pools' do
expect(redis).to receive(:exists).with("vmpooler__ready__poolpoolpool").and_return(false)
post "#{prefix}/vm/poolpoolpool", ''
expect_json(ok = false, http = 404)
end
it 'returns multiple VMs' do
post "#{prefix}/vm/pool1+pool2", ''
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
},
pool2: {
hostname: 'qrstuvwxyz012345'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = true, http = 200)
end
context '(auth not configured)' do
let(:config) { { auth: false } }
it 'does not extend VM lifetime if auth token is provided' do
expect(redis).not_to receive(:hset).with("vmpooler__vm__abcdefghijklmnop", "lifetime", 2)
post "#{prefix}/vm/pool1", '', {
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
}
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = true, http = 200)
end
end
context '(auth configured)' do
let(:config) { { auth: true } }
it 'extends VM lifetime if auth token is provided' do
expect(redis).to receive(:hset).with("vmpooler__vm__abcdefghijklmnop", "lifetime", 2).once
post "#{prefix}/vm/pool1", '', {
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
}
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = true, http = 200)
end
it 'does not extend VM lifetime if auth token is not provided' do
expect(redis).not_to receive(:hset).with("vmpooler__vm__abcdefghijklmnop", "lifetime", 2)
post "#{prefix}/vm/pool1", ''
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = true, http = 200)
end
end
end
end
describe '/vm/:hostname' do describe '/vm/:hostname' do
let(:redis) { double('redis') } let(:redis) { double('redis') }
let(:prefix) { '/api/v1' } let(:prefix) { '/api/v1' }

View file

@ -188,6 +188,10 @@
# The name of the pool. # The name of the pool.
# (required) # (required)
# #
# - alias
# Other names this pool can be requested as.
# (optional)
#
# - template # - template
# The template or virtual machine target to spawn clones from. # The template or virtual machine target to spawn clones from.
# (required) # (required)
@ -221,6 +225,7 @@
:pools: :pools:
- name: 'debian-7-i386' - name: 'debian-7-i386'
alias: [ 'debian-7-32' ]
template: 'Templates/debian-7-i386' template: 'Templates/debian-7-i386'
folder: 'Pooled VMs/debian-7-i386' folder: 'Pooled VMs/debian-7-i386'
datastore: 'vmstorage' datastore: 'vmstorage'
@ -228,6 +233,7 @@
timeout: 15 timeout: 15
ready_ttl: 1440 ready_ttl: 1440
- name: 'debian-7-x86_64' - name: 'debian-7-x86_64'
alias: [ 'debian-7-64', 'debian-7-amd64' ]
template: 'Templates/debian-7-x86_64' template: 'Templates/debian-7-x86_64'
folder: 'Pooled VMs/debian-7-x86_64' folder: 'Pooled VMs/debian-7-x86_64'
datastore: 'vmstorage' datastore: 'vmstorage'