diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 44998af..0000000 --- a/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -# Run vmpooler in a Docker container! Configuration can either be embedded -# and built within the current working directory, or stored in a -# VMPOOLER_CONFIG environment value and passed to the Docker daemon. -# -# BUILD: -# docker build -t vmpooler . -# -# RUN: -# docker run -e VMPOOLER_CONFIG -p 80:4567 -it vmpooler - -FROM jruby:9.1-jdk - -RUN mkdir -p /var/lib/vmpooler - -WORKDIR /var/lib/vmpooler - -ADD Gemfile* /var/lib/vmpooler/ -RUN bundle install --system - -RUN ln -s /opt/jruby/bin/jruby /usr/bin/jruby - -COPY . /var/lib/vmpooler - -ENV VMPOOLER_LOG /var/log/vmpooler.log -CMD \ - /var/lib/vmpooler/scripts/vmpooler_init.sh start \ - && while [ ! -f ${VMPOOLER_LOG} ]; do sleep 1; done ; \ - tail -f ${VMPOOLER_LOG} diff --git a/Gemfile b/Gemfile index d12abf5..3a74d05 100644 --- a/Gemfile +++ b/Gemfile @@ -1,29 +1,16 @@ source ENV['GEM_SOURCE'] || 'https://rubygems.org' -gem 'puma', '>= 3.6.0' -# Rack 2.x requires ruby 2.2 or above. -# As VMPooler should work in older jruby, we need to be Ruby 1.9.3 compatible. -gem 'rack', '~> 1.6' -gem 'rake', '>= 10.4' -gem 'rbvmomi', '>= 1.8' -gem 'sinatra', '>= 1.4' -gem 'net-ldap', '>= 0.16.1' -gem 'statsd-ruby', '>= 1.3.0', :require => 'statsd' -gem 'connection_pool', '>= 2.2.1' -gem 'nokogiri', '>= 1.8.2' -gem 'vmpooler', path: './' -# Pin gems against Ruby version -# Note we can't use platform restrictions easily so use -# lowest version range any platform -# ---- -# redis -if RUBY_VERSION =~ /^2\.[1]/ - gem 'redis', '~> 3.0' -elsif RUBY_VERSION =~ /^2\.2\.[01]/ - gem 'redis', '~> 3.0' -else - gem 'redis', '>= 3.2' -end +gem 'json', '>= 1.8' +gem 'puma', '~> 3.11' +gem 'rack', '~> 2.0' +gem 'rake', '~> 12.3' +gem 'redis', '~> 4.0' +gem 'rbvmomi', '~> 1.13' +gem 'sinatra', '~> 2.0' +gem 'net-ldap', '~> 0.16' +gem 'statsd-ruby', '~> 1.4.0', :require => 'statsd' +gem 'connection_pool', '~> 2.2' +gem 'nokogiri', '~> 1.8' group :development do gem 'pry' diff --git a/bin/vmpooler b/bin/vmpooler new file mode 100755 index 0000000..eec75c0 --- /dev/null +++ b/bin/vmpooler @@ -0,0 +1,58 @@ +#!/usr/bin/env ruby + +$LOAD_PATH.unshift(File.dirname(__FILE__)) + +require 'rubygems' unless defined?(Gem) +require 'lib/vmpooler' + +config = Vmpooler.config +redis_host = config[:redis]['server'] +redis_port = config[:redis]['port'] +redis_password = config[:redis]['password'] +logger_file = config[:config]['logfile'] +api_logger_file = config[:config]['api_logfile'] + +metrics = Vmpooler.new_metrics(config) + +torun_threads = [] +if ARGV.count == 0 + torun = ['api', 'manager'] +else + torun = [] + torun << 'api' if ARGV.include? 'api' + torun << 'manager' if ARGV.include? 'manager' + exit(2) if torun.empty? +end + +if torun.include? 'api' + api = Thread.new do + thr = Vmpooler::API.new + redis = Vmpooler.new_redis(redis_host, redis_port, redis_password) + thr.helpers.configure(config, redis, metrics) + thr.helpers.execute! + end + torun_threads << api +end + +if torun.include? 'manager' + manager = Thread.new do + Vmpooler::PoolManager.new( + config, + Vmpooler.new_logger(logger_file), + Vmpooler.new_redis(redis_host, redis_port, redis_password), + metrics + ).execute! + end + torun_threads << manager +end + +if ENV['VMPOOLER_DEBUG'] + trap('INT') do + puts 'Shutting down.' + torun_threads.each(&:exit) + end +end + +torun_threads.each do |th| + th.join +end diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..63467bc --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,21 @@ +# Run vmpooler in a Docker container! Configuration can either be embedded +# and built within the current working directory, or stored in a +# VMPOOLER_CONFIG environment value and passed to the Docker daemon. +# +# BUILD: +# docker build -t vmpooler . +# +# RUN: +# docker run -e VMPOOLER_CONFIG -p 80:4567 -it vmpooler + +FROM jruby:9.1-jdk + +COPY ./docker/docker-entrypoint.sh /usr/local/bin/ + +ENV LOGFILE=/dev/stdout \ + RACK_ENV=production + +RUN gem install vmpooler && \ + chmod +x /usr/local/bin/docker-entrypoint.sh + +ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/Dockerfile-aio b/docker/Dockerfile-aio similarity index 100% rename from Dockerfile-aio rename to docker/Dockerfile-aio diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100644 index 0000000..9d21ac0 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -e + +set -- /var/lib/vmpooler/vmpooler "$@" + +exec "$@" diff --git a/lib/vmpooler.rb b/lib/vmpooler.rb index 73042d9..8f283eb 100644 --- a/lib/vmpooler.rb +++ b/lib/vmpooler.rb @@ -1,5 +1,3 @@ -require 'rubygems' unless defined?(Gem) - module Vmpooler require 'date' require 'json' @@ -14,11 +12,7 @@ module Vmpooler require 'set' %w[api graphite logger pool_manager statsd dummy_statsd generic_connection_pool providers].each do |lib| - begin - require "vmpooler/#{lib}" - rescue LoadError - require File.expand_path(File.join(File.dirname(__FILE__), 'vmpooler', lib)) - end + require "vmpooler/#{lib}" end def self.config(filepath = 'vmpooler.yaml') @@ -31,36 +25,78 @@ module Vmpooler else # Take the name of the config file either from an ENV variable or from the filepath argument config_file = ENV['VMPOOLER_CONFIG_FILE'] || filepath - parsed_config = YAML.load_file(config_file) + parsed_config = YAML.load_file(config_file) if File.exist? config_file end - exit unless parsed_config + parsed_config ||= { config: {} } # Bail out if someone attempts to start vmpooler with dummy authentication # without enbaling debug mode. - if parsed_config[:auth]['provider'] == 'dummy' - unless ENV['VMPOOLER_DEBUG'] - warning = [ - 'Dummy authentication should not be used outside of debug mode', - 'please set environment variable VMPOOLER_DEBUG to \'true\' if you want to use dummy authentication' - ] + if parsed_config.has_key? :auth + if parsed_config[:auth]['provider'] == 'dummy' + unless ENV['VMPOOLER_DEBUG'] + warning = [ + 'Dummy authentication should not be used outside of debug mode', + 'please set environment variable VMPOOLER_DEBUG to \'true\' if you want to use dummy authentication' + ] - raise warning.join(";\s") + raise warning.join(";\s") + end end end # Set some configuration defaults - parsed_config[:redis] ||= {} - parsed_config[:redis]['server'] ||= 'localhost' - parsed_config[:redis]['data_ttl'] ||= 168 + parsed_config[:config]['task_limit'] = ENV['TASK_LIMIT'] || parsed_config[:config]['task_limit'] || 10 + parsed_config[:config]['migration_limit'] = ENV['MIGRATION_LIMIT'] if ENV['MIGRATION_LIMIT'] + parsed_config[:config]['vm_checktime'] = ENV['VM_CHECKTIME'] || parsed_config[:config]['vm_checktime'] || 15 + parsed_config[:config]['vm_lifetime'] = ENV['VM_LIFETIME'] || parsed_config[:config]['vm_lifetime'] || 24 + parsed_config[:config]['prefix'] = ENV['VM_PREFIX'] || parsed_config[:config]['prefix'] || '' - parsed_config[:config]['task_limit'] ||= 10 - parsed_config[:config]['vm_checktime'] ||= 15 - parsed_config[:config]['vm_lifetime'] ||= 24 - 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_NAME'] if ENV['DOMAIN_NAME'] + parsed_config[:config]['clone_target'] = ENV['CLONE_TARGET'] if ENV['CLONE_TARGET'] + parsed_config[:config]['timeout'] = ENV['TIMEOUT'] if ENV['TIMEOUT'] + parsed_config[:config]['vm_lifetime_auth'] = ENV['VM_LIFETIME_AUTH'] if ENV['VM_LIFETIME_AUTH'] + parsed_config[:config]['ssh_key'] = ENV['SSH_KEY'] if ENV['SSH_KEY'] + parsed_config[:config]['max_tries'] = ENV['MAX_TRIES'] if ENV['MAX_TRIES'] + parsed_config[:config]['retry_factor'] = ENV['RETRY_FACTOR'] if ENV['RETRY_FACTOR'] + parsed_config[:config]['create_folders'] = ENV['CREATE_FOLDERS'] if ENV['CREATE_FOLDERS'] + parsed_config[:config]['create_template_delta_disks'] = ENV['CREATE_TEMPLATE_DELTA_DISKS'] if ENV['CREATE_TEMPLATE_DELTA_DISKS'] + parsed_config[:config]['experimental_features'] = ENV['EXPERIMENTAL_FEATURES'] if ENV['EXPERIMENTAL_FEATURES'] + + parsed_config[:redis] = parsed_config[:redis] || {} + parsed_config[:redis]['server'] = ENV['REDIS_SERVER'] || parsed_config[:redis]['server'] || 'localhost' + parsed_config[:redis]['port'] = ENV['REDIS_PORT'] if ENV['REDIS_PORT'] + parsed_config[:redis]['password'] = ENV['REDIS_PASSWORD'] if ENV['REDIS_PASSWORD'] + parsed_config[:redis]['data_ttl'] = ENV['REDIS_DATA_TTL'] || parsed_config[:redis]['data_ttl'] || 168 + + parsed_config[:statsd] = parsed_config[:statsd] || {} if ENV['STATSD_SERVER'] + parsed_config[:statsd]['server'] = ENV['STATSD_SERVER'] if ENV['STATSD_SERVER'] + parsed_config[:statsd]['prefix'] = ENV['STATSD_PREFIX'] if ENV['STATSD_PREFIX'] + parsed_config[:statsd]['port'] = ENV['STATSD_PORT'] if ENV['STATSD_PORT'] + + parsed_config[:graphite] = parsed_config[:graphite] || {} if ENV['GRAPHITE_SERVER'] + parsed_config[:graphite]['server'] = ENV['GRAPHITE_SERVER'] if ENV['GRAPHITE_SERVER'] + + parsed_config[:auth] = parsed_config[:auth] || {} if ENV['AUTH_PROVIDER'] + if parsed_config.has_key? :auth + parsed_config[:auth]['provider'] = ENV['AUTH_PROVIDER'] if ENV['AUTH_PROVIDER'] + parsed_config[:auth][:ldap] = parsed_config[:auth][:ldap] || {} if parsed_config[:auth]['provider'] == 'ldap' + parsed_config[:auth][:ldap]['server'] = ENV['LDAP_SERVER'] if ENV['LDAP_SERVER'] + parsed_config[:auth][:ldap]['port'] = ENV['LDAP_PORT'] if ENV['LDAP_PORT'] + parsed_config[:auth][:ldap]['base'] = ENV['LDAP_BASE'] if ENV['LDAP_BASE'] + parsed_config[:auth][:ldap]['user_object'] = ENV['LDAP_USER_OBJECT'] if ENV['LDAP_USER_OBJECT'] + end # Create an index of pool aliases parsed_config[:pool_names] = Set.new + unless parsed_config[:pools] + redis = new_redis(parsed_config[:redis]['server'], parsed_config[:redis]['port'], parsed_config[:redis]['password']) + parsed_config[:pools] = load_pools_from_redis(redis) + end + parsed_config[:pools].each do |pool| parsed_config[:pool_names] << pool['name'] if pool['alias'] @@ -84,9 +120,23 @@ module Vmpooler end parsed_config[:uptime] = Time.now + parsed_config end + def self.load_pools_from_redis(redis) + pools = [] + redis.smembers('vmpooler__pools').each do |pool| + pool_hash = {} + redis.hgetall("vmpooler__pool__#{pool}").each do |k, v| + pool_hash[k] = v + end + pool_hash['alias'] = pool_hash['alias'].split(',') + pools << pool_hash + end + pools + end + def self.new_redis(host = 'localhost', port = nil, password = nil) Redis.new(host: host, port: port, password: password) end diff --git a/lib/vmpooler/api.rb b/lib/vmpooler/api.rb index 25fa7f6..a9a9e83 100644 --- a/lib/vmpooler/api.rb +++ b/lib/vmpooler/api.rb @@ -4,8 +4,6 @@ module Vmpooler super end - set :environment, :production - not_found do content_type :json @@ -42,11 +40,10 @@ module Vmpooler use Vmpooler::API::Reroute use Vmpooler::API::V1 - def configure(config, redis, metrics, environment = :production) + def configure(config, redis, metrics) self.settings.set :config, config self.settings.set :redis, redis self.settings.set :metrics, metrics - self.settings.set :environment, environment end def execute! diff --git a/lib/vmpooler/dashboard.rb b/lib/vmpooler/dashboard.rb index 56259d6..b875465 100644 --- a/lib/vmpooler/dashboard.rb +++ b/lib/vmpooler/dashboard.rb @@ -1,8 +1,13 @@ module Vmpooler class Dashboard < Sinatra::Base + + def config + Vmpooler.config + end + get '/dashboard/?' do erb :dashboard, locals: { - site_name: $config[:config]['site_name'] || 'vmpooler' + site_name: ENV['SITE_NAME'] || config[:config]['site_name'] || 'vmpooler' } end end diff --git a/lib/vmpooler/pool_manager.rb b/lib/vmpooler/pool_manager.rb index e7159ae..6e36bba 100644 --- a/lib/vmpooler/pool_manager.rb +++ b/lib/vmpooler/pool_manager.rb @@ -32,6 +32,31 @@ module Vmpooler $config end + # Place pool configuration in redis so an API instance can discover running pool configuration + def load_pools_to_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.has_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 + return + end + # Check the state of a VM def check_pending_vm(vm, pool, timeout, provider) Thread.new do @@ -927,6 +952,9 @@ module Vmpooler end end + # Load running pool configuration into redis so API server can retrieve it + load_pools_to_redis + # Get pool loop settings $config[:config] = {} if $config[:config].nil? check_loop_delay_min = $config[:config]['check_loop_delay_min'] || CHECK_LOOP_DELAY_MIN_DEFAULT diff --git a/spec/integration/dashboard_spec.rb b/spec/integration/dashboard_spec.rb index 06d2a86..147ec46 100644 --- a/spec/integration/dashboard_spec.rb +++ b/spec/integration/dashboard_spec.rb @@ -22,12 +22,10 @@ describe Vmpooler::API do end context '/dashboard/' do - let(:config) { { - config: {'site_name' => 'test pooler'} - } } + ENV['SITE_NAME'] = 'test pooler' + ENV['VMPOOLER_CONFIG'] = 'thing' before do - $config = config get '/dashboard/' end diff --git a/vmpooler b/vmpooler deleted file mode 100755 index 57478f9..0000000 --- a/vmpooler +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env ruby - -$LOAD_PATH.unshift(File.dirname(__FILE__)) - -require 'rubygems' unless defined?(Gem) -require 'lib/vmpooler' - -config = Vmpooler.config -redis_host = config[:redis]['server'] -redis_port = config[:redis]['port'] -redis_password = config[:redis]['password'] -logger_file = config[:config]['logfile'] - -metrics = Vmpooler.new_metrics(config) - -api = Thread.new do - thr = Vmpooler::API.new - thr.helpers.configure(config, Vmpooler.new_redis(redis_host, redis_port, redis_password), metrics) - thr.helpers.execute! -end - -manager = Thread.new do - Vmpooler::PoolManager.new( - config, - Vmpooler.new_logger(logger_file), - Vmpooler.new_redis(redis_host, redis_port, redis_password), - metrics - ).execute! -end - -if ENV['VMPOOLER_DEBUG'] - trap('INT') do - puts 'Shutting down.' - [api, manager].each(&:exit) - end -end - -[api, manager].each(&:join) diff --git a/vmpooler.gemspec b/vmpooler.gemspec index 409b558..4df9609 100644 --- a/vmpooler.gemspec +++ b/vmpooler.gemspec @@ -2,33 +2,29 @@ lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'vmpooler/version' -Gem::Specification.new do |spec| - spec.name = 'vmpooler' - spec.version = Vmpooler::VERSION - spec.authors = ['Puppet'] - spec.email = ['support@puppet.com'] +Gem::Specification.new do |s| + s.name = 'vmpooler' + s.version = Vmpooler::VERSION + s.authors = ['Puppet'] + s.email = ['support@puppet.com'] - spec.summary = 'vmpooler provides configurable pools of instantly-available (running) virtual machines' - spec.description = 'vmpooler provides configurable pools of instantly-available (running) virtual machines' - spec.homepage = 'https://github.com/puppetlabs/vmpooler' - spec.license = 'MIT' - - spec.files = `git ls-files -z`.split("\x0").reject do |f| - f.match(%r{^(test|spec|features)/}) - end - spec.bindir = 'exe' - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } - spec.require_paths = ['lib'] - spec.add_dependency 'puma', '>= 3.6.0' - spec.add_dependency 'rack', '~> 1.6' - spec.add_dependency 'rake', '>= 10.4' - spec.add_dependency 'rbvmomi', '>= 1.8' - spec.add_dependency 'sinatra', '>= 1.4' - spec.add_dependency 'net-ldap', '>= 0.16.1' - spec.add_dependency 'statsd-ruby', '>= 1.3.0' - spec.add_dependency 'connection_pool', '>= 2.2.1' - spec.add_dependency 'nokogiri', '>= 1.8.2' - # we should lock ruby support down to 2.2.2+ and update redis version 3.2 - spec.add_dependency 'redis', '>= 3.0' + s.summary = 'vmpooler provides configurable pools of instantly-available (running) virtual machines' + s.homepage = 'https://github.com/puppetlabs/vmpooler' + s.license = 'Apache-2.0' + s.required_ruby_version = Gem::Requirement.new('>= 2.3.0') + s.files = Dir[ "bin/*", "lib/**/*" ] + s.bindir = 'bin' + s.executables = 'vmpooler' + s.require_paths = ["lib"] + s.add_dependency 'puma', '~> 3.11' + s.add_dependency 'rack', '~> 2.0' + s.add_dependency 'rake', '~> 12.3' + s.add_dependency 'redis', '~> 4.0' + s.add_dependency 'rbvmomi', '~> 1.13' + s.add_dependency 'sinatra', '~> 2.0' + s.add_dependency 'net-ldap', '~> 0.16' + s.add_dependency 'statsd-ruby', '~> 1.4' + s.add_dependency 'connection_pool', '~> 2.2' + s.add_dependency 'nokogiri', '~> 1.8' end