From 17b24d69adb84f7c97d13a78cd153d676662458e Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Wed, 4 Nov 2015 19:31:32 -0800 Subject: [PATCH 1/2] Allow pool 'alias' names The following pool configuration would allow a pool to be aliased in POST requests as 'centos-6-x86_64', 'centos-6-amd64', or 'centos-6-64': ````yaml - name: 'centos-6-x86_64' alias: [ 'centos-6-amd64', 'centos-6-64' ] template: 'templates/centos-6-x86_64' folder: 'vmpooler/centos-6-x86_64' datastore: 'instance1' size: 5 ```` The 'alias' configuration can be either a string or an array. Note that even when requesting an alias, the pool's 'name' is returned in the JSON response: ```` $ curl -d '{"centos-6-64":"1"}' --url vmpooler/api/v1/vm ```` ````json { "ok": true, "centos-6-x86_64": { "hostname": "cuna2qeahwlzji7" }, "domain": "company.com" } ```` --- lib/vmpooler.rb | 14 ++++ lib/vmpooler/api/v1.rb | 156 ++++++++++++++++++++--------------------- vmpooler.yaml.example | 6 ++ 3 files changed, 95 insertions(+), 81 deletions(-) diff --git a/lib/vmpooler.rb b/lib/vmpooler.rb index 697a4c8..a9eb4eb 100644 --- a/lib/vmpooler.rb +++ b/lib/vmpooler.rb @@ -33,6 +33,20 @@ module Vmpooler parsed_config[:config]['vm_checktime'] ||= 15 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'] parsed_config[:graphite]['prefix'] ||= 'vmpooler' end diff --git a/lib/vmpooler/api/v1.rb b/lib/vmpooler/api/v1.rb index 33a55fa..ae6fff1 100644 --- a/lib/vmpooler/api/v1.rb +++ b/lib/vmpooler/api/v1.rb @@ -28,6 +28,60 @@ module Vmpooler validate_token(backend) 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 content_type :json @@ -277,11 +331,15 @@ module Vmpooler post "#{api_prefix}/vm/?" do content_type :json - result = {} + result = { 'ok' => false } - available = 1 + jdata = alias_deref(JSON.parse(request.body.read)) - jdata = JSON.parse(request.body.read) + if not jdata.nil? and not jdata.empty? + available = 1 + else + status 404 + end jdata.each do |key, val| if backend.scard('vmpooler__ready__' + key).to_i < val.to_i @@ -293,46 +351,10 @@ module Vmpooler result['ok'] = true jdata.each do |key, val| - result[key] ||= {} - val.to_i.times do |_i| - vm = backend.spop('vmpooler__ready__' + key) - - 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 - - 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 + result = checkout_vm(key, result) end end - else - status 503 - result['ok'] = false end if result['ok'] && config['domain'] @@ -345,7 +367,7 @@ module Vmpooler post "#{api_prefix}/vm/:template/?" do content_type :json - result = {} + result = { 'ok' => false } payload = {} params[:template].split('+').each do |template| @@ -353,10 +375,16 @@ module Vmpooler payload[template] = payload[template] + 1 end - available = 1 + payload = alias_deref(payload) - payload.keys.each do |template| - if backend.scard('vmpooler__ready__' + template) < payload[template] + if not payload.nil? and not payload.empty? + available = 1 + else + status 404 + end + + payload.each do |key, val| + if backend.scard('vmpooler__ready__' + key).to_i < val.to_i available = 0 end end @@ -364,45 +392,11 @@ module Vmpooler if (available == 1) result['ok'] = true - params[:template].split('+').each do |template| - result[template] ||= {} - - 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 + payload.each do |key, val| + val.to_i.times do |_i| + result = checkout_vm(key, result) end end - else - status 503 - result['ok'] = false end if result['ok'] && config['domain'] diff --git a/vmpooler.yaml.example b/vmpooler.yaml.example index e5fa3be..6e1c5d4 100644 --- a/vmpooler.yaml.example +++ b/vmpooler.yaml.example @@ -188,6 +188,10 @@ # The name of the pool. # (required) # +# - alias +# Other names this pool can be requested as. +# (optional) +# # - template # The template or virtual machine target to spawn clones from. # (required) @@ -221,6 +225,7 @@ :pools: - name: 'debian-7-i386' + alias: [ 'debian-7-32' ] template: 'Templates/debian-7-i386' folder: 'Pooled VMs/debian-7-i386' datastore: 'vmstorage' @@ -228,6 +233,7 @@ timeout: 15 ready_ttl: 1440 - name: 'debian-7-x86_64' + alias: [ 'debian-7-64', 'debian-7-amd64' ] template: 'Templates/debian-7-x86_64' folder: 'Pooled VMs/debian-7-x86_64' datastore: 'vmstorage' From 1fcda8612498d3e9c19df8590352fa3d5dfebbfa Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Thu, 5 Nov 2015 11:52:32 -0800 Subject: [PATCH 2/2] Spec tests for pool aliases, /vm/:template This PR adds spec testing for pool 'alias' functionality introduced in 17b24d6, as well as the following previously non-existant tests: - new tests for handling requests for a VM from a nonexistant pool - new tests for the `POST /vm/:template` endpoint --- spec/vmpooler/api/v1_spec.rb | 180 ++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/spec/vmpooler/api/v1_spec.rb b/spec/vmpooler/api/v1_spec.rb index 5f7643a..b5971fc 100644 --- a/spec/vmpooler/api/v1_spec.rb +++ b/spec/vmpooler/api/v1_spec.rb @@ -190,7 +190,8 @@ describe Vmpooler::API::V1 do pools: [ {'name' => 'pool1', 'size' => 5}, {'name' => 'pool2', 'size' => 10} - ] + ], + alias: { 'poolone' => 'pool1' } } } before do @@ -222,6 +223,31 @@ describe Vmpooler::API::V1 do 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":"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 post "#{prefix}/vm", '{"pool1":"1","pool2":"1"}' @@ -305,6 +331,158 @@ describe Vmpooler::API::V1 do 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 let(:redis) { double('redis') } let(:prefix) { '/api/v1' }