Merge pull request #158 from puppetlabs/merge-master-into-ci.next

(QENG-4070) Consistently return 503 if valid pool is empty
This commit is contained in:
Rick Bradley 2016-07-11 13:38:34 -05:00 committed by GitHub
commit 4738a0b8b9
20 changed files with 1299 additions and 1268 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.ruby-version
Gemfile.lock
vendor

View file

@ -1,6 +1,8 @@
cache: bundler cache: bundler
sudo: false sudo: false
language: ruby language: ruby
services:
- redis-server
rvm: rvm:
- 1.9.3 - 1.9.3
- 2.1.1 - 2.1.1

View file

@ -1,18 +1,21 @@
source ENV['GEM_SOURCE'] || 'https://rubygems.org' source ENV['GEM_SOURCE'] || 'https://rubygems.org'
gem 'json', '>= 1.8' if RUBY_VERSION =~ /^1\.9\./
gem 'json', '~> 1.8'
else
gem 'json', '>= 1.8'
end
gem 'rack', '>= 1.6' gem 'rack', '>= 1.6'
gem 'rake', '>= 10.4' gem 'rake', '>= 10.4'
gem 'rbvmomi', '>= 1.8' gem 'rbvmomi', '>= 1.8'
gem 'redis', '>= 3.2' gem 'redis', '>= 3.2'
gem 'sinatra', '>= 1.4' gem 'sinatra', '>= 1.4'
gem 'net-ldap', '<= 0.12.1' # keep compatibility w/ jruby & mri-1.9.3 gem 'net-ldap', '<= 0.12.1' # keep compatibility w/ jruby & mri-1.9.3
gem 'statsd-ruby', '>= 1.3.0'
# Test deps # Test deps
group :test do group :test do
gem 'rack-test', '>= 0.6' gem 'rack-test', '>= 0.6'
gem 'rspec', '>= 3.2' gem 'rspec', '>= 3.2'
gem 'simplecov', '>= 0.11.2'
gem 'yarjuf', '>= 2.0' gem 'yarjuf', '>= 2.0'
end end

View file

@ -80,7 +80,8 @@ A dashboard is provided to offer real-time statistics and historical graphs. It
## Command-line Utility ## Command-line Utility
The [vmpooler_client.py](https://github.com/puppetlabs/vmpooler-client) CLI utility provides easy access to the vmpooler service. The tool is cross-platform and written in Python. - The [vmpooler_client.py](https://github.com/puppetlabs/vmpooler-client) CLI utility provides easy access to the vmpooler service. The tool is cross-platform and written in Python.
- [vmfloaty](https://github.com/briancain/vmfloaty) is a ruby based CLI tool and scripting library written in ruby.
## Build status ## Build status

View file

@ -7,10 +7,10 @@ module Vmpooler
require 'rbvmomi' require 'rbvmomi'
require 'redis' require 'redis'
require 'sinatra/base' require 'sinatra/base'
require "statsd-ruby"
require 'time' require 'time'
require 'timeout' require 'timeout'
require 'yaml' require 'yaml'
require 'set'
%w( api graphite logger pool_manager vsphere_helper ).each do |lib| %w( api graphite logger pool_manager vsphere_helper ).each do |lib|
begin begin
@ -36,15 +36,19 @@ module Vmpooler
parsed_config[:config]['prefix'] ||= '' parsed_config[:config]['prefix'] ||= ''
# Create an index of pool aliases # Create an index of pool aliases
parsed_config[:pool_names] = Set.new
parsed_config[:pools].each do |pool| parsed_config[:pools].each do |pool|
parsed_config[:pool_names] << pool['name']
if pool['alias'] if pool['alias']
if pool['alias'].kind_of?(Array) if pool['alias'].kind_of?(Array)
pool['alias'].each do |a| pool['alias'].each do |a|
parsed_config[:alias] ||= {} parsed_config[:alias] ||= {}
parsed_config[:alias][a] = pool['name'] parsed_config[:alias][a] = pool['name']
parsed_config[:pool_names] << a
end end
elsif pool['alias'].kind_of?(String) elsif pool['alias'].kind_of?(String)
parsed_config[:alias][pool['alias']] = pool['name'] parsed_config[:alias][pool['alias']] = pool['name']
parsed_config[:pool_names] << pool['alias']
end end
end end
end end
@ -53,13 +57,6 @@ module Vmpooler
parsed_config[:graphite]['prefix'] ||= 'vmpooler' parsed_config[:graphite]['prefix'] ||= 'vmpooler'
end end
# statsd is an addition and my not be present in YAML configuration
if parsed_config[:statsd]
if parsed_config[:statsd]['server']
parsed_config[:statsd]['prefix'] ||= 'vmpooler'
end
end
if parsed_config[:tagfilter] if parsed_config[:tagfilter]
parsed_config[:tagfilter].keys.each do |tag| parsed_config[:tagfilter].keys.each do |tag|
parsed_config[:tagfilter][tag] = Regexp.new(parsed_config[:tagfilter][tag]) parsed_config[:tagfilter][tag] = Regexp.new(parsed_config[:tagfilter][tag])
@ -87,14 +84,6 @@ module Vmpooler
end end
end end
def self.new_statsd(server, port)
if server.nil? || server.empty?
nil
else
Statsd.new server, port
end
end
def self.pools(conf) def self.pools(conf)
conf[:pools] conf[:pools]
end end

View file

@ -42,10 +42,9 @@ module Vmpooler
use Vmpooler::API::Reroute use Vmpooler::API::Reroute
use Vmpooler::API::V1 use Vmpooler::API::V1
def configure(config, redis, statsd, environment = :production) def configure(config, redis, environment = :production)
self.settings.set :config, config self.settings.set :config, config
self.settings.set :redis, redis self.settings.set :redis, redis
self.settings.set :statsd, statsd
self.settings.set :environment, environment self.settings.set :environment, environment
end end

View file

@ -12,16 +12,6 @@ module Vmpooler
Vmpooler::API.settings.redis Vmpooler::API.settings.redis
end end
def statsd
Vmpooler::API.settings.statsd
end
def statsd_prefix
if Vmpooler::API.settings.statsd
Vmpooler::API.settings.config[:statsd]['prefix'] ? Vmpooler::API.settings.config[:statsd]['prefix'] : 'vmpooler'
end
end
def config def config
Vmpooler::API.settings.config[:config] Vmpooler::API.settings.config[:config]
end end
@ -30,6 +20,10 @@ module Vmpooler
Vmpooler::API.settings.config[:pools] Vmpooler::API.settings.config[:pools]
end end
def pool_exists?(template)
Vmpooler::API.settings.config[:pool_names].include?(template)
end
def need_auth! def need_auth!
validate_auth(backend) validate_auth(backend)
end end
@ -38,28 +32,19 @@ module Vmpooler
validate_token(backend) validate_token(backend)
end end
def alias_deref(hash) def fetch_single_vm(template)
newhash = {} vm = backend.spop('vmpooler__ready__' + template)
hash.each do |key, val| return [vm, template] if vm
if Vmpooler::API.settings.config[:alias][key]
key = Vmpooler::API.settings.config[:alias][key]
end
if backend.exists('vmpooler__ready__' + key) aliases = Vmpooler::API.settings.config[:alias]
newhash[key] = val if aliases && aliased_template = aliases[template]
elsif backend.exists('vmpooler__empty__' + key) vm = backend.spop('vmpooler__ready__' + aliased_template)
newhash['empty'] = (newhash['empty'] || 0) + val.to_i
else return [vm, aliased_template] if vm
newhash['invalid'] = (newhash['invalid'] || 0) + val.to_i
end
end end
newhash [nil, nil]
end
def fetch_single_vm(template)
backend.spop('vmpooler__ready__' + template)
end end
def return_vm_to_ready_state(template, vm) def return_vm_to_ready_state(template, vm)
@ -96,35 +81,31 @@ module Vmpooler
end end
def atomically_allocate_vms(payload) def atomically_allocate_vms(payload)
return false unless payload and !payload.empty?
result = { 'ok' => false } result = { 'ok' => false }
failed = false failed = false
vms = [] vms = []
payload.each do |template, count| payload.each do |template, count|
count.to_i.times do |_i| count.to_i.times do |_i|
vm = fetch_single_vm(template) vm, name = fetch_single_vm(template)
if !vm if !vm
failed = true failed = true
statsd.increment(statsd_prefix + '.checkout.fail.' + template, 1)
break break
else else
statsd.increment(statsd_prefix + '.checkout.success.' + template, 1) vms << [ name, vm ]
vms << [ template, vm ]
end end
end end
end end
if failed if failed
vms.each do |(template, vm)| vms.each do |(name, vm)|
return_vm_to_ready_state(template, vm) return_vm_to_ready_state(name, vm)
status 503
end end
status 503
else else
vms.each do |(template, vm)| vms.each do |(name, vm)|
account_for_starting_vm(template, vm) account_for_starting_vm(name, vm)
update_result_hosts(result, template, vm) update_result_hosts(result, name, vm)
end end
result['ok'] = true result['ok'] = true
@ -386,20 +367,13 @@ module Vmpooler
end end
post "#{api_prefix}/vm/?" do post "#{api_prefix}/vm/?" do
jdata = alias_deref(JSON.parse(request.body.read))
content_type :json content_type :json
result = { 'ok' => false } result = { 'ok' => false }
if jdata payload = JSON.parse(request.body.read)
empty = jdata.delete('empty')
invalid = jdata.delete('invalid') if all_templates_valid?(payload)
statsd.increment(statsd_prefix + '.checkout.empty', empty) if empty result = atomically_allocate_vms(payload)
statsd.increment(statsd_prefix + '.checkout.invalid', invalid) if invalid
unless jdata.empty?
result = atomically_allocate_vms(jdata)
else
status 404
end
else else
status 404 status 404
end end
@ -418,21 +392,22 @@ module Vmpooler
payload payload
end end
def all_templates_valid?(payload)
return false unless payload
payload.keys.all? do |templates|
pool_exists?(templates)
end
end
post "#{api_prefix}/vm/:template/?" do post "#{api_prefix}/vm/:template/?" do
payload = alias_deref(extract_templates_from_query_params(params[:template]))
content_type :json content_type :json
result = { 'ok' => false } result = { 'ok' => false }
if payload payload = extract_templates_from_query_params(params[:template])
empty = payload.delete('empty')
invalid = payload.delete('invalid') if all_templates_valid?(payload)
statsd.increment(statsd_prefix + '.checkout.empty', empty) if empty result = atomically_allocate_vms(payload)
statsd.increment(statsd_prefix + '.checkout.invalid', invalid) if invalid
unless payload.empty?
result = atomically_allocate_vms(payload)
else
status 404
end
else else
status 404 status 404
end end
@ -488,6 +463,15 @@ module Vmpooler
result[params[:hostname]]['disk'] = rdata['disk'].split(':') result[params[:hostname]]['disk'] = rdata['disk'].split(':')
end end
# Look up IP address of the hostname
begin
ipAddress = TCPSocket.gethostbyname(params[:hostname])[3]
rescue
ipAddress = ""
end
result[params[:hostname]]['ip'] = ipAddress
if config['domain'] if config['domain']
result[params[:hostname]]['domain'] = config['domain'] result[params[:hostname]]['domain'] = config['domain']
end end

View file

@ -1,15 +1,12 @@
module Vmpooler module Vmpooler
class PoolManager class PoolManager
def initialize(config, logger, redis, graphite = nil, statsd = nil) def initialize(config, logger, redis, graphite=nil)
$config = config $config = config
# Load logger library # Load logger library
$logger = logger $logger = logger
# statsd and graphite are mutex in the context of vmpooler unless graphite.nil?
if statsd
$statsd = statsd
elsif graphite
$graphite = graphite $graphite = graphite
end end
@ -261,8 +258,7 @@ module Vmpooler
$redis.decr('vmpooler__tasks__clone') $redis.decr('vmpooler__tasks__clone')
begin begin
$statsd.timing($config[:statsd]['prefix'] + ".clone.#{vm['template']}", finish) if $statsd $graphite.log($config[:graphite]['prefix'] + ".clone.#{vm['template']}", finish) if defined? $graphite
$graphite.log($config[:graphite]['prefix'] + ".clone.#{vm['template']}", finish) if $graphite
rescue rescue
end end
end end
@ -298,7 +294,7 @@ module Vmpooler
$logger.log('s', "[-] [#{pool}] '#{vm}' destroyed in #{finish} seconds") $logger.log('s', "[-] [#{pool}] '#{vm}' destroyed in #{finish} seconds")
$graphite.log($config[:graphite]['prefix'] + ".destroy.#{pool}", finish) if $graphite $graphite.log($config[:graphite]['prefix'] + ".destroy.#{pool}", finish) if defined? $graphite
end end
end end
end end
@ -569,10 +565,7 @@ module Vmpooler
total = $redis.scard('vmpooler__pending__' + pool['name']) + ready total = $redis.scard('vmpooler__pending__' + pool['name']) + ready
begin begin
if $statsd if defined? $graphite
$statsd.gauge($config[:statsd]['prefix'] + '.ready.' + pool['name'], $redis.scard('vmpooler__ready__' + pool['name']))
$statsd.gauge($config[:statsd]['prefix'] + '.running.' + pool['name'], $redis.scard('vmpooler__running__' + pool['name']))
elsif $graphite
$graphite.log($config[:graphite]['prefix'] + '.ready.' + pool['name'], $redis.scard('vmpooler__ready__' + pool['name'])) $graphite.log($config[:graphite]['prefix'] + '.ready.' + pool['name'], $redis.scard('vmpooler__ready__' + pool['name']))
$graphite.log($config[:graphite]['prefix'] + '.running.' + pool['name'], $redis.scard('vmpooler__running__' + pool['name'])) $graphite.log($config[:graphite]['prefix'] + '.running.' + pool['name'], $redis.scard('vmpooler__running__' + pool['name']))
end end

View file

@ -1,9 +0,0 @@
require 'rubygems' unless defined?(Gem)
module Vmpooler
class Statsd
def initialize(server = 'statsd', port = 8125)
@server = Statsd.new(server, port)
end
end
end

View file

@ -1,7 +1,12 @@
def expect_json( def redis
ok = true, unless @redis
http = 200 @redis = Redis.new
) @redis.select(15) # let's use the highest numbered database available in a default install
end
@redis
end
def expect_json(ok = true, http = 200)
expect(last_response.header['Content-Type']).to eq('application/json') expect(last_response.header['Content-Type']).to eq('application/json')
if (ok == true) then if (ok == true) then
@ -12,3 +17,64 @@ def expect_json(
expect(last_response.status).to eq(http) expect(last_response.status).to eq(http)
end end
def create_token(token, user, timestamp)
redis.hset("vmpooler__token__#{token}", 'user', user)
redis.hset("vmpooler__token__#{token}", 'created', timestamp)
end
def get_token_data(token)
redis.hgetall("vmpooler__token__#{token}")
end
def token_exists?(token)
result = get_token_data
result && !result.empty?
end
def create_ready_vm(template, name, token = nil)
create_vm(name, token)
redis.sadd("vmpooler__ready__#{template}", name)
# REMIND: should be __vm__?
redis.hset("vmpooler_vm_#{name}", "template", template)
end
def create_running_vm(template, name, token = nil)
create_vm(name, token)
redis.sadd("vmpooler__running__#{template}", name)
redis.hset("vmpooler__vm__#{name}", "template", template)
end
def create_vm(name, token = nil)
redis.hset("vmpooler__vm__#{name}", 'checkout', Time.now)
if token
redis.hset("vmpooler__vm__#{name}", 'token:token', token)
end
end
def fetch_vm(vm)
redis.hgetall("vmpooler__vm__#{vm}")
end
def snapshot_vm(vm, snapshot = '12345678901234567890123456789012')
redis.sadd('vmpooler__tasks__snapshot', "#{vm}:#{snapshot}")
redis.hset("vmpooler__vm__#{vm}", "snapshot:#{snapshot}", "1")
end
def has_vm_snapshot?(vm)
redis.smembers('vmpooler__tasks__snapshot').any? do |snapshot|
instance, sha = snapshot.split(':')
vm == instance
end
end
def vm_reverted_to_snapshot?(vm, snapshot = nil)
redis.smembers('vmpooler__tasks__snapshot-revert').any? do |action|
instance, sha = action.split(':')
instance == vm and (snapshot ? (sha == snapshot) : true)
end
end
def pool_has_ready_vm?(pool, vm)
!!redis.sismember('vmpooler__ready__' + pool, vm)
end

View file

@ -1,8 +1,5 @@
require 'simplecov'
SimpleCov.start do
add_filter '/spec/'
end
require 'helpers' require 'helpers'
require 'rbvmomi' require 'rbvmomi'
require 'rspec' require 'rspec'
require 'vmpooler' require 'vmpooler'
require 'redis'

View file

@ -0,0 +1,173 @@
require 'spec_helper'
require 'rack/test'
module Vmpooler
class API
module Helpers
def authenticate(auth, username_str, password_str)
username_str == 'admin' and password_str == 's3cr3t'
end
end
end
end
describe Vmpooler::API::V1 do
include Rack::Test::Methods
def app()
Vmpooler::API
end
describe '/token' do
let(:prefix) { '/api/v1' }
let(:current_time) { Time.now }
let(:config) { { } }
before do
app.settings.set :config, config
app.settings.set :redis, redis
end
describe 'GET /token' do
context '(auth not configured)' do
let(:config) { { auth: false } }
it 'returns a 404' do
get "#{prefix}/token"
expect_json(ok = false, http = 404)
end
end
context '(auth configured)' do
let(:config) { { auth: true } }
it 'returns a 401 if not authed' do
get "#{prefix}/token"
expect_json(ok = false, http = 401)
end
it 'returns a list of tokens if authed' do
create_token "abc", "admin", current_time
authorize 'admin', 's3cr3t'
get "#{prefix}/token"
expect_json(ok = true, http = 200)
expect(JSON.parse(last_response.body)['abc']['created']).to eq(current_time.to_s)
end
end
end
describe 'POST /token' do
context '(auth not configured)' do
let(:config) { { auth: false } }
it 'returns a 404' do
post "#{prefix}/token"
expect_json(ok = false, http = 404)
end
end
context '(auth configured)' do
let(:config) { { auth: true } }
it 'returns a 401 if not authed' do
post "#{prefix}/token"
expect_json(ok = false, http = 401)
end
it 'returns a newly created token if authed' do
authorize 'admin', 's3cr3t'
post "#{prefix}/token"
expect_json(ok = true, http = 200)
returned_token = JSON.parse(last_response.body)['token']
expect(returned_token.length).to be(32)
expect(get_token_data(returned_token)['user']).to eq("admin")
end
end
end
end
describe '/token/:token' do
let(:prefix) { '/api/v1' }
let(:current_time) { Time.now }
before do
app.settings.set :config, config
app.settings.set :redis, redis
end
def create_vm_for_token(token, pool, vm)
redis.sadd("vmpooler__running__#{pool}", vm)
redis.hset("vmpooler__vm__#{vm}", "token:token", token)
end
describe 'GET /token/:token' do
context '(auth not configured)' do
let(:config) { { auth: false } }
it 'returns a 404' do
get "#{prefix}/token/this"
expect_json(ok = false, http = 404)
end
end
context '(auth configured)' do
let(:config) { {
auth: true,
pools: [
{'name' => 'pool1', 'size' => 5}
]
} }
it 'returns a token' do
create_token "mytoken", "admin", current_time
create_vm_for_token "mytoken", "pool1", "vmhostname"
get "#{prefix}/token/mytoken"
expect_json(ok = true, http = 200)
expect(JSON.parse(last_response.body)['ok']).to eq(true)
expect(JSON.parse(last_response.body)['mytoken']['user']).to eq('admin')
expect(JSON.parse(last_response.body)['mytoken']['vms']['running']).to include('vmhostname')
end
end
end
describe 'DELETE /token/:token' do
context '(auth not configured)' do
let(:config) { { auth: false } }
it 'returns a 404' do
delete "#{prefix}/token/this"
expect_json(ok = false, http = 404)
end
end
context '(auth configured)' do
let(:config) { { auth: true } }
it 'returns a 401 if not authed' do
delete "#{prefix}/token/this"
expect_json(ok = false, http = 401)
end
it 'deletes a token if authed' do
create_token("mytoken", "admin", current_time)
authorize 'admin', 's3cr3t'
delete "#{prefix}/token/mytoken"
expect_json(ok = true, http = 200)
end
it 'fails if token does not exist' do
authorize 'admin', 's3cr3t'
delete "#{prefix}/token/missingtoken"
expect_json(ok = false, http = 401) # TODO: should this be 404?
end
end
end
end
end

View file

@ -0,0 +1,317 @@
require 'spec_helper'
require 'rack/test'
module Vmpooler
class API
module Helpers
def authenticate(auth, username_str, password_str)
username_str == 'admin' and password_str == 's3cr3t'
end
end
end
end
def has_set_tag?(vm, tag, value)
value == redis.hget("vmpooler__vm__#{vm}", "tag:#{tag}")
end
describe Vmpooler::API::V1 do
include Rack::Test::Methods
def app()
Vmpooler::API
end
describe '/vm/:hostname' do
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' },
}
}
let(:current_time) { Time.now }
before(:each) do
redis.flushdb
app.settings.set :config, config
app.settings.set :redis, redis
app.settings.set :config, auth: false
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
end
describe 'PUT /vm/:hostname' do
it 'allows tags to be set' do
create_vm('testhost')
put "#{prefix}/vm/testhost", '{"tags":{"tested_by":"rspec"}}'
expect_json(ok = true, http = 200)
expect has_set_tag?('testhost', 'tested_by', 'rspec')
end
it 'skips empty tags' do
create_vm('testhost')
put "#{prefix}/vm/testhost", '{"tags":{"tested_by":""}}'
expect_json(ok = true, http = 200)
expect !has_set_tag?('testhost', 'tested_by', '')
end
it 'does not set tags if request body format is invalid' do
create_vm('testhost')
put "#{prefix}/vm/testhost", '{"tags":{"tested"}}'
expect_json(ok = false, http = 400)
expect !has_set_tag?('testhost', 'tested', '')
end
context '(allowed_tags configured)' do
it 'fails if specified tag is not in allowed_tags array' do
app.settings.set :config,
{ :config => { 'allowed_tags' => ['created_by', 'project', 'url'] } }
create_vm('testhost')
put "#{prefix}/vm/testhost", '{"tags":{"created_by":"rspec","tested_by":"rspec"}}'
expect_json(ok = false, http = 400)
expect !has_set_tag?('testhost', 'tested_by', 'rspec')
end
end
context '(tagfilter configured)' do
let(:config) { {
tagfilter: { 'url' => '(.*)\/' },
} }
it 'correctly filters tags' do
create_vm('testhost')
put "#{prefix}/vm/testhost", '{"tags":{"url":"foo.com/something.html"}}'
expect_json(ok = true, http = 200)
expect has_set_tag?('testhost', 'url', 'foo.com')
end
it "doesn't eat tags not matching filter" do
create_vm('testhost')
put "#{prefix}/vm/testhost", '{"tags":{"url":"foo.com"}}'
expect_json(ok = true, http = 200)
expect has_set_tag?('testhost', 'url', 'foo.com')
end
end
context '(auth not configured)' do
let(:config) { { auth: false } }
it 'allows VM lifetime to be modified without a token' do
create_vm('testhost')
put "#{prefix}/vm/testhost", '{"lifetime":"1"}'
expect_json(ok = true, http = 200)
vm = fetch_vm('testhost')
expect(vm['lifetime'].to_i).to eq(1)
end
it 'does not allow a lifetime to be 0' do
create_vm('testhost')
put "#{prefix}/vm/testhost", '{"lifetime":"0"}'
expect_json(ok = false, http = 400)
vm = fetch_vm('testhost')
expect(vm['lifetime']).to be_nil
end
end
context '(auth configured)' do
before(:each) do
app.settings.set :config, auth: true
end
it 'allows VM lifetime to be modified with a token' do
create_vm('testhost')
put "#{prefix}/vm/testhost", '{"lifetime":"1"}', {
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
}
expect_json(ok = true, http = 200)
vm = fetch_vm('testhost')
expect(vm['lifetime'].to_i).to eq(1)
end
it 'does not allows VM lifetime to be modified without a token' do
create_vm('testhost')
put "#{prefix}/vm/testhost", '{"lifetime":"1"}'
expect_json(ok = false, http = 401)
end
end
end
describe 'DELETE /vm/:hostname' do
context '(auth not configured)' do
it 'does not delete a non-existant VM' do
delete "#{prefix}/vm/testhost"
expect_json(ok = false, http = 404)
end
it 'deletes an existing VM' do
create_running_vm('pool1', 'testhost')
expect fetch_vm('testhost')
delete "#{prefix}/vm/testhost"
expect_json(ok = true, http = 200)
expect !fetch_vm('testhost')
end
end
context '(auth configured)' do
before(:each) do
app.settings.set :config, auth: true
end
context '(checked-out without token)' do
it 'deletes a VM without supplying a token' do
create_running_vm('pool1', 'testhost')
expect fetch_vm('testhost')
delete "#{prefix}/vm/testhost"
expect_json(ok = true, http = 200)
expect !fetch_vm('testhost')
end
end
context '(checked-out with token)' do
it 'fails to delete a VM without supplying a token' do
create_running_vm('pool1', 'testhost', 'abcdefghijklmnopqrstuvwxyz012345')
expect fetch_vm('testhost')
delete "#{prefix}/vm/testhost"
expect_json(ok = false, http = 401)
expect fetch_vm('testhost')
end
it 'deletes a VM when token is supplied' do
create_running_vm('pool1', 'testhost', 'abcdefghijklmnopqrstuvwxyz012345')
expect fetch_vm('testhost')
delete "#{prefix}/vm/testhost", "", {
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
}
expect_json(ok = true, http = 200)
expect !fetch_vm('testhost')
end
end
end
end
describe 'POST /vm/:hostname/snapshot' do
context '(auth not configured)' do
it 'creates a snapshot' do
create_vm('testhost')
post "#{prefix}/vm/testhost/snapshot"
expect_json(ok = true, http = 202)
expect(JSON.parse(last_response.body)['testhost']['snapshot'].length).to be(32)
end
end
context '(auth configured)' do
before(:each) do
app.settings.set :config, auth: true
end
it 'returns a 401 if not authed' do
post "#{prefix}/vm/testhost/snapshot"
expect_json(ok = false, http = 401)
expect !has_vm_snapshot?('testhost')
end
it 'creates a snapshot if authed' do
create_vm('testhost')
snapshot_vm('testhost', 'testsnapshot')
post "#{prefix}/vm/testhost/snapshot", "", {
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
}
expect_json(ok = true, http = 202)
expect(JSON.parse(last_response.body)['testhost']['snapshot'].length).to be(32)
expect has_vm_snapshot?('testhost')
end
end
end
describe 'POST /vm/:hostname/snapshot/:snapshot' do
context '(auth not configured)' do
it 'reverts to a snapshot' do
create_vm('testhost')
snapshot_vm('testhost', 'testsnapshot')
post "#{prefix}/vm/testhost/snapshot/testsnapshot"
expect_json(ok = true, http = 202)
expect vm_reverted_to_snapshot?('testhost', 'testsnapshot')
end
it 'fails if the specified snapshot does not exist' do
create_vm('testhost')
post "#{prefix}/vm/testhost/snapshot/testsnapshot", "", {
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
}
expect_json(ok = false, http = 404)
expect !vm_reverted_to_snapshot?('testhost', 'testsnapshot')
end
end
context '(auth configured)' do
before(:each) do
app.settings.set :config, auth: true
end
it 'returns a 401 if not authed' do
create_vm('testhost')
snapshot_vm('testhost', 'testsnapshot')
post "#{prefix}/vm/testhost/snapshot/testsnapshot"
expect_json(ok = false, http = 401)
expect !vm_reverted_to_snapshot?('testhost', 'testsnapshot')
end
it 'fails if authed and the specified snapshot does not exist' do
create_vm('testhost')
post "#{prefix}/vm/testhost/snapshot/testsnapshot", "", {
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
}
expect_json(ok = false, http = 404)
expect !vm_reverted_to_snapshot?('testhost', 'testsnapshot')
end
it 'reverts to a snapshot if authed' do
create_vm('testhost')
snapshot_vm('testhost', 'testsnapshot')
post "#{prefix}/vm/testhost/snapshot/testsnapshot", "", {
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
}
expect_json(ok = true, http = 202)
expect vm_reverted_to_snapshot?('testhost', 'testsnapshot')
end
end
end
end
end

View file

@ -0,0 +1,327 @@
require 'spec_helper'
require 'rack/test'
module Vmpooler
class API
module Helpers
def authenticate(auth, username_str, password_str)
username_str == 'admin' and password_str == 's3cr3t'
end
end
end
end
describe Vmpooler::API::V1 do
include Rack::Test::Methods
def app()
Vmpooler::API
end
describe '/vm' do
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' },
pool_names: [ 'pool1', 'pool2', 'poolone' ]
}
}
let(:current_time) { Time.now }
before(:each) do
redis.flushdb
app.settings.set :config, config
app.settings.set :redis, redis
app.settings.set :config, auth: false
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
end
describe 'POST /vm' do
it 'returns a single VM' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm", '{"pool1":"1"}'
expect_json(ok = true, http = 200)
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
end
it 'returns a single VM for an alias' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm", '{"poolone":"1"}'
expect_json(ok = true, http = 200)
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
end
it 'fails on nonexistant pools' do
post "#{prefix}/vm", '{"poolpoolpool":"1"}'
expect_json(ok = false, http = 404)
end
it 'returns 503 for empty pool when aliases are not defined' do
Vmpooler::API.settings.config.delete(:alias)
Vmpooler::API.settings.config[:pool_names] = ['pool1', 'pool2']
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm/pool1"
post "#{prefix}/vm/pool1"
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
end
it 'returns 503 for empty pool referenced by alias' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm/poolone"
post "#{prefix}/vm/poolone"
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
end
it 'returns multiple VMs' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
create_ready_vm 'pool2', 'qrstuvwxyz012345'
post "#{prefix}/vm", '{"pool1":"1","pool2":"1"}'
expect_json(ok = true, http = 200)
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
},
pool2: {
hostname: 'qrstuvwxyz012345'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
end
it 'returns multiple VMs even when multiple instances from the same pool are requested' do
create_ready_vm 'pool1', '1abcdefghijklmnop'
create_ready_vm 'pool1', '2abcdefghijklmnop'
create_ready_vm 'pool2', 'qrstuvwxyz012345'
post "#{prefix}/vm", '{"pool1":"2","pool2":"1"}'
expected = {
ok: true,
pool1: {
hostname: [ '1abcdefghijklmnop', '2abcdefghijklmnop' ]
},
pool2: {
hostname: 'qrstuvwxyz012345'
}
}
result = JSON.parse(last_response.body)
expect(result['ok']).to eq(true)
expect(result['pool1']['hostname']).to include('1abcdefghijklmnop', '2abcdefghijklmnop')
expect(result['pool2']['hostname']).to eq('qrstuvwxyz012345')
expect_json(ok = true, http = 200)
end
it 'returns multiple VMs even when multiple instances from multiple pools are requested' do
create_ready_vm 'pool1', '1abcdefghijklmnop'
create_ready_vm 'pool1', '2abcdefghijklmnop'
create_ready_vm 'pool2', '1qrstuvwxyz012345'
create_ready_vm 'pool2', '2qrstuvwxyz012345'
create_ready_vm 'pool2', '3qrstuvwxyz012345'
post "#{prefix}/vm", '{"pool1":"2","pool2":"3"}'
expected = {
ok: true,
pool1: {
hostname: [ '1abcdefghijklmnop', '2abcdefghijklmnop' ]
},
pool2: {
hostname: [ '1qrstuvwxyz012345', '2qrstuvwxyz012345', '3qrstuvwxyz012345' ]
}
}
result = JSON.parse(last_response.body)
expect(result['ok']).to eq(true)
expect(result['pool1']['hostname']).to include('1abcdefghijklmnop', '2abcdefghijklmnop')
expect(result['pool2']['hostname']).to include('1qrstuvwxyz012345', '2qrstuvwxyz012345', '3qrstuvwxyz012345')
expect_json(ok = true, http = 200)
end
it 'fails when not all requested vms can be allocated' do
create_ready_vm 'pool1', '1abcdefghijklmnop'
post "#{prefix}/vm", '{"pool1":"1","pool2":"1"}'
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
end
it 'returns any checked out vms to their pools when not all requested vms can be allocated' do
create_ready_vm 'pool1', '1abcdefghijklmnop'
post "#{prefix}/vm", '{"pool1":"1","pool2":"1"}'
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
expect(pool_has_ready_vm?('pool1', '1abcdefghijklmnop')).to eq(true)
end
it 'fails when not all requested vms can be allocated, when requesting multiple instances from a pool' do
create_ready_vm 'pool1', '1abcdefghijklmnop'
post "#{prefix}/vm", '{"pool1":"2","pool2":"1"}'
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
end
it 'returns any checked out vms to their pools when not all requested vms can be allocated, when requesting multiple instances from a pool' do
create_ready_vm 'pool1', '1abcdefghijklmnop'
post "#{prefix}/vm", '{"pool1":"2","pool2":"1"}'
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
expect(pool_has_ready_vm?('pool1', '1abcdefghijklmnop')).to eq(true)
end
it 'fails when not all requested vms can be allocated, when requesting multiple instances from multiple pools' do
create_ready_vm 'pool1', '1abcdefghijklmnop'
post "#{prefix}/vm", '{"pool1":"2","pool2":"3"}'
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
end
it 'returns any checked out vms to their pools when not all requested vms can be allocated, when requesting multiple instances from multiple pools' do
create_ready_vm 'pool1', '1abcdefghijklmnop'
create_ready_vm 'pool1', '2abcdefghijklmnop'
post "#{prefix}/vm", '{"pool1":"2","pool2":"3"}'
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
expect(pool_has_ready_vm?('pool1', '1abcdefghijklmnop')).to eq(true)
expect(pool_has_ready_vm?('pool1', '2abcdefghijklmnop')).to eq(true)
end
context '(auth not configured)' do
it 'does not extend VM lifetime if auth token is provided' do
app.settings.set :config, auth: false
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm", '{"pool1":"1"}', {
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
}
expect_json(ok = true, http = 200)
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
vm = fetch_vm('abcdefghijklmnop')
expect(vm['lifetime']).to be_nil
end
end
context '(auth configured)' do
it 'extends VM lifetime if auth token is provided' do
app.settings.set :config, auth: true
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm", '{"pool1":"1"}', {
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
}
expect_json(ok = true, http = 200)
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
vm = fetch_vm('abcdefghijklmnop')
expect(vm['lifetime'].to_i).to eq(2)
end
it 'does not extend VM lifetime if auth token is not provided' do
app.settings.set :config, auth: true
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm", '{"pool1":"1"}'
expect_json(ok = true, http = 200)
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
vm = fetch_vm('abcdefghijklmnop')
expect(vm['lifetime']).to be_nil
end
end
end
end
end

View file

@ -0,0 +1,307 @@
require 'spec_helper'
require 'rack/test'
module Vmpooler
class API
module Helpers
def authenticate(auth, username_str, password_str)
username_str == 'admin' and password_str == 's3cr3t'
end
end
end
end
describe Vmpooler::API::V1 do
include Rack::Test::Methods
def app()
Vmpooler::API
end
describe '/vm/:template' do
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' },
pool_names: [ 'pool1', 'pool2', 'poolone' ]
}
}
let(:current_time) { Time.now }
before(:each) do
redis.flushdb
app.settings.set :config, config
app.settings.set :redis, redis
app.settings.set :config, auth: false
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
end
describe 'POST /vm/:template' do
it 'returns a single VM' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm/pool1", ''
expect_json(ok = true, http = 200)
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
end
it 'returns a single VM for an alias' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm/poolone", ''
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect_json(ok = true, http = 200)
expect(last_response.body).to eq(JSON.pretty_generate(expected))
end
it 'fails on nonexistant pools' do
post "#{prefix}/vm/poolpoolpool", ''
expect_json(ok = false, http = 404)
end
it 'returns 503 for empty pool when aliases are not defined' do
Vmpooler::API.settings.config.delete(:alias)
Vmpooler::API.settings.config[:pool_names] = ['pool1', 'pool2']
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm/pool1"
post "#{prefix}/vm/pool1"
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
end
it 'returns 503 for empty pool referenced by alias' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm/poolone"
post "#{prefix}/vm/poolone"
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
end
it 'returns multiple VMs' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
create_ready_vm 'pool2', 'qrstuvwxyz012345'
post "#{prefix}/vm/pool1+pool2", ''
expect_json(ok = true, http = 200)
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
},
pool2: {
hostname: 'qrstuvwxyz012345'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
end
it 'returns multiple VMs even when multiple instances from multiple pools are requested' do
create_ready_vm 'pool1', '1abcdefghijklmnop'
create_ready_vm 'pool1', '2abcdefghijklmnop'
create_ready_vm 'pool2', '1qrstuvwxyz012345'
create_ready_vm 'pool2', '2qrstuvwxyz012345'
create_ready_vm 'pool2', '3qrstuvwxyz012345'
post "#{prefix}/vm/pool1+pool1+pool2+pool2+pool2", ''
expected = {
ok: true,
pool1: {
hostname: [ '1abcdefghijklmnop', '2abcdefghijklmnop' ]
},
pool2: {
hostname: [ '1qrstuvwxyz012345', '2qrstuvwxyz012345', '3qrstuvwxyz012345' ]
}
}
result = JSON.parse(last_response.body)
expect(result['ok']).to eq(true)
expect(result['pool1']['hostname']).to include('1abcdefghijklmnop', '2abcdefghijklmnop')
expect(result['pool2']['hostname']).to include('1qrstuvwxyz012345', '2qrstuvwxyz012345', '3qrstuvwxyz012345')
expect_json(ok = true, http = 200)
end
it 'fails when not all requested vms can be allocated' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm/pool1+pool2", ''
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
end
it 'returns any checked out vms to their pools when not all requested vms can be allocated' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm/pool1+pool2", ''
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
expect(pool_has_ready_vm?('pool1', 'abcdefghijklmnop')).to eq(true)
end
it 'fails when not all requested vms can be allocated, when requesting multiple instances from a pool' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
create_ready_vm 'pool1', '0123456789012345'
post "#{prefix}/vm/pool1+pool1+pool2", ''
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
end
it 'returns any checked out vms to their pools when not all requested vms can be allocated, when requesting multiple instances from a pool' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
create_ready_vm 'pool1', '0123456789012345'
post "#{prefix}/vm/pool1+pool1+pool2", ''
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
expect(pool_has_ready_vm?('pool1', 'abcdefghijklmnop')).to eq(true)
expect(pool_has_ready_vm?('pool1', '0123456789012345')).to eq(true)
end
it 'fails when not all requested vms can be allocated, when requesting multiple instances from multiple pools' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
create_ready_vm 'pool2', '0123456789012345'
post "#{prefix}/vm/pool1+pool1+pool2+pool2+pool2", ''
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
end
it 'returns any checked out vms to their pools when not all requested vms can be allocated, when requesting multiple instances from multiple pools' do
create_ready_vm 'pool1', 'abcdefghijklmnop'
create_ready_vm 'pool2', '0123456789012345'
post "#{prefix}/vm/pool1+pool1+pool2+pool2+pool2", ''
expected = { ok: false }
expect(last_response.body).to eq(JSON.pretty_generate(expected))
expect_json(ok = false, http = 503)
expect(pool_has_ready_vm?('pool1', 'abcdefghijklmnop')).to eq(true)
expect(pool_has_ready_vm?('pool2', '0123456789012345')).to eq(true)
end
context '(auth not configured)' do
it 'does not extend VM lifetime if auth token is provided' do
app.settings.set :config, auth: false
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm/pool1", '', {
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
}
expect_json(ok = true, http = 200)
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
vm = fetch_vm('abcdefghijklmnop')
expect(vm['lifetime']).to be_nil
end
end
context '(auth configured)' do
it 'extends VM lifetime if auth token is provided' do
app.settings.set :config, auth: true
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm/pool1", '', {
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
}
expect_json(ok = true, http = 200)
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect(last_response.body).to eq(JSON.pretty_generate(expected))
vm = fetch_vm('abcdefghijklmnop')
expect(vm['lifetime'].to_i).to eq(2)
end
it 'does not extend VM lifetime if auth token is not provided' do
app.settings.set :config, auth: true
create_ready_vm 'pool1', 'abcdefghijklmnop'
post "#{prefix}/vm/pool1", ''
expected = {
ok: true,
pool1: {
hostname: 'abcdefghijklmnop'
}
}
expect_json(ok = true, http = 200)
expect(last_response.body).to eq(JSON.pretty_generate(expected))
vm = fetch_vm('abcdefghijklmnop')
expect(vm['lifetime']).to be_nil
end
end
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,10 @@ describe Vmpooler::API do
describe 'Dashboard' do describe 'Dashboard' do
before(:each) do
redis.flushdb
end
context '/' do context '/' do
before { get '/' } before { get '/' }
@ -38,7 +42,6 @@ describe Vmpooler::API do
it { expect(last_response.status).to eq(404) } it { expect(last_response.status).to eq(404) }
it { expect(last_response.header['Content-Type']).to eq('application/json') } it { expect(last_response.header['Content-Type']).to eq('application/json') }
it { expect(last_response.body).to eq(JSON.pretty_generate({ok: false})) } it { expect(last_response.body).to eq(JSON.pretty_generate({ok: false})) }
end end
describe '/dashboard/stats/vmpooler/pool' do describe '/dashboard/stats/vmpooler/pool' do
@ -49,7 +52,6 @@ describe Vmpooler::API do
], ],
graphite: {} graphite: {}
} } } }
let(:redis) { double('redis') }
before do before do
$config = config $config = config
@ -59,13 +61,12 @@ describe Vmpooler::API do
end end
context 'without history param' do context 'without history param' do
it 'returns basic JSON' do it 'returns basic JSON' do
allow(redis).to receive(:scard) create_ready_vm('pool1', 'vm1')
allow(redis).to receive(:scard).with('vmpooler__ready__pool1').and_return(3) create_ready_vm('pool1', 'vm2')
allow(redis).to receive(:scard).with('vmpooler__ready__pool2').and_return(2) create_ready_vm('pool1', 'vm3')
create_ready_vm('pool2', 'vm4')
expect(redis).to receive(:scard).twice create_ready_vm('pool2', 'vm5')
get '/dashboard/stats/vmpooler/pool' get '/dashboard/stats/vmpooler/pool'
@ -78,19 +79,15 @@ describe Vmpooler::API do
expect(last_response.body).to eq(JSON.pretty_generate(json_hash)) expect(last_response.body).to eq(JSON.pretty_generate(json_hash))
expect(last_response.header['Content-Type']).to eq('application/json') expect(last_response.header['Content-Type']).to eq('application/json')
end end
end end
context 'with history param' do context 'with history param' do
it 'returns JSON with null history when redis does not has values' do it 'returns JSON with zeroed history when redis does not have values' do
allow(redis).to receive(:scard)
expect(redis).to receive(:scard).exactly(4).times
get '/dashboard/stats/vmpooler/pool', :history => true get '/dashboard/stats/vmpooler/pool', :history => true
json_hash = { json_hash = {
pool1: {size: 5, ready: nil, history: [nil]}, pool1: {size: 5, ready: 0, history: [0]},
pool2: {size: 1, ready: nil, history: [nil]} pool2: {size: 1, ready: 0, history: [0]}
} }
expect(last_response).to be_ok expect(last_response).to be_ok
@ -99,10 +96,11 @@ describe Vmpooler::API do
end end
it 'returns JSON with history when redis has values' do it 'returns JSON with history when redis has values' do
allow(redis).to receive(:scard).with('vmpooler__ready__pool1').and_return(3) create_ready_vm('pool1', 'vm1')
allow(redis).to receive(:scard).with('vmpooler__ready__pool2').and_return(2) create_ready_vm('pool1', 'vm2')
create_ready_vm('pool1', 'vm3')
expect(redis).to receive(:scard).exactly(4).times create_ready_vm('pool2', 'vm4')
create_ready_vm('pool2', 'vm5')
get '/dashboard/stats/vmpooler/pool', :history => true get '/dashboard/stats/vmpooler/pool', :history => true
@ -115,9 +113,7 @@ describe Vmpooler::API do
expect(last_response.body).to eq(JSON.pretty_generate(json_hash)) expect(last_response.body).to eq(JSON.pretty_generate(json_hash))
expect(last_response.header['Content-Type']).to eq('application/json') expect(last_response.header['Content-Type']).to eq('application/json')
end end
end end
end end
describe '/dashboard/stats/vmpooler/running' do describe '/dashboard/stats/vmpooler/running' do
@ -129,7 +125,6 @@ describe Vmpooler::API do
], ],
graphite: {} graphite: {}
} } } }
let(:redis) { double('redis') }
before do before do
$config = config $config = config
@ -141,10 +136,6 @@ describe Vmpooler::API do
context 'without history param' do context 'without history param' do
it 'returns basic JSON' do it 'returns basic JSON' do
allow(redis).to receive(:scard)
expect(redis).to receive(:scard).exactly(3).times
get '/dashboard/stats/vmpooler/running' get '/dashboard/stats/vmpooler/running'
json_hash = {pool: {running: 0}, diffpool: {running: 0}} json_hash = {pool: {running: 0}, diffpool: {running: 0}}
@ -155,9 +146,18 @@ describe Vmpooler::API do
end end
it 'adds major correctly' do it 'adds major correctly' do
allow(redis).to receive(:scard).with('vmpooler__running__pool-1').and_return(3) create_running_vm('pool-1', 'vm1')
allow(redis).to receive(:scard).with('vmpooler__running__pool-2').and_return(5) create_running_vm('pool-1', 'vm2')
allow(redis).to receive(:scard).with('vmpooler__running__diffpool-1').and_return(2) create_running_vm('pool-1', 'vm3')
create_running_vm('pool-2', 'vm4')
create_running_vm('pool-2', 'vm5')
create_running_vm('pool-2', 'vm6')
create_running_vm('pool-2', 'vm7')
create_running_vm('pool-2', 'vm8')
create_running_vm('diffpool-1', 'vm9')
create_running_vm('diffpool-1', 'vm10')
get '/dashboard/stats/vmpooler/running' get '/dashboard/stats/vmpooler/running'
@ -167,10 +167,7 @@ describe Vmpooler::API do
expect(last_response.body).to eq(JSON.pretty_generate(json_hash)) expect(last_response.body).to eq(JSON.pretty_generate(json_hash))
expect(last_response.header['Content-Type']).to eq('application/json') expect(last_response.header['Content-Type']).to eq('application/json')
end end
end end
end end
end end
end end

View file

@ -5,12 +5,13 @@ describe 'Pool Manager' do
let(:logger) { double('logger') } let(:logger) { double('logger') }
let(:redis) { double('redis') } let(:redis) { double('redis') }
let(:config) { {} } let(:config) { {} }
let(:graphite) { nil }
let(:pool) { 'pool1' } let(:pool) { 'pool1' }
let(:vm) { 'vm1' } let(:vm) { 'vm1' }
let(:timeout) { 5 } let(:timeout) { 5 }
let(:host) { double('host') } let(:host) { double('host') }
subject { Vmpooler::PoolManager.new(config, logger, redis) } subject { Vmpooler::PoolManager.new(config, logger, redis, graphite) }
describe '#_check_pending_vm' do describe '#_check_pending_vm' do
let(:pool_helper) { double('pool') } let(:pool_helper) { double('pool') }
@ -251,86 +252,6 @@ describe 'Pool Manager' do
end end
end end
describe '#_stats_running_ready' do
let(:pool_helper) { double('pool') }
let(:vsphere) { {pool => pool_helper} }
let(:graphite) { double('graphite') }
let(:config) { {
config: { task_limit: 10 },
pools: [ {'name' => 'pool1', 'size' => 5} ],
graphite: { 'prefix' => 'vmpooler' }
} }
before do
expect(subject).not_to be_nil
$vsphere = vsphere
allow(logger).to receive(:log)
allow(pool_helper).to receive(:find_folder)
allow(redis).to receive(:smembers).and_return([])
allow(redis).to receive(:set)
allow(redis).to receive(:get).with('vmpooler__tasks__clone').and_return(0)
allow(redis).to receive(:get).with('vmpooler__empty__pool1').and_return(nil)
end
context 'graphite' do
let(:graphite) { double('graphite') }
subject { Vmpooler::PoolManager.new(config, logger, redis, graphite) }
it 'increments graphite when enabled and statsd disabled' do
allow(redis).to receive(:scard).with('vmpooler__ready__pool1').and_return(1)
allow(redis).to receive(:scard).with('vmpooler__cloning__pool1').and_return(0)
allow(redis).to receive(:scard).with('vmpooler__pending__pool1').and_return(0)
allow(redis).to receive(:scard).with('vmpooler__running__pool1').and_return(5)
expect(graphite).to receive(:log).with('vmpooler.ready.pool1', 1)
expect(graphite).to receive(:log).with('vmpooler.running.pool1', 5)
subject._check_pool(config[:pools][0])
end
it 'increments graphite when ready with 0 when pool empty and statsd disabled' do
allow(redis).to receive(:scard).with('vmpooler__ready__pool1').and_return(0)
allow(redis).to receive(:scard).with('vmpooler__cloning__pool1').and_return(0)
allow(redis).to receive(:scard).with('vmpooler__pending__pool1').and_return(0)
allow(redis).to receive(:scard).with('vmpooler__running__pool1').and_return(5)
expect(graphite).to receive(:log).with('vmpooler.ready.pool1', 0)
expect(graphite).to receive(:log).with('vmpooler.running.pool1', 5)
subject._check_pool(config[:pools][0])
end
end
context 'statsd' do
let(:statsd) { double('statsd') }
let(:config) { {
config: { task_limit: 10 },
pools: [ {'name' => 'pool1', 'size' => 5} ],
statsd: { 'prefix' => 'vmpooler' }
} }
subject { Vmpooler::PoolManager.new(config, logger, redis, graphite, statsd) }
it 'increments statsd when configured' do
allow(redis).to receive(:scard).with('vmpooler__ready__pool1').and_return(1)
allow(redis).to receive(:scard).with('vmpooler__cloning__pool1').and_return(0)
allow(redis).to receive(:scard).with('vmpooler__pending__pool1').and_return(0)
allow(redis).to receive(:scard).with('vmpooler__running__pool1').and_return(5)
expect(statsd).to receive(:gauge).with('vmpooler.ready.pool1', 1)
expect(statsd).to receive(:gauge).with('vmpooler.running.pool1', 5)
subject._check_pool(config[:pools][0])
end
it 'increments statsd ready with 0 when pool empty' do
allow(redis).to receive(:scard).with('vmpooler__running__pool1').and_return(1)
allow(redis).to receive(:scard).with('vmpooler__ready__pool1').and_return(0)
allow(redis).to receive(:scard).with('vmpooler__pending__pool1').and_return(0)
allow(statsd).to receive(:gauge).with('vmpooler.running.pool1', 1)
expect(statsd).to receive(:gauge).with('vmpooler.ready.pool1', 0)
subject._check_pool(config[:pools][0])
end
end
end
describe '#_create_vm_snapshot' do describe '#_create_vm_snapshot' do
let(:snapshot_manager) { 'snapshot_manager' } let(:snapshot_manager) { 'snapshot_manager' }
let(:pool_helper) { double('snapshot_manager') } let(:pool_helper) { double('snapshot_manager') }

View file

@ -9,19 +9,10 @@ config = Vmpooler.config
redis_host = config[:redis]['server'] redis_host = config[:redis]['server']
logger_file = config[:config]['logfile'] logger_file = config[:config]['logfile']
graphite = config[:graphite]['server'] ? config[:graphite]['server'] : nil graphite = config[:graphite]['server'] ? config[:graphite]['server'] : nil
# statsd is an addition and my not be present in YAML configuration
if config[:statsd]
statsd = config[:statsd]['server'] ? config[:statsd]['server'] : nil
statsd_port = config[:statsd]['port'] ? config[:statsd]['port'] : 8125
end
api = Thread.new { api = Thread.new {
thr = Vmpooler::API.new thr = Vmpooler::API.new
if statsd thr.helpers.configure(config, Vmpooler.new_redis(redis_host))
thr.helpers.configure(config, Vmpooler.new_redis(redis_host), Vmpooler.new_statsd(statsd, statsd_port))
else
thr.helpers.configure(config, Vmpooler.new_redis(redis_host), statsd=nil)
end
thr.helpers.execute! thr.helpers.execute!
} }
@ -30,8 +21,7 @@ manager = Thread.new {
config, config,
Vmpooler.new_logger(logger_file), Vmpooler.new_logger(logger_file),
Vmpooler.new_redis(redis_host), Vmpooler.new_redis(redis_host),
Vmpooler.new_graphite(graphite), Vmpooler.new_graphite(graphite)
Vmpooler.new_statsd(statsd, statsd_port)
).execute! ).execute!
} }

View file

@ -53,39 +53,10 @@
:redis: :redis:
server: 'redis.company.com' server: 'redis.company.com'
# :statsd:
#
# This section contains the connection information required to store
# historical data via statsd. This is mutually exclusive with graphite
# and takes precedence.
#
# Available configuration parameters:
#
# - server
# The FQDN hostname of the statsd daemon.
# (optional)
#
# - prefix
# The prefix to use while storing statsd data.
# (optional; default: 'vmpooler')
#
# - port
# The UDP port to communicate with statsd daemon.
# (optional; default: 8125)
# Example:
:statsd:
server: 'statsd.company.com'
prefix: 'vmpooler'
port: 8125
# :graphite: # :graphite:
# #
# This section contains the connection information required to store # This section contains the connection information required to store
# historical data in an external Graphite database. This is mutually exclusive # historical data in an external Graphite database.
# with statsd.
# #
# Available configuration parameters: # Available configuration parameters:
# #