diff --git a/.travis.yml b/.travis.yml index 129267b5a..810b76445 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,3 +9,12 @@ rvm: - 2.2.1 - 2.2.2 - jruby-1.7.8 +# This below is a temporary shim to bypass these bundler+jruby bugs: +# +# /~https://github.com/bundler/bundler/issues/4975 +# /~https://github.com/bundler/bundler/issues/4984 +install: + - gem install bundler --version 1.12.5 + - bundle _1.12.5_ install --jobs=3 --retry=3 --path=${BUNDLE_PATH:-vendor/bundle} +script: + - bundle _1.12.5_ exec rake diff --git a/lib/vmpooler/api/v1.rb b/lib/vmpooler/api/v1.rb index 4bd9852d5..3c8c8aaa8 100644 --- a/lib/vmpooler/api/v1.rb +++ b/lib/vmpooler/api/v1.rb @@ -119,10 +119,67 @@ def atomically_allocate_vms(payload) result end + # Provide run-time statistics + # + # Example: + # + # { + # "boot": { + # "duration": { + # "average": 163.6, + # "min": 65.49, + # "max": 830.07, + # "total": 247744.71000000002 + # }, + # "count": { + # "total": 1514 + # } + # }, + # "capacity": { + # "current": 968, + # "total": 975, + # "percent": 99.3 + # }, + # "clone": { + # "duration": { + # "average": 17.0, + # "min": 4.66, + # "max": 637.96, + # "total": 25634.15 + # }, + # "count": { + # "total": 1507 + # } + # }, + # "queue": { + # "pending": 12, + # "cloning": 0, + # "booting": 12, + # "ready": 968, + # "running": 367, + # "completed": 0, + # "total": 1347 + # }, + # "pools": { + # "ready": 100, + # "running": 120, + # "pending": 5, + # "max": 250, + # } + # "status": { + # "ok": true, + # "message": "Battle station fully armed and operational.", + # "empty": [ # NOTE: would not have 'ok: true' w/ "empty" pools + # "redhat-7-x86_64", + # "ubuntu-1404-i386" + # ], + # "uptime": 179585.9 + # } get "#{api_prefix}/status/?" do content_type :json result = { + pools: {}, status: { ok: true, message: 'Battle station fully armed and operational.' @@ -136,7 +193,21 @@ def atomically_allocate_vms(payload) # Check for empty pools pools.each do |pool| - if backend.scard('vmpooler__ready__' + pool['name']).to_i == 0 + # REMIND: move this out of the API and into the back-end + ready = backend.scard('vmpooler__ready__' + pool['name']).to_i + running = backend.scard('vmpooler__running__' + pool['name']).to_i + pending = backend.scard('vmpooler__pending__' + pool['name']).to_i + max = pool['size'] + + result[:pools][pool['name']] = { + ready: ready, + running: running, + pending: pending, + max: max + } + + # for backwards compatibility, include separate "empty" stats in "status" block + if ready == 0 result[:status][:empty] ||= [] result[:status][:empty].push(pool['name']) diff --git a/spec/helpers.rb b/spec/helpers.rb index 292b9f43c..712cdab0f 100644 --- a/spec/helpers.rb +++ b/spec/helpers.rb @@ -35,8 +35,7 @@ def token_exists?(token) 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) + redis.hset("vmpooler__vm__#{name}", "template", template) end def create_running_vm(template, name, token = nil) @@ -45,6 +44,12 @@ def create_running_vm(template, name, token = nil) redis.hset("vmpooler__vm__#{name}", "template", template) end +def create_pending_vm(template, name, token = nil) + create_vm(name, token) + redis.sadd("vmpooler__pending__#{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 diff --git a/spec/vmpooler/api/v1/status_spec.rb b/spec/vmpooler/api/v1/status_spec.rb new file mode 100644 index 000000000..6abe9425a --- /dev/null +++ b/spec/vmpooler/api/v1/status_spec.rb @@ -0,0 +1,111 @@ +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 '/status' 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 'GET /status' do + it 'returns the configured maximum size for each pool' do + get "#{prefix}/status/" + + # of course /status doesn't conform to the weird standard everything else uses... + expect(last_response.header['Content-Type']).to eq('application/json') + result = JSON.parse(last_response.body) + expect(result["pools"]["pool1"]["max"]).to be(5) + expect(result["pools"]["pool2"]["max"]).to be(10) + end + + it 'returns the number of ready vms for each pool' do + 3.times {|i| create_ready_vm("pool1", "vm-#{i}") } + get "#{prefix}/status/" + + # of course /status doesn't conform to the weird standard everything else uses... + expect(last_response.header['Content-Type']).to eq('application/json') + result = JSON.parse(last_response.body) + expect(result["pools"]["pool1"]["ready"]).to be(3) + expect(result["pools"]["pool2"]["ready"]).to be(0) + end + + it 'returns the number of running vms for each pool' do + 3.times {|i| create_running_vm("pool1", "vm-#{i}") } + 4.times {|i| create_running_vm("pool2", "vm-#{i}") } + + get "#{prefix}/status/" + + # of course /status doesn't conform to the weird standard everything else uses... + expect(last_response.header['Content-Type']).to eq('application/json') + result = JSON.parse(last_response.body) + expect(result["pools"]["pool1"]["running"]).to be(3) + expect(result["pools"]["pool2"]["running"]).to be(4) + end + + it 'returns the number of pending vms for each pool' do + 3.times {|i| create_pending_vm("pool1", "vm-#{i}") } + 4.times {|i| create_pending_vm("pool2", "vm-#{i}") } + + get "#{prefix}/status/" + + # of course /status doesn't conform to the weird standard everything else uses... + expect(last_response.header['Content-Type']).to eq('application/json') + result = JSON.parse(last_response.body) + expect(result["pools"]["pool1"]["pending"]).to be(3) + expect(result["pools"]["pool2"]["pending"]).to be(4) + end + + it '(for v1 backwards compatibility) lists any empty pools in the status section' do + get "#{prefix}/status/" + + # of course /status doesn't conform to the weird standard everything else uses... + expect(last_response.header['Content-Type']).to eq('application/json') + result = JSON.parse(last_response.body) + expect(result["status"]["empty"].sort).to eq(["pool1", "pool2"]) + end + end + end +end