(POOLER-160) Add Prometheus to pooler startup

This is a re-architect of the vmpooler initialisation code to:
1. Allow an API service for both manager and the api
2. Add the Prometheus endpoints to the web service.
   Needed to change the way the Rack Service is started as instantiating
   using ".New" leads to a failure to initialise the http Stats
   collection.
3. Selectively load the pooler api and/or Prometheus endpoints.
4. Rework API Spec tests for revised API loading. Needed to tidy up the
   initialisation and perform a reset! after each test to avoid "leaks"
   and dependencies between the tests.
This commit is contained in:
John O'Connor 2020-04-23 16:37:29 +01:00
parent ffab7def9e
commit bbd76bde4c
15 changed files with 166 additions and 88 deletions

View file

@ -8,6 +8,7 @@ gem 'rake', '~> 13.0'
gem 'redis', '~> 4.1' gem 'redis', '~> 4.1'
gem 'rbvmomi', '~> 2.1' gem 'rbvmomi', '~> 2.1'
gem 'sinatra', '~> 2.0' gem 'sinatra', '~> 2.0'
gem 'prometheus-client', '~> 2.0'
gem 'net-ldap', '~> 0.16' gem 'net-ldap', '~> 0.16'
gem 'statsd-ruby', '~> 1.4.0', :require => 'statsd' gem 'statsd-ruby', '~> 1.4.0', :require => 'statsd'
gem 'connection_pool', '~> 2.2' gem 'connection_pool', '~> 2.2'

View file

@ -25,12 +25,16 @@ end
if torun.include? 'api' if torun.include? 'api'
api = Thread.new do api = Thread.new do
thr = Vmpooler::API.new
redis = Vmpooler.new_redis(redis_host, redis_port, redis_password) redis = Vmpooler.new_redis(redis_host, redis_port, redis_password)
thr.helpers.configure(config, redis, metrics) Vmpooler::API.execute(torun, config, redis, metrics)
thr.helpers.execute!
end end
torun_threads << api torun_threads << api
elsif metrics.respond_to?(:setup_prometheus_metrics)
# Run the cut down API - Prometheus Metrics only.
prometheus_only_api = Thread.new do
Vmpooler::API.execute(torun, config, nil, metrics)
end
torun_threads << prometheus_only_api
end end
if torun.include? 'manager' if torun.include? 'manager'

View file

@ -2,10 +2,24 @@
module Vmpooler module Vmpooler
class API < Sinatra::Base class API < Sinatra::Base
def initialize # Load API components
super %w[helpers dashboard reroute v1 request_logger].each do |lib|
require "vmpooler/api/#{lib}"
end end
# Load dashboard components
require 'vmpooler/dashboard'
def self.execute(torun, config, redis, metrics)
self.settings.set :config, config
self.settings.set :redis, redis unless redis.nil?
self.settings.set :metrics, metrics
self.settings.set :checkoutlock, Mutex.new
# Deflating in all situations
# https://www.schneems.com/2017/11/08/80-smaller-rails-footprint-with-rack-deflate/
use Rack::Deflater
# not_found clause placed here to fix rspec test issue.
not_found do not_found do
content_type :json content_type :json
@ -16,37 +30,24 @@ module Vmpooler
JSON.pretty_generate(result) JSON.pretty_generate(result)
end end
# Load dashboard components if metrics.respond_to?(:setup_prometheus_metrics)
begin # Prometheus metrics are only setup if actually specified
require 'dashboard' # in the config file.
rescue LoadError metrics.setup_prometheus_metrics
require File.expand_path(File.join(File.dirname(__FILE__), 'dashboard'))
use Prometheus::Middleware::Collector, metrics_prefix: "#{metrics.metrics_prefix}_http"
use Prometheus::Middleware::Exporter, path: metrics.endpoint
end end
if torun.include? 'api'
use Vmpooler::Dashboard use Vmpooler::Dashboard
# Load API components
%w[helpers dashboard reroute v1].each do |lib|
begin
require "api/#{lib}"
rescue LoadError
require File.expand_path(File.join(File.dirname(__FILE__), 'api', lib))
end
end
use Vmpooler::API::Dashboard use Vmpooler::API::Dashboard
use Vmpooler::API::Reroute use Vmpooler::API::Reroute
use Vmpooler::API::V1 use Vmpooler::API::V1
def configure(config, redis, metrics)
self.settings.set :config, config
self.settings.set :redis, redis
self.settings.set :metrics, metrics
self.settings.set :checkoutlock, Mutex.new
end end
def execute! # Get thee started O WebServer
self.settings.run! self.run!
end end
end end
end end

View file

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'prometheus/client'
module Vmpooler module Vmpooler
class Promstats < Metrics class Promstats < Metrics
attr_reader :prefix, :endpoint, :metrics_prefix attr_reader :prefix, :endpoint, :metrics_prefix

View file

@ -8,6 +8,13 @@ describe Vmpooler::API::V1 do
Vmpooler::API Vmpooler::API
end end
# Added to ensure no leakage in rack state from previous tests.
# Removes all routes, filters, middleware and extension hooks from the current class
# https://rubydoc.info/gems/sinatra/Sinatra/Base#reset!-class_method
before(:each) do
app.reset!
end
let(:config) { let(:config) {
{ {
config: { config: {
@ -32,9 +39,8 @@ describe Vmpooler::API::V1 do
let(:current_time) { Time.now } let(:current_time) { Time.now }
before(:each) do before(:each) do
app.settings.set :config, config expect(app).to receive(:run!).once
app.settings.set :redis, redis app.execute(['api'], config, redis, metrics)
app.settings.set :metrics, metrics
app.settings.set :config, auth: false app.settings.set :config, auth: false
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
end end

View file

@ -5,7 +5,14 @@ describe Vmpooler::API::V1 do
include Rack::Test::Methods include Rack::Test::Methods
def app() def app()
Vmpooler::API end Vmpooler::API
end
# Added to ensure no leakage in rack state from previous tests.
# Removes all routes, filters, middleware and extension hooks from the current class
# https://rubydoc.info/gems/sinatra/Sinatra/Base#reset!-class_method
before(:each) do
app.reset!
end
describe '/ondemandvm' do describe '/ondemandvm' do
let(:prefix) { '/api/v1' } let(:prefix) { '/api/v1' }
@ -32,13 +39,11 @@ describe Vmpooler::API::V1 do
let(:current_time) { Time.now } let(:current_time) { Time.now }
let(:vmname) { 'abcdefghijkl' } let(:vmname) { 'abcdefghijkl' }
let(:checkoutlock) { Mutex.new } let(:checkoutlock) { Mutex.new }
let(:redis) { MockRedis.new }
let(:uuid) { SecureRandom.uuid } let(:uuid) { SecureRandom.uuid }
before(:each) do before(:each) do
app.settings.set :config, config expect(app).to receive(:run!).once
app.settings.set :redis, redis app.execute(['api'], config, redis, metrics)
app.settings.set :metrics, metrics
app.settings.set :config, auth: false app.settings.set :config, auth: false
app.settings.set :checkoutlock, checkoutlock app.settings.set :checkoutlock, checkoutlock
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)

View file

@ -8,6 +8,10 @@ describe Vmpooler::API::V1 do
Vmpooler::API Vmpooler::API
end end
after(:each) do
Vmpooler::API.reset!
end
let(:config) { let(:config) {
{ {
config: { config: {
@ -32,9 +36,8 @@ describe Vmpooler::API::V1 do
let(:current_time) { Time.now } let(:current_time) { Time.now }
before(:each) do before(:each) do
app.settings.set :config, config expect(app).to receive(:run!).once
app.settings.set :redis, redis app.execute(['api'], config, redis, metrics)
app.settings.set :metrics, metrics
app.settings.set :config, auth: false app.settings.set :config, auth: false
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
end end

View file

@ -12,6 +12,13 @@ describe Vmpooler::API::V1 do
Vmpooler::API Vmpooler::API
end end
# Added to ensure no leakage in rack state from previous tests.
# Removes all routes, filters, middleware and extension hooks from the current class
# https://rubydoc.info/gems/sinatra/Sinatra/Base#reset!-class_method
before(:each) do
app.reset!
end
describe 'status and metrics endpoints' do describe 'status and metrics endpoints' do
let(:prefix) { '/api/v1' } let(:prefix) { '/api/v1' }
@ -32,8 +39,8 @@ describe Vmpooler::API::V1 do
let(:current_time) { Time.now } let(:current_time) { Time.now }
before(:each) do before(:each) do
app.settings.set :config, config expect(app).to receive(:run!).once
app.settings.set :redis, redis app.execute(['api'], config, redis, nil)
app.settings.set :config, auth: false app.settings.set :config, auth: false
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
end end

View file

@ -8,14 +8,21 @@ describe Vmpooler::API::V1 do
Vmpooler::API Vmpooler::API
end end
# Added to ensure no leakage in rack state from previous tests.
# Removes all routes, filters, middleware and extension hooks from the current class
# https://rubydoc.info/gems/sinatra/Sinatra/Base#reset!-class_method
before(:each) do
app.reset!
end
describe '/token' do describe '/token' do
let(:prefix) { '/api/v1' } let(:prefix) { '/api/v1' }
let(:current_time) { Time.now } let(:current_time) { Time.now }
let(:config) { { } } let(:config) { { } }
before do before(:each) do
app.settings.set :config, config expect(app).to receive(:run!).once
app.settings.set :redis, redis app.execute(['api'], config, redis, nil)
end end
describe 'GET /token' do describe 'GET /token' do
@ -97,7 +104,9 @@ describe Vmpooler::API::V1 do
let(:prefix) { '/api/v1' } let(:prefix) { '/api/v1' }
let(:current_time) { Time.now } let(:current_time) { Time.now }
before do before(:each) do
expect(app).to receive(:run!).once
app.execute(['api'], config, redis, nil)
app.settings.set :config, config app.settings.set :config, config
app.settings.set :redis, redis app.settings.set :redis, redis
end end

View file

@ -12,8 +12,16 @@ describe Vmpooler::API::V1 do
Vmpooler::API Vmpooler::API
end end
# Added to ensure no leakage in rack state from previous tests.
# Removes all routes, filters, middleware and extension hooks from the current class
# https://rubydoc.info/gems/sinatra/Sinatra/Base#reset!-class_method
before(:each) do
app.reset!
end
describe '/vm/:hostname' do describe '/vm/:hostname' do
let(:prefix) { '/api/v1' } let(:prefix) { '/api/v1' }
let(:metrics) { Vmpooler::DummyStatsd.new }
let(:config) { let(:config) {
{ {
@ -33,11 +41,9 @@ describe Vmpooler::API::V1 do
let(:current_time) { Time.now } let(:current_time) { Time.now }
let(:redis) { MockRedis.new }
before(:each) do before(:each) do
app.settings.set :config, config expect(app).to receive(:run!).once
app.settings.set :redis, redis app.execute(['api'], config, redis, metrics)
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
end end
@ -81,9 +87,9 @@ describe Vmpooler::API::V1 do
end end
context '(tagfilter configured)' do context '(tagfilter configured)' do
let(:config) { { before(:each) do
tagfilter: { 'url' => '(.*)\/' }, app.settings.set :config, tagfilter: { 'url' => '(.*)\/' }
} } end
it 'correctly filters tags' do it 'correctly filters tags' do
create_vm('testhost', redis) create_vm('testhost', redis)
@ -104,7 +110,9 @@ describe Vmpooler::API::V1 do
end end
context '(auth not configured)' do context '(auth not configured)' do
let(:config) { { auth: false } } before(:each) do
app.settings.set :config, auth: false
end
it 'allows VM lifetime to be modified without a token' do it 'allows VM lifetime to be modified without a token' do
create_vm('testhost', redis) create_vm('testhost', redis)

View file

@ -8,6 +8,13 @@ describe Vmpooler::API::V1 do
Vmpooler::API Vmpooler::API
end end
# Added to ensure no leakage in rack state from previous tests.
# Removes all routes, filters, middleware and extension hooks from the current class
# https://rubydoc.info/gems/sinatra/Sinatra/Base#reset!-class_method
before(:each) do
app.reset!
end
describe '/vm' do describe '/vm' do
let(:prefix) { '/api/v1' } let(:prefix) { '/api/v1' }
let(:metrics) { Vmpooler::DummyStatsd.new } let(:metrics) { Vmpooler::DummyStatsd.new }
@ -32,9 +39,8 @@ describe Vmpooler::API::V1 do
let(:checkoutlock) { Mutex.new } let(:checkoutlock) { Mutex.new }
before(:each) do before(:each) do
app.settings.set :config, config expect(app).to receive(:run!).once
app.settings.set :redis, redis app.execute(['api'], config, redis, metrics)
app.settings.set :metrics, metrics
app.settings.set :config, auth: false app.settings.set :config, auth: false
app.settings.set :checkoutlock, checkoutlock app.settings.set :checkoutlock, checkoutlock
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
@ -104,8 +110,8 @@ describe Vmpooler::API::V1 do
end end
it 'returns 503 for empty pool when aliases are not defined' do it 'returns 503 for empty pool when aliases are not defined' do
Vmpooler::API.settings.config.delete(:alias) app.settings.config.delete(:alias)
Vmpooler::API.settings.config[:pool_names] = ['pool1', 'pool2'] app.settings.config[:pool_names] = ['pool1', 'pool2']
create_ready_vm 'pool1', vmname, redis create_ready_vm 'pool1', vmname, redis
post "#{prefix}/vm/pool1" post "#{prefix}/vm/pool1"

View file

@ -8,6 +8,13 @@ describe Vmpooler::API::V1 do
Vmpooler::API Vmpooler::API
end end
# Added to ensure no leakage in rack state from previous tests.
# Removes all routes, filters, middleware and extension hooks from the current class
# https://rubydoc.info/gems/sinatra/Sinatra/Base#reset!-class_method
before(:each) do
app.reset!
end
describe '/vm/:template' do describe '/vm/:template' do
let(:prefix) { '/api/v1' } let(:prefix) { '/api/v1' }
let(:metrics) { Vmpooler::DummyStatsd.new } let(:metrics) { Vmpooler::DummyStatsd.new }
@ -33,9 +40,8 @@ describe Vmpooler::API::V1 do
let(:checkoutlock) { Mutex.new } let(:checkoutlock) { Mutex.new }
before(:each) do before(:each) do
app.settings.set :config, config expect(app).to receive(:run!).once
app.settings.set :redis, redis app.execute(['api'], config, redis, metrics)
app.settings.set :metrics, metrics
app.settings.set :config, auth: false app.settings.set :config, auth: false
app.settings.set :checkoutlock, checkoutlock app.settings.set :checkoutlock, checkoutlock
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
@ -84,8 +90,8 @@ describe Vmpooler::API::V1 do
end end
it 'returns 503 for empty pool when aliases are not defined' do it 'returns 503 for empty pool when aliases are not defined' do
Vmpooler::API.settings.config.delete(:alias) app.settings.config.delete(:alias)
Vmpooler::API.settings.config[:pool_names] = ['pool1', 'pool2'] app.settings.config[:pool_names] = ['pool1', 'pool2']
create_ready_vm 'pool1', 'abcdefghijklmnop', redis create_ready_vm 'pool1', 'abcdefghijklmnop', redis
post "#{prefix}/vm/pool1" post "#{prefix}/vm/pool1"

View file

@ -5,13 +5,35 @@ describe Vmpooler::API do
include Rack::Test::Methods include Rack::Test::Methods
def app() def app()
described_class Vmpooler::API
end
# Added to ensure no leakage in rack state from previous tests.
# Removes all routes, filters, middleware and extension hooks from the current class
# https://rubydoc.info/gems/sinatra/Sinatra/Base#reset!-class_method
before(:each) do
app.reset!
end end
describe 'Dashboard' do describe 'Dashboard' do
let(:config) { {
pools: [
{'name' => 'pool1', 'size' => 5}
],
graphite: {}
} }
before(:each) do
expect(app).to receive(:run!)
app.execute(['api'], config, redis, nil)
app.settings.set :config, auth: false
end
context '/' do context '/' do
before { get '/' } before(:each) do
get '/'
end
it { expect(last_response.status).to eq(302) } it { expect(last_response.status).to eq(302) }
it { expect(last_response.location).to eq('http://example.org/dashboard/') } it { expect(last_response.location).to eq('http://example.org/dashboard/') }
@ -21,7 +43,7 @@ describe Vmpooler::API do
ENV['SITE_NAME'] = 'test pooler' ENV['SITE_NAME'] = 'test pooler'
ENV['VMPOOLER_CONFIG'] = 'thing' ENV['VMPOOLER_CONFIG'] = 'thing'
before do before(:each) do
get '/dashboard/' get '/dashboard/'
end end
@ -31,10 +53,12 @@ describe Vmpooler::API do
end end
context 'unknown route' do context 'unknown route' do
before { get '/doesnotexist' } before(:each) do
get '/doesnotexist'
end
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.status).to eq(404) }
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
@ -47,10 +71,8 @@ describe Vmpooler::API do
graphite: {} graphite: {}
} } } }
before do before(:each) do
$config = config
app.settings.set :config, config app.settings.set :config, config
app.settings.set :redis, redis
app.settings.set :environment, :test app.settings.set :environment, :test
end end
@ -120,10 +142,8 @@ describe Vmpooler::API do
graphite: {} graphite: {}
} } } }
before do before(:each) do
$config = config
app.settings.set :config, config app.settings.set :config, config
app.settings.set :redis, redis
app.settings.set :environment, :test app.settings.set :environment, :test
end end

View file

@ -27,7 +27,6 @@ describe 'Pool Manager' do
timeout: 5 timeout: 5
) { MockRedis.new } ) { MockRedis.new }
} }
let(:redis) { MockRedis.new }
let(:provider) { Vmpooler::PoolManager::Provider::Base.new(config, logger, metrics, redis_connection_pool, 'mock_provider', provider_options) } let(:provider) { Vmpooler::PoolManager::Provider::Base.new(config, logger, metrics, redis_connection_pool, 'mock_provider', provider_options) }

View file

@ -24,6 +24,7 @@ Gem::Specification.new do |s|
s.add_dependency 'redis', '~> 4.1' s.add_dependency 'redis', '~> 4.1'
s.add_dependency 'rbvmomi', '~> 2.1' s.add_dependency 'rbvmomi', '~> 2.1'
s.add_dependency 'sinatra', '~> 2.0' s.add_dependency 'sinatra', '~> 2.0'
s.add_dependency 'prometheus-client', '~> 2.0'
s.add_dependency 'net-ldap', '~> 0.16' s.add_dependency 'net-ldap', '~> 0.16'
s.add_dependency 'statsd-ruby', '~> 1.4' s.add_dependency 'statsd-ruby', '~> 1.4'
s.add_dependency 'connection_pool', '~> 2.2' s.add_dependency 'connection_pool', '~> 2.2'