From 60cc1ef1783f416028a28066e2462813f524f393 Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Wed, 5 Mar 2014 12:57:25 -0800 Subject: [PATCH 1/8] Reworked into a single namespaced Ruby application --- lib/graphite.rb | 18 - lib/logger.rb | 20 - lib/require_relative.rb | 21 - lib/vmpooler.rb | 12 + lib/vmpooler/api.rb | 233 +++++++++ lib/vmpooler/graphite.rb | 20 + lib/vmpooler/logger.rb | 21 + lib/vmpooler/pool_manager.rb | 459 ++++++++++++++++++ {public => lib/vmpooler/public}/dashboard.css | 0 {public => lib/vmpooler/public}/img/logo.jpg | Bin .../vmpooler/public}/img/spinner.svg | 0 .../public}/lib/stats-vmpooler-numbers.js | 0 .../public}/lib/stats-vmpooler-pool.js | 0 .../public}/lib/stats-vmpooler-running.js | 0 {views => lib/vmpooler/views}/dashboard.erb | 0 {views => lib/vmpooler/views}/layout.erb | 0 lib/vmpooler/vsphere_helper.rb | 180 +++++++ lib/vsphere_helper.rb | 180 ------- vmpooler | 459 +----------------- vmpooler-api | 231 --------- 20 files changed, 930 insertions(+), 924 deletions(-) delete mode 100755 lib/graphite.rb delete mode 100755 lib/logger.rb delete mode 100755 lib/require_relative.rb create mode 100644 lib/vmpooler.rb create mode 100644 lib/vmpooler/api.rb create mode 100644 lib/vmpooler/graphite.rb create mode 100644 lib/vmpooler/logger.rb create mode 100644 lib/vmpooler/pool_manager.rb rename {public => lib/vmpooler/public}/dashboard.css (100%) rename {public => lib/vmpooler/public}/img/logo.jpg (100%) rename {public => lib/vmpooler/public}/img/spinner.svg (100%) rename {public => lib/vmpooler/public}/lib/stats-vmpooler-numbers.js (100%) rename {public => lib/vmpooler/public}/lib/stats-vmpooler-pool.js (100%) rename {public => lib/vmpooler/public}/lib/stats-vmpooler-running.js (100%) rename {views => lib/vmpooler/views}/dashboard.erb (100%) rename {views => lib/vmpooler/views}/layout.erb (100%) create mode 100644 lib/vmpooler/vsphere_helper.rb delete mode 100755 lib/vsphere_helper.rb delete mode 100755 vmpooler-api diff --git a/lib/graphite.rb b/lib/graphite.rb deleted file mode 100755 index b52e011..0000000 --- a/lib/graphite.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'rubygems' unless defined?(Gem) - -class Graphite - def initialize( - s = 'graphite' - ) - @server = s - end - - def log path, value - Thread.new { - socket = TCPSocket.new(@server, 2003) - socket.puts "#{path} #{value} #{Time.now.to_i}" - socket.close - } - end -end - diff --git a/lib/logger.rb b/lib/logger.rb deleted file mode 100755 index 2ec8918..0000000 --- a/lib/logger.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'rubygems' unless defined?(Gem) - -class Logger - def initialize( - f = '/var/log/vmpooler.log' - ) - @file = f - end - - def log level, string - time = Time.new - stamp = time.strftime('%Y-%m-%d %H:%M:%S') - puts "[#{stamp}] #{string}" - - open(@file, 'a') do |f| - f.puts "[#{stamp}] #{string}" - end - end -end - diff --git a/lib/require_relative.rb b/lib/require_relative.rb deleted file mode 100755 index ab9114c..0000000 --- a/lib/require_relative.rb +++ /dev/null @@ -1,21 +0,0 @@ -# require_relative was introduced in 1.9.2. This makes it -# available to younger rubies. It trys hard to not re-require -# files. - -unless Kernel.respond_to?(:require_relative) - module Kernel - def require_relative(path) - desired_path = File.expand_path('../'+path.to_str, caller[0]) - shortest = desired_path - $:.each do |path| - path += '/' - if desired_path.index(path) == 0 - candidate = desired_path.sub(path, '') - shortest = candidate if candidate.size < shortest.size - end - end - require shortest - end - end -end - diff --git a/lib/vmpooler.rb b/lib/vmpooler.rb new file mode 100644 index 0000000..4e4d901 --- /dev/null +++ b/lib/vmpooler.rb @@ -0,0 +1,12 @@ +require 'rubygems' unless defined?(Gem) + +module Vmpooler + %w( api graphite logger pool_manager vsphere_helper ).each do |lib| + begin + require "vmpooler/#{lib}" + rescue LoadError + require File.expand_path(File.join(File.dirname(__FILE__), 'vmpooler', lib)) + end + end +end + diff --git a/lib/vmpooler/api.rb b/lib/vmpooler/api.rb new file mode 100644 index 0000000..4a84234 --- /dev/null +++ b/lib/vmpooler/api.rb @@ -0,0 +1,233 @@ +module Vmpooler + class API + def initialize + require 'sinatra/base' + + require 'json' + require 'open-uri' + require 'redis' + require 'yaml' + + # Load the configuration file + config_file = File.expand_path('vmpooler.yaml') + $config = YAML.load_file(config_file) + + pools = $config[:pools] + redis = $config[:redis] + + # Set some defaults + $config[:redis] ||= Hash.new + $config[:redis]['server'] ||= 'localhost' + + # Connect to Redis + $redis = Redis.new(:host => $config[:redis]['server']) + end + + def execute! + my_app = Sinatra.new { + + set :environment, 'production' + + get '/' do + erb :dashboard, locals: { + site_name: $config[:config]['site_name'] || 'vmpooler', + } + end + + get '/dashboard/stats/vmpooler/numbers/?' do + result = Hash.new + result['pending'] = 0 + result['cloning'] = 0 + result['booting'] = 0 + result['ready'] = 0 + result['running'] = 0 + result['completed'] = 0 + + $config[:pools].each do |pool| + result['pending'] += $redis.scard( 'vmpooler__pending__' + pool['name'] ) + result['ready'] += $redis.scard( 'vmpooler__ready__' + pool['name'] ) + result['running'] += $redis.scard( 'vmpooler__running__' + pool['name'] ) + result['completed'] += $redis.scard( 'vmpooler__completed__' + pool['name'] ) + end + + result['cloning'] = $redis.get( 'vmpooler__tasks__clone' ) + result['booting'] = result['pending'].to_i - result['cloning'].to_i + result['booting'] = 0 if result['booting'] < 0 + result['total'] = result['pending'].to_i + result['ready'].to_i + result['running'].to_i + result['completed'].to_i + + content_type :json + JSON.pretty_generate(result) + end + + get '/dashboard/stats/vmpooler/pool/?' do + result = Hash.new + + $config[:pools].each do |pool| + result[pool['name']] ||= Hash.new + result[pool['name']]['size'] = pool['size'] + result[pool['name']]['ready'] = $redis.scard( 'vmpooler__ready__' + pool['name'] ) + end + + if ( params[:history] ) + if ( $config[:config]['graphite'] ) + history ||= Hash.new + + begin + buffer = open( 'http://'+$config[:config]['graphite']+'/render?target=vmpooler.ready.*&from=-1hour&format=json' ).read + history = JSON.parse( buffer ) + + history.each do |pool| + if pool['target'] =~ /.*\.(.*)$/ + pool['name'] = $1 + + if ( result[pool['name']] ) + pool['last'] = result[pool['name']]['size'] + result[pool['name']]['history'] ||= Array.new + + pool['datapoints'].each do |metric| + 8.times do |n| + if ( metric[0] ) + pool['last'] = metric[0].to_i + result[pool['name']]['history'].push( metric[0].to_i ) + else + result[pool['name']]['history'].push( pool['last'] ) + end + end + end + end + end + end + rescue + end + else + $config[:pools].each do |pool| + result[pool['name']] ||= Hash.new + result[pool['name']]['history'] = [ $redis.scard( 'vmpooler__ready__' + pool['name'] ) ] + end + end + end + + content_type :json + JSON.pretty_generate(result) + end + + get '/dashboard/stats/vmpooler/running/?' do + result = Hash.new + + $config[:pools].each do |pool| + running = $redis.scard( 'vmpooler__running__' + pool['name'] ) + pool['major'] = $1 if pool['name'] =~ /^(\w+)\-/ + + result[pool['major']] ||= Hash.new + + result[pool['major']]['running'] = result[pool['major']]['running'].to_i + running.to_i + end + + if ( params[:history] ) + if ( $config[:config]['graphite'] ) + begin + buffer = open( 'http://'+$config[:config]['graphite']+'/render?target=vmpooler.running.*&from=-1hour&format=json' ).read + JSON.parse( buffer ).each do |pool| + if pool['target'] =~ /.*\.(.*)$/ + pool['name'] = $1 + + pool['major'] = $1 if pool['name'] =~ /^(\w+)\-/ + + result[pool['major']]['history'] ||= Array.new + + for i in 0..pool['datapoints'].length + if ( + pool['datapoints'][i] and + pool['datapoints'][i][0] + ) + pool['last'] = pool['datapoints'][i][0] + + result[pool['major']]['history'][i] ||= 0 + result[pool['major']]['history'][i] = result[pool['major']]['history'][i].to_i + pool['datapoints'][i][0].to_i + else + result[pool['major']]['history'][i] = result[pool['major']]['history'][i].to_i + pool['last'].to_i + end + end + + end + end + rescue + end + end + end + + content_type :json + JSON.pretty_generate(result) + end + + get '/vm/?' do + content_type :json + + result = [] + + $config[:pools].each do |pool| + result.push(pool['name']) + end + + JSON.pretty_generate(result) + end + + get '/vm/:template/?' do + content_type :json + + result = {} + result[params[:template]] = {} + result[params[:template]]['hosts'] = $redis.smembers('vmpooler__ready__'+params[:template]) + + JSON.pretty_generate(result) + end + + post '/vm/:template/?' do + content_type :json + + result = {} + result[params[:template]] = {} + + if ( $redis.scard('vmpooler__ready__'+params[:template]) > 0 ) + vm = $redis.spop('vmpooler__ready__'+params[:template]) + + unless (vm.nil?) + $redis.sadd('vmpooler__running__'+params[:template], vm) + $redis.hset('vmpooler__active__'+params[:template], vm, Time.now) + + result[params[:template]]['ok'] = true + result[params[:template]]['hostname'] = vm + else + result[params[:template]]['ok'] = false + end + else + result[params[:template]]['ok'] = false + end + + JSON.pretty_generate(result) + end + + delete '/vm/:hostname/?' do + content_type :json + + result = {} + + result['ok'] = false + + $config[:pools].each do |pool| + if $redis.sismember('vmpooler__running__'+pool['name'], params[:hostname]) + $redis.srem('vmpooler__running__'+pool['name'], params[:hostname]) + $redis.sadd('vmpooler__completed__'+pool['name'], params[:hostname]) + result['ok'] = true + end + end + + JSON.pretty_generate(result) + end + } + + my_app.run! + end + end +end + diff --git a/lib/vmpooler/graphite.rb b/lib/vmpooler/graphite.rb new file mode 100644 index 0000000..ecdb8ea --- /dev/null +++ b/lib/vmpooler/graphite.rb @@ -0,0 +1,20 @@ +require 'rubygems' unless defined?(Gem) + +module Vmpooler + class Graphite + def initialize( + s = 'graphite' + ) + @server = s + end + + def log path, value + Thread.new { + socket = TCPSocket.new(@server, 2003) + socket.puts "#{path} #{value} #{Time.now.to_i}" + socket.close + } + end + end +end + diff --git a/lib/vmpooler/logger.rb b/lib/vmpooler/logger.rb new file mode 100644 index 0000000..de97481 --- /dev/null +++ b/lib/vmpooler/logger.rb @@ -0,0 +1,21 @@ +require 'rubygems' unless defined?(Gem) + +module Vmpooler + class Logger + def initialize( + f = '/var/log/vmpooler.log' + ) + @file = f + end + + def log level, string + time = Time.new + stamp = time.strftime('%Y-%m-%d %H:%M:%S') + + open(@file, 'a') do |f| + f.puts "[#{stamp}] #{string}" + end + end + end +end + diff --git a/lib/vmpooler/pool_manager.rb b/lib/vmpooler/pool_manager.rb new file mode 100644 index 0000000..c85ec21 --- /dev/null +++ b/lib/vmpooler/pool_manager.rb @@ -0,0 +1,459 @@ +module Vmpooler + class PoolManager + def initialize + require 'json' + require 'rbvmomi' + require 'redis' + require 'time' + require 'timeout' + require 'yaml' + + # Load the configuration file + config_file = File.expand_path('vmpooler.yaml') + $config = YAML.load_file(config_file) + + $pools = $config[:pools] + vsphere = $config[:vsphere] + redis = $config[:redis] + + # Load logger library + $logger = Vmpooler::Logger.new $config[:config]['logfile'] + + # Load Graphite helper library (if configured) + if (defined? $config[:config]['graphite']) + $graphite = Vmpooler::Graphite.new $config[:config]['graphite'] + end + + # Set some defaults + $config[:config]['task_limit'] ||= 10 + $config[:config]['vm_checktime'] ||= 15 + $config[:config]['vm_lifetime'] ||= 24 + $config[:redis] ||= Hash.new + $config[:redis]['server'] ||= 'localhost' + + # Connect to Redis + $redis = Redis.new(:host => $config[:redis]['server']) + + # vSphere object + $vsphere = {} + + # Our thread-tracker object + $threads = {} + end + + + # Check the state of a VM + def check_pending_vm vm, pool, timeout + Thread.new { + host = $vsphere[pool].find_vm(vm) + + if (host) + if ( + (host.summary) and + (host.summary.guest) and + (host.summary.guest.hostName) and + (host.summary.guest.hostName == vm) + ) + begin + Socket.getaddrinfo(vm, nil) + rescue + end + + $redis.smove('vmpooler__pending__'+pool, 'vmpooler__ready__'+pool, vm) + + $logger.log('s', "[>] [#{pool}] '#{vm}' moved to 'ready' queue") + end + else + clone_stamp = $redis.hget('vmpooler__vm__'+vm, 'clone') + + if ( + (clone_stamp) and + (((Time.now - Time.parse(clone_stamp))/60) > timeout) + ) + $redis.smove('vmpooler__pending__'+pool, 'vmpooler__completed__'+pool, vm) + + $logger.log('d', "[!] [#{pool}] '#{vm}' marked as 'failed' after #{timeout} minutes") + end + end + } + end + + def check_ready_vm vm, pool, ttl + Thread.new { + if (ttl > 0) + if ((((Time.now - host.runtime.bootTime)/60).to_s[/^\d+\.\d{1}/].to_f) > ttl) + $redis.smove('vmpooler__ready__'+pool, 'vmpooler__completed__'+pool, vm) + + $logger.log('d', "[!] [#{pool}] '#{vm}' reached end of TTL after #{ttl} minutes, removed from 'ready' queue") + end + end + + check_stamp = $redis.hget('vmpooler__vm__'+vm, 'check') + + if ( + (! check_stamp) or + (((Time.now - Time.parse(check_stamp))/60) > $config[:config]['vm_checktime']) + ) + $redis.hset('vmpooler__vm__'+vm, 'check', Time.now) + + host = $vsphere[pool].find_vm(vm) || + $vsphere[pool].find_vm_heavy(vm)[vm] + + if (host) + if ( + (host.runtime) and + (host.runtime.powerState) and + (host.runtime.powerState != 'poweredOn') + ) + $redis.smove('vmpooler__ready__'+pool, 'vmpooler__completed__'+pool, vm) + + $logger.log('d', "[!] [#{pool}] '#{vm}' appears to be powered off, removed from 'ready' queue") + end + + if ( + (host.summary.guest) and + (host.summary.guest.hostName) and + (host.summary.guest.hostName != vm) + ) + $redis.smove('vmpooler__ready__'+pool, 'vmpooler__completed__'+pool, vm) + + $logger.log('d', "[!] [#{pool}] '#{vm}' has mismatched hostname, removed from 'ready' queue") + end + else + $redis.srem('vmpooler__ready__'+pool, vm) + + $logger.log('s', "[!] [#{pool}] '#{vm}' not found in vCenter inventory, removed from 'ready' queue") + end + + begin + Timeout::timeout(5) { + TCPSocket.new vm, 22 + } + rescue + if ($redis.smove('vmpooler__ready__'+pool, 'vmpooler__completed__'+pool, vm)) + $logger.log('d', "[!] [#{pool}] '#{vm}' is unreachable, removed from 'ready' queue") + end + end + end + } + end + + def check_running_vm vm, pool, ttl + Thread.new { + host = $vsphere[pool].find_vm(vm) + + if (host) + if ( + (host.runtime) and + (host.runtime.powerState != 'poweredOn') + ) + $redis.smove('vmpooler__running__'+pool, 'vmpooler__completed__'+pool, vm) + + $logger.log('d', "[!] [#{pool}] '#{vm}' appears to be powered off or dead") + else + if ( + (host.runtime) and + (host.runtime.bootTime) + ((((Time.now - host.runtime.bootTime)/60).to_s[/^\d+\.\d{1}/].to_f) > ttl) + ) + $redis.smove('vmpooler__running__'+pool, 'vmpooler__completed__'+pool, vm) + + $logger.log('d', "[!] [#{pool}] '#{vm}' reached end of TTL after #{ttl} minutes") + end + end + end + } + end + + # Clone a VM + def clone_vm template, pool, folder, datastore + Thread.new { + vm = {} + + if template =~ /\// + templatefolders = template.split('/') + vm['template'] = templatefolders.pop + end + + if templatefolders + vm[vm['template']] = $vsphere[vm['template']].find_folder(templatefolders.join('/')).find(vm['template']) + else + raise "Please provide a full path to the template" + end + + if vm['template'].length == 0 + raise "Unable to find template '#{vm['template']}'!" + end + + # Generate a randomized hostname + o = [('a'..'z'),('0'..'9')].map{|r| r.to_a}.flatten + vm['hostname'] = o[rand(25)]+(0...14).map{o[rand(o.length)]}.join + + # Add VM to Redis inventory ('pending' pool) + $redis.sadd('vmpooler__pending__'+vm['template'], vm['hostname']) + $redis.hset('vmpooler__vm__'+vm['hostname'], 'clone', Time.now) + + # Annotate with creation time, origin template, etc. + configSpec = RbVmomi::VIM.VirtualMachineConfigSpec( + :annotation => JSON.pretty_generate({ + name: vm['hostname'], + created_by: $config[:vsphere]['username'], + base_template: vm['template'], + creation_timestamp: Time.now.utc + }) + ) + + # Put the VM in the specified folder and resource pool + relocateSpec = RbVmomi::VIM.VirtualMachineRelocateSpec( + :datastore => $vsphere[vm['template']].find_datastore(datastore), + :pool => $vsphere[vm['template']].find_pool(pool), + :diskMoveType => :moveChildMostDiskBacking + ) + + # Create a clone spec + spec = RbVmomi::VIM.VirtualMachineCloneSpec( + :location => relocateSpec, + :config => configSpec, + :powerOn => true, + :template => false + ) + + # Clone the VM + $logger.log('d', "[ ] [#{vm['template']}] '#{vm['hostname']}' is being cloned from '#{vm['template']}'") + + begin + start = Time.now + vm[vm['template']].CloneVM_Task( + :folder => $vsphere[vm['template']].find_folder(folder), + :name => vm['hostname'], + :spec => spec + ).wait_for_completion + finish = '%.2f' % (Time.now-start) + + $logger.log('s', "[+] [#{vm['template']}] '#{vm['hostname']}' cloned from '#{vm['template']}' in #{finish} seconds") + rescue + $logger.log('s', "[!] [#{vm['template']}] '#{vm['hostname']}' clone appears to have failed") + $redis.srem('vmpooler__pending__'+vm['template'], vm['hostname']) + end + + $redis.decr('vmpooler__tasks__clone') + + begin + $graphite.log("vmpooler.clone.#{vm['template']}", finish) if defined? $graphite + rescue + end + } + end + + # Destroy a VM + def destroy_vm vm, pool + Thread.new { + $redis.srem('vmpooler__completed__'+pool, vm) + $redis.hdel('vmpooler__active__'+pool, vm) + $redis.del('vmpooler__vm__'+vm) + + host = $vsphere[pool].find_vm(vm) || + $vsphere[pool].find_vm_heavy(vm)[vm] + + if (host) + start = Time.now + + if ( + (host.runtime) and + (host.runtime.powerState) and + (host.runtime.powerState == 'poweredOn') + ) + $logger.log('d', "[ ] [#{pool}] '#{vm}' is being shut down") + host.PowerOffVM_Task.wait_for_completion + end + + host.Destroy_Task.wait_for_completion + finish = '%.2f' % (Time.now-start) + + $logger.log('s', "[-] [#{pool}] '#{vm}' destroyed in #{finish} seconds") + + $graphite.log("vmpooler.destroy.#{pool}", finish) if defined? $graphite + end + } + end + + def check_pool pool + $logger.log('d', "[*] [#{pool['name']}] starting worker thread") + + $threads[pool['name']] = Thread.new { + $vsphere[pool['name']] ||= Vmpooler::VsphereHelper.new + + loop do + # INVENTORY + inventory = {} + begin + base = $vsphere[pool['name']].find_pool(pool['pool']) + + base.vm.each do |vm| + if ( + (! $redis.sismember('vmpooler__running__'+pool['name'], vm['name'])) and + (! $redis.sismember('vmpooler__ready__'+pool['name'], vm['name'])) and + (! $redis.sismember('vmpooler__pending__'+pool['name'], vm['name'])) and + (! $redis.sismember('vmpooler__completed__'+pool['name'], vm['name'])) and + (! $redis.sismember('vmpooler__discovered__'+pool['name'], vm['name'])) + ) + $redis.sadd('vmpooler__discovered__'+pool['name'], vm['name']) + + $logger.log('s', "[?] [#{pool['name']}] '#{vm['name']}' added to 'discovered' queue") + end + + inventory[vm['name']] = 1 + end + rescue + end + + # RUNNING + $redis.smembers('vmpooler__running__'+pool['name']).each do |vm| + if (inventory[vm]) + if (pool['running_ttl']) + begin + check_running_vm(vm, pool['name'], pool['running_ttl']) + rescue + end + else + begin + check_running_vm(vm, pool['name'], '720') + rescue + end + end + end + end + + # READY + $redis.smembers('vmpooler__ready__'+pool['name']).each do |vm| + if (inventory[vm]) + begin + check_ready_vm(vm, pool['name'], pool['ready_ttl'] || 0) + rescue + end + end + end + + # PENDING + $redis.smembers('vmpooler__pending__'+pool['name']).each do |vm| + pool['timeout'] ||= 15 + + if (inventory[vm]) + begin + check_pending_vm(vm, pool['name'], pool['timeout']) + rescue + end + end + end + + # COMPLETED + $redis.smembers('vmpooler__completed__'+pool['name']).each do |vm| + if (inventory[vm]) + begin + destroy_vm(vm, pool['name']) + rescue + $logger.log('s', "[!] [#{pool['name']}] '#{vm}' destroy appears to have failed") + $redis.srem('vmpooler__completed__'+pool['name'], vm) + $redis.hdel('vmpooler__active__'+pool['name'], vm) + $redis.del('vmpooler__vm__'+vm) + end + else + $logger.log('s', "[!] [#{pool['name']}] '#{vm}' not found in inventory, removed from 'completed' queue") + $redis.srem('vmpooler__completed__'+pool['name'], vm) + $redis.hdel('vmpooler__active__'+pool['name'], vm) + $redis.del('vmpooler__vm__'+vm) + end + end + + # DISCOVERED + $redis.smembers('vmpooler__discovered__'+pool['name']).each do |vm| + ['pending', 'ready', 'running', 'completed'].each do |queue| + if ($redis.sismember('vmpooler__'+queue+'__'+pool['name'], vm)) + $logger.log('d', "[!] [#{pool['name']}] '#{vm}' found in '#{queue}', removed from 'discovered' queue") + $redis.srem('vmpooler__discovered__'+pool['name'], vm) + end + end + + if ($redis.sismember('vmpooler__discovered__'+pool['name'], vm)) + $redis.smove('vmpooler__discovered__'+pool['name'], 'vmpooler__completed__'+pool['name'], vm) + end + end + + # LONG-RUNNING + $redis.smembers('vmpooler__running__'+pool['name']).each do |vm| + if ($redis.hget('vmpooler__active__'+pool['name'], vm)) + running = (Time.now - Time.parse($redis.hget('vmpooler__active__'+pool['name'], vm)))/60/60 + if ( + ($config[:config]['vm_lifetime'] > 0) and + (running > $config[:config]['vm_lifetime']) + ) + $redis.smove('vmpooler__running__'+pool['name'], 'vmpooler__completed__'+pool['name'], vm) + + $logger.log('d', "[!] [#{pool['name']}] '#{vm}' reached end of TTL after #{$config[:config]['vm_lifetime']} hours") + end + end + end + + # REPOPULATE + total = $redis.scard('vmpooler__ready__'+pool['name']) + + $redis.scard('vmpooler__pending__'+pool['name']) + + begin + if (defined? $graphite) + $graphite.log('vmpooler.ready.'+pool['name'], $redis.scard('vmpooler__ready__'+pool['name'])) + $graphite.log('vmpooler.running.'+pool['name'], $redis.scard('vmpooler__running__'+pool['name'])) + end + rescue + end + + if (total < pool['size']) + (1..(pool['size'] - total)).each { |i| + + if ($redis.get('vmpooler__tasks__clone').to_i < $config[:config]['task_limit']) + begin + $redis.incr('vmpooler__tasks__clone') + + clone_vm( + pool['template'], + pool['pool'], + pool['folder'], + pool['datastore'] + ) + rescue + $logger.log('s', "[!] [#{pool['name']}] clone appears to have failed") + $redis.decr('vmpooler__tasks__clone') + end + end + } + end + + sleep(1) + end + } + end + + def execute! + $logger.log('d', "starting vmpooler") + + # Clear out the tasks manager, as we don't know about any tasks at this point + $redis.set('vmpooler__tasks__clone', 0) + + loop do + $pools.each do |pool| + if (! $threads[pool['name']]) + check_pool(pool) + else + if (! $threads[pool['name']].alive?) + $logger.log('d', "[!] [#{pool['name']}] worker thread died, restarting") + check_pool(pool) + end + end + end + + sleep(1) + end + end + + end +end + diff --git a/public/dashboard.css b/lib/vmpooler/public/dashboard.css similarity index 100% rename from public/dashboard.css rename to lib/vmpooler/public/dashboard.css diff --git a/public/img/logo.jpg b/lib/vmpooler/public/img/logo.jpg similarity index 100% rename from public/img/logo.jpg rename to lib/vmpooler/public/img/logo.jpg diff --git a/public/img/spinner.svg b/lib/vmpooler/public/img/spinner.svg similarity index 100% rename from public/img/spinner.svg rename to lib/vmpooler/public/img/spinner.svg diff --git a/public/lib/stats-vmpooler-numbers.js b/lib/vmpooler/public/lib/stats-vmpooler-numbers.js similarity index 100% rename from public/lib/stats-vmpooler-numbers.js rename to lib/vmpooler/public/lib/stats-vmpooler-numbers.js diff --git a/public/lib/stats-vmpooler-pool.js b/lib/vmpooler/public/lib/stats-vmpooler-pool.js similarity index 100% rename from public/lib/stats-vmpooler-pool.js rename to lib/vmpooler/public/lib/stats-vmpooler-pool.js diff --git a/public/lib/stats-vmpooler-running.js b/lib/vmpooler/public/lib/stats-vmpooler-running.js similarity index 100% rename from public/lib/stats-vmpooler-running.js rename to lib/vmpooler/public/lib/stats-vmpooler-running.js diff --git a/views/dashboard.erb b/lib/vmpooler/views/dashboard.erb similarity index 100% rename from views/dashboard.erb rename to lib/vmpooler/views/dashboard.erb diff --git a/views/layout.erb b/lib/vmpooler/views/layout.erb similarity index 100% rename from views/layout.erb rename to lib/vmpooler/views/layout.erb diff --git a/lib/vmpooler/vsphere_helper.rb b/lib/vmpooler/vsphere_helper.rb new file mode 100644 index 0000000..058e859 --- /dev/null +++ b/lib/vmpooler/vsphere_helper.rb @@ -0,0 +1,180 @@ +require 'rubygems' unless defined?(Gem) + +module Vmpooler + class VsphereHelper + def initialize vInfo = {} + begin + require 'rbvmomi' + rescue LoadError + raise "Unable to load RbVmomi, please ensure its installed" + end + + config_file = File.expand_path('vmpooler.yaml') + vsphere = YAML.load_file(config_file)[:vsphere] + + @connection = RbVmomi::VIM.connect :host => vsphere['server'], + :user => vsphere['username'], + :password => vsphere['password'], + :insecure => true + end + + + + def find_datastore datastorename + begin + @connection.serviceInstance.CurrentTime + rescue + initialize() + end + + datacenter = @connection.serviceInstance.find_datacenter + datacenter.find_datastore(datastorename) + end + + + + def find_folder foldername + begin + @connection.serviceInstance.CurrentTime + rescue + initialize() + end + + datacenter = @connection.serviceInstance.find_datacenter + base = datacenter.vmFolder + folders = foldername.split('/') + folders.each do |folder| + case base + when RbVmomi::VIM::Folder + base = base.childEntity.find { |f| f.name == folder } + else + abort "Unexpected object type encountered (#{base.class}) while finding folder" + end + end + + base + end + + + + def find_pool poolname + begin + @connection.serviceInstance.CurrentTime + rescue + initialize() + end + + datacenter = @connection.serviceInstance.find_datacenter + base = datacenter.hostFolder + pools = poolname.split('/') + pools.each do |pool| + case base + when RbVmomi::VIM::Folder + base = base.childEntity.find { |f| f.name == pool } + when RbVmomi::VIM::ClusterComputeResource + base = base.resourcePool.resourcePool.find { |f| f.name == pool } + when RbVmomi::VIM::ResourcePool + base = base.resourcePool.find { |f| f.name == pool } + else + abort "Unexpected object type encountered (#{base.class}) while finding resource pool" + end + end + + base = base.resourcePool unless base.is_a?(RbVmomi::VIM::ResourcePool) and base.respond_to?(:resourcePool) + base + end + + + + def find_vm vmname + begin + @connection.serviceInstance.CurrentTime + rescue + initialize() + end + + @connection.searchIndex.FindByDnsName(:vmSearch => true, :dnsName => vmname) + end + + + + def find_vm_heavy vmname + begin + @connection.serviceInstance.CurrentTime + rescue + initialize() + end + + vmname = vmname.is_a?(Array) ? vmname : [ vmname ] + containerView = get_base_vm_container_from @connection + propertyCollector = @connection.propertyCollector + + objectSet = [{ + :obj => containerView, + :skip => true, + :selectSet => [ RbVmomi::VIM::TraversalSpec.new({ + :name => 'gettingTheVMs', + :path => 'view', + :skip => false, + :type => 'ContainerView' + }) ] + }] + + propSet = [{ + :pathSet => [ 'name' ], + :type => 'VirtualMachine' + }] + + results = propertyCollector.RetrievePropertiesEx({ + :specSet => [{ + :objectSet => objectSet, + :propSet => propSet + }], + :options => { :maxObjects => nil } + }) + + vms = {} + results.objects.each do |result| + name = result.propSet.first.val + next unless vmname.include? name + vms[name] = result.obj + end + + while results.token do + results = propertyCollector.ContinueRetrievePropertiesEx({:token => results.token}) + results.objects.each do |result| + name = result.propSet.first.val + next unless vmname.include? name + vms[name] = result.obj + end + end + + vms + end + + + + def get_base_vm_container_from connection + begin + connection.serviceInstance.CurrentTime + rescue + initialize() + end + + viewManager = connection.serviceContent.viewManager + viewManager.CreateContainerView({ + :container => connection.serviceContent.rootFolder, + :recursive => true, + :type => [ 'VirtualMachine' ] + }) + end + + + + def close + @connection.close + end + + end +end + diff --git a/lib/vsphere_helper.rb b/lib/vsphere_helper.rb deleted file mode 100755 index 1dfa018..0000000 --- a/lib/vsphere_helper.rb +++ /dev/null @@ -1,180 +0,0 @@ -require 'yaml' unless defined?(YAML) -require 'rubygems' unless defined?(Gem) - -class VsphereHelper - def initialize vInfo = {} - begin - require 'rbvmomi' - rescue LoadError - raise "Unable to load RbVmomi, please ensure its installed" - end - - Dir.chdir(File.dirname(__FILE__)) - - config_file = File.expand_path('../vmpooler.yaml') - vsphere = YAML.load_file(config_file)[:vsphere] - - @connection = RbVmomi::VIM.connect :host => vsphere['server'], - :user => vsphere['username'], - :password => vsphere['password'], - :insecure => true - end - - - - def find_datastore datastorename - begin - @connection.serviceInstance.CurrentTime - rescue - initialize() - end - - datacenter = @connection.serviceInstance.find_datacenter - datacenter.find_datastore(datastorename) - end - - - - def find_folder foldername - begin - @connection.serviceInstance.CurrentTime - rescue - initialize() - end - - datacenter = @connection.serviceInstance.find_datacenter - base = datacenter.vmFolder - folders = foldername.split('/') - folders.each do |folder| - case base - when RbVmomi::VIM::Folder - base = base.childEntity.find { |f| f.name == folder } - else - abort "Unexpected object type encountered (#{base.class}) while finding folder" - end - end - - base - end - - - - def find_pool poolname - begin - @connection.serviceInstance.CurrentTime - rescue - initialize() - end - - datacenter = @connection.serviceInstance.find_datacenter - base = datacenter.hostFolder - pools = poolname.split('/') - pools.each do |pool| - case base - when RbVmomi::VIM::Folder - base = base.childEntity.find { |f| f.name == pool } - when RbVmomi::VIM::ClusterComputeResource - base = base.resourcePool.resourcePool.find { |f| f.name == pool } - when RbVmomi::VIM::ResourcePool - base = base.resourcePool.find { |f| f.name == pool } - else - abort "Unexpected object type encountered (#{base.class}) while finding resource pool" - end - end - - base = base.resourcePool unless base.is_a?(RbVmomi::VIM::ResourcePool) and base.respond_to?(:resourcePool) - base - end - - - - def find_vm vmname - begin - @connection.serviceInstance.CurrentTime - rescue - initialize() - end - - @connection.searchIndex.FindByDnsName(:vmSearch => true, :dnsName => vmname) - end - - - - def find_vm_heavy vmname - begin - @connection.serviceInstance.CurrentTime - rescue - initialize() - end - - vmname = vmname.is_a?(Array) ? vmname : [ vmname ] - containerView = get_base_vm_container_from @connection - propertyCollector = @connection.propertyCollector - - objectSet = [{ - :obj => containerView, - :skip => true, - :selectSet => [ RbVmomi::VIM::TraversalSpec.new({ - :name => 'gettingTheVMs', - :path => 'view', - :skip => false, - :type => 'ContainerView' - }) ] - }] - - propSet = [{ - :pathSet => [ 'name' ], - :type => 'VirtualMachine' - }] - - results = propertyCollector.RetrievePropertiesEx({ - :specSet => [{ - :objectSet => objectSet, - :propSet => propSet - }], - :options => { :maxObjects => nil } - }) - - vms = {} - results.objects.each do |result| - name = result.propSet.first.val - next unless vmname.include? name - vms[name] = result.obj - end - - while results.token do - results = propertyCollector.ContinueRetrievePropertiesEx({:token => results.token}) - results.objects.each do |result| - name = result.propSet.first.val - next unless vmname.include? name - vms[name] = result.obj - end - end - - vms - end - - - - def get_base_vm_container_from connection - begin - connection.serviceInstance.CurrentTime - rescue - initialize() - end - - viewManager = connection.serviceContent.viewManager - viewManager.CreateContainerView({ - :container => connection.serviceContent.rootFolder, - :recursive => true, - :type => [ 'VirtualMachine' ] - }) - end - - - - def close - @connection.close - end -end - diff --git a/vmpooler b/vmpooler index a304eca..0b720ad 100755 --- a/vmpooler +++ b/vmpooler @@ -1,463 +1,14 @@ -#!/usr/bin/ruby - -require 'json' -require 'rbvmomi' -require 'redis' -require 'time' -require 'timeout' -require 'yaml' +#!/usr/bin/env ruby $:.unshift(File.dirname(__FILE__)) -require 'lib/logger' -require 'lib/require_relative' -require 'lib/vsphere_helper' -Dir.chdir(File.dirname(__FILE__)) +require 'rubygems' unless defined?(Gem) +require 'lib/vmpooler' -# Load the configuration file -config_file = File.expand_path('vmpooler.yaml') -$config = YAML.load_file(config_file) - -pools = $config[:pools] -vsphere = $config[:vsphere] -redis = $config[:redis] - -# Load logger library -$logger = Logger.new $config[:config]['logfile'] - -# Load Graphite helper library (if configured) -if (defined? $config[:config]['graphite']) - require 'lib/graphite' - $graphite = Graphite.new $config[:config]['graphite'] -end - -# Set some defaults -$config[:config]['task_limit'] ||= 10 -$config[:config]['vm_checktime'] ||= 15 -$config[:config]['vm_lifetime'] ||= 24 -$config[:redis] ||= Hash.new -$config[:redis]['server'] ||= 'localhost' - -# Connect to Redis -$redis = Redis.new(:host => $config[:redis]['server']) - -# vSphere object -$vsphere = {} - -# Our thread-tracker object -$threads = {} - - - -# Check the state of a VM -def check_pending_vm vm, pool, timeout - Thread.new { - host = $vsphere[pool].find_vm(vm) - - if (host) - if ( - (host.summary) and - (host.summary.guest) and - (host.summary.guest.hostName) and - (host.summary.guest.hostName == vm) - ) - begin - Socket.getaddrinfo(vm, nil) - rescue - end - - $redis.smove('vmpooler__pending__'+pool, 'vmpooler__ready__'+pool, vm) - - $logger.log('s', "[>] [#{pool}] '#{vm}' moved to 'ready' queue") - end - else - clone_stamp = $redis.hget('vmpooler__vm__'+vm, 'clone') - - if ( - (clone_stamp) and - (((Time.now - Time.parse(clone_stamp))/60) > timeout) - ) - $redis.smove('vmpooler__pending__'+pool, 'vmpooler__completed__'+pool, vm) - - $logger.log('d', "[!] [#{pool}] '#{vm}' marked as 'failed' after #{timeout} minutes") - end - end - } -end - -def check_ready_vm vm, pool, ttl - Thread.new { - if (ttl > 0) - if ((((Time.now - host.runtime.bootTime)/60).to_s[/^\d+\.\d{1}/].to_f) > ttl) - $redis.smove('vmpooler__ready__'+pool, 'vmpooler__completed__'+pool, vm) - - $logger.log('d', "[!] [#{pool}] '#{vm}' reached end of TTL after #{ttl} minutes, removed from 'ready' queue") - end - end - - check_stamp = $redis.hget('vmpooler__vm__'+vm, 'check') - - if ( - (! check_stamp) or - (((Time.now - Time.parse(check_stamp))/60) > $config[:config]['vm_checktime']) - ) - $redis.hset('vmpooler__vm__'+vm, 'check', Time.now) - - host = $vsphere[pool].find_vm(vm) || - $vsphere[pool].find_vm_heavy(vm)[vm] - - if (host) - if ( - (host.runtime) and - (host.runtime.powerState) and - (host.runtime.powerState != 'poweredOn') - ) - $redis.smove('vmpooler__ready__'+pool, 'vmpooler__completed__'+pool, vm) - - $logger.log('d', "[!] [#{pool}] '#{vm}' appears to be powered off, removed from 'ready' queue") - end - - if ( - (host.summary.guest) and - (host.summary.guest.hostName) and - (host.summary.guest.hostName != vm) - ) - $redis.smove('vmpooler__ready__'+pool, 'vmpooler__completed__'+pool, vm) - - $logger.log('d', "[!] [#{pool}] '#{vm}' has mismatched hostname, removed from 'ready' queue") - end - else - $redis.srem('vmpooler__ready__'+pool, vm) - - $logger.log('s', "[!] [#{pool}] '#{vm}' not found in vCenter inventory, removed from 'ready' queue") - end - - begin - Timeout::timeout(5) { - TCPSocket.new vm, 22 - } - rescue - if ($redis.smove('vmpooler__ready__'+pool, 'vmpooler__completed__'+pool, vm)) - $logger.log('d', "[!] [#{pool}] '#{vm}' is unreachable, removed from 'ready' queue") - end - end - end - } -end - -def check_running_vm vm, pool, ttl - Thread.new { - host = $vsphere[pool].find_vm(vm) - - if (host) - if ( - (host.runtime) and - (host.runtime.powerState != 'poweredOn') - ) - $redis.smove('vmpooler__running__'+pool, 'vmpooler__completed__'+pool, vm) - - $logger.log('d', "[!] [#{pool}] '#{vm}' appears to be powered off or dead") - else - if ( - (host.runtime) and - (host.runtime.bootTime) - ((((Time.now - host.runtime.bootTime)/60).to_s[/^\d+\.\d{1}/].to_f) > ttl) - ) - $redis.smove('vmpooler__running__'+pool, 'vmpooler__completed__'+pool, vm) - - $logger.log('d', "[!] [#{pool}] '#{vm}' reached end of TTL after #{ttl} minutes") - end - end - end - } -end - -# Clone a VM -def clone_vm template, pool, folder, datastore - Thread.new { - vm = {} - - if template =~ /\// - templatefolders = template.split('/') - vm['template'] = templatefolders.pop - end - - if templatefolders - vm[vm['template']] = $vsphere[vm['template']].find_folder(templatefolders.join('/')).find(vm['template']) - else - raise "Please provide a full path to the template" - end - - if vm['template'].length == 0 - raise "Unable to find template '#{vm['template']}'!" - end - - # Generate a randomized hostname - o = [('a'..'z'),('0'..'9')].map{|r| r.to_a}.flatten - vm['hostname'] = o[rand(25)]+(0...14).map{o[rand(o.length)]}.join - - # Add VM to Redis inventory ('pending' pool) - $redis.sadd('vmpooler__pending__'+vm['template'], vm['hostname']) - $redis.hset('vmpooler__vm__'+vm['hostname'], 'clone', Time.now) - - # Annotate with creation time, origin template, etc. - configSpec = RbVmomi::VIM.VirtualMachineConfigSpec( - :annotation => JSON.pretty_generate({ - name: vm['hostname'], - created_by: $config[:vsphere]['username'], - base_template: vm['template'], - creation_timestamp: Time.now.utc - }) - ) - - # Put the VM in the specified folder and resource pool - relocateSpec = RbVmomi::VIM.VirtualMachineRelocateSpec( - :datastore => $vsphere[vm['template']].find_datastore(datastore), - :pool => $vsphere[vm['template']].find_pool(pool), - :diskMoveType => :moveChildMostDiskBacking - ) - - # Create a clone spec - spec = RbVmomi::VIM.VirtualMachineCloneSpec( - :location => relocateSpec, - :config => configSpec, - :powerOn => true, - :template => false - ) - - # Clone the VM - $logger.log('d', "[ ] [#{vm['template']}] '#{vm['hostname']}' is being cloned from '#{vm['template']}'") - - begin - start = Time.now - vm[vm['template']].CloneVM_Task( - :folder => $vsphere[vm['template']].find_folder(folder), - :name => vm['hostname'], - :spec => spec - ).wait_for_completion - finish = '%.2f' % (Time.now-start) - - $logger.log('s', "[+] [#{vm['template']}] '#{vm['hostname']}' cloned from '#{vm['template']}' in #{finish} seconds") - rescue - $logger.log('s', "[!] [#{vm['template']}] '#{vm['hostname']}' clone appears to have failed") - $redis.srem('vmpooler__pending__'+vm['template'], vm['hostname']) - end - - $redis.decr('vmpooler__tasks__clone') - - begin - $graphite.log("vmpooler.clone.#{vm['template']}", finish) if defined? $graphite - rescue - end - } -end - -# Destroy a VM -def destroy_vm vm, pool - Thread.new { - $redis.srem('vmpooler__completed__'+pool, vm) - $redis.hdel('vmpooler__active__'+pool, vm) - $redis.del('vmpooler__vm__'+vm) - - host = $vsphere[pool].find_vm(vm) || - $vsphere[pool].find_vm_heavy(vm)[vm] - - if (host) - start = Time.now - - if ( - (host.runtime) and - (host.runtime.powerState) and - (host.runtime.powerState == 'poweredOn') - ) - $logger.log('d', "[ ] [#{pool}] '#{vm}' is being shut down") - host.PowerOffVM_Task.wait_for_completion - end - - host.Destroy_Task.wait_for_completion - finish = '%.2f' % (Time.now-start) - - $logger.log('s', "[-] [#{pool}] '#{vm}' destroyed in #{finish} seconds") - - $graphite.log("vmpooler.destroy.#{pool}", finish) if defined? $graphite - end - } -end - -def check_pool pool - $logger.log('d', "[*] [#{pool['name']}] starting worker thread") - - $threads[pool['name']] = Thread.new { - $vsphere[pool['name']] ||= VsphereHelper.new - - loop do - # INVENTORY - inventory = {} - begin - base = $vsphere[pool['name']].find_pool(pool['pool']) - - base.vm.each do |vm| - if ( - (! $redis.sismember('vmpooler__running__'+pool['name'], vm['name'])) and - (! $redis.sismember('vmpooler__ready__'+pool['name'], vm['name'])) and - (! $redis.sismember('vmpooler__pending__'+pool['name'], vm['name'])) and - (! $redis.sismember('vmpooler__completed__'+pool['name'], vm['name'])) and - (! $redis.sismember('vmpooler__discovered__'+pool['name'], vm['name'])) - ) - $redis.sadd('vmpooler__discovered__'+pool['name'], vm['name']) - - $logger.log('s', "[?] [#{pool['name']}] '#{vm['name']}' added to 'discovered' queue") - end - - inventory[vm['name']] = 1 - end - rescue - end - - # RUNNING - $redis.smembers('vmpooler__running__'+pool['name']).each do |vm| - if (inventory[vm]) - if (pool['running_ttl']) - begin - check_running_vm(vm, pool['name'], pool['running_ttl']) - rescue - end - else - begin - check_running_vm(vm, pool['name'], '720') - rescue - end - end - end - end - - # READY - $redis.smembers('vmpooler__ready__'+pool['name']).each do |vm| - if (inventory[vm]) - begin - check_ready_vm(vm, pool['name'], pool['ready_ttl'] || 0) - rescue - end - end - end - - # PENDING - $redis.smembers('vmpooler__pending__'+pool['name']).each do |vm| - pool['timeout'] ||= 15 - - if (inventory[vm]) - begin - check_pending_vm(vm, pool['name'], pool['timeout']) - rescue - end - end - end - - # COMPLETED - $redis.smembers('vmpooler__completed__'+pool['name']).each do |vm| - if (inventory[vm]) - begin - destroy_vm(vm, pool['name']) - rescue - $logger.log('s', "[!] [#{pool['name']}] '#{vm}' destroy appears to have failed") - $redis.srem('vmpooler__completed__'+pool['name'], vm) - $redis.hdel('vmpooler__active__'+pool['name'], vm) - $redis.del('vmpooler__vm__'+vm) - end - else - $logger.log('s', "[!] [#{pool['name']}] '#{vm}' not found in inventory, removed from 'completed' queue") - $redis.srem('vmpooler__completed__'+pool['name'], vm) - $redis.hdel('vmpooler__active__'+pool['name'], vm) - $redis.del('vmpooler__vm__'+vm) - end - end - - # DISCOVERED - $redis.smembers('vmpooler__discovered__'+pool['name']).each do |vm| - ['pending', 'ready', 'running', 'completed'].each do |queue| - if ($redis.sismember('vmpooler__'+queue+'__'+pool['name'], vm)) - $logger.log('d', "[!] [#{pool['name']}] '#{vm}' found in '#{queue}', removed from 'discovered' queue") - $redis.srem('vmpooler__discovered__'+pool['name'], vm) - end - end - - if ($redis.sismember('vmpooler__discovered__'+pool['name'], vm)) - $redis.smove('vmpooler__discovered__'+pool['name'], 'vmpooler__completed__'+pool['name'], vm) - end - end - - # LONG-RUNNING - $redis.smembers('vmpooler__running__'+pool['name']).each do |vm| - if ($redis.hget('vmpooler__active__'+pool['name'], vm)) - running = (Time.now - Time.parse($redis.hget('vmpooler__active__'+pool['name'], vm)))/60/60 - if ( - ($config[:config]['vm_lifetime'] > 0) and - (running > $config[:config]['vm_lifetime']) - ) - $redis.smove('vmpooler__running__'+pool['name'], 'vmpooler__completed__'+pool['name'], vm) - - $logger.log('d', "[!] [#{pool['name']}] '#{vm}' reached end of TTL after #{$config[:config]['vm_lifetime']} hours") - end - end - end - - # REPOPULATE - total = $redis.scard('vmpooler__ready__'+pool['name']) + - $redis.scard('vmpooler__pending__'+pool['name']) - - begin - if (defined? $graphite) - $graphite.log('vmpooler.ready.'+pool['name'], $redis.scard('vmpooler__ready__'+pool['name'])) - $graphite.log('vmpooler.running.'+pool['name'], $redis.scard('vmpooler__running__'+pool['name'])) - end - rescue - end - - if (total < pool['size']) - (1..(pool['size'] - total)).each { |i| - - if ($redis.get('vmpooler__tasks__clone').to_i < $config[:config]['task_limit']) - begin - $redis.incr('vmpooler__tasks__clone') - - clone_vm( - pool['template'], - pool['pool'], - pool['folder'], - pool['datastore'] - ) - rescue - $logger.log('s', "[!] [#{pool['name']}] clone appears to have failed") - $redis.decr('vmpooler__tasks__clone') - end - end - } - end - - sleep(1) - end - } -end - - - -$logger.log('d', "starting vmpooler") - -# Clear out the tasks manager, as we don't know about any tasks at this point -$redis.set('vmpooler__tasks__clone', 0) +Thread.new { Vmpooler::API.new.execute! } +Thread.new { Vmpooler::PoolManager.new.execute! } loop do - pools.each do |pool| - if (! $threads[pool['name']]) - check_pool(pool) - else - if (! $threads[pool['name']].alive?) - $logger.log('d', "[!] [#{pool['name']}] worker thread died, restarting") - check_pool(pool) - end - end - end - sleep(1) end diff --git a/vmpooler-api b/vmpooler-api deleted file mode 100755 index 6ecf611..0000000 --- a/vmpooler-api +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/ruby - -require 'rubygems' - -require 'json' -require 'open-uri' -require 'redis' -require 'sinatra' -require 'yaml' - -$:.unshift(File.dirname(__FILE__)) -require 'lib/logger' -require 'lib/require_relative' - -Dir.chdir(File.dirname(__FILE__)) - -# Load the configuration file -config_file = File.expand_path('vmpooler.yaml') -$config = YAML.load_file(config_file) - -pools = $config[:pools] -redis = $config[:redis] - -# Load logger library -$logger = Logger.new $config[:config]['logfile'] - -# Set some defaults -$config[:redis] ||= Hash.new -$config[:redis]['server'] ||= 'localhost' - -# Connect to Redis -$redis = Redis.new(:host => $config[:redis]['server']) - -# Sinatra! -get '/' do - erb :dashboard, locals: { - site_name: $config[:config]['site_name'] || 'vmpooler', - } -end - -get '/dashboard/stats/vmpooler/numbers/?' do - result = Hash.new - result['pending'] = 0 - result['cloning'] = 0 - result['booting'] = 0 - result['ready'] = 0 - result['running'] = 0 - result['completed'] = 0 - - $config[:pools].each do |pool| - result['pending'] += $redis.scard( 'vmpooler__pending__' + pool['name'] ) - result['ready'] += $redis.scard( 'vmpooler__ready__' + pool['name'] ) - result['running'] += $redis.scard( 'vmpooler__running__' + pool['name'] ) - result['completed'] += $redis.scard( 'vmpooler__completed__' + pool['name'] ) - end - - result['cloning'] = $redis.get( 'vmpooler__tasks__clone' ) - result['booting'] = result['pending'].to_i - result['cloning'].to_i - result['booting'] = 0 if result['booting'] < 0 - result['total'] = result['pending'].to_i + result['ready'].to_i + result['running'].to_i + result['completed'].to_i - - content_type :json - JSON.pretty_generate(result) -end - -get '/dashboard/stats/vmpooler/pool/?' do - result = Hash.new - - $config[:pools].each do |pool| - result[pool['name']] ||= Hash.new - result[pool['name']]['size'] = pool['size'] - result[pool['name']]['ready'] = $redis.scard( 'vmpooler__ready__' + pool['name'] ) - end - - if ( params[:history] ) - if ( $config[:config]['graphite'] ) - history ||= Hash.new - - begin - buffer = open( 'http://'+$config[:config]['graphite']+'/render?target=vmpooler.ready.*&from=-1hour&format=json' ).read - history = JSON.parse( buffer ) - - history.each do |pool| - if pool['target'] =~ /.*\.(.*)$/ - pool['name'] = $1 - - if ( result[pool['name']] ) - pool['last'] = result[pool['name']]['size'] - result[pool['name']]['history'] ||= Array.new - - pool['datapoints'].each do |metric| - 8.times do |n| - if ( metric[0] ) - pool['last'] = metric[0].to_i - result[pool['name']]['history'].push( metric[0].to_i ) - else - result[pool['name']]['history'].push( pool['last'] ) - end - end - end - end - end - end - rescue - end - else - $config[:pools].each do |pool| - result[pool['name']] ||= Hash.new - result[pool['name']]['history'] = [ $redis.scard( 'vmpooler__ready__' + pool['name'] ) ] - end - end - end - - content_type :json - JSON.pretty_generate(result) -end - -get '/dashboard/stats/vmpooler/running/?' do - result = Hash.new - - $config[:pools].each do |pool| - running = $redis.scard( 'vmpooler__running__' + pool['name'] ) - pool['major'] = $1 if pool['name'] =~ /^(\w+)\-/ - - result[pool['major']] ||= Hash.new - - result[pool['major']]['running'] = result[pool['major']]['running'].to_i + running.to_i - end - - if ( params[:history] ) - if ( $config[:config]['graphite'] ) - begin - buffer = open( 'http://'+$config[:config]['graphite']+'/render?target=vmpooler.running.*&from=-1hour&format=json' ).read - JSON.parse( buffer ).each do |pool| - if pool['target'] =~ /.*\.(.*)$/ - pool['name'] = $1 - - pool['major'] = $1 if pool['name'] =~ /^(\w+)\-/ - - result[pool['major']]['history'] ||= Array.new - - for i in 0..pool['datapoints'].length - if ( - pool['datapoints'][i] and - pool['datapoints'][i][0] - ) - pool['last'] = pool['datapoints'][i][0] - - result[pool['major']]['history'][i] ||= 0 - result[pool['major']]['history'][i] = result[pool['major']]['history'][i].to_i + pool['datapoints'][i][0].to_i - else - result[pool['major']]['history'][i] = result[pool['major']]['history'][i].to_i + pool['last'].to_i - end - end - - end - end - rescue - end - end - end - - content_type :json - JSON.pretty_generate(result) -end - -get '/vm/?' do - content_type :json - - result = [] - - pools.each do |pool| - result.push(pool['name']) - end - - JSON.pretty_generate(result) -end - -get '/vm/:template/?' do - content_type :json - - result = {} - result[params[:template]] = {} - result[params[:template]]['hosts'] = $redis.smembers('vmpooler__ready__'+params[:template]) - - JSON.pretty_generate(result) -end - -post '/vm/:template/?' do - content_type :json - - result = {} - result[params[:template]] = {} - - if ( $redis.scard('vmpooler__ready__'+params[:template]) > 0 ) - vm = $redis.spop('vmpooler__ready__'+params[:template]) - - unless (vm.nil?) - $redis.sadd('vmpooler__running__'+params[:template], vm) - $redis.hset('vmpooler__active__'+params[:template], vm, Time.now) - - result[params[:template]]['ok'] = true - result[params[:template]]['hostname'] = vm - else - result[params[:template]]['ok'] = false - end - else - result[params[:template]]['ok'] = false - end - - JSON.pretty_generate(result) -end - -delete '/vm/:hostname/?' do - content_type :json - - result = {} - - result['ok'] = false - - pools.each do |pool| - if $redis.sismember('vmpooler__running__'+pool['name'], params[:hostname]) - $redis.srem('vmpooler__running__'+pool['name'], params[:hostname]) - $redis.sadd('vmpooler__completed__'+pool['name'], params[:hostname]) - result['ok'] = true - end - end - - JSON.pretty_generate(result) -end - From 60eead64558b804e9fe6f46695a53036cf81d397 Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Wed, 5 Mar 2014 13:19:43 -0800 Subject: [PATCH 2/8] Centralize external Gem loading --- lib/vmpooler.rb | 9 +++++++++ lib/vmpooler/api.rb | 7 ------- lib/vmpooler/pool_manager.rb | 7 ------- lib/vmpooler/vsphere_helper.rb | 6 ------ 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/lib/vmpooler.rb b/lib/vmpooler.rb index 4e4d901..ef820da 100644 --- a/lib/vmpooler.rb +++ b/lib/vmpooler.rb @@ -1,6 +1,15 @@ require 'rubygems' unless defined?(Gem) module Vmpooler + require 'json' + require 'open-uri' + require 'rbvmomi' + require 'redis' + require 'sinatra/base' + require 'time' + require 'timeout' + require 'yaml' + %w( api graphite logger pool_manager vsphere_helper ).each do |lib| begin require "vmpooler/#{lib}" diff --git a/lib/vmpooler/api.rb b/lib/vmpooler/api.rb index 4a84234..c2d6b57 100644 --- a/lib/vmpooler/api.rb +++ b/lib/vmpooler/api.rb @@ -1,13 +1,6 @@ module Vmpooler class API def initialize - require 'sinatra/base' - - require 'json' - require 'open-uri' - require 'redis' - require 'yaml' - # Load the configuration file config_file = File.expand_path('vmpooler.yaml') $config = YAML.load_file(config_file) diff --git a/lib/vmpooler/pool_manager.rb b/lib/vmpooler/pool_manager.rb index c85ec21..20e25f3 100644 --- a/lib/vmpooler/pool_manager.rb +++ b/lib/vmpooler/pool_manager.rb @@ -1,13 +1,6 @@ module Vmpooler class PoolManager def initialize - require 'json' - require 'rbvmomi' - require 'redis' - require 'time' - require 'timeout' - require 'yaml' - # Load the configuration file config_file = File.expand_path('vmpooler.yaml') $config = YAML.load_file(config_file) diff --git a/lib/vmpooler/vsphere_helper.rb b/lib/vmpooler/vsphere_helper.rb index 058e859..402530b 100644 --- a/lib/vmpooler/vsphere_helper.rb +++ b/lib/vmpooler/vsphere_helper.rb @@ -3,12 +3,6 @@ require 'rubygems' unless defined?(Gem) module Vmpooler class VsphereHelper def initialize vInfo = {} - begin - require 'rbvmomi' - rescue LoadError - raise "Unable to load RbVmomi, please ensure its installed" - end - config_file = File.expand_path('vmpooler.yaml') vsphere = YAML.load_file(config_file)[:vsphere] From 3af680f86033111e6415bfdfadfa99916ddb0614 Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Thu, 6 Mar 2014 10:53:27 -0800 Subject: [PATCH 3/8] Allow a configurable Graphite namespace (prefix) --- lib/vmpooler/api.rb | 19 ++++++++++++------- lib/vmpooler/pool_manager.rb | 27 +++++++++++++-------------- vmpooler.yaml.example | 25 ++++++++++++++++++++----- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/lib/vmpooler/api.rb b/lib/vmpooler/api.rb index c2d6b57..abe091f 100644 --- a/lib/vmpooler/api.rb +++ b/lib/vmpooler/api.rb @@ -5,13 +5,14 @@ module Vmpooler config_file = File.expand_path('vmpooler.yaml') $config = YAML.load_file(config_file) - pools = $config[:pools] - redis = $config[:redis] - # Set some defaults $config[:redis] ||= Hash.new $config[:redis]['server'] ||= 'localhost' + if ($config[:graphite]['server']) + $config[:graphite]['prefix'] ||= 'vmpooler' + end + # Connect to Redis $redis = Redis.new(:host => $config[:redis]['server']) end @@ -62,11 +63,13 @@ module Vmpooler end if ( params[:history] ) - if ( $config[:config]['graphite'] ) + if ( $config[:graphite]['server'] ) history ||= Hash.new begin - buffer = open( 'http://'+$config[:config]['graphite']+'/render?target=vmpooler.ready.*&from=-1hour&format=json' ).read + buffer = open( + 'http://'+$config[:graphite]['server']+'/render?target='+$config[:graphite]['prefix']+'.ready.*&from=-1hour&format=json' + ).read history = JSON.parse( buffer ) history.each do |pool| @@ -117,9 +120,11 @@ module Vmpooler end if ( params[:history] ) - if ( $config[:config]['graphite'] ) + if ( $config[:graphite]['server'] ) begin - buffer = open( 'http://'+$config[:config]['graphite']+'/render?target=vmpooler.running.*&from=-1hour&format=json' ).read + buffer = open( + 'http://'+$config[:graphite]['server']+'/render?target='+$config[:graphite]['prefix']+'.running.*&from=-1hour&format=json' + ).read JSON.parse( buffer ).each do |pool| if pool['target'] =~ /.*\.(.*)$/ pool['name'] = $1 diff --git a/lib/vmpooler/pool_manager.rb b/lib/vmpooler/pool_manager.rb index 20e25f3..a990d70 100644 --- a/lib/vmpooler/pool_manager.rb +++ b/lib/vmpooler/pool_manager.rb @@ -6,16 +6,6 @@ module Vmpooler $config = YAML.load_file(config_file) $pools = $config[:pools] - vsphere = $config[:vsphere] - redis = $config[:redis] - - # Load logger library - $logger = Vmpooler::Logger.new $config[:config]['logfile'] - - # Load Graphite helper library (if configured) - if (defined? $config[:config]['graphite']) - $graphite = Vmpooler::Graphite.new $config[:config]['graphite'] - end # Set some defaults $config[:config]['task_limit'] ||= 10 @@ -24,6 +14,15 @@ module Vmpooler $config[:redis] ||= Hash.new $config[:redis]['server'] ||= 'localhost' + # Load logger library + $logger = Vmpooler::Logger.new $config[:config]['logfile'] + + # Load Graphite helper library (if configured) + if (defined? $config[:graphite]['server']) + $config[:graphite]['prefix'] ||= 'vmpooler' + $graphite = Vmpooler::Graphite.new $config[:graphite]['server'] + end + # Connect to Redis $redis = Redis.new(:host => $config[:redis]['server']) @@ -232,7 +231,7 @@ module Vmpooler $redis.decr('vmpooler__tasks__clone') begin - $graphite.log("vmpooler.clone.#{vm['template']}", finish) if defined? $graphite + $graphite.log($config[:graphite]['prefix']+".clone.#{vm['template']}", finish) if defined? $graphite rescue end } @@ -265,7 +264,7 @@ module Vmpooler $logger.log('s', "[-] [#{pool}] '#{vm}' destroyed in #{finish} seconds") - $graphite.log("vmpooler.destroy.#{pool}", finish) if defined? $graphite + $graphite.log($config[:graphite]['prefix']+".destroy.#{pool}", finish) if defined? $graphite end } end @@ -393,8 +392,8 @@ module Vmpooler begin if (defined? $graphite) - $graphite.log('vmpooler.ready.'+pool['name'], $redis.scard('vmpooler__ready__'+pool['name'])) - $graphite.log('vmpooler.running.'+pool['name'], $redis.scard('vmpooler__running__'+pool['name'])) + $graphite.log($config[:graphite]['prefix']+'.ready.'+pool['name'], $redis.scard('vmpooler__ready__'+pool['name'])) + $graphite.log($config[:graphite]['prefix']+'.running.'+pool['name'], $redis.scard('vmpooler__running__'+pool['name'])) end rescue end diff --git a/vmpooler.yaml.example b/vmpooler.yaml.example index 4746db2..c0d4b44 100644 --- a/vmpooler.yaml.example +++ b/vmpooler.yaml.example @@ -49,6 +49,26 @@ :redis: server: 'redis.company.com' +# :graphite: +# +# This section contains the connection information required to store +# historical data in an external Graphite database. +# +# Available configuration parameters: +# +# - server +# The FQDN hostname of the Graphite server. +# (optional) +# +# - prefix +# The prefix to use while storing Graphite data. +# (optional; default: 'vmpooler') + +# Example: + +:graphite: + server: 'graphite.company.com' + # :config: # # This section contains global configuration information. @@ -63,10 +83,6 @@ # The path to vmpooler's log file. # (optional; default: '/var/log/vmpooler.log') # -# - graphite -# The FQDN hostname of the Graphite server. -# (optional) -# # - task_limit # The number of concurrent VMware vSphere tasks to perform. # (optional; default: '10') @@ -84,7 +100,6 @@ :config: site_name: 'vmpooler' logfile: '/var/log/vmpooler.log' - graphite: 'graphite.company.com' task_limit: 10 vm_checktime: 15 vm_lifetime: 12 From 4b3b05ee3734999ac0fdd764fc627bc36e1f9f48 Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Tue, 11 Mar 2014 12:49:42 -0700 Subject: [PATCH 4/8] Add textured backgrounds to SVG area pathes --- lib/vmpooler/public/dashboard.css | 2 +- lib/vmpooler/public/img/bg.png | Bin 0 -> 27246 bytes .../public/lib/stats-vmpooler-pool.js | 23 ++++++++++++++- .../public/lib/stats-vmpooler-running.js | 27 ++++++++++++++++-- 4 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 lib/vmpooler/public/img/bg.png diff --git a/lib/vmpooler/public/dashboard.css b/lib/vmpooler/public/dashboard.css index 3134e8d..86680a9 100644 --- a/lib/vmpooler/public/dashboard.css +++ b/lib/vmpooler/public/dashboard.css @@ -140,5 +140,5 @@ body, .tick { fill: none; - stroke: #eee; + stroke: #ddd; } diff --git a/lib/vmpooler/public/img/bg.png b/lib/vmpooler/public/img/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..f07c531af447364d833baa513ab8d29deef65802 GIT binary patch literal 27246 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 Date: Mon, 9 Jun 2014 10:36:17 -0700 Subject: [PATCH 5/8] Longer naps for main process --- vmpooler | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vmpooler b/vmpooler index 0b720ad..0ffc1d7 100755 --- a/vmpooler +++ b/vmpooler @@ -9,6 +9,6 @@ Thread.new { Vmpooler::API.new.execute! } Thread.new { Vmpooler::PoolManager.new.execute! } loop do - sleep(1) + sleep(10) end From d6b6f83fc7df369a134643b4be8ca2ad717bbd8a Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Mon, 9 Jun 2014 15:15:05 -0700 Subject: [PATCH 6/8] Support batch (multiple VM) requests This PR allows a single API request to return multiple VM objects. It supports the following formats: - POST /vm/ eg. 'curl -d '{"debian-7-i386":"3","debian-7-x86_64":"1"}' --url vmpooler/vm - POST /vm/++... eg. 'curl -d --url vmpooler/vm/debian-7-i386+debian-7-i386+debian-7-i386+debian-7-x86_64 Both commands listed above will return a JSON hash of 3 debian-7-i386 VMs and 1 debian-7-x86_64 VM: { "ok": true, "debian-7-i386": { "hostname": [ "v49pwwk5yzg6oad", "ylghlgote5uso54", "wt0c4xovvulo7ge" ] }, "debian-7-x86_64": { "hostname": "v3dkrulttp360fm" } } --- lib/vmpooler/api.rb | 95 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 11 deletions(-) diff --git a/lib/vmpooler/api.rb b/lib/vmpooler/api.rb index abe091f..d137f86 100644 --- a/lib/vmpooler/api.rb +++ b/lib/vmpooler/api.rb @@ -170,6 +170,54 @@ module Vmpooler JSON.pretty_generate(result) end + post '/vm/?' do + content_type :json + + result = {} + + available = 1 + + jdata = JSON.parse(request.body.read) + + jdata.each do |template, count| + if ( $redis.scard('vmpooler__ready__'+template) < count.to_i ) + available = 0 + end + end + + if ( available == 1 ) + result['ok'] = true + + jdata.each do |template, count| + result[template] ||= {} + + count.to_i.times do |i| + vm = $redis.spop('vmpooler__ready__'+template) + + unless (vm.nil?) + $redis.sadd('vmpooler__running__'+template, vm) + $redis.hset('vmpooler__active__'+template, vm, Time.now) + + result[template] ||= {} + + if ( result[template]['hostname'] ) + result[template]['hostname'] = [result[template]['hostname']] if ! result[template]['hostname'].is_a?(Array) + result[template]['hostname'].push(vm) + else + result[template]['hostname'] = vm + end + else + result['ok'] = false + end + end + end + else + result['ok'] = false + end + + JSON.pretty_generate(result) + end + get '/vm/:template/?' do content_type :json @@ -184,22 +232,47 @@ module Vmpooler content_type :json result = {} - result[params[:template]] = {} + request = {} - if ( $redis.scard('vmpooler__ready__'+params[:template]) > 0 ) - vm = $redis.spop('vmpooler__ready__'+params[:template]) + params[:template].split('+').each do |template| + request[template] ||= 0 + request[template] = request[template] + 1 + end - unless (vm.nil?) - $redis.sadd('vmpooler__running__'+params[:template], vm) - $redis.hset('vmpooler__active__'+params[:template], vm, Time.now) + available = 1 - result[params[:template]]['ok'] = true - result[params[:template]]['hostname'] = vm - else - result[params[:template]]['ok'] = false + request.keys.each do |template| + if ( $redis.scard('vmpooler__ready__'+template) < request[template] ) + available = 0 + end + end + + if ( available == 1 ) + result['ok'] = true + + params[:template].split('+').each do |template| + result[template] ||= {} + + vm = $redis.spop('vmpooler__ready__'+template) + + unless (vm.nil?) + $redis.sadd('vmpooler__running__'+template, vm) + $redis.hset('vmpooler__active__'+template, vm, Time.now) + + result[template] ||= {} + + if ( result[template]['hostname'] ) + result[template]['hostname'] = [result[template]['hostname']] if ! result[template]['hostname'].is_a?(Array) + result[template]['hostname'].push(vm) + else + result[template]['hostname'] = vm + end + else + result['ok'] = false + end end else - result[params[:template]]['ok'] = false + result['ok'] = false end JSON.pretty_generate(result) From 9f80e699e4889f748a101714a200e3cfa8fc5c4d Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Tue, 10 Jun 2014 10:07:45 -0700 Subject: [PATCH 7/8] s/Authors/Authors and Contributors/ --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ff52058..059f569 100644 --- a/README.md +++ b/README.md @@ -121,9 +121,9 @@ A dashboard is provided to offer real-time statistics and historical graphs. It [Graphite](http://graphite.wikidot.com/) is required for historical data retrieval. See the provided YAML configuration example, [vmpooler.yaml.example](vmpooler.yaml.example), for details. -## Author +## Authors and Contributors -Scott Schneider (sschneid@gmail.com) +- Scott Schneider (sschneid@gmail.com) ## License From 31e9f525e1e7b2667131f7d339a457215c5a6c62 Mon Sep 17 00:00:00 2001 From: Scott Schneider Date: Tue, 10 Jun 2014 10:15:58 -0700 Subject: [PATCH 8/8] Updating API documentation --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 059f569..28de840 100644 --- a/README.md +++ b/README.md @@ -84,21 +84,63 @@ $ curl --url vmpooler.company.com/vm ] ``` +#### POST /vm +Useful for batch operations; post JSON (see format below), get back VMs. + +``` +$ curl -d '{"debian-7-i386":"2","debian-7-x86_64":"1"}' --url vmpooler.company.com/vm +``` +```json +{ + "ok": true, + "debian-7-i386": { + "hostname": [ + "o41xtodlvnvu5cw", + "khirruvwfjlmx3y" + ] + }, + "debian-7-x86_64": { + "hostname": "y91qbrpbfj6d13q" + } +} +``` + #### POST /vm/ -Check-out a VM. +Check-out a VM or VMs. ``` $ curl -d --url vmpooler.company.com/vm/debian-7-i386 ``` ```json { + "ok": true, "debian-7-i386": { - "ok": true, "hostname": "fq6qlpjlsskycq6" } } ``` +Multiple VMs can be requested by using multiple query parameters in the URL: + +``` +$ curl -d --url vmpooler.company.com/vm/debian-7-i386+debian-7-i386+debian-7-x86_64 +``` + +```json +{ + "ok": true, + "debian-7-i386": { + "hostname": [ + "sc0o4xqtodlul5w", + "4m4dkhqiufnjmxy" + ] + }, + "debian-7-x86_64": { + "hostname": "zb91y9qbrbf6d3q" + } +} +``` + #### DELETE /vm/ Schedule a checked-out VM for deletion.