mirror of
https://github.com/puppetlabs/vmpooler.git
synced 2026-01-26 10:08:40 -05:00
(POOLER-107) Add configuration API endpoint
This commit adds a configuration endpoint to the vmpooler API. Pool size, and pool template, can be adjusted for pools that are configured at vmpooler application start time. Pool template changes trigger a pool refresh, and the new template has delta disks created automatically by vmpooler. Additionally, the capability to create template delta disks is added to the vsphere provider, and this is implemented to ensure that templates have delta disks created at application start time. The mechanism used to find template VM objects is simplified to make the flow of logic easier to understand. As an additional benefit, performance of this lookup is improved by using FindByInventoryPath.
This commit is contained in:
parent
00970ffc9e
commit
9758adccfe
9 changed files with 803 additions and 44 deletions
160
spec/integration/api/v1/config_spec.rb
Normal file
160
spec/integration/api/v1/config_spec.rb
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
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 '/config/pooltemplate' do
|
||||
let(:prefix) { '/api/v1' }
|
||||
let(:metrics) { Vmpooler::DummyStatsd.new }
|
||||
let(:config) {
|
||||
{
|
||||
config: {
|
||||
'site_name' => 'test pooler',
|
||||
'vm_lifetime_auth' => 2,
|
||||
},
|
||||
pools: [
|
||||
{'name' => 'pool1', 'size' => 5, 'template' => 'templates/pool1'},
|
||||
{'name' => 'pool2', 'size' => 10}
|
||||
],
|
||||
statsd: { 'prefix' => 'stats_prefix'},
|
||||
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 :metrics, metrics
|
||||
app.settings.set :config, auth: false
|
||||
create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time)
|
||||
end
|
||||
|
||||
describe 'POST /config/pooltemplate' do
|
||||
it 'updates a pool template' do
|
||||
post "#{prefix}/config/pooltemplate", '{"pool1":"templates/new_template"}'
|
||||
expect_json(ok = true, http = 201)
|
||||
|
||||
expected = { ok: true }
|
||||
|
||||
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||
end
|
||||
|
||||
it 'fails on nonexistent pools' do
|
||||
post "#{prefix}/config/pooltemplate", '{"poolpoolpool":"templates/newtemplate"}'
|
||||
expect_json(ok = false, http = 404)
|
||||
end
|
||||
|
||||
it 'updates multiple pools' do
|
||||
post "#{prefix}/config/pooltemplate", '{"pool1":"templates/new_template","pool2":"templates/new_template2"}'
|
||||
expect_json(ok = true, http = 201)
|
||||
|
||||
expected = { ok: true }
|
||||
|
||||
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||
end
|
||||
|
||||
it 'fails when not all pools exist' do
|
||||
post "#{prefix}/config/pooltemplate", '{"pool1":"templates/new_template","pool3":"templates/new_template2"}'
|
||||
expect_json(ok = false, http = 404)
|
||||
|
||||
expected = { ok: false }
|
||||
|
||||
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||
end
|
||||
|
||||
it 'returns no changes when the template does not change' do
|
||||
post "#{prefix}/config/pooltemplate", '{"pool1":"templates/pool1"}'
|
||||
expect_json(ok = true, http = 200)
|
||||
|
||||
expected = { ok: true }
|
||||
|
||||
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||
end
|
||||
|
||||
it 'fails when a invalid template parameter is provided' do
|
||||
post "#{prefix}/config/pooltemplate", '{"pool1":"template1"}'
|
||||
expect_json(ok = false, http = 404)
|
||||
|
||||
expected = { ok: false }
|
||||
|
||||
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /config/poolsize' do
|
||||
it 'changes a pool size' do
|
||||
post "#{prefix}/config/poolsize", '{"pool1":"2"}'
|
||||
expect_json(ok = true, http = 201)
|
||||
|
||||
expected = { ok: true }
|
||||
|
||||
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||
end
|
||||
|
||||
it 'changes a pool size for multiple pools' do
|
||||
post "#{prefix}/config/poolsize", '{"pool1":"2","pool2":"2"}'
|
||||
expect_json(ok = true, http = 201)
|
||||
|
||||
expected = { ok: true }
|
||||
|
||||
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||
end
|
||||
|
||||
it 'fails when a specified pool does not exist' do
|
||||
post "#{prefix}/config/poolsize", '{"pool10":"2"}'
|
||||
expect_json(ok = false, http = 404)
|
||||
|
||||
expected = { ok: false }
|
||||
|
||||
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||
end
|
||||
|
||||
it 'succeeds with 200 when no change is required' do
|
||||
post "#{prefix}/config/poolsize", '{"pool1":"5"}'
|
||||
expect_json(ok = true, http = 200)
|
||||
|
||||
expected = { ok: true }
|
||||
|
||||
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||
end
|
||||
|
||||
it 'succeeds with 201 when at least one pool changes' do
|
||||
post "#{prefix}/config/poolsize", '{"pool1":"5","pool2":"5"}'
|
||||
expect_json(ok = true, http = 201)
|
||||
|
||||
expected = { ok: true }
|
||||
|
||||
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||
end
|
||||
|
||||
it 'fails when a non-integer value is provided for size' do
|
||||
post "#{prefix}/config/poolsize", '{"pool1":"four"}'
|
||||
expect_json(ok = false, http = 404)
|
||||
|
||||
expected = { ok: false }
|
||||
|
||||
expect(last_response.body).to eq(JSON.pretty_generate(expected))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1503,6 +1503,220 @@ EOT
|
|||
end
|
||||
end
|
||||
|
||||
describe 'update_pool_template' do
|
||||
let(:template) { 'templates/pool_template' }
|
||||
let(:new_template) { 'templates/new_pool_template' }
|
||||
let(:vsphere_provider) { double('vsphere_provider') }
|
||||
let(:config) {
|
||||
YAML.load(<<-EOT
|
||||
---
|
||||
:pools:
|
||||
- name: #{pool}
|
||||
template: "#{template}"
|
||||
EOT
|
||||
)
|
||||
}
|
||||
|
||||
before(:each) do
|
||||
expect(subject).not_to be_nil
|
||||
redis.del('vmpooler__template')
|
||||
redis.del('vmpooler__config__template')
|
||||
redis.del('vmpooler__config__updating')
|
||||
end
|
||||
|
||||
it 'returns when vmpooler config template is not set' do
|
||||
expect(subject.update_pool_template(config[:pools][0], provider)).to be_nil
|
||||
end
|
||||
|
||||
context 'with template that requires no change' do
|
||||
before(:each) do
|
||||
redis.hset('vmpooler__template', pool, template)
|
||||
redis.hset('vmpooler__config__template', pool, template)
|
||||
end
|
||||
|
||||
it 'should return' do
|
||||
expect(subject.update_pool_template(config[:pools][0], provider)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a pool that requires an update' do
|
||||
before(:each) do
|
||||
redis.hset('vmpooler__template', pool, template)
|
||||
redis.hset('vmpooler__config__template', pool, new_template)
|
||||
allow(logger).to receive(:log)
|
||||
allow(redis).to receive(:hset)
|
||||
expect(provider).to receive(:create_template_delta_disks).with(config[:pools][0])
|
||||
end
|
||||
|
||||
it 'should update the configuration value' do
|
||||
expect(redis).to receive(:hset).with('vmpooler__template', pool, new_template)
|
||||
|
||||
subject.update_pool_template(config[:pools][0], provider)
|
||||
end
|
||||
|
||||
it 'should log a message for updating the template' do
|
||||
expect(logger).to receive(:log).with('s', "[*] [#{pool}] template updated from #{template} to #{new_template}")
|
||||
|
||||
subject.update_pool_template(config[:pools][0], provider)
|
||||
end
|
||||
|
||||
it 'should log messages for creating template deltas' do
|
||||
expect(logger).to receive(:log).with('s', "[*] [#{pool}] creating template deltas")
|
||||
expect(logger).to receive(:log).with('s', "[*] [#{pool}] template deltas have been created")
|
||||
|
||||
subject.update_pool_template(config[:pools][0], provider)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ready and pending vms' do
|
||||
let(:vmname) { 'vm2' }
|
||||
before(:each) do
|
||||
create_ready_vm(pool,vmname)
|
||||
create_pending_vm(pool,vmname)
|
||||
redis.hset('vmpooler__template', pool, template)
|
||||
redis.hset('vmpooler__config__template', pool, new_template)
|
||||
allow(logger).to receive(:log)
|
||||
allow(redis).to receive(:smove)
|
||||
expect(provider).to receive(:create_template_delta_disks).with(config[:pools][0])
|
||||
end
|
||||
|
||||
it 'should log a message for removing ready vms' do
|
||||
|
||||
expect(logger).to receive(:log).with('s', "[*] [#{pool}] removing ready and pending instances")
|
||||
|
||||
subject.update_pool_template(config[:pools][0], provider)
|
||||
end
|
||||
it 'should remove ready vms' do
|
||||
expect(redis).to receive(:smove).with("vmpooler__ready__#{pool}", "vmpooler__completed__#{pool}", vmname)
|
||||
|
||||
subject.update_pool_template(config[:pools][0], provider)
|
||||
end
|
||||
|
||||
it 'should remove pending vms' do
|
||||
expect(redis).to receive(:smove).with("vmpooler__pending__#{pool}", "vmpooler__completed__#{pool}", vmname)
|
||||
|
||||
subject.update_pool_template(config[:pools][0], provider)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when already updating' do
|
||||
before(:each) do
|
||||
redis.hset('vmpooler__template', pool, template)
|
||||
redis.hset('vmpooler__config__template', pool, new_template)
|
||||
redis.hset('vmpooler__config__updating', pool, 1)
|
||||
end
|
||||
|
||||
it 'should return' do
|
||||
expect(subject.update_pool_template(config[:pools][0], provider)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remove_excess_vms' do
|
||||
let(:config) {
|
||||
YAML.load(<<-EOT
|
||||
---
|
||||
:pools:
|
||||
- name: #{pool}
|
||||
size: 2
|
||||
EOT
|
||||
)
|
||||
}
|
||||
|
||||
before(:each) do
|
||||
expect(subject).not_to be_nil
|
||||
end
|
||||
|
||||
context 'with a nil ready value' do
|
||||
it 'should return nil' do
|
||||
expect(subject.remove_excess_vms(config[:pools][0], provider, nil, nil)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a total size less than the pool size' do
|
||||
it 'should return nil' do
|
||||
expect(subject.remove_excess_vms(config[:pools][0], provider, 1, 2)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a total size greater than the pool size' do
|
||||
it 'should remove excess ready vms' do
|
||||
expect(subject).to receive(:move_vm_queue).exactly(2).times
|
||||
|
||||
subject.remove_excess_vms(config[:pools][0], provider, 4, 4)
|
||||
end
|
||||
|
||||
it 'should remove excess pending vms' do
|
||||
create_pending_vm(pool,'vm1')
|
||||
create_pending_vm(pool,'vm2')
|
||||
create_pending_vm(pool,'vm3')
|
||||
expect(subject).to receive(:move_vm_queue).exactly(3).times
|
||||
|
||||
subject.remove_excess_vms(config[:pools][0], provider, 2, 5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'prepare_template' do
|
||||
let(:config) { YAML.load(<<-EOT
|
||||
---
|
||||
:config:
|
||||
create_template_delta_disks: true
|
||||
:providers:
|
||||
:mock:
|
||||
:pools:
|
||||
- name: '#{pool}'
|
||||
size: 1
|
||||
template: 'templates/pool1'
|
||||
EOT
|
||||
)
|
||||
}
|
||||
|
||||
it 'should return if a pool configuration is updating' do
|
||||
redis.hset('vmpooler__config__updating', pool, 1)
|
||||
|
||||
expect(subject.prepare_template(config[:pools][0], provider)).to be_nil
|
||||
end
|
||||
|
||||
it 'should return when a template is prepared' do
|
||||
redis.hset('vmpooler__template__prepared', pool, pool['template'])
|
||||
|
||||
expect(subject.prepare_template(config[:pools][0], provider)).to be_nil
|
||||
end
|
||||
|
||||
context 'when creating the template delta disks' do
|
||||
before(:each) do
|
||||
allow(redis).to receive(:hset)
|
||||
allow(redis).to receive(:hdel)
|
||||
allow(provider).to receive(:create_template_delta_disks)
|
||||
end
|
||||
|
||||
it 'should mark the pool as updating' do
|
||||
expect(redis).to receive(:hset).with('vmpooler__config__updating', pool, 1)
|
||||
|
||||
subject.prepare_template(config[:pools][0], provider)
|
||||
end
|
||||
|
||||
it 'should run create template delta disks' do
|
||||
expect(provider).to receive(:create_template_delta_disks).with(config[:pools][0])
|
||||
|
||||
subject.prepare_template(config[:pools][0], provider)
|
||||
end
|
||||
|
||||
it 'should mark the template as prepared' do
|
||||
expect(redis).to receive(:hset).with('vmpooler__template__prepared', pool, config[:pools][0]['template'])
|
||||
|
||||
subject.prepare_template(config[:pools][0], provider)
|
||||
end
|
||||
|
||||
it' should mark the configuration as completed' do
|
||||
expect(redis).to receive(:hdel).with('vmpooler__config__updating', pool)
|
||||
|
||||
subject.prepare_template(config[:pools][0], provider)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#execute!" do
|
||||
let(:config) {
|
||||
YAML.load(<<-EOT
|
||||
|
|
@ -1824,6 +2038,8 @@ EOT
|
|||
it 'should run startup tasks only once' do
|
||||
expect(redis).to receive(:set).with('vmpooler__tasks__clone', 0).once
|
||||
expect(redis).to receive(:del).with('vmpooler__migration').once
|
||||
expect(redis).to receive(:del).with('vmpooler__config__updating').once
|
||||
expect(redis).to receive(:del).with('vmpooler__template__prepared').once
|
||||
|
||||
subject.execute!(maxloop,0)
|
||||
end
|
||||
|
|
@ -2785,6 +3001,54 @@ EOT
|
|||
end
|
||||
end
|
||||
|
||||
context 'when a pool size configuration change is detected' do
|
||||
let(:poolsize) { 2 }
|
||||
let(:newpoolsize) { 3 }
|
||||
before(:each) do
|
||||
config[:pools][0]['size'] = poolsize
|
||||
redis.hset('vmpooler__config__poolsize', pool, newpoolsize)
|
||||
expect(provider).to receive(:vms_in_pool).with(pool).and_return([])
|
||||
end
|
||||
|
||||
it 'should change the pool size configuration' do
|
||||
subject._check_pool(config[:pools][0],provider)
|
||||
|
||||
expect(config[:pools][0]['size']).to be(newpoolsize)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a pool template is updating' do
|
||||
before(:each) do
|
||||
redis.hset('vmpooler__config__updating', pool, 1)
|
||||
expect(provider).to receive(:vms_in_pool).with(pool).and_return([])
|
||||
end
|
||||
|
||||
it 'should not call clone_vm to populate the pool' do
|
||||
pool_size = 5
|
||||
config[:pools][0]['size'] = pool_size
|
||||
|
||||
expect(subject).to_not receive(:clone_vm)
|
||||
|
||||
subject._check_pool(pool_object,provider)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an excess number of ready vms exist' do
|
||||
|
||||
before(:each) do
|
||||
allow(redis).to receive(:scard)
|
||||
expect(redis).to receive(:scard).with("vmpooler__ready__#{pool}").and_return(1)
|
||||
expect(redis).to receive(:scard).with("vmpooler__pending__#{pool}").and_return(1)
|
||||
expect(provider).to receive(:vms_in_pool).with(pool).and_return([])
|
||||
end
|
||||
|
||||
it 'should call remove_excess_vms' do
|
||||
expect(subject).to receive(:remove_excess_vms).with(config[:pools][0], provider, 1, 2)
|
||||
|
||||
subject._check_pool(config[:pools][0],provider)
|
||||
end
|
||||
end
|
||||
|
||||
context 'export metrics' do
|
||||
it 'increments metrics for ready queue' do
|
||||
create_ready_vm(pool,'vm1')
|
||||
|
|
|
|||
|
|
@ -283,6 +283,7 @@ EOT
|
|||
|
||||
let(:clone_vm_task) { mock_RbVmomi_VIM_Task() }
|
||||
let(:new_vm_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname }) }
|
||||
let(:new_template_object) { mock_RbVmomi_VIM_VirtualMachine({ :name => vmname }) }
|
||||
|
||||
before(:each) do
|
||||
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
|
||||
|
|
@ -305,19 +306,10 @@ EOT
|
|||
end
|
||||
end
|
||||
|
||||
context 'Given a template path that does not exist' do
|
||||
before(:each) do
|
||||
config[:pools][0]['template'] = 'missing_Templates/pool1'
|
||||
end
|
||||
|
||||
it 'should raise an error' do
|
||||
expect{ subject.create_vm(poolname, vmname) }.to raise_error(/specifies a template folder of .+ which does not exist/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'Given a template VM that does not exist' do
|
||||
before(:each) do
|
||||
config[:pools][0]['template'] = 'Templates/missing_template'
|
||||
expect(subject).to receive(:find_template_vm).and_raise("specifies a template VM of #{vmname} which does not exist")
|
||||
end
|
||||
|
||||
it 'should raise an error' do
|
||||
|
|
@ -327,7 +319,8 @@ EOT
|
|||
|
||||
context 'Given a successful creation' do
|
||||
before(:each) do
|
||||
template_vm = subject.find_folder('Templates',connection,datacenter_name).find('pool1')
|
||||
template_vm = new_template_object
|
||||
allow(subject).to receive(:find_template_vm).and_return(new_template_object)
|
||||
allow(template_vm).to receive(:CloneVM_Task).and_return(clone_vm_task)
|
||||
allow(clone_vm_task).to receive(:wait_for_completion).and_return(new_vm_object)
|
||||
end
|
||||
|
|
@ -339,7 +332,7 @@ EOT
|
|||
end
|
||||
|
||||
it 'should use the appropriate Create_VM spec' do
|
||||
template_vm = subject.find_folder('Templates',connection,datacenter_name).find('pool1')
|
||||
template_vm = new_template_object
|
||||
expect(template_vm).to receive(:CloneVM_Task)
|
||||
.with(create_vm_spec(vmname,'pool1','datastore0'))
|
||||
.and_return(clone_vm_task)
|
||||
|
|
@ -3578,5 +3571,52 @@ EOT
|
|||
end
|
||||
end
|
||||
|
||||
describe 'find_template_vm' do
|
||||
let(:vm_object) { mock_RbVmomi_VIM_VirtualMachine() }
|
||||
|
||||
before(:each) do
|
||||
allow(connection.searchIndex).to receive(:FindByInventoryPath)
|
||||
end
|
||||
it 'should raise an error when the datacenter cannot be found' do
|
||||
config[:providers][:vsphere]['datacenter'] = nil
|
||||
|
||||
expect{ subject.find_template_vm(config[:pools][0],connection) }.to raise_error('cannot find datacenter')
|
||||
end
|
||||
|
||||
it 'should raise an error when the template specified cannot be found' do
|
||||
expect(connection.searchIndex).to receive(:FindByInventoryPath).and_return(nil)
|
||||
|
||||
expect{ subject.find_template_vm(config[:pools][0],connection) }.to raise_error("Pool #{poolname} specifies a template VM of #{config[:pools][0]['template']} which does not exist for the provider vsphere")
|
||||
end
|
||||
|
||||
it 'should return the vm object' do
|
||||
expect(connection.searchIndex).to receive(:FindByInventoryPath).and_return(vm_object)
|
||||
|
||||
subject.find_template_vm(config[:pools][0],connection)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'create_template_delta_disks' do
|
||||
let(:template_object) { mock_RbVmomi_VIM_VirtualMachine({
|
||||
:name => vmname,
|
||||
})
|
||||
}
|
||||
|
||||
before(:each) do
|
||||
allow(subject).to receive(:connect_to_vsphere).and_return(connection)
|
||||
end
|
||||
|
||||
context 'with a template VM found' do
|
||||
|
||||
before(:each) do
|
||||
expect(subject).to receive(:find_template_vm).and_return(template_object)
|
||||
end
|
||||
|
||||
it 'should reconfigure the VM creating delta disks' do
|
||||
expect(template_object).to receive(:add_delta_disk_layer_on_all_disks)
|
||||
|
||||
subject.create_template_delta_disks(config[:pools][0])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue