mirror of
https://github.com/puppetlabs/vmpooler.git
synced 2026-01-27 02:18:41 -05:00
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:
commit
4738a0b8b9
20 changed files with 1299 additions and 1268 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.ruby-version
|
||||||
|
Gemfile.lock
|
||||||
|
vendor
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
7
Gemfile
7
Gemfile
|
|
@ -1,18 +1,21 @@
|
||||||
source ENV['GEM_SOURCE'] || 'https://rubygems.org'
|
source ENV['GEM_SOURCE'] || 'https://rubygems.org'
|
||||||
|
|
||||||
|
if RUBY_VERSION =~ /^1\.9\./
|
||||||
|
gem 'json', '~> 1.8'
|
||||||
|
else
|
||||||
gem 'json', '>= 1.8'
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
newhash = {}
|
|
||||||
|
|
||||||
hash.each do |key, val|
|
|
||||||
if Vmpooler::API.settings.config[:alias][key]
|
|
||||||
key = Vmpooler::API.settings.config[:alias][key]
|
|
||||||
end
|
|
||||||
|
|
||||||
if backend.exists('vmpooler__ready__' + key)
|
|
||||||
newhash[key] = val
|
|
||||||
elsif backend.exists('vmpooler__empty__' + key)
|
|
||||||
newhash['empty'] = (newhash['empty'] || 0) + val.to_i
|
|
||||||
else
|
|
||||||
newhash['invalid'] = (newhash['invalid'] || 0) + val.to_i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
newhash
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_single_vm(template)
|
def fetch_single_vm(template)
|
||||||
backend.spop('vmpooler__ready__' + template)
|
vm = backend.spop('vmpooler__ready__' + template)
|
||||||
|
|
||||||
|
return [vm, template] if vm
|
||||||
|
|
||||||
|
aliases = Vmpooler::API.settings.config[:alias]
|
||||||
|
if aliases && aliased_template = aliases[template]
|
||||||
|
vm = backend.spop('vmpooler__ready__' + aliased_template)
|
||||||
|
|
||||||
|
return [vm, aliased_template] if vm
|
||||||
|
end
|
||||||
|
|
||||||
|
[nil, nil]
|
||||||
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,24 +392,25 @@ 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
|
|
||||||
statsd.increment(statsd_prefix + '.checkout.invalid', invalid) if invalid
|
|
||||||
unless payload.empty?
|
|
||||||
result = atomically_allocate_vms(payload)
|
result = atomically_allocate_vms(payload)
|
||||||
else
|
else
|
||||||
status 404
|
status 404
|
||||||
end
|
end
|
||||||
else
|
|
||||||
status 404
|
|
||||||
end
|
|
||||||
|
|
||||||
JSON.pretty_generate(result)
|
JSON.pretty_generate(result)
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
173
spec/vmpooler/api/v1/token_spec.rb
Normal file
173
spec/vmpooler/api/v1/token_spec.rb
Normal 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
|
||||||
317
spec/vmpooler/api/v1/vm_hostname_spec.rb
Normal file
317
spec/vmpooler/api/v1/vm_hostname_spec.rb
Normal 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
|
||||||
327
spec/vmpooler/api/v1/vm_spec.rb
Normal file
327
spec/vmpooler/api/v1/vm_spec.rb
Normal 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
|
||||||
307
spec/vmpooler/api/v1/vm_template_spec.rb
Normal file
307
spec/vmpooler/api/v1/vm_template_spec.rb
Normal 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
|
|
@ -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
|
||||||
|
|
@ -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') }
|
||||||
|
|
|
||||||
14
vmpooler
14
vmpooler
|
|
@ -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!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
#
|
#
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue