mirror of
https://github.com/puppetlabs/vmpooler.git
synced 2026-01-26 10:08:40 -05:00
Merge pull request #98 from sschneid/tokens_ftw
Add basic auth token functionality
This commit is contained in:
commit
c4ad99c493
5 changed files with 430 additions and 67 deletions
55
README.md
55
README.md
|
|
@ -70,6 +70,57 @@ vmpooler provides an API and web front-end (dashboard) on port `:4567`. See the
|
||||||
|
|
||||||
vmpooler provides a REST API for VM management. The following examples use `curl` for communication.
|
vmpooler provides a REST API for VM management. The following examples use `curl` for communication.
|
||||||
|
|
||||||
|
#### Token operations
|
||||||
|
|
||||||
|
Token-based authentication can be used when requesting or modifying VMs. The `/token` route can be used to create, query, or delete tokens. See the provided YAML configuration example, [vmpooler.yaml.example](vmpooler.yaml.example), for information on configuring an authentication store to use when performing token operations.
|
||||||
|
|
||||||
|
##### GET /token/<token>
|
||||||
|
|
||||||
|
Get information about an existing token.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl -u sschneid --url vmpooler.company.com/token/utpg2i2xswor6h8ttjhu3d47z53yy47y
|
||||||
|
Enter host password for user 'sschneid':
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"utpg2i2xswor6h8ttjhu3d47z53yy47y": {
|
||||||
|
"user": "sschneid",
|
||||||
|
"timestamp": "2015-04-28 19:17:47 -0700"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### POST /token
|
||||||
|
|
||||||
|
Generate a new authentication token.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl -X POST -u sschneid --url vmpooler.company.com/token
|
||||||
|
Enter host password for user 'sschneid':
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"token": "utpg2i2xswor6h8ttjhu3d47z53yy47y"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### DELETE /token/<token>
|
||||||
|
|
||||||
|
Delete an authentication token.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl -X DELETE -u sschneid --url vmpooler.company.com/token/utpg2i2xswor6h8ttjhu3d47z53yy47y
|
||||||
|
Enter host password for user 'sschneid':
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### VM operations
|
#### VM operations
|
||||||
|
|
||||||
##### GET /vm
|
##### GET /vm
|
||||||
|
|
@ -90,6 +141,8 @@ $ curl --url vmpooler.company.com/vm
|
||||||
|
|
||||||
Useful for batch operations; post JSON (see format below), get back VMs.
|
Useful for batch operations; post JSON (see format below), get back VMs.
|
||||||
|
|
||||||
|
If an authentication store is configured, an authentication token supplied via the `X-AUTH-TOKEN` HTTP header will modify a VM's default lifetime. See the provided YAML configuration example, [vmpooler.yaml.example](vmpooler.yaml.example), and the 'token operations' section above for more information.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ curl -d '{"debian-7-i386":"2","debian-7-x86_64":"1"}' --url vmpooler.company.com/vm
|
$ curl -d '{"debian-7-i386":"2","debian-7-x86_64":"1"}' --url vmpooler.company.com/vm
|
||||||
```
|
```
|
||||||
|
|
@ -177,6 +230,8 @@ parameter | description | required structure
|
||||||
|
|
||||||
Any modifications can be verified using the [GET /vm/<hostname>](#get-vmhostname) endpoint.
|
Any modifications can be verified using the [GET /vm/<hostname>](#get-vmhostname) endpoint.
|
||||||
|
|
||||||
|
If an authentication store is configured, an authentication token is required (via the `X-AUTH-TOKEN` HTTP header) to access this route. See the provided YAML configuration example, [vmpooler.yaml.example](vmpooler.yaml.example), and the 'token operations' section above for more information.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ curl -X PUT -d '{"lifetime":"2"}' --url vmpooler.company.com/vm/fq6qlpjlsskycq6
|
$ curl -X PUT -d '{"lifetime":"2"}' --url vmpooler.company.com/vm/fq6qlpjlsskycq6
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,28 @@ module Vmpooler
|
||||||
|
|
||||||
module Helpers
|
module Helpers
|
||||||
|
|
||||||
def protected!
|
def has_token?
|
||||||
|
request.env['HTTP_X_AUTH_TOKEN'].nil? ? false : true
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_token?(backend)
|
||||||
|
return false unless has_token?
|
||||||
|
|
||||||
|
backend.exists('vmpooler__token__' + request.env['HTTP_X_AUTH_TOKEN']) ? true : false
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_token(backend)
|
||||||
|
return if valid_token?(backend)
|
||||||
|
|
||||||
|
content_type :json
|
||||||
|
|
||||||
|
result = { 'ok' => false }
|
||||||
|
|
||||||
|
headers['WWW-Authenticate'] = 'Basic realm="Authentication required"'
|
||||||
|
halt 401, JSON.pretty_generate(result)
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_auth(backend)
|
||||||
return if authorized?
|
return if authorized?
|
||||||
|
|
||||||
content_type :json
|
content_type :json
|
||||||
|
|
@ -75,11 +96,11 @@ module Vmpooler
|
||||||
hostname
|
hostname
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_task_times(redis, task, date_str)
|
def get_task_times(backend, task, date_str)
|
||||||
redis.hvals("vmpooler__#{task}__" + date_str).map(&:to_f)
|
backend.hvals("vmpooler__#{task}__" + date_str).map(&:to_f)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_capacity_metrics(pools, redis)
|
def get_capacity_metrics(pools, backend)
|
||||||
capacity = {
|
capacity = {
|
||||||
current: 0,
|
current: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
|
@ -87,7 +108,7 @@ module Vmpooler
|
||||||
}
|
}
|
||||||
|
|
||||||
pools.each do |pool|
|
pools.each do |pool|
|
||||||
pool['capacity'] = redis.scard('vmpooler__ready__' + pool['name']).to_i
|
pool['capacity'] = backend.scard('vmpooler__ready__' + pool['name']).to_i
|
||||||
|
|
||||||
capacity[:current] += pool['capacity']
|
capacity[:current] += pool['capacity']
|
||||||
capacity[:total] += pool['size'].to_i
|
capacity[:total] += pool['size'].to_i
|
||||||
|
|
@ -100,7 +121,7 @@ module Vmpooler
|
||||||
capacity
|
capacity
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_queue_metrics(pools, redis)
|
def get_queue_metrics(pools, backend)
|
||||||
queue = {
|
queue = {
|
||||||
pending: 0,
|
pending: 0,
|
||||||
cloning: 0,
|
cloning: 0,
|
||||||
|
|
@ -112,13 +133,13 @@ module Vmpooler
|
||||||
}
|
}
|
||||||
|
|
||||||
pools.each do |pool|
|
pools.each do |pool|
|
||||||
queue[:pending] += redis.scard('vmpooler__pending__' + pool['name']).to_i
|
queue[:pending] += backend.scard('vmpooler__pending__' + pool['name']).to_i
|
||||||
queue[:ready] += redis.scard('vmpooler__ready__' + pool['name']).to_i
|
queue[:ready] += backend.scard('vmpooler__ready__' + pool['name']).to_i
|
||||||
queue[:running] += redis.scard('vmpooler__running__' + pool['name']).to_i
|
queue[:running] += backend.scard('vmpooler__running__' + pool['name']).to_i
|
||||||
queue[:completed] += redis.scard('vmpooler__completed__' + pool['name']).to_i
|
queue[:completed] += backend.scard('vmpooler__completed__' + pool['name']).to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
queue[:cloning] = redis.get('vmpooler__tasks__clone').to_i
|
queue[:cloning] = backend.get('vmpooler__tasks__clone').to_i
|
||||||
queue[:booting] = queue[:pending].to_i - queue[:cloning].to_i
|
queue[:booting] = queue[:pending].to_i - queue[:cloning].to_i
|
||||||
queue[:booting] = 0 if queue[:booting] < 0
|
queue[:booting] = 0 if queue[:booting] < 0
|
||||||
queue[:total] = queue[:pending].to_i + queue[:ready].to_i + queue[:running].to_i + queue[:completed].to_i
|
queue[:total] = queue[:pending].to_i + queue[:ready].to_i + queue[:running].to_i + queue[:completed].to_i
|
||||||
|
|
@ -126,7 +147,7 @@ module Vmpooler
|
||||||
queue
|
queue
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_task_metrics(redis, task_str, date_str, opts = {})
|
def get_task_metrics(backend, task_str, date_str, opts = {})
|
||||||
opts = {:bypool => false}.merge(opts)
|
opts = {:bypool => false}.merge(opts)
|
||||||
|
|
||||||
task = {
|
task = {
|
||||||
|
|
@ -141,7 +162,7 @@ module Vmpooler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task[:count][:total] = redis.hlen('vmpooler__' + task_str + '__' + date_str).to_i
|
task[:count][:total] = backend.hlen('vmpooler__' + task_str + '__' + date_str).to_i
|
||||||
|
|
||||||
if task[:count][:total] > 0
|
if task[:count][:total] > 0
|
||||||
if opts[:bypool] == true
|
if opts[:bypool] == true
|
||||||
|
|
@ -150,7 +171,7 @@ module Vmpooler
|
||||||
task[:count][:pool] = {}
|
task[:count][:pool] = {}
|
||||||
task[:duration][:pool] = {}
|
task[:duration][:pool] = {}
|
||||||
|
|
||||||
redis.hgetall('vmpooler__' + task_str + '__' + date_str).each do |key, value|
|
backend.hgetall('vmpooler__' + task_str + '__' + date_str).each do |key, value|
|
||||||
pool = 'unknown'
|
pool = 'unknown'
|
||||||
hostname = 'unknown'
|
hostname = 'unknown'
|
||||||
|
|
||||||
|
|
@ -176,7 +197,7 @@ module Vmpooler
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
task_times = get_task_times(redis, task_str, date_str)
|
task_times = get_task_times(backend, task_str, date_str)
|
||||||
|
|
||||||
task[:duration][:total] = task_times.reduce(:+).to_f
|
task[:duration][:total] = task_times.reduce(:+).to_f
|
||||||
task[:duration][:average] = (task[:duration][:total] / task[:count][:total]).round(1)
|
task[:duration][:average] = (task[:duration][:total] / task[:count][:total]).round(1)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,30 @@ module Vmpooler
|
||||||
include Vmpooler::API::Helpers
|
include Vmpooler::API::Helpers
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def backend
|
||||||
|
Vmpooler::API.settings.redis
|
||||||
|
end
|
||||||
|
|
||||||
|
def config
|
||||||
|
Vmpooler::API.settings.config[:config]
|
||||||
|
end
|
||||||
|
|
||||||
|
def pools
|
||||||
|
Vmpooler::API.settings.config[:pools]
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_valid_token?
|
||||||
|
valid_token?(backend)
|
||||||
|
end
|
||||||
|
|
||||||
|
def need_auth!
|
||||||
|
validate_auth(backend)
|
||||||
|
end
|
||||||
|
|
||||||
|
def need_token!
|
||||||
|
validate_token(backend)
|
||||||
|
end
|
||||||
|
|
||||||
get "#{api_prefix}/status/?" do
|
get "#{api_prefix}/status/?" do
|
||||||
content_type :json
|
content_type :json
|
||||||
|
|
||||||
|
|
@ -18,14 +42,14 @@ module Vmpooler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result[:capacity] = get_capacity_metrics(Vmpooler::API.settings.config[:pools], Vmpooler::API.settings.redis)
|
result[:capacity] = get_capacity_metrics(pools, backend)
|
||||||
result[:queue] = get_queue_metrics(Vmpooler::API.settings.config[:pools], Vmpooler::API.settings.redis)
|
result[:queue] = get_queue_metrics(pools, backend)
|
||||||
result[:clone] = get_task_metrics(Vmpooler::API.settings.redis, 'clone', Date.today.to_s)
|
result[:clone] = get_task_metrics(backend, 'clone', Date.today.to_s)
|
||||||
result[:boot] = get_task_metrics(Vmpooler::API.settings.redis, 'boot', Date.today.to_s)
|
result[:boot] = get_task_metrics(backend, 'boot', Date.today.to_s)
|
||||||
|
|
||||||
# Check for empty pools
|
# Check for empty pools
|
||||||
Vmpooler::API.settings.config[:pools].each do |pool|
|
pools.each do |pool|
|
||||||
if Vmpooler::API.settings.redis.scard('vmpooler__ready__' + pool['name']).to_i == 0
|
if backend.scard('vmpooler__ready__' + pool['name']).to_i == 0
|
||||||
result[:status][:empty] ||= []
|
result[:status][:empty] ||= []
|
||||||
result[:status][:empty].push(pool['name'])
|
result[:status][:empty].push(pool['name'])
|
||||||
|
|
||||||
|
|
@ -99,8 +123,8 @@ module Vmpooler
|
||||||
(from_date..to_date).each do |date|
|
(from_date..to_date).each do |date|
|
||||||
daily = {
|
daily = {
|
||||||
date: date.to_s,
|
date: date.to_s,
|
||||||
boot: get_task_metrics(Vmpooler::API.settings.redis, 'boot', date.to_s, :bypool => true),
|
boot: get_task_metrics(backend, 'boot', date.to_s, :bypool => true),
|
||||||
clone: get_task_metrics(Vmpooler::API.settings.redis, 'clone', date.to_s, :bypool => true)
|
clone: get_task_metrics(backend, 'clone', date.to_s, :bypool => true)
|
||||||
}
|
}
|
||||||
|
|
||||||
result[:daily].push(daily)
|
result[:daily].push(daily)
|
||||||
|
|
@ -182,9 +206,9 @@ module Vmpooler
|
||||||
if Vmpooler::API.settings.config[:auth]
|
if Vmpooler::API.settings.config[:auth]
|
||||||
status 401
|
status 401
|
||||||
|
|
||||||
protected!
|
need_auth!
|
||||||
|
|
||||||
token = Vmpooler::API.settings.redis.hgetall('vmpooler__token__' + params[:token])
|
token = backend.hgetall('vmpooler__token__' + params[:token])
|
||||||
|
|
||||||
if not token.nil? and not token.empty?
|
if not token.nil? and not token.empty?
|
||||||
status 200
|
status 200
|
||||||
|
|
@ -206,9 +230,9 @@ module Vmpooler
|
||||||
if Vmpooler::API.settings.config[:auth]
|
if Vmpooler::API.settings.config[:auth]
|
||||||
status 401
|
status 401
|
||||||
|
|
||||||
protected!
|
need_auth!
|
||||||
|
|
||||||
if Vmpooler::API.settings.redis.del('vmpooler__token__' + params[:token]).to_i > 0
|
if backend.del('vmpooler__token__' + params[:token]).to_i > 0
|
||||||
status 200
|
status 200
|
||||||
result['ok'] = true
|
result['ok'] = true
|
||||||
end
|
end
|
||||||
|
|
@ -226,13 +250,13 @@ module Vmpooler
|
||||||
if Vmpooler::API.settings.config[:auth]
|
if Vmpooler::API.settings.config[:auth]
|
||||||
status 401
|
status 401
|
||||||
|
|
||||||
protected!
|
need_auth!
|
||||||
|
|
||||||
o = [('a'..'z'), ('0'..'9')].map(&:to_a).flatten
|
o = [('a'..'z'), ('0'..'9')].map(&:to_a).flatten
|
||||||
result['token'] = o[rand(25)] + (0...31).map { o[rand(o.length)] }.join
|
result['token'] = o[rand(25)] + (0...31).map { o[rand(o.length)] }.join
|
||||||
|
|
||||||
Vmpooler::API.settings.redis.hset('vmpooler__token__' + result['token'], 'user', @auth.username)
|
backend.hset('vmpooler__token__' + result['token'], 'user', @auth.username)
|
||||||
Vmpooler::API.settings.redis.hset('vmpooler__token__' + result['token'], 'timestamp', Time.now)
|
backend.hset('vmpooler__token__' + result['token'], 'timestamp', Time.now)
|
||||||
|
|
||||||
status 200
|
status 200
|
||||||
result['ok'] = true
|
result['ok'] = true
|
||||||
|
|
@ -246,7 +270,7 @@ module Vmpooler
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
Vmpooler::API.settings.config[:pools].each do |pool|
|
pools.each do |pool|
|
||||||
result.push(pool['name'])
|
result.push(pool['name'])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -263,7 +287,7 @@ module Vmpooler
|
||||||
jdata = JSON.parse(request.body.read)
|
jdata = JSON.parse(request.body.read)
|
||||||
|
|
||||||
jdata.each do |key, val|
|
jdata.each do |key, val|
|
||||||
if Vmpooler::API.settings.redis.scard('vmpooler__ready__' + key) < val.to_i
|
if backend.scard('vmpooler__ready__' + key).to_i < val.to_i
|
||||||
available = 0
|
available = 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -277,12 +301,16 @@ module Vmpooler
|
||||||
result[key]['ok'] = true ##
|
result[key]['ok'] = true ##
|
||||||
|
|
||||||
val.to_i.times do |_i|
|
val.to_i.times do |_i|
|
||||||
vm = Vmpooler::API.settings.redis.spop('vmpooler__ready__' + key)
|
vm = backend.spop('vmpooler__ready__' + key)
|
||||||
|
|
||||||
unless vm.nil?
|
unless vm.nil?
|
||||||
Vmpooler::API.settings.redis.sadd('vmpooler__running__' + key, vm)
|
backend.sadd('vmpooler__running__' + key, vm)
|
||||||
Vmpooler::API.settings.redis.hset('vmpooler__active__' + key, vm, Time.now)
|
backend.hset('vmpooler__active__' + key, vm, Time.now)
|
||||||
Vmpooler::API.settings.redis.hset('vmpooler__vm__' + vm, 'checkout', Time.now)
|
backend.hset('vmpooler__vm__' + vm, 'checkout', Time.now)
|
||||||
|
|
||||||
|
if Vmpooler::API.settings.config[:auth] and has_valid_token? and config['vm_lifetime_auth'].to_i > 0
|
||||||
|
backend.hset('vmpooler__vm__' + vm, 'lifetime', config['vm_lifetime_auth'].to_i)
|
||||||
|
end
|
||||||
|
|
||||||
result[key] ||= {}
|
result[key] ||= {}
|
||||||
|
|
||||||
|
|
@ -307,8 +335,8 @@ module Vmpooler
|
||||||
result['ok'] = false
|
result['ok'] = false
|
||||||
end
|
end
|
||||||
|
|
||||||
if result['ok'] && Vmpooler::API.settings.config[:config]['domain']
|
if result['ok'] && config['domain']
|
||||||
result['domain'] = Vmpooler::API.settings.config[:config]['domain']
|
result['domain'] = config['domain']
|
||||||
end
|
end
|
||||||
|
|
||||||
JSON.pretty_generate(result)
|
JSON.pretty_generate(result)
|
||||||
|
|
@ -328,7 +356,7 @@ module Vmpooler
|
||||||
available = 1
|
available = 1
|
||||||
|
|
||||||
request.keys.each do |template|
|
request.keys.each do |template|
|
||||||
if Vmpooler::API.settings.redis.scard('vmpooler__ready__' + template) < request[template]
|
if backend.scard('vmpooler__ready__' + template) < request[template]
|
||||||
available = 0
|
available = 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -341,12 +369,12 @@ module Vmpooler
|
||||||
|
|
||||||
result[template]['ok'] = true ##
|
result[template]['ok'] = true ##
|
||||||
|
|
||||||
vm = Vmpooler::API.settings.redis.spop('vmpooler__ready__' + template)
|
vm = backend.spop('vmpooler__ready__' + template)
|
||||||
|
|
||||||
unless vm.nil?
|
unless vm.nil?
|
||||||
Vmpooler::API.settings.redis.sadd('vmpooler__running__' + template, vm)
|
backend.sadd('vmpooler__running__' + template, vm)
|
||||||
Vmpooler::API.settings.redis.hset('vmpooler__active__' + template, vm, Time.now)
|
backend.hset('vmpooler__active__' + template, vm, Time.now)
|
||||||
Vmpooler::API.settings.redis.hset('vmpooler__vm__' + vm, 'checkout', Time.now)
|
backend.hset('vmpooler__vm__' + vm, 'checkout', Time.now)
|
||||||
|
|
||||||
result[template] ||= {}
|
result[template] ||= {}
|
||||||
|
|
||||||
|
|
@ -368,8 +396,8 @@ module Vmpooler
|
||||||
result['ok'] = false
|
result['ok'] = false
|
||||||
end
|
end
|
||||||
|
|
||||||
if result['ok'] && Vmpooler::API.settings.config[:config]['domain']
|
if result['ok'] && config['domain']
|
||||||
result['domain'] = Vmpooler::API.settings.config[:config]['domain']
|
result['domain'] = config['domain']
|
||||||
end
|
end
|
||||||
|
|
||||||
JSON.pretty_generate(result)
|
JSON.pretty_generate(result)
|
||||||
|
|
@ -383,18 +411,18 @@ module Vmpooler
|
||||||
status 404
|
status 404
|
||||||
result['ok'] = false
|
result['ok'] = false
|
||||||
|
|
||||||
params[:hostname] = hostname_shorten(params[:hostname], Vmpooler::API.settings.config[:config]['domain'])
|
params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
|
||||||
|
|
||||||
if Vmpooler::API.settings.redis.exists('vmpooler__vm__' + params[:hostname])
|
if backend.exists('vmpooler__vm__' + params[:hostname])
|
||||||
status 200
|
status 200
|
||||||
result['ok'] = true
|
result['ok'] = true
|
||||||
|
|
||||||
rdata = Vmpooler::API.settings.redis.hgetall('vmpooler__vm__' + params[:hostname])
|
rdata = backend.hgetall('vmpooler__vm__' + params[:hostname])
|
||||||
|
|
||||||
result[params[:hostname]] = {}
|
result[params[:hostname]] = {}
|
||||||
|
|
||||||
result[params[:hostname]]['template'] = rdata['template']
|
result[params[:hostname]]['template'] = rdata['template']
|
||||||
result[params[:hostname]]['lifetime'] = (rdata['lifetime'] || Vmpooler::API.settings.config[:config]['vm_lifetime']).to_i
|
result[params[:hostname]]['lifetime'] = (rdata['lifetime'] || config['vm_lifetime']).to_i
|
||||||
|
|
||||||
if rdata['destroy']
|
if rdata['destroy']
|
||||||
result[params[:hostname]]['running'] = ((Time.parse(rdata['destroy']) - Time.parse(rdata['checkout'])) / 60 / 60).round(2)
|
result[params[:hostname]]['running'] = ((Time.parse(rdata['destroy']) - Time.parse(rdata['checkout'])) / 60 / 60).round(2)
|
||||||
|
|
@ -409,8 +437,8 @@ module Vmpooler
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if Vmpooler::API.settings.config[:config]['domain']
|
if config['domain']
|
||||||
result[params[:hostname]]['domain'] = Vmpooler::API.settings.config[:config]['domain']
|
result[params[:hostname]]['domain'] = config['domain']
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -425,12 +453,12 @@ module Vmpooler
|
||||||
status 404
|
status 404
|
||||||
result['ok'] = false
|
result['ok'] = false
|
||||||
|
|
||||||
params[:hostname] = hostname_shorten(params[:hostname], Vmpooler::API.settings.config[:config]['domain'])
|
params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
|
||||||
|
|
||||||
Vmpooler::API.settings.config[:pools].each do |pool|
|
pools.each do |pool|
|
||||||
if Vmpooler::API.settings.redis.sismember('vmpooler__running__' + pool['name'], params[:hostname])
|
if backend.sismember('vmpooler__running__' + pool['name'], params[:hostname])
|
||||||
Vmpooler::API.settings.redis.srem('vmpooler__running__' + pool['name'], params[:hostname])
|
backend.srem('vmpooler__running__' + pool['name'], params[:hostname])
|
||||||
Vmpooler::API.settings.redis.sadd('vmpooler__completed__' + pool['name'], params[:hostname])
|
backend.sadd('vmpooler__completed__' + pool['name'], params[:hostname])
|
||||||
|
|
||||||
status 200
|
status 200
|
||||||
result['ok'] = true
|
result['ok'] = true
|
||||||
|
|
@ -443,27 +471,26 @@ module Vmpooler
|
||||||
put "#{api_prefix}/vm/:hostname/?" do
|
put "#{api_prefix}/vm/:hostname/?" do
|
||||||
content_type :json
|
content_type :json
|
||||||
|
|
||||||
|
status 404
|
||||||
|
result = { 'ok' => false }
|
||||||
|
|
||||||
failure = false
|
failure = false
|
||||||
|
|
||||||
result = {}
|
params[:hostname] = hostname_shorten(params[:hostname], config['domain'])
|
||||||
|
|
||||||
status 404
|
if backend.exists('vmpooler__vm__' + params[:hostname])
|
||||||
result['ok'] = false
|
|
||||||
|
|
||||||
params[:hostname] = hostname_shorten(params[:hostname], Vmpooler::API.settings.config[:config]['domain'])
|
|
||||||
|
|
||||||
if Vmpooler::API.settings.redis.exists('vmpooler__vm__' + params[:hostname])
|
|
||||||
begin
|
begin
|
||||||
jdata = JSON.parse(request.body.read)
|
jdata = JSON.parse(request.body.read)
|
||||||
rescue
|
rescue
|
||||||
status 400
|
halt 400, JSON.pretty_generate(result)
|
||||||
return JSON.pretty_generate(result)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Validate data payload
|
# Validate data payload
|
||||||
jdata.each do |param, arg|
|
jdata.each do |param, arg|
|
||||||
case param
|
case param
|
||||||
when 'lifetime'
|
when 'lifetime'
|
||||||
|
need_token! if Vmpooler::API.settings.config[:auth]
|
||||||
|
|
||||||
unless arg.to_i > 0
|
unless arg.to_i > 0
|
||||||
failure = true
|
failure = true
|
||||||
end
|
end
|
||||||
|
|
@ -482,12 +509,14 @@ module Vmpooler
|
||||||
jdata.each do |param, arg|
|
jdata.each do |param, arg|
|
||||||
case param
|
case param
|
||||||
when 'lifetime'
|
when 'lifetime'
|
||||||
|
need_token! if Vmpooler::API.settings.config[:auth]
|
||||||
|
|
||||||
arg = arg.to_i
|
arg = arg.to_i
|
||||||
|
|
||||||
Vmpooler::API.settings.redis.hset('vmpooler__vm__' + params[:hostname], param, arg)
|
backend.hset('vmpooler__vm__' + params[:hostname], param, arg)
|
||||||
when 'tags'
|
when 'tags'
|
||||||
arg.keys.each do |tag|
|
arg.keys.each do |tag|
|
||||||
Vmpooler::API.settings.redis.hset('vmpooler__vm__' + params[:hostname], 'tag:' + tag, arg[tag])
|
backend.hset('vmpooler__vm__' + params[:hostname], 'tag:' + tag, arg[tag])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,227 @@ describe Vmpooler::API::V1 do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '/vm' do
|
||||||
|
let(:redis) { double('redis') }
|
||||||
|
let(:prefix) { '/api/v1' }
|
||||||
|
let(:config) { {
|
||||||
|
config: {
|
||||||
|
'site_name' => 'test pooler',
|
||||||
|
'vm_lifetime_auth' => 2
|
||||||
|
},
|
||||||
|
pools: [
|
||||||
|
{'name' => 'pool1', 'size' => 5},
|
||||||
|
{'name' => 'pool2', 'size' => 10}
|
||||||
|
]
|
||||||
|
} }
|
||||||
|
|
||||||
|
before do
|
||||||
|
app.settings.set :config, config
|
||||||
|
app.settings.set :redis, redis
|
||||||
|
|
||||||
|
allow(redis).to receive(:exists).and_return '1'
|
||||||
|
allow(redis).to receive(:hset).and_return '1'
|
||||||
|
allow(redis).to receive(:sadd).and_return '1'
|
||||||
|
allow(redis).to receive(:scard).and_return '5'
|
||||||
|
allow(redis).to receive(:spop).with('vmpooler__ready__pool1').and_return 'abcdefghijklmnop'
|
||||||
|
allow(redis).to receive(:spop).with('vmpooler__ready__pool2').and_return 'qrstuvwxyz012345'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /vm' do
|
||||||
|
it 'returns a single VM' do
|
||||||
|
post "#{prefix}/vm", '{"pool1":"1"}'
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
ok: true,
|
||||||
|
pool1: {
|
||||||
|
ok: true,
|
||||||
|
hostname: 'abcdefghijklmnop'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(last_response).to be_ok
|
||||||
|
expect(last_response.header['Content-Type']).to eq('application/json')
|
||||||
|
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||||
|
expect(last_response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns multiple VMs' do
|
||||||
|
post "#{prefix}/vm", '{"pool1":"1","pool2":"1"}'
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
ok: true,
|
||||||
|
pool1: {
|
||||||
|
ok: true,
|
||||||
|
hostname: 'abcdefghijklmnop'
|
||||||
|
},
|
||||||
|
pool2: {
|
||||||
|
ok: true,
|
||||||
|
hostname: 'qrstuvwxyz012345'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(last_response).to be_ok
|
||||||
|
expect(last_response.header['Content-Type']).to eq('application/json')
|
||||||
|
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||||
|
expect(last_response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
context '(auth not configured)' do
|
||||||
|
let(:config) { { auth: false } }
|
||||||
|
|
||||||
|
it 'does not extend VM lifetime if auth token is provided' do
|
||||||
|
expect(redis).not_to receive(:hset).with("vmpooler__vm__abcdefghijklmnop", "lifetime", 2)
|
||||||
|
|
||||||
|
post "#{prefix}/vm", '{"pool1":"1"}', {
|
||||||
|
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
ok: true,
|
||||||
|
pool1: {
|
||||||
|
ok: true,
|
||||||
|
hostname: 'abcdefghijklmnop'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(last_response).to be_ok
|
||||||
|
expect(last_response.header['Content-Type']).to eq('application/json')
|
||||||
|
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||||
|
expect(last_response.status).to eq(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context '(auth configured)' do
|
||||||
|
let(:config) { { auth: true } }
|
||||||
|
|
||||||
|
it 'extends VM lifetime if auth token is provided' do
|
||||||
|
expect(redis).to receive(:hset).with("vmpooler__vm__abcdefghijklmnop", "lifetime", 2).once
|
||||||
|
|
||||||
|
post "#{prefix}/vm", '{"pool1":"1"}', {
|
||||||
|
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
ok: true,
|
||||||
|
pool1: {
|
||||||
|
ok: true,
|
||||||
|
hostname: 'abcdefghijklmnop'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(last_response).to be_ok
|
||||||
|
expect(last_response.header['Content-Type']).to eq('application/json')
|
||||||
|
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||||
|
expect(last_response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not extend VM lifetime if auth token is not provided' do
|
||||||
|
expect(redis).not_to receive(:hset).with("vmpooler__vm__abcdefghijklmnop", "lifetime", 2)
|
||||||
|
|
||||||
|
post "#{prefix}/vm", '{"pool1":"1"}'
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
ok: true,
|
||||||
|
pool1: {
|
||||||
|
ok: true,
|
||||||
|
hostname: 'abcdefghijklmnop'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(last_response).to be_ok
|
||||||
|
expect(last_response.header['Content-Type']).to eq('application/json')
|
||||||
|
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||||
|
expect(last_response.status).to eq(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '/vm/:hostname' do
|
||||||
|
let(:redis) { double('redis') }
|
||||||
|
let(:prefix) { '/api/v1' }
|
||||||
|
let(:config) { {
|
||||||
|
pools: [
|
||||||
|
{'name' => 'pool1', 'size' => 5},
|
||||||
|
{'name' => 'pool2', 'size' => 10}
|
||||||
|
]
|
||||||
|
} }
|
||||||
|
|
||||||
|
before do
|
||||||
|
app.settings.set :config, config
|
||||||
|
app.settings.set :redis, redis
|
||||||
|
|
||||||
|
allow(redis).to receive(:exists).and_return '1'
|
||||||
|
allow(redis).to receive(:hset).and_return '1'
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT /vm/:hostname' do
|
||||||
|
it 'allows tags to be set' do
|
||||||
|
put "#{prefix}/vm/testhost", '{"tags":{"tested_by":"rspec"}}'
|
||||||
|
|
||||||
|
expect(last_response).to be_ok
|
||||||
|
expect(last_response.header['Content-Type']).to eq('application/json')
|
||||||
|
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => true}))
|
||||||
|
expect(last_response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not set tags if request body format is invalid' do
|
||||||
|
put "#{prefix}/vm/testhost", '{"tags":{"tested"}}'
|
||||||
|
|
||||||
|
expect(last_response).to_not be_ok
|
||||||
|
expect(last_response.header['Content-Type']).to eq('application/json')
|
||||||
|
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => false}))
|
||||||
|
expect(last_response.status).to eq(400)
|
||||||
|
end
|
||||||
|
|
||||||
|
context '(auth not configured)' do
|
||||||
|
let(:config) { { auth: false } }
|
||||||
|
|
||||||
|
it 'allows VM lifetime to be modified without a token' do
|
||||||
|
put "#{prefix}/vm/testhost", '{"lifetime":"1"}'
|
||||||
|
|
||||||
|
expect(last_response).to be_ok
|
||||||
|
expect(last_response.header['Content-Type']).to eq('application/json')
|
||||||
|
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => true}))
|
||||||
|
expect(last_response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not allow a lifetime to be 0' do
|
||||||
|
put "#{prefix}/vm/testhost", '{"lifetime":"0"}'
|
||||||
|
|
||||||
|
expect(last_response).to_not be_ok
|
||||||
|
expect(last_response.header['Content-Type']).to eq('application/json')
|
||||||
|
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => false}))
|
||||||
|
expect(last_response.status).to eq(400)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context '(auth configured)' do
|
||||||
|
let(:config) { { auth: true } }
|
||||||
|
|
||||||
|
it 'allows VM lifetime to be modified with a token' do
|
||||||
|
put "#{prefix}/vm/testhost", '{"lifetime":"1"}', {
|
||||||
|
'HTTP_X_AUTH_TOKEN' => 'abcdefghijklmnopqrstuvwxyz012345'
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(last_response).to be_ok
|
||||||
|
expect(last_response.header['Content-Type']).to eq('application/json')
|
||||||
|
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => true}))
|
||||||
|
expect(last_response.status).to eq(200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not allows VM lifetime to be modified without a token' do
|
||||||
|
put "#{prefix}/vm/testhost", '{"lifetime":"1"}'
|
||||||
|
|
||||||
|
expect(last_response).to_not be_ok
|
||||||
|
expect(last_response.header['Content-Type']).to eq('application/json')
|
||||||
|
expect(last_response.body).to eq(JSON.pretty_generate({'ok' => false}))
|
||||||
|
expect(last_response.status).to eq(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,39 @@
|
||||||
:graphite:
|
:graphite:
|
||||||
server: 'graphite.company.com'
|
server: 'graphite.company.com'
|
||||||
|
|
||||||
|
# :auth:
|
||||||
|
#
|
||||||
|
# This section contains information related to authenticating users
|
||||||
|
# for token operations.
|
||||||
|
#
|
||||||
|
# Currently the only supported provider is LDAP; the following parameters
|
||||||
|
# will all be under an ':ldap:' subsection (see example below).
|
||||||
|
#
|
||||||
|
# Available configuration parameters:
|
||||||
|
#
|
||||||
|
# - host
|
||||||
|
# The FQDN hostname of the LDAP server.
|
||||||
|
#
|
||||||
|
# - port
|
||||||
|
# The port used to connect to the LDAP service.
|
||||||
|
# (optional; default: '389')
|
||||||
|
#
|
||||||
|
# - base
|
||||||
|
# The base DN used for LDAP searches.
|
||||||
|
#
|
||||||
|
# - user_object
|
||||||
|
# The LDAP object-type used to designate a user object.
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
|
||||||
|
:auth:
|
||||||
|
provider: 'ldap'
|
||||||
|
:ldap:
|
||||||
|
host: 'ldap.company.com'
|
||||||
|
port: 389
|
||||||
|
base: 'ou=users,dc=company,dc=com'
|
||||||
|
user_object: 'uid'
|
||||||
|
|
||||||
# :config:
|
# :config:
|
||||||
#
|
#
|
||||||
# This section contains global configuration information.
|
# This section contains global configuration information.
|
||||||
|
|
@ -107,6 +140,10 @@
|
||||||
# How long (in hours) to keep VMs in 'running' queues before destroying.
|
# How long (in hours) to keep VMs in 'running' queues before destroying.
|
||||||
# (optional; default: '24')
|
# (optional; default: '24')
|
||||||
#
|
#
|
||||||
|
# - vm_lifetime_auth
|
||||||
|
# Same as vm_lifetime, but applied if a valid authentication token is
|
||||||
|
# included during the request.
|
||||||
|
#
|
||||||
# - domain
|
# - domain
|
||||||
# If set, returns a top-level 'domain' JSON key in POST requests
|
# If set, returns a top-level 'domain' JSON key in POST requests
|
||||||
|
|
||||||
|
|
@ -119,6 +156,7 @@
|
||||||
timeout: 15
|
timeout: 15
|
||||||
vm_checktime: 15
|
vm_checktime: 15
|
||||||
vm_lifetime: 12
|
vm_lifetime: 12
|
||||||
|
vm_lifetime_auth: 24
|
||||||
domain: 'company.com'
|
domain: 'company.com'
|
||||||
|
|
||||||
# :pools:
|
# :pools:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue