# frozen_string_literal: true require 'vmpooler/providers' require 'vmpooler/util/parsing' require 'spicy-proton' require 'resolv' # ruby standard lib module Vmpooler class PoolManager CHECK_LOOP_DELAY_MIN_DEFAULT = 5 CHECK_LOOP_DELAY_MAX_DEFAULT = 60 CHECK_LOOP_DELAY_DECAY_DEFAULT = 2.0 def initialize(config, logger, redis_connection_pool, metrics) $config = config # Load logger library $logger = logger # metrics logging handle $metrics = metrics # Redis connection pool @redis = redis_connection_pool # VM Provider objects $providers = Concurrent::Hash.new # Our thread-tracker object $threads = Concurrent::Hash.new # Pool mutex @reconfigure_pool = Concurrent::Hash.new @vm_mutex = Concurrent::Hash.new # Name generator for generating host names @name_generator = Spicy::Proton.new # load specified providers from config file load_used_providers end def config $config end # Place pool configuration in redis so an API instance can discover running pool configuration def load_pools_to_redis @redis.with_metrics do |redis| previously_configured_pools = redis.smembers('vmpooler__pools') currently_configured_pools = [] config[:pools].each do |pool| currently_configured_pools << pool['name'] redis.sadd('vmpooler__pools', pool['name']) pool_keys = pool.keys pool_keys.delete('alias') to_set = {} pool_keys.each do |k| to_set[k] = pool[k] end to_set['alias'] = pool['alias'].join(',') if to_set.key?('alias') redis.hmset("vmpooler__pool__#{pool['name']}", to_set.to_a.flatten) unless to_set.empty? end previously_configured_pools.each do |pool| unless currently_configured_pools.include? pool redis.srem('vmpooler__pools', pool) redis.del("vmpooler__pool__#{pool}") end end end nil end # Check the state of a VM def check_pending_vm(vm, pool, timeout, provider) Thread.new do begin _check_pending_vm(vm, pool, timeout, provider) rescue StandardError => e $logger.log('s', "[!] [#{pool}] '#{vm}' #{timeout} #{provider} errored while checking a pending vm : #{e}") @redis.with_metrics do |redis| fail_pending_vm(vm, pool, timeout, redis) end raise end end end def _check_pending_vm(vm, pool, timeout, provider) mutex = vm_mutex(vm) return if mutex.locked? mutex.synchronize do @redis.with_metrics do |redis| request_id = redis.hget("vmpooler__vm__#{vm}", 'request_id') if provider.vm_ready?(pool, vm) move_pending_vm_to_ready(vm, pool, redis, request_id) else fail_pending_vm(vm, pool, timeout, redis) end end end end def remove_nonexistent_vm(vm, pool, redis) redis.srem("vmpooler__pending__#{pool}", vm) $logger.log('d', "[!] [#{pool}] '#{vm}' no longer exists. Removing from pending.") end def fail_pending_vm(vm, pool, timeout, redis, exists: true) clone_stamp = redis.hget("vmpooler__vm__#{vm}", 'clone') time_since_clone = (Time.now - Time.parse(clone_stamp)) / 60 if time_since_clone > timeout if exists request_id = redis.hget("vmpooler__vm__#{vm}", 'request_id') pool_alias = redis.hget("vmpooler__vm__#{vm}", 'pool_alias') if request_id redis.multi redis.smove("vmpooler__pending__#{pool}", "vmpooler__completed__#{pool}", vm) redis.zadd('vmpooler__odcreate__task', 1, "#{pool_alias}:#{pool}:1:#{request_id}") if request_id redis.exec $metrics.increment("errors.markedasfailed.#{pool}") $logger.log('d', "[!] [#{pool}] '#{vm}' marked as 'failed' after #{timeout} minutes") else remove_nonexistent_vm(vm, pool, redis) end end true rescue StandardError => e $logger.log('d', "Fail pending VM failed with an error: #{e}") false end def move_pending_vm_to_ready(vm, pool, redis, request_id = nil) clone_time = redis.hget("vmpooler__vm__#{vm}", 'clone') finish = format('%