From ac96550f57d02c359b699a799ddb8deeb12f88bb Mon Sep 17 00:00:00 2001 From: Jake Spain Date: Fri, 3 Feb 2023 17:02:47 -0500 Subject: [PATCH 01/19] Stub out dns provider usage --- lib/vmpooler/dns.rb | 67 +++++++++++++++++++++ lib/vmpooler/dns/base.rb | 69 +++++++++++++++++++++ lib/vmpooler/pool_manager.rb | 60 +++++++++++++++++-- lib/vmpooler/providers/base.rb | 4 ++ spec/unit/pool_manager_spec.rb | 106 +++++++++++++++++++++++++++++++++ 5 files changed, 300 insertions(+), 6 deletions(-) create mode 100644 lib/vmpooler/dns.rb create mode 100644 lib/vmpooler/dns/base.rb diff --git a/lib/vmpooler/dns.rb b/lib/vmpooler/dns.rb new file mode 100644 index 0000000..e11f229 --- /dev/null +++ b/lib/vmpooler/dns.rb @@ -0,0 +1,67 @@ +require 'pathname' + +module Vmpooler + class Dns + + # Load one or more VMPooler DNS plugin gems by name + # + # @param names [Array] The list of gem names to load + def self.load_by_name(names) + names = Array(names) + instance = new + names.map { |name| instance.load_from_gems(name) }.flatten + end + + # Returns the plugin class for the specified dns config by name + # + # @param config [Object] The entire VMPooler config object + # @param name [Symbol] The name of the dns config key to get the dns class + # @return [String] The plugin class for the specifid dns config + def self.get_dns_plugin_class_by_name(config, name) + dns_configs = config[:dns_configs].keys + plugin_class = '' + + dns_configs.map do |dns_config_name| + if dns_config_name.to_s == name + plugin_class = config[:dns_configs][dns_config_name]['dns_class'] + end + end + + plugin_class + end + + # Returns a list of DNS plugin classes specified in the vmpooler configuration + # + # @param config [Object] The entire VMPooler config object + # @return [Array e if request_id $logger.log('s', "[!] [#{pool_name}] failed while cloning VM for request #{request_id} with an error: #{e}") @@ -414,7 +421,7 @@ module Vmpooler [dns_ip, false] end - def _clone_vm(pool_name, provider, request_id = nil, pool_alias = nil) + def _clone_vm(pool_name, provider, dns_plugin, request_id = nil, pool_alias = nil) new_vmname = find_unique_hostname(pool_name) pool_domain = Parsing.get_domain_for_pool(config, pool_name) mutex = vm_mutex(new_vmname) @@ -447,6 +454,8 @@ module Vmpooler $logger.log('s', "[+] [#{pool_name}] '#{new_vmname}' cloned in #{finish} seconds") $metrics.timing("clone.#{pool_name}", finish) + + dns_plugin.create_or_replace_record(new_vmname) rescue StandardError @redis.with_metrics do |redis| redis.pipelined do |pipeline| @@ -632,6 +641,12 @@ module Vmpooler result end + # load only dns plugins used in config file + def load_used_dns_plugins + dns_plugins = Vmpooler::Dns.get_dns_plugin_config_classes(config) + Vmpooler::Dns.load_by_name(dns_plugins) + end + # load only providers used in config file def load_used_providers Vmpooler::Providers.load_by_name(used_providers) @@ -679,6 +694,15 @@ module Vmpooler $providers[provider_name] end + def get_dns_plugin_class_for_pool(pool_name) + pool = $config[:pools].find { |p| p['name'] == pool_name } + return nil unless pool + + plugin_name = pool.fetch('dns_plugin') + plugin_class = Vmpooler::Dns.get_dns_plugin_class_by_name(config, plugin_name) + $dns_plugins[plugin_class] + end + def check_disk_queue(maxloop = 0, loop_delay = 5) $logger.log('d', '[*] [disk_manager] starting worker thread') @@ -1290,6 +1314,8 @@ module Vmpooler $metrics.gauge("ready.#{pool_name}", ready) $metrics.gauge("running.#{pool_name}", running) + dns_plugin = get_dns_plugin_class_for_pool(pool_name) + unless pool_size == 0 if redis.get("vmpooler__empty__#{pool_name}") redis.del("vmpooler__empty__#{pool_name}") unless ready == 0 @@ -1304,7 +1330,7 @@ module Vmpooler begin redis.incr('vmpooler__tasks__clone') pool_check_response[:cloned_vms] += 1 - clone_vm(pool_name, provider) + clone_vm(pool_name, provider, dns_plugin) rescue StandardError => e $logger.log('s', "[!] [#{pool_name}] clone failed during check_pool with an error: #{e}") redis.decr('vmpooler__tasks__clone') @@ -1390,6 +1416,16 @@ module Vmpooler raise("Provider '#{provider_class}' is unknown for pool with provider name '#{provider_name}'") if provider_klass.nil? end + def create_dns_object(config, logger, metrics, redis_connection_pool, dns_class, dns_name, options) + dns_klass = Vmpooler::PoolManager::Dns + dns_klass.constants.each do |classname| + next unless classname.to_s.casecmp(dns_class) == 0 + + return dns_klass.const_get(classname).new(config, logger, metrics, redis_connection_pool, dns_name, options) + end + raise("DNS '#{dns_class}' is unknown for pool with dns name '#{dns_name}'") if dns_klass.nil? + end + def check_ondemand_requests(maxloop = 0, loop_delay_min = CHECK_LOOP_DELAY_MIN_DEFAULT, loop_delay_max = CHECK_LOOP_DELAY_MAX_DEFAULT, @@ -1473,20 +1509,21 @@ module Vmpooler pool_alias, pool, count, request_id = request.split(':') count = count.to_i provider = get_provider_for_pool(pool) + dns_plugin = get_dns_plugin_class_for_pool(pool) slots = ondemand_clone_limit - clone_count break if slots == 0 if slots >= count count.times do redis.incr('vmpooler__tasks__ondemandclone') - clone_vm(pool, provider, request_id, pool_alias) + clone_vm(pool, provider, dns_plugin, request_id, pool_alias) end redis.zrem(queue_key, request) else remaining_count = count - slots slots.times do redis.incr('vmpooler__tasks__ondemandclone') - clone_vm(pool, provider, request_id, pool_alias) + clone_vm(pool, provider, dns_plugin, request_id, pool_alias) end redis.pipelined do |pipeline| pipeline.zrem(queue_key, request) @@ -1601,6 +1638,7 @@ module Vmpooler # Create the providers $config[:pools].each do |pool| provider_name = pool['provider'] + dns_plugin_name = pool['dns_plugin'] # The provider_class parameter can be defined in the provider's data eg # :providers: # :vsphere: @@ -1621,12 +1659,22 @@ module Vmpooler else provider_class = $config[:providers][provider_name.to_sym]['provider_class'] end + begin $providers[provider_name] = create_provider_object($config, $logger, $metrics, @redis, provider_class, provider_name, {}) if $providers[provider_name].nil? rescue StandardError => e $logger.log('s', "Error while creating provider for pool #{pool['name']}: #{e}") raise end + + dns_plugin_class = $config[:dns_configs][dns_plugin_name.to_sym]['dns_class'] + + begin + $dns_plugins[dns_plugin_class] = create_dns_object($config, $logger, $metrics, @redis, dns_plugin_class, dns_plugin_name, {}) if $dns_plugins[dns_plugin_class].nil? + rescue StandardError => e + $logger.log('s', "Error while creating dns plugin for pool #{pool['name']}: #{e}") + raise + end end purge_unused_vms_and_resources diff --git a/lib/vmpooler/providers/base.rb b/lib/vmpooler/providers/base.rb index 1147aa8..f02e088 100644 --- a/lib/vmpooler/providers/base.rb +++ b/lib/vmpooler/providers/base.rb @@ -34,6 +34,10 @@ module Vmpooler # Helper Methods + def get_dns_plugin_for_pool(pool_name) + + end + # inputs # [String] pool_name : Name of the pool to get the configuration # returns diff --git a/spec/unit/pool_manager_spec.rb b/spec/unit/pool_manager_spec.rb index df335fc..bfcead3 100644 --- a/spec/unit/pool_manager_spec.rb +++ b/spec/unit/pool_manager_spec.rb @@ -75,6 +75,27 @@ EOT end end + describe '#load_used_dns_plugins' do + let(:config) { YAML.load(<<-EOT +--- +:config: +:dns_configs: + :base: +:pools: + - name: '#{pool}' + size: 1 + provider: 'spoof' + EOT + ) + } + it do + files = ['vmpooler/dns/base'] + expect(subject.load_used_dns_plugins).to eq(files) + end + + + end + describe '#used_providers' do context 'with no named providers' do let(:config) { YAML.load(<<-EOT @@ -1724,6 +1745,51 @@ EOT end end + describe '#get_dns_plugin_class_name_for_pool' do + let(:config) { YAML.load(<<-EOT +--- +:dns_configs: + :mock: + dns_class: base +:pools: + - name: #{pool} + dns_plugin: 'mock' +EOT + + )} + before(:each) do + allow(Vmpooler::Dns).to receive(:load_used_dns_plugins).and_return('vmpooler/dns/mock') + end + + it 'calls Vmpooler::Dns.get_dns_plugin_class_by_name' do + expect(Vmpooler::Dns).to receive(:get_dns_plugin_class_by_name).with(config, 'mock') + + subject.get_dns_plugin_class_name_for_pool(pool) + end + end + + describe '#get_dns_plugin_domain_for_pool' do + let(:config) { YAML.load(<<-EOT +--- +:dns_configs: + :mock: + dns_class: base +:pools: + - name: #{pool} + dns_plugin: 'mock' +EOT + )} + before(:each) do + allow(Vmpooler::Dns).to receive(:load_used_dns_plugins).and_return('vmpooler/dns/mock') + end + + it 'calls Vmpooler::Dns.get_dns_plugin_domain_for_pool' do + expect(Vmpooler::Dns).to receive(:get_dns_plugin_domain_by_name).with(config, 'mock') + + subject.get_dns_plugin_domain_for_pool(pool) + end + end + describe '#check_disk_queue' do let(:threads) {[]} @@ -2817,6 +2883,46 @@ EOT subject.execute!(1,0) end + context 'creating Dns plugins' do + let(:mock_dns_plugin) { double('mock_dns_plugin') } + let(:config) { + YAML.load(<<-EOT +--- +:dns_configs: + :mock: + dns_class: base +:pools: + - name: #{pool} + dns_plugin: 'mock' + - name: 'dummy' + dns_plugin: 'mock' + - name: 'dummy2' + dns_plugin: 'mock' +EOT + )} + + it 'should call create_dns_object idempotently' do + # Even though there are two pools using the mock dns plugin, it should only + # create the dns object once. + expect(subject).to receive(:create_dns_object).and_return(mock_dns_plugin) + + subject.execute!(1,0) + end + + it 'should raise an error if the dns plugin cannot be created' do + expect(subject).to receive(:create_dns_object).and_raise(RuntimeError, "MockError") + + expect{ subject.execute!(1,0) }.to raise_error(/MockError/) + end + + it 'should log a message if the dns plugin cannot be created' do + expect(subject).to receive(:create_dns_object).and_raise(RuntimeError, "MockError") + expect(logger).to receive(:log).with('s',"Error while creating dns plugin for pool #{pool}: MockError") + + expect{ subject.execute!(1,0) }.to raise_error(/MockError/) + end + end + context 'creating Providers' do let(:vsphere_provider) { double('vsphere_provider') } let(:config) { From 65f04254a8b8995f0fdf3c51c0db57bf6a20675f Mon Sep 17 00:00:00 2001 From: Jake Spain Date: Tue, 7 Feb 2023 06:40:25 -0500 Subject: [PATCH 02/19] Add delete_record --- lib/vmpooler/dns.rb | 18 ++++++++++++++++++ lib/vmpooler/dns/base.rb | 4 ++++ lib/vmpooler/pool_manager.rb | 25 +++++++++++++++++++++---- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/lib/vmpooler/dns.rb b/lib/vmpooler/dns.rb index e11f229..f4c06ca 100644 --- a/lib/vmpooler/dns.rb +++ b/lib/vmpooler/dns.rb @@ -30,6 +30,24 @@ module Vmpooler plugin_class end + # Returns the plugin domain for the specified dns config by name + # + # @param config [Object] The entire VMPooler config object + # @param name [Symbol] The name of the dns config key to get the dns domain + # @return [String] The domain for the specifid dns config + def self.get_dns_plugin_domain_by_name(config, name) + dns_configs = config[:dns_configs].keys + plugin_domain = '' + + dns_configs.map do |dns_config_name| + if dns_config_name.to_s == name + plugin_domain = config[:dns_configs][dns_config_name]['domain'] + end + end + + plugin_domain + end + # Returns a list of DNS plugin classes specified in the vmpooler configuration # # @param config [Object] The entire VMPooler config object diff --git a/lib/vmpooler/dns/base.rb b/lib/vmpooler/dns/base.rb index 739395b..7cc7c21 100644 --- a/lib/vmpooler/dns/base.rb +++ b/lib/vmpooler/dns/base.rb @@ -63,6 +63,10 @@ module Vmpooler def create_or_replace_record(hostname) raise("#{self.class.name} does not implement create_or_replace_record") end + + def delete_record(hostname) + raise("#{self.class.name} does not implement delete_record") + end end end end diff --git a/lib/vmpooler/pool_manager.rb b/lib/vmpooler/pool_manager.rb index a766dd2..4fab1b0 100644 --- a/lib/vmpooler/pool_manager.rb +++ b/lib/vmpooler/pool_manager.rb @@ -113,6 +113,10 @@ module Vmpooler def remove_nonexistent_vm(vm, pool, redis) redis.srem("vmpooler__pending__#{pool}", vm) + dns_plugin = get_dns_plugin_class_for_pool(pool) + domain = get_dns_plugin_domain_for_pool(pool) + fqdn = vm + '.' + domain + dns_plugin.delete_record(fqdn) $logger.log('d', "[!] [#{pool}] '#{vm}' no longer exists. Removing from pending.") end @@ -475,10 +479,10 @@ module Vmpooler end # Destroy a VM - def destroy_vm(vm, pool, provider) + def destroy_vm(vm, pool, provider, dns_plugin) Thread.new do begin - _destroy_vm(vm, pool, provider) + _destroy_vm(vm, pool, provider, dns_plugin) rescue StandardError => e $logger.log('d', "[!] [#{pool}] '#{vm}' failed while destroying the VM with an error: #{e}") raise @@ -486,7 +490,7 @@ module Vmpooler end end - def _destroy_vm(vm, pool, provider) + def _destroy_vm(vm, pool, provider, dns_plugin) mutex = vm_mutex(vm) return if mutex.locked? @@ -503,6 +507,9 @@ module Vmpooler start = Time.now provider.destroy_vm(pool, vm) + domain = get_dns_plugin_domain_for_pool(pool) + fqdn = vm + '.' + domain + dns_plugin.delete_record(fqdn) redis.srem("vmpooler__completed__#{pool}", vm) @@ -703,6 +710,15 @@ module Vmpooler $dns_plugins[plugin_class] end + def get_dns_plugin_domain_for_pool(pool_name) + pool = $config[:pools].find { |p| p['name'] == pool_name } + return nil unless pool + + plugin_name = pool.fetch('dns_plugin') + plugin_domain = Vmpooler::Dns.get_dns_plugin_domain_by_name(config, plugin_name) + plugin_domain + end + def check_disk_queue(maxloop = 0, loop_delay = 5) $logger.log('d', '[*] [disk_manager] starting worker thread') @@ -1247,7 +1263,8 @@ module Vmpooler if inventory[vm] begin pool_check_response[:destroyed_vms] += 1 - destroy_vm(vm, pool_name, provider) + dns_plugin = get_dns_plugin_class_for_pool(pool_name) + destroy_vm(vm, pool_name, provider, dns_plugin) rescue StandardError => e redis.pipelined do |pipeline| pipeline.srem("vmpooler__completed__#{pool_name}", vm) From b1e20a2fc072f3b7e154251cdcc45592abe2537f Mon Sep 17 00:00:00 2001 From: Jake Spain Date: Tue, 7 Feb 2023 07:28:50 -0500 Subject: [PATCH 03/19] Get zone from config and add dns/base_spec --- lib/vmpooler/dns/base.rb | 12 +++ lib/vmpooler/providers/base.rb | 4 - spec/unit/dns/base_spec.rb | 172 +++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 spec/unit/dns/base_spec.rb diff --git a/lib/vmpooler/dns/base.rb b/lib/vmpooler/dns/base.rb index 7cc7c21..c2cd72d 100644 --- a/lib/vmpooler/dns/base.rb +++ b/lib/vmpooler/dns/base.rb @@ -34,6 +34,18 @@ module Vmpooler nil end + # Returns this dns plugin's configuration + # + # @returns [Hashtable] This dns plugins's configuration from the config file. Returns nil if the dns plugin config does not exist + def dns_config + @config[:dns_configs].each do |dns| + # Convert the symbol from the config into a string for comparison + return (dns[1].nil? ? {} : dns[1]) if dns[0].to_s == @dns_plugin_name + end + + nil + end + def global_config # This entire VM Pooler config @config diff --git a/lib/vmpooler/providers/base.rb b/lib/vmpooler/providers/base.rb index f02e088..1147aa8 100644 --- a/lib/vmpooler/providers/base.rb +++ b/lib/vmpooler/providers/base.rb @@ -34,10 +34,6 @@ module Vmpooler # Helper Methods - def get_dns_plugin_for_pool(pool_name) - - end - # inputs # [String] pool_name : Name of the pool to get the configuration # returns diff --git a/spec/unit/dns/base_spec.rb b/spec/unit/dns/base_spec.rb new file mode 100644 index 0000000..9fb656b --- /dev/null +++ b/spec/unit/dns/base_spec.rb @@ -0,0 +1,172 @@ +require 'spec_helper' +require 'vmpooler/dns/base' + +# This spec does not really exercise code paths but is merely used +# to enforce that certain methods are defined in the base classes + +describe 'Vmpooler::PoolManager::Dns::Base' do + let(:logger) { MockLogger.new } + let(:metrics) { Vmpooler::Metrics::DummyStatsd.new } + let(:config) { {} } + let(:dns_plugin_name) { 'base' } + let(:dns_options) { { 'param' => 'value' } } + + let(:fake_vm) { + fake_vm = {} + fake_vm['name'] = 'vm1' + fake_vm['hostname'] = 'vm1' + fake_vm['template'] = 'pool1' + fake_vm['boottime'] = Time.now + fake_vm['powerstate'] = 'PoweredOn' + + fake_vm + } + + let(:redis_connection_pool) { Vmpooler::PoolManager::GenericConnectionPool.new( + metrics: metrics, + connpool_type: 'redis_connection_pool', + connpool_provider: 'testprovider', + size: 1, + timeout: 5 + ) { MockRedis.new } + } + + subject { Vmpooler::PoolManager::Dns::Base.new(config, logger, metrics, redis_connection_pool, dns_plugin_name, dns_options) } + + # Helper attr_reader methods + describe '#logger' do + it 'should come from the provider initialization' do + expect(subject.logger).to be(logger) + end + end + + describe '#metrics' do + it 'should come from the provider initialization' do + expect(subject.metrics).to be(metrics) + end + end + + describe '#dns_options' do + it 'should come from the provider initialization' do + expect(subject.dns_options).to be(dns_options) + end + end + + describe '#pool_config' do + let(:poolname) { 'pool1' } + let(:config) { YAML.load(<<-EOT +--- +:pools: + - name: '#{poolname}' + alias: [ 'mockpool' ] + template: 'Templates/pool1' + folder: 'Pooler/pool1' + datastore: 'datastore0' + size: 5 + timeout: 10 + ready_ttl: 1440 + clone_target: 'cluster1' +EOT + ) + } + context 'Given a pool that does not exist' do + it 'should return nil' do + expect(subject.pool_config('missing_pool')).to be_nil + end + end + + context 'Given a pool that does exist' do + it 'should return the pool\'s configuration' do + result = subject.pool_config(poolname) + expect(result['name']).to eq(poolname) + end + end + end + + describe '#dns_config' do + let(:poolname) { 'pool1' } + let(:config) { YAML.load(<<-EOT +--- +:dns_configs: + :#{dns_plugin_name}: + option1: 'value1' +EOT + ) + } + + context 'Given a dns plugin with no configuration' do + let(:config) { YAML.load(<<-EOT +--- +:dns_configs: + :bad_dns: + option1: 'value1' + option2: 'value1' +EOT + ) + } + it 'should return nil' do + expect(subject.dns_config).to be_nil + end + end + + context 'Given a correct dns config name' do + it 'should return the dns\'s configuration' do + result = subject.dns_config + expect(result['option1']).to eq('value1') + end + end + end + + describe '#global_config' do + it 'should come from the dns initialization' do + expect(subject.global_config).to be(config) + end + end + + describe '#name' do + it "should come from the dns initialization" do + expect(subject.name).to eq(dns_plugin_name) + end + end + + describe '#get_ip' do + it 'calls redis hget with vm name and ip' do + redis_connection_pool.with do |redis| + expect(redis).to receive(:hget).with("vmpooler__vm__vm1", 'ip') + end + subject.get_ip(fake_vm['name']) + end + end + + describe '#provided_pools' do + let(:config) { YAML.load(<<-EOT +--- +:pools: + - name: 'pool1' + dns_config: 'base' + - name: 'pool2' + dns_config: 'base' + - name: 'otherpool' + dns_config: 'other provider' + - name: 'no name' +EOT + ) + } + + it "should return pools serviced by this provider" do + expect(subject.provided_pools).to eq(['pool1','pool2']) + end + end + + describe '#create_or_replace_record' do + it 'should raise error' do + expect{subject.create_or_replace_record('pool')}.to raise_error(/does not implement create_or_replace_record/) + end + end + + describe '#delete_record' do + it 'should raise error' do + expect{subject.delete_record('pool')}.to raise_error(/does not implement delete_record/) + end + end +end From 89a4273760af429a11e3eb76324f2843a169e383 Mon Sep 17 00:00:00 2001 From: Jake Spain Date: Tue, 7 Feb 2023 08:08:16 -0500 Subject: [PATCH 04/19] Add migration to readme --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 08af5d3..03731f3 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ - [VMPooler](#vmpooler) - [Usage](#usage) + - [Migrating to v3](#migrating-to-v3) - [v2.0.0 note](#v200-note) - [Installation](#installation) - [Dependencies](#dependencies) @@ -30,6 +31,38 @@ VMPooler provides configurable 'pools' of instantly-available (pre-provisioned) At [Puppet, Inc.](http://puppet.com) we run acceptance tests on thousands of disposable VMs every day. VMPooler manages the life cycle of these VMs from request through deletion, with options available to pool ready instances, and provision on demand. +### Migrating to v3 + +Starting with the v3.x release, management of DNS records is implemented as DNS plugins, similar to compute providers. This means each pool configuration should be pointing to a configuration object in `:dns_config` to determine it's method of record management. + +For those using the global `DOMAIN` environment variable or global `:config.domain` key, this means records were not previously being managed by VMPooler (presumably managed via dynamic dns), so it's value should be moved to `:dns_configs::domain` with the value for `dns_class` for the config set to `dynamic-dns`. + +For example, the following < v3.x configuration: + +```yaml +:config: + domain: 'example.com' +``` + +becomes: + +```yaml +:dns_configs: + :example: + dns_class: dynamic-dns + domain: 'example.com' +``` + +Then any pools that should have records created via the dns config above should now reference the named dns config in the `dns_plugin` key: + +```yaml +:pools: + - name: 'debian-8-x86_64' + dns_plugin: 'example' +``` + +For those using the GCE provider, [vmpooler-provider-gce](https://github.com/puppetlabs/vmpooler-provider-gce), as of version 1.x the DNS management has been decoupled. See + ### v2.0.0 note As of version 2.0.0, all providers other than the dummy one are now separate gems. Historically the vSphere provider was included within VMPooler itself. That code has been moved to the [puppetlabs/vmpooler-provider-vsphere](https://github.com/puppetlabs/vmpooler-provider-vsphere) repository and the `vmpooler-provider-vsphere` gem. To migrate from VMPooler 1.x to 2.0 you will need to ensure that `vmpooler-provider-vsphere` is installed along side the `vmpooler` gem. See the [Provider API](docs/PROVIDER_API.md) docs for more information. From 6f3d853271c78724280b116c3ec32a3d65a94839 Mon Sep 17 00:00:00 2001 From: Jake Spain Date: Wed, 15 Feb 2023 09:45:03 -0500 Subject: [PATCH 05/19] Do not create/delete records if using dynamic dns --- lib/vmpooler/pool_manager.rb | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/vmpooler/pool_manager.rb b/lib/vmpooler/pool_manager.rb index 4fab1b0..bdbecc3 100644 --- a/lib/vmpooler/pool_manager.rb +++ b/lib/vmpooler/pool_manager.rb @@ -114,9 +114,10 @@ module Vmpooler def remove_nonexistent_vm(vm, pool, redis) redis.srem("vmpooler__pending__#{pool}", vm) dns_plugin = get_dns_plugin_class_for_pool(pool) + dns_plugin_class_name = get_dns_plugin_class_name_for_pool(pool) domain = get_dns_plugin_domain_for_pool(pool) fqdn = vm + '.' + domain - dns_plugin.delete_record(fqdn) + dns_plugin.delete_record(fqdn) unless dns_plugin_class_name == 'dynamic-dns' $logger.log('d', "[!] [#{pool}] '#{vm}' no longer exists. Removing from pending.") end @@ -459,7 +460,8 @@ module Vmpooler $metrics.timing("clone.#{pool_name}", finish) - dns_plugin.create_or_replace_record(new_vmname) + dns_plugin_class_name = get_dns_plugin_class_name_for_pool(pool_name) + dns_plugin.create_or_replace_record(new_vmname) unless dns_plugin_class_name == 'dynamic-dns' rescue StandardError @redis.with_metrics do |redis| redis.pipelined do |pipeline| @@ -509,7 +511,9 @@ module Vmpooler provider.destroy_vm(pool, vm) domain = get_dns_plugin_domain_for_pool(pool) fqdn = vm + '.' + domain - dns_plugin.delete_record(fqdn) + + dns_plugin_class_name = get_dns_plugin_class_name_for_pool(pool) + dns_plugin.delete_record(fqdn) unless dns_plugin_class_name == 'dynamic-dns' redis.srem("vmpooler__completed__#{pool}", vm) @@ -701,6 +705,15 @@ module Vmpooler $providers[provider_name] end + def get_dns_plugin_class_name_for_pool(pool_name) + pool = $config[:pools].find { |p| p['name'] == pool_name } + return nil unless pool + + plugin_name = pool.fetch('dns_plugin') + plugin_class = Vmpooler::Dns.get_dns_plugin_class_by_name(config, plugin_name) + plugin_class + end + def get_dns_plugin_class_for_pool(pool_name) pool = $config[:pools].find { |p| p['name'] == pool_name } return nil unless pool From 268ff9f981170b673a76f4ecfad154ff0319bf4e Mon Sep 17 00:00:00 2001 From: Jake Spain Date: Wed, 15 Feb 2023 11:21:26 -0500 Subject: [PATCH 06/19] Add dns_config method to provider base --- lib/vmpooler/providers/base.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/vmpooler/providers/base.rb b/lib/vmpooler/providers/base.rb index 1147aa8..b81e96a 100644 --- a/lib/vmpooler/providers/base.rb +++ b/lib/vmpooler/providers/base.rb @@ -58,6 +58,10 @@ module Vmpooler nil end + def dns_config(dns_config_name) + return Vmpooler::Dns.get_dns_plugin_domain_by_name(@config, dns_config_name) + end + # returns # [Hashtable] : The entire VMPooler configuration def global_config From aeabb7e134f917f1b1a72f866bdb6e99060528ed Mon Sep 17 00:00:00 2001 From: Jake Spain Date: Thu, 16 Feb 2023 07:47:42 -0500 Subject: [PATCH 07/19] Remove global domain usage from pool manager --- lib/vmpooler/dns.rb | 20 ++++++++++++++++++++ lib/vmpooler/pool_manager.rb | 14 +++++--------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/vmpooler/dns.rb b/lib/vmpooler/dns.rb index f4c06ca..d852982 100644 --- a/lib/vmpooler/dns.rb +++ b/lib/vmpooler/dns.rb @@ -30,6 +30,26 @@ module Vmpooler plugin_class end + # Returns the domain for the specified pool + # + # @param config [String] - the full config structure + # @param pool_name [String] - the name of the pool + # @return [String] - domain name for pool, which is set via reference to the dns_configs block + def self.get_domain_for_pool(config, pool_name) + pool = config[:pools].find { |p| p['name'] == pool_name } + pool_dns_config = pool['dns_plugin'] + dns_configs = config[:dns_configs].keys + pool_domain = '' + + dns_configs.map do |dns_config_name| + if dns_config_name.to_s == pool_dns_config + pool_domain = config[:dns_configs][dns_config_name]['domain'] + end + end + + pool_domain + end + # Returns the plugin domain for the specified dns config by name # # @param config [Object] The entire VMPooler config object diff --git a/lib/vmpooler/pool_manager.rb b/lib/vmpooler/pool_manager.rb index bdbecc3..b819933 100644 --- a/lib/vmpooler/pool_manager.rb +++ b/lib/vmpooler/pool_manager.rb @@ -67,7 +67,7 @@ module Vmpooler to_set[k] = pool[k] end to_set['alias'] = pool['alias'].join(',') if to_set.key?('alias') - to_set['domain'] = Parsing.get_domain_for_pool(config, pool['name']) + to_set['domain'] = Vmpooler::Dns.get_domain_for_pool(config, pool['name']) redis.hmset("vmpooler__pool__#{pool['name']}", to_set.to_a.flatten) unless to_set.empty? end previously_configured_pools.each do |pool| @@ -380,12 +380,8 @@ module Vmpooler max_hostname_retries = 3 while hostname_retries < max_hostname_retries hostname, hostname_available = generate_and_check_hostname - domain = Parsing.get_domain_for_pool(config, pool_name) - if domain - fqdn = "#{hostname}.#{domain}" - else - fqdn = hostname - end + domain = Vmpooler::Dns.get_domain_for_pool(config, pool_name) + fqdn = "#{hostname}.#{domain}" # skip dns check if the provider is set to skip_dns_check_before_creating_vm provider = get_provider_for_pool(pool_name) @@ -428,7 +424,7 @@ module Vmpooler def _clone_vm(pool_name, provider, dns_plugin, request_id = nil, pool_alias = nil) new_vmname = find_unique_hostname(pool_name) - pool_domain = Parsing.get_domain_for_pool(config, pool_name) + pool_domain = Vmpooler::Dns.get_domain_for_pool(config, pool_name) mutex = vm_mutex(new_vmname) mutex.synchronize do @redis.with_metrics do |redis| @@ -438,7 +434,7 @@ module Vmpooler redis.hset("vmpooler__vm__#{new_vmname}", 'clone', Time.now) redis.hset("vmpooler__vm__#{new_vmname}", 'template', pool_name) # This value is used to represent the pool. redis.hset("vmpooler__vm__#{new_vmname}", 'pool', pool_name) - redis.hset("vmpooler__vm__#{new_vmname}", 'domain', pool_domain) if pool_domain + redis.hset("vmpooler__vm__#{new_vmname}", 'domain', pool_domain) redis.hset("vmpooler__vm__#{new_vmname}", 'request_id', request_id) if request_id redis.hset("vmpooler__vm__#{new_vmname}", 'pool_alias', pool_alias) if pool_alias redis.exec From 35dc7cb26f3f7d0bc48f1aecb4b8912a926b9e9e Mon Sep 17 00:00:00 2001 From: Jake Spain Date: Thu, 16 Feb 2023 08:55:33 -0500 Subject: [PATCH 08/19] Update domain in V2 api --- lib/vmpooler.rb | 1 - lib/vmpooler/api.rb | 3 +-- lib/vmpooler/api/v2.rb | 26 ++++++++++---------------- lib/vmpooler/public/lib/dashboard.js | 6 +++--- lib/vmpooler/util/parsing.rb | 20 -------------------- 5 files changed, 14 insertions(+), 42 deletions(-) diff --git a/lib/vmpooler.rb b/lib/vmpooler.rb index 516843f..cfb2e2c 100644 --- a/lib/vmpooler.rb +++ b/lib/vmpooler.rb @@ -76,7 +76,6 @@ module Vmpooler parsed_config[:config]['prefix'] = ENV['PREFIX'] || parsed_config[:config]['prefix'] || '' parsed_config[:config]['logfile'] = ENV['LOGFILE'] if ENV['LOGFILE'] parsed_config[:config]['site_name'] = ENV['SITE_NAME'] if ENV['SITE_NAME'] - parsed_config[:config]['domain'] = ENV['DOMAIN'] if ENV['DOMAIN'] parsed_config[:config]['clone_target'] = ENV['CLONE_TARGET'] if ENV['CLONE_TARGET'] parsed_config[:config]['timeout'] = string_to_int(ENV['TIMEOUT']) if ENV['TIMEOUT'] parsed_config[:config]['vm_lifetime_auth'] = string_to_int(ENV['VM_LIFETIME_AUTH']) if ENV['VM_LIFETIME_AUTH'] diff --git a/lib/vmpooler/api.rb b/lib/vmpooler/api.rb index a7eea30..5397637 100644 --- a/lib/vmpooler/api.rb +++ b/lib/vmpooler/api.rb @@ -3,7 +3,7 @@ module Vmpooler class API < Sinatra::Base # Load API components - %w[helpers dashboard reroute v1 v2 request_logger healthcheck].each do |lib| + %w[helpers dashboard reroute v2 request_logger healthcheck].each do |lib| require "vmpooler/api/#{lib}" end # Load dashboard components @@ -53,7 +53,6 @@ module Vmpooler use Vmpooler::Dashboard use Vmpooler::API::Dashboard use Vmpooler::API::Reroute - use Vmpooler::API::V1 use Vmpooler::API::V2 end diff --git a/lib/vmpooler/api/v2.rb b/lib/vmpooler/api/v2.rb index 44dad6e..d7feb78 100644 --- a/lib/vmpooler/api/v2.rb +++ b/lib/vmpooler/api/v2.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'vmpooler/api/v1' +require 'vmpooler/util/parsing' +require 'vmpooler/dns' module Vmpooler class API @@ -87,9 +88,7 @@ module Vmpooler # last vm added. The part of the response is only being retained for # backwards compatibility as the hostnames are now fqdn's instead of bare # hostnames. This change is a result of now being able to specify a domain - # per pool. If no vm's in the result had a domain sepcified then the - # domain key will be omitted similar to how it was previously omitted if - # the global option domain wasn't specified. + # per pool. def atomically_allocate_vms(payload) tracer.in_span("Vmpooler::API::V2.#{__method__}") do |span| result = { 'ok' => false } @@ -126,15 +125,10 @@ module Vmpooler else vm_names = [] vms.each do |(vmpool, vmname, vmtemplate)| - vmdomain = Parsing.get_domain_for_pool(full_config, vmpool) - if vmdomain - vmfqdn = "#{vmname}.#{vmdomain}" - update_result_hosts(result, vmtemplate, vmfqdn) - vm_names.append(vmfqdn) - else - update_result_hosts(result, vmtemplate, vmname) - vm_names.append(vmname) - end + vmdomain = Dns.get_domain_for_pool(full_config, vmpool) + vmfqdn = "#{vmname}.#{vmdomain}" + update_result_hosts(result, vmtemplate, vmfqdn) + vm_names.append(vmfqdn) end span.set_attribute('vmpooler.vm_names', vm_names.join(',')) unless vm_names.empty? @@ -320,7 +314,7 @@ module Vmpooler result[params[:hostname]]['ip'] = ipAddress if rdata['pool'] - vmdomain = Parsing.get_domain_for_pool(full_config, rdata['pool']) + vmdomain = Dns.get_domain_for_pool(full_config, rdata['pool']) if vmdomain result[params[:hostname]]['fqdn'] = "#{params[:hostname]}.#{vmdomain}" end @@ -436,8 +430,8 @@ module Vmpooler result['ready'] = true Parsing.get_platform_pool_count(request_hash['requested']) do |platform_alias, pool, _count| instances = backend.smembers("vmpooler__#{request_id}__#{platform_alias}__#{pool}") - domain = Parsing.get_domain_for_pool(full_config, pool) - instances.map! { |instance| instance.concat(".#{domain}") } if domain + domain = Dns.get_domain_for_pool(full_config, pool) + instances.map! { |instance| instance.concat(".#{domain}") } if result.key?(platform_alias) result[platform_alias][:hostname] = result[platform_alias][:hostname] + instances diff --git a/lib/vmpooler/public/lib/dashboard.js b/lib/vmpooler/public/lib/dashboard.js index e726b01..11bc868 100644 --- a/lib/vmpooler/public/lib/dashboard.js +++ b/lib/vmpooler/public/lib/dashboard.js @@ -29,10 +29,10 @@ Date.prototype.yyyymmdd = function() { var data_url = { 'capacity': '/dashboard/stats/vmpooler/pool', - 'pools' : '/api/v1/vm', + 'pools' : '/api/v2/vm', 'running' : '/dashboard/stats/vmpooler/running', - 'status' : '/api/v1/status', - 'summary' : '/api/v1/summary' + 'status' : '/api/v2/status', + 'summary' : '/api/v2/summary' }; diff --git a/lib/vmpooler/util/parsing.rb b/lib/vmpooler/util/parsing.rb index 3949a55..1ca6b13 100644 --- a/lib/vmpooler/util/parsing.rb +++ b/lib/vmpooler/util/parsing.rb @@ -12,25 +12,5 @@ module Vmpooler yield platform_alias, pool, count end end - - # @param config [String] - the full config structure - # @param pool_name [String] - the name of the pool - # @return [String] - domain name for pool, if set in the provider for the pool or in the config block - def self.get_domain_for_pool(config, pool_name) - pool = config[:pools].find { |p| p['name'] == pool_name } - return nil unless pool - - provider_name = pool.fetch('provider', 'vsphere') # see vmpooler.yaml.example where it states defaulting to vsphere - - if config[:providers] && config[:providers][provider_name.to_sym] && config[:providers][provider_name.to_sym]['domain'] - domain = config[:providers][provider_name.to_sym]['domain'] - elsif config[:config] && config[:config]['domain'] - domain = config[:config]['domain'] - else - domain = nil - end - - domain - end end end From 0119126cd1813f5357093aef8717fd968dfa0a44 Mon Sep 17 00:00:00 2001 From: Jake Spain Date: Thu, 16 Feb 2023 09:57:18 -0500 Subject: [PATCH 09/19] Update hostname_shorten and callers --- lib/vmpooler/api/helpers.rb | 3 +- lib/vmpooler/api/v2.rb | 411 ++++++++++++++++++++++++++++++++-- spec/unit/api/helpers_spec.rb | 14 +- 3 files changed, 394 insertions(+), 34 deletions(-) diff --git a/lib/vmpooler/api/helpers.rb b/lib/vmpooler/api/helpers.rb index 93d571b..e393466 100644 --- a/lib/vmpooler/api/helpers.rb +++ b/lib/vmpooler/api/helpers.rb @@ -181,8 +181,7 @@ module Vmpooler /^\d{4}-\d{2}-\d{2}$/ === date_str end - # NOTE: domain is not needed here, so we should update the callers of this method - def hostname_shorten(hostname, domain=nil) + def hostname_shorten(hostname) hostname[/[^.]+/] end diff --git a/lib/vmpooler/api/v2.rb b/lib/vmpooler/api/v2.rb index d7feb78..607d9a5 100644 --- a/lib/vmpooler/api/v2.rb +++ b/lib/vmpooler/api/v2.rb @@ -261,7 +261,7 @@ module Vmpooler status 404 result['ok'] = false - params[:hostname] = hostname_shorten(params[:hostname], nil) + params[:hostname] = hostname_shorten(params[:hostname]) rdata = backend.hgetall("vmpooler__vm__#{params[:hostname]}") unless rdata.empty? @@ -467,32 +467,395 @@ module Vmpooler end end - # Endpoints that only use bits from the V1 api are called here - # Note that traces will be named based on the route used in the V1 api - # but the http.url trace attribute will still have the actual requested url in it - - delete "#{api_prefix}/*" do - versionless_path_info = request.path_info.delete_prefix("#{api_prefix}/") - request.path_info = "/api/v1/#{versionless_path_info}" - call env + delete "#{api_prefix}/vm/:hostname/?" do + content_type :json + metrics.increment('http_requests_vm_total.delete.vm.hostname') + + result = {} + + status 404 + result['ok'] = false + + params[:hostname] = hostname_shorten(params[:hostname]) + + rdata = backend.hgetall("vmpooler__vm__#{params[:hostname]}") + unless rdata.empty? + need_token! if rdata['token:token'] + + if backend.srem("vmpooler__running__#{rdata['template']}", params[:hostname]) + backend.sadd("vmpooler__completed__#{rdata['template']}", params[:hostname]) + + status 200 + result['ok'] = true + metrics.increment('delete.success') + update_user_metrics('destroy', params[:hostname]) if Vmpooler::API.settings.config[:config]['usage_stats'] + else + metrics.increment('delete.failed') + end + end + + JSON.pretty_generate(result) end - - get "#{api_prefix}/*" do - versionless_path_info = request.path_info.delete_prefix("#{api_prefix}/") - request.path_info = "/api/v1/#{versionless_path_info}" - call env + + put "#{api_prefix}/vm/:hostname/?" do + content_type :json + metrics.increment('http_requests_vm_total.put.vm.modify') + + status 404 + result = { 'ok' => false } + + failure = [] + + params[:hostname] = hostname_shorten(params[:hostname]) + + if backend.exists?("vmpooler__vm__#{params[:hostname]}") + begin + jdata = JSON.parse(request.body.read) + rescue StandardError => e + span = OpenTelemetry::Trace.current_span + span.record_exception(e) + span.status = OpenTelemetry::Trace::Status.error(e.to_s) + halt 400, JSON.pretty_generate(result) + end + + # Validate data payload + jdata.each do |param, arg| + case param + when 'lifetime' + need_token! if Vmpooler::API.settings.config[:auth] + + # in hours, defaults to one week + max_lifetime_upper_limit = config['max_lifetime_upper_limit'] + if max_lifetime_upper_limit + max_lifetime_upper_limit = max_lifetime_upper_limit.to_i + if arg.to_i >= max_lifetime_upper_limit + failure.push("You provided a lifetime (#{arg}) that exceeds the configured maximum of #{max_lifetime_upper_limit}.") + end + end + + # validate lifetime is within boundaries + unless arg.to_i > 0 + failure.push("You provided a lifetime (#{arg}) but you must provide a positive number.") + end + + when 'tags' + failure.push("You provided tags (#{arg}) as something other than a hash.") unless arg.is_a?(Hash) + failure.push("You provided unsuppored tags (#{arg}).") if config['allowed_tags'] && !(arg.keys - config['allowed_tags']).empty? + else + failure.push("Unknown argument #{arg}.") + end + end + + if !failure.empty? + status 400 + result['failure'] = failure + else + jdata.each do |param, arg| + case param + when 'lifetime' + need_token! if Vmpooler::API.settings.config[:auth] + + arg = arg.to_i + + backend.hset("vmpooler__vm__#{params[:hostname]}", param, arg) + when 'tags' + filter_tags(arg) + export_tags(backend, params[:hostname], arg) + end + end + + status 200 + result['ok'] = true + end + end + + JSON.pretty_generate(result) end - - post "#{api_prefix}/*" do - versionless_path_info = request.path_info.delete_prefix("#{api_prefix}/") - request.path_info = "/api/v1/#{versionless_path_info}" - call env + + post "#{api_prefix}/vm/:hostname/disk/:size/?" do + content_type :json + metrics.increment('http_requests_vm_total.post.vm.disksize') + + need_token! if Vmpooler::API.settings.config[:auth] + + status 404 + result = { 'ok' => false } + + params[:hostname] = hostname_shorten(params[:hostname]) + + if ((params[:size].to_i > 0 )and (backend.exists?("vmpooler__vm__#{params[:hostname]}"))) + result[params[:hostname]] = {} + result[params[:hostname]]['disk'] = "+#{params[:size]}gb" + + backend.sadd('vmpooler__tasks__disk', "#{params[:hostname]}:#{params[:size]}") + + status 202 + result['ok'] = true + end + + JSON.pretty_generate(result) end - - put "#{api_prefix}/*" do - versionless_path_info = request.path_info.delete_prefix("#{api_prefix}/") - request.path_info = "/api/v1/#{versionless_path_info}" - call env + + post "#{api_prefix}/vm/:hostname/snapshot/?" do + content_type :json + metrics.increment('http_requests_vm_total.post.vm.snapshot') + + need_token! if Vmpooler::API.settings.config[:auth] + + status 404 + result = { 'ok' => false } + + params[:hostname] = hostname_shorten(params[:hostname]) + + if backend.exists?("vmpooler__vm__#{params[:hostname]}") + result[params[:hostname]] = {} + + o = [('a'..'z'), ('0'..'9')].map(&:to_a).flatten + result[params[:hostname]]['snapshot'] = o[rand(25)] + (0...31).map { o[rand(o.length)] }.join + + backend.sadd('vmpooler__tasks__snapshot', "#{params[:hostname]}:#{result[params[:hostname]]['snapshot']}") + + status 202 + result['ok'] = true + end + + JSON.pretty_generate(result) + end + + post "#{api_prefix}/vm/:hostname/snapshot/:snapshot/?" do + content_type :json + metrics.increment('http_requests_vm_total.post.vm.snapshot') + + need_token! if Vmpooler::API.settings.config[:auth] + + status 404 + result = { 'ok' => false } + + params[:hostname] = hostname_shorten(params[:hostname]) + + unless backend.hget("vmpooler__vm__#{params[:hostname]}", "snapshot:#{params[:snapshot]}").to_i.zero? + backend.sadd('vmpooler__tasks__snapshot-revert', "#{params[:hostname]}:#{params[:snapshot]}") + + status 202 + result['ok'] = true + end + + JSON.pretty_generate(result) + end + + delete "#{api_prefix}/config/poolsize/:pool/?" do + content_type :json + result = { 'ok' => false } + + if config['experimental_features'] + need_token! if Vmpooler::API.settings.config[:auth] + + if pool_exists?(params[:pool]) + result = reset_pool_size(params[:pool]) + else + metrics.increment('config.invalid.unknown') + status 404 + end + else + status 405 + end + + JSON.pretty_generate(result) + end + + post "#{api_prefix}/config/poolsize/?" do + content_type :json + result = { 'ok' => false } + + if config['experimental_features'] + need_token! if Vmpooler::API.settings.config[:auth] + + payload = JSON.parse(request.body.read) + + if payload + invalid = invalid_template_or_size(payload) + if invalid.empty? + result = update_pool_size(payload) + else + invalid.each do |bad_template| + metrics.increment("config.invalid.#{bad_template}") + end + result[:not_configured] = invalid + status 400 + end + else + metrics.increment('config.invalid.unknown') + status 404 + end + else + status 405 + end + + JSON.pretty_generate(result) + end + + delete "#{api_prefix}/config/pooltemplate/:pool/?" do + content_type :json + result = { 'ok' => false } + + if config['experimental_features'] + need_token! if Vmpooler::API.settings.config[:auth] + + if pool_exists?(params[:pool]) + result = reset_pool_template(params[:pool]) + else + metrics.increment('config.invalid.unknown') + status 404 + end + else + status 405 + end + + JSON.pretty_generate(result) + end + + post "#{api_prefix}/config/pooltemplate/?" do + content_type :json + result = { 'ok' => false } + + if config['experimental_features'] + need_token! if Vmpooler::API.settings.config[:auth] + + payload = JSON.parse(request.body.read) + + if payload + invalid = invalid_template_or_path(payload) + if invalid.empty? + result = update_pool_template(payload) + else + invalid.each do |bad_template| + metrics.increment("config.invalid.#{bad_template}") + end + result[:bad_templates] = invalid + status 400 + end + else + metrics.increment('config.invalid.unknown') + status 404 + end + else + status 405 + end + + JSON.pretty_generate(result) + end + + post "#{api_prefix}/poolreset/?" do + content_type :json + result = { 'ok' => false } + + if config['experimental_features'] + need_token! if Vmpooler::API.settings.config[:auth] + + begin + payload = JSON.parse(request.body.read) + if payload + invalid = invalid_templates(payload) + if invalid.empty? + result = reset_pool(payload) + else + invalid.each do |bad_pool| + metrics.increment("poolreset.invalid.#{bad_pool}") + end + result[:bad_pools] = invalid + status 400 + end + else + metrics.increment('poolreset.invalid.unknown') + status 404 + end + rescue JSON::ParserError + span = OpenTelemetry::Trace.current_span + span.record_exception(e) + span.status = OpenTelemetry::Trace::Status.error('JSON payload could not be parsed') + status 400 + result = { + 'ok' => false, + 'message' => 'JSON payload could not be parsed' + } + end + else + status 405 + end + + JSON.pretty_generate(result) + end + + post "#{api_prefix}/config/clonetarget/?" do + content_type :json + result = { 'ok' => false } + + if config['experimental_features'] + need_token! if Vmpooler::API.settings.config[:auth] + + payload = JSON.parse(request.body.read) + + if payload + invalid = invalid_pool(payload) + if invalid.empty? + result = update_clone_target(payload) + else + invalid.each do |bad_template| + metrics.increment("config.invalid.#{bad_template}") + end + result[:bad_templates] = invalid + status 400 + end + else + metrics.increment('config.invalid.unknown') + status 404 + end + else + status 405 + end + + JSON.pretty_generate(result) + end + + get "#{api_prefix}/config/?" do + content_type :json + result = { 'ok' => false } + status 404 + + if pools + sync_pool_sizes + sync_pool_templates + + pool_configuration = [] + pools.each do |pool| + pool['template_ready'] = template_ready?(pool, backend) + pool_configuration << pool + end + + result = { + pool_configuration: pool_configuration, + status: { + ok: true + } + } + + status 200 + end + JSON.pretty_generate(result) + end + + get "#{api_prefix}/full_config/?" do + content_type :json + + result = { + full_config: full_config, + status: { + ok: true + } + } + + status 200 + JSON.pretty_generate(result) end end end diff --git a/spec/unit/api/helpers_spec.rb b/spec/unit/api/helpers_spec.rb index 5991fa3..a25ad61 100644 --- a/spec/unit/api/helpers_spec.rb +++ b/spec/unit/api/helpers_spec.rb @@ -16,14 +16,12 @@ describe Vmpooler::API::Helpers do describe '#hostname_shorten' do [ - ['example.com', 'not-example.com', 'example'], - ['example.com', 'example.com', 'example'], - ['sub.example.com', 'example.com', 'sub'], - ['adjective-noun.example.com', 'example.com', 'adjective-noun'], - ['abc123.example.com', 'example.com', 'abc123'], - ['example.com', nil, 'example'] - ].each do |hostname, domain, expected| - it { expect(subject.hostname_shorten(hostname, domain)).to eq expected } + ['example.com', 'example'], + ['sub.example.com', 'sub'], + ['adjective-noun.example.com', 'adjective-noun'], + ['abc123.example.com', 'abc123'] + ].each do |hostname, expected| + it { expect(subject.hostname_shorten(hostname)).to eq expected } end end From da4015f5b3e9cefbfc63f68a755b849c9bd4f0d0 Mon Sep 17 00:00:00 2001 From: Jake Spain Date: Fri, 17 Feb 2023 09:02:13 -0500 Subject: [PATCH 10/19] Refactor obtaining and saving ip address --- lib/vmpooler/pool_manager.rb | 14 ++++++++++---- lib/vmpooler/providers/base.rb | 4 ++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/vmpooler/pool_manager.rb b/lib/vmpooler/pool_manager.rb index b819933..a901018 100644 --- a/lib/vmpooler/pool_manager.rb +++ b/lib/vmpooler/pool_manager.rb @@ -445,16 +445,22 @@ module Vmpooler start = Time.now provider.create_vm(pool_name, new_vmname) finish = format('%