# -*- mode: ruby -*-
# vi: set ft=ruby noet :

# This is: Oh My Vagrant!
# Copyright (C) 2012-2015+ James Shubin and the Oh-My-Vagrant contributors
# Written by James Shubin <james@shubin.ca> and the Oh-My-Vagrant contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = '2'

require 'ipaddr'
require 'yaml'
require 'erb'
require 'ostruct'
require 'base64'
require 'fileutils'

#
#	globals
#
# thanks to the gluster.org community for the public box hosting
default_boxurlprefix = 'https://dl.fedoraproject.org/pub/alt/purpleidea/vagrant'
really_use_rm = false	# this causes the rm -rf to actually happen if set true
really_use_rm_once = nil	# this causes the rm -rf to happen once

#
#	vms
#
# NOTE: you can specify the list of vms here, or in the omv.yaml file...
# NOTE: if you set the vagrant vms array to nil, you'll default to this list
vms = [
#	{:name => 'example1', :docker => true, :puppet => true, },	# example1
#	{:name => 'example2', :docker => ['centos', 'fedora'], },	# example2
#	{:name => 'example3', :docker =>
#		[
#			{:name => 'centos', },
#			{:name => 'fedora', },
#		],
#	},								# example3
#	{:name => 'example4', :image => 'centos-6', :puppet => true, },	# example4
#	{:name => 'example5', :image => 'rhel-7.0', :poolid => true, },	# example5
#	{:name => 'example6', :puppet => true, :classes =>		# example6
#		['module1', 'module2', 'module3'],
#	},
#	{:name => 'example7', :puppet => true, :classes =>		# example7
#		{
#			'module1' => {'arg1' => 'val1', 'arg2' => 'val2'},
#			'module2' => nil,
#		},
#	},
#	{:name => 'example8', :puppet => true, :ip => '192.168.123.3'},
]

# mutable by ARGV and settings file
domain = 'example.com'		# demain domain to use (yes this *can* work)
network = '192.168.123.0/24'	# default network to use
image = 'centos-7.1'		# default image name
cpus = ''			# default vcpu count in int (default to undef)
memory = ''			# default memory size in mb (default to undef)
disks = 0                       # default additional disks (default to none)
disksize = '40G'                # default additional disk size (defaults to 20G)
boxurlprefix = ''		# default url prefix (useful for private boxes)
sync = 'rsync'			# default sync type
syncdir = ''			# default directory to put project folder into
syncsrc = ''			# default directory to get project folder from
folder = ''			# default folder prefix
extern = []			# default external module definitions
cd = ''				# default directory to cd into if using vscreen
puppet = false			# default use of puppet or not
classes = []			# default list or hash of classes to include
shell = []			# default list of shell scripts to run
docker = false			# default use of docker or not
kubernetes = false		# default use of kubernetes or not
ansible = []			# default ansible group list
playbook = []			# default ansible playbook
ansible_extras = {}		# default ansible extras
cachier = false			# default cachier usage
#vms = []			# default list of vms to build (defaults above)
namespace = 'omv'		# default namespace (also used as network name)
count = 1			# default number of hosts to build
# TODO: subscription manager needs to grow a way to generate an 'API key' that
# can be used instead of a password so that it can be safely used in scripts...
username = ''			# default subscription manager username
password = ''			# default subscription manager password
poolid = true			# default list of poolid's (true to auto-attach)
repos = []			# default list of extra repos's to enable
update = false			# determine whether or not to update the box on vagrant up
reboot = false			# reboot the box after provisioning; requires vagrant-reload plugin
unsafe = false			# true will trade data safety for performance gains
nested = false			# should we allow vm nesting
tests = []			# default list of tests to run
comment = ''			# store a comment field in the omv.yaml file

def array_values_to_array_of_hashes(l)
	result = l
	if l.is_a?(Array) and l.length > 0
		if not l[0].is_a?(Hash)	# check the first value
			result = l.each_with_object([]) { |x, a| a.push({:name => x}) }
		end
	end
	return result
end

# useful for unindenting heredoc's
class String
	def unindent
		gsub(/^#{scan(/^\s*/).min_by{|l|l.length}}/, '')
	end
end
# eg:
#test = <<-EOT.unindent
#	42
#EOT

vagrantfiledir = File.expand_path File.dirname(__FILE__)	# Vagrantfile dir
projectdir = File.expand_path File.dirname(__FILE__)	# vagrant project dir!!

#
#	~/.config/oh-my-vagrant/ parsing
#
# Vagrant is using it's own gem path. Using local xdg gem
# Another option could be adding xdg gem directly to vagrant
begin
	gemlibdir = File.join(vagrantfiledir, 'gems/xdg/lib')
	$:.unshift(gemlibdir)	# add local path for gems
	require 'xdg'
	auth_config = File.join("#{XDG['CONFIG']}/oh-my-vagrant/", 'auth.yaml')

rescue LoadError
	$stderr.puts "Warning: Can't to load 'xdg' gem - check local gem path"
	$stderr.puts gemlibdir
	auth_config = File.join("#{Dir.home}/.oh-my-vagrant/", 'auth.yaml')
	if not File.exist?(auth_config)
		auth_config = File.join("#{Dir.home}/.oh-my-vagrant.yaml")
	end
end
if File.exist?(auth_config)
	settings = YAML::load_file auth_config
	global_username = settings[:username]
	global_password = settings[:password]
end

#
#	mainstream mode
#
if "#{ENV['VAGRANT_CWD']}" != ''	# if called by omv.sh, this is set...
	projectdir = "#{Dir.pwd}"	# and we're calling from the new root
	omvsearch = projectdir
	while omvsearch != '/'		# search upwards for omv.yaml
		if File.exist?(File.join(omvsearch, 'omv.yaml'))
			projectdir = omvsearch	# found one!
			break
		end
		omvsearch = File.expand_path('..', omvsearch)
	end
end
#ENV['VAGRANT_DOTFILE_PATH'] = File.join(projectdir, '.vagrant/')

basename = File.basename(projectdir)	# name of dir that vagrant project is in

#
#	ARGV parsing
#
f = g = File.join(projectdir, 'omv.yaml')
# check for omv.yaml in folder subdirectory
load_folder = false
if File.exist?(f)
	settings = YAML::load_file f
	folder = settings[:folder]

	(0..ARGV.length-1).each do |i|
		if ARGV[i].start_with?(arg='--omv-folder=')
			v = ARGV.delete_at(i).dup
			v.slice! arg
			folder = v.to_s		# set folder prefix
			load_folder = true
			break
		end
	end

	# if folder is '', then this doesn't do any different from the original
	ff = File.join(projectdir, folder, 'omv.yaml')
	if File.exist?(ff)
		f = ff
	end
end

# load settings
if File.exist?(f)
	settings = YAML::load_file f
	domain = settings[:domain]
	network = settings[:network]
	image = settings[:image]
	cpus = settings[:cpus]
	memory = settings[:memory]
	disks = settings[:disks]
	disksize = settings[:disksize]
	boxurlprefix = settings[:boxurlprefix]
	sync = settings[:sync]
	syncdir = settings[:syncdir]
	syncsrc = settings[:syncsrc]
	folder = settings[:folder] if not(load_folder)
	extern = settings[:extern]
	cd = settings[:cd]
	puppet = settings[:puppet]
	classes = settings[:classes]
	shell = settings[:shell]
	docker = settings[:docker]
	kubernetes = settings [:kubernetes]
	ansible = settings[:ansible]
	playbook = settings[:playbook]
	ansible_extras = settings[:ansible_extras]
	cachier = settings[:cachier]
	if settings[:vms].is_a?(Array)
		vms = settings[:vms]
	end
	namespace = settings[:namespace]
	count = settings[:count]
	username = settings[:username]
	password = settings[:password]
	poolid = settings[:poolid]
	repos = settings[:repos]
	update = settings[:update]
	reboot = settings[:reboot]
	unsafe = settings[:unsafe]
	nested = settings[:nested]
	tests = settings[:tests]
	comment = settings[:comment]
	really_use_rm = settings[:reallyrm]
	if [nil, 'nil', 'null', 'none'].include?(really_use_rm.to_s.downcase)
		really_use_rm = nil	# don't rm or bug user at all
	end
end

# ARGV parser
skip = 0
while skip < ARGV.length
	#puts "#{skip}, #{ARGV[skip]}"	# debug
	if ARGV[skip].start_with?(arg='--omv-domain=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg
		#puts "#{arg}, #{v}"	# debug

		domain = v.to_s		# set domain

	elsif ARGV[skip].start_with?(arg='--omv-network=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		network = v.to_s	# set network range

	elsif ARGV[skip].start_with?(arg='--omv-image=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		image = v.to_s	# set base image

	elsif ARGV[skip].start_with?(arg='--omv-cpus=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		cpus = v.to_i		# set cpus count

	elsif ARGV[skip].start_with?(arg='--omv-memory=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		memory = v.to_i		# set memory amount

        elsif ARGV[skip].start_with?(arg='--omv-disks=')
                v = ARGV.delete_at(skip).dup
                v.slice! arg

                disks = v.to_i		# set extra disk amount

        elsif ARGV[skip].start_with?(arg='--omv-disksize=')
                v = ARGV.delete_at(skip).dup
                v.slice! arg

                disksize = v.to_s	# set extra disk size

	elsif ARGV[skip].start_with?(arg='--omv-boxurlprefix=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		boxurlprefix = v.to_s	# set box url prefix

	elsif ARGV[skip].start_with?(arg='--omv-sync=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		sync = v.to_s		# set sync type

	elsif ARGV[skip].start_with?(arg='--omv-syncdir=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		syncdir = v.to_s		# set syncdir arg

	elsif ARGV[skip].start_with?(arg='--omv-syncsrc=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		syncsrc = v.to_s		# set syncsrc arg

	elsif ARGV[skip].start_with?(arg='--omv-folder=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		folder = v.to_s		# set folder prefix

	elsif ARGV[skip].start_with?(arg='--omv-extern=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		if v.is_a?(String) and v.include? ',' and v.split(',').length > 0
			extern = v.split(',')
			# FIXME: further parse the chunks into hashes...
		else
			extern = []
		end

	elsif ARGV[skip].start_with?(arg='--omv-cd=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		cd = v.to_s		# set cd arg

	elsif ARGV[skip].start_with?(arg='--omv-puppet=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		puppet = v.to_s		# set puppet flag
		if ['true', 'yes'].include?(puppet.downcase)
			puppet = true
		else
			puppet = false
		end

	elsif ARGV[skip].start_with?(arg='--omv-puppet-classes=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		if v.is_a?(String) and v.include? ',' and v.split(',').length > 0
			classes = v.split(',')
		else
			classes = []
		end

	elsif ARGV[skip].start_with?(arg='--omv-shell=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		if v.is_a?(String) and v.split(',').length > 0
			shell = v.split(',')
		else
			shell = []
		end

	elsif ARGV[skip].start_with?(arg='--omv-docker=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		if v.is_a?(String) and v.include? ',' and v.split(',').length > 0
			v = v.split(',')
			docker = array_values_to_array_of_hashes(v)
		else
			docker = v.to_s		# set docker flag
			if ['true', 'yes'].include?(docker.downcase)
				docker = true
			else
				docker = false
			end
		end

	elsif ARGV[skip].start_with?(arg='--omv-kubernetes=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		kubernetes = v.to_s		# set kubernetes flag
		if ['true', 'yes'].include?(kubernetes.downcase)
			kubernetes = true
		else
			kubernetes = false
		end

	elsif ARGV[skip].start_with?(arg='--omv-ansible=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		if v.is_a?(String) and v.split(',').length > 0
			ansible = v.split(',')
		else
			ansible = []
		end

	elsif ARGV[skip].start_with?(arg='--omv-playbook=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		if v.is_a?(String) and v.split(',').length > 0
			playbook = v.split(',')
		else
			playbook = []
		end

	elsif ARGV[skip].start_with?(arg='--omv-ansible-extras=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		# TODO: support this command line arg in some way...
		ansible_extras = {}

	elsif ARGV[skip].start_with?(arg='--omv-cachier=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		cachier = v.to_s	# set cachier flag
		if ['true', 'yes'].include?(cachier.downcase)
			cachier = true
		else
			cachier = false
		end

	elsif ARGV[skip].start_with?(arg='--omv-vms=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		if v.is_a?(String) and v.include? ',' and v.split(',').length > 0
			v = v.split(',')
			vms = array_values_to_array_of_hashes(v)
		end

	elsif ARGV[skip].start_with?(arg='--omv-namespace=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		namespace = v.to_s	# set namespace

	elsif ARGV[skip].start_with?(arg='--omv-count=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		count = v.to_i		# set host count

	elsif ARGV[skip].start_with?(arg='--omv-username=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		username = v.to_s	# set username

	elsif ARGV[skip].start_with?(arg='--omv-password=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		password = v.to_s	# set password

	elsif ARGV[skip].start_with?(arg='--omv-poolid=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		if v.is_a?(String) and v.include? ',' and v.split(',').length > 0
			poolid = v.split(',')
		else
			poolid = v.to_s		# set poolid boolean flag
			if ['true', 'auto'].include?(poolid.downcase)
				poolid = true
			else
				poolid = false
			end
		end

	elsif ARGV[skip].start_with?(arg='--omv-repos=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		if v.is_a?(String) and v.include? ',' and v.split(',').length > 0
			repos = v.split(',')
		else
			repos = v.to_s		# set repos boolean flag
			if ['true', 'all'].include?(repos.downcase)
				repos = true
			else
				repos = false
			end
		end

	elsif ARGV[skip].start_with?(arg='--omv-update=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		update = v.to_s
		if ['true'].include?(update.downcase)
			update = true
		else
			update = false
		end

	elsif ARGV[skip].start_with?(arg='--omv-reboot=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		reboot = v.to_s
		if ['true'].include?(reboot.downcase)
			reboot = true
		else
			reboot = false
		end

	elsif ARGV[skip].start_with?(arg='--omv-unsafe=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		unsafe = v.to_s
		if ['true'].include?(unsafe.downcase)
			unsafe = true
		else
			unsafe = false
		end

	elsif ARGV[skip].start_with?(arg='--omv-nested=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		nested = v.to_s
		if ['true'].include?(nested.downcase)
			nested = true
		else
			nested = false
		end

	elsif ARGV[skip].start_with?(arg='--omv-tests=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		if v.is_a?(String) and v.split(',').length > 0
			tests = v.split(',')
		else
			tests = []
		end

	elsif ARGV[skip].start_with?(arg='--omv-comment=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		comment = v.to_s		# set comment

	elsif ARGV[skip].start_with?(arg='--omv-reallyrm=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		really_use_rm = v.to_s
		if ['true'].include?(really_use_rm.downcase)
			really_use_rm = true
		elsif [nil, 'nil', 'null', 'none'].include?(really_use_rm.downcase)
			really_use_rm = nil	# don't rm or bug user at all
		else
			really_use_rm = false
		end

	elsif ARGV[skip].start_with?(arg='--omv-reallyrmonce=')
		v = ARGV.delete_at(skip).dup
		v.slice! arg

		really_use_rm_once = v.to_s
		if ['true'].include?(really_use_rm_once.downcase)
			really_use_rm_once = true
		else
			really_use_rm_once = false
		end

	else	# skip over "official" vagrant args
		skip = skip + 1
	end
end

# save settings (ARGV overrides)
settings = {
	:domain => domain,
	:network => network,
	:image => image,
	:cpus => cpus,
	:memory => memory,
	:disks => disks,
	:disksize => disksize,
	:boxurlprefix => boxurlprefix,
	:sync => sync,
	:syncdir => syncdir,
	:syncsrc => syncsrc,
	:folder => folder,
	:extern => extern,
	:cd => cd,
	:puppet => puppet,
	:classes => classes,
	:shell => shell,
	:docker => docker,
	:kubernetes => kubernetes,
	:ansible => ansible,
	:playbook => playbook,
	:ansible_extras => ansible_extras,
	:cachier => cachier,
	:vms => vms,
	:namespace => namespace,
	:count => count,
	:username => username,
	:password => password,
	:poolid => poolid,
	:repos => repos,
	:update => update,
	:reboot => reboot,
	:unsafe => unsafe,
	:nested => nested,
	:tests => tests,
	:comment => comment,
	:reallyrm => really_use_rm.nil?? 'nil' : really_use_rm,
}
File.open(f, 'w') do |file|
	file.write settings.to_yaml
end
File.open(g, 'w') do |file|
	file.write settings.to_yaml
end

username = global_username if "#{username}" == ''
password = global_password if "#{password}" == ''
username = '' if username.nil?
password = '' if password.nil?
#puts "ARGV: #{ARGV}"	# debug

# networking
network_obj = IPAddr.new network
range = network_obj.to_range.to_a
#cidr = (32-(Math.log(range.length)/Math.log(2))).to_i
offset = 100		# start hosts after here
# remove reserved values
range[0] = '__reserved_network_addr'	# network
range[1] = '__reserved_router_addr'	# router (reserved)
#puts range[2].to_s	# puppetmaster
#puts range[3].to_s	# vip

# prepend $count vms onto the vms list...
extra = []
(1..count).each do |i|
	h = "#{namespace}#{i}"
	# generate names and add in the defaults
	extra.push({:name => h, :docker => docker})
end
vms = extra.concat vms

# add in puppet host if requested
if puppet
	extra = [{:name => 'puppet', :ip => range[2].to_s}]
	range[2] = '__reserved_puppet_addr'
	vms = extra.concat vms

	if not(classes.is_a?(Hash)) and not(classes.is_a?(Array))
		classes = []
	end
end

# transform docker value if necessary
docker = array_values_to_array_of_hashes(docker)

# erase host information from puppet so that the user can do partial rebuilds
snoop = ARGV.select { |x| !x.start_with?('-') }
if snoop.length > 1 and snoop[0] == 'destroy'
	snoop.shift	# left over array snoop should be list of hosts
	if snoop.include?('puppet')	# doesn't matter then...
		snoop = []
	end
else
	# important! clear snoop because we're not using 'destroy'
	snoop = []
end

# figure out which hosts are getting destroyed
destroy = ARGV.select { |x| !x.start_with?('-') }
if destroy.length > 0 and destroy[0] == 'destroy'
	destroy.shift	# left over array destroy should be list of hosts or []
	if destroy.length == 0
		destroy = true	# destroy everything
	end
else
	destroy = false		# destroy nothing
end

# figure out which hosts are getting provisioned
provision = ARGV.select { |x| !x.start_with?('-') }
if provision.length > 0 and ['up', 'provision'].include?(provision[0])
	provision.shift	# left over array provision should be list of hosts or []
	if provision.length == 0
		provision = true	# provision everything
	end
else
	provision = false		# provision nothing
end

# XXX: workaround for: https://github.com/mitchellh/vagrant/issues/2447
# only run on 'vagrant init' or if it's the first time running vagrant
if sync == 'nfs' and ((ARGV.length > 0 and ARGV[0] == 'init') or not(File.exist?(f)))
	`sudo systemctl restart nfs-server`
	`firewall-cmd --permanent --zone public --add-service mountd`
	`firewall-cmd --permanent --zone public --add-service rpc-bind`
	`firewall-cmd --permanent --zone public --add-service nfs`
	`firewall-cmd --reload`
end

create_directories = true
ssh_config = ARGV.select { |x| !x.start_with?('-') }
if ssh_config.length > 0 and ssh_config[0] == 'ssh-config'
	# don't create a new Vagrant environment if the user accidentally
	# runs vscreen in the wrong directory.
	create_directories = false
end

# this extern code should be one of the *last* things before the big configure!
folder = (folder + '/') if not(folder.end_with?('/'))	# ensure trailing slash
folder = '' if folder.start_with?('/')	# only relative paths are allowed...
#puts "folder is: #{folder}"	# debug

puppet_basedir = File.join(projectdir, folder, 'puppet/', 'modules/')
shell_basedir = File.join(projectdir, folder, 'shell/')
# NOTE: i called the ansible child dir 'modules' because i didn't know
# what to call it. Suggestions welcome! It's not really 'playbooks' or
# 'roles' really, so it's 'modules' until something better comes along
ansible_basedir = File.join(projectdir, folder, 'ansible/', 'modules/')
docker_basedir = File.join(projectdir, folder, 'docker/')
kubernetes_basedir = File.join(projectdir, folder, 'kubernetes/', 'applications/')
ktemplates_basedir = File.join(projectdir, folder, 'kubernetes/', 'templates/')
other_basedir = File.join(projectdir, folder)	# this is the root dir...

if create_directories
	# mkdir in case these folders are missing
	mkdirp = 'mkdir -p'
	mkdirp += " #{puppet_basedir}"
	mkdirp += " #{shell_basedir}"
	mkdirp += " #{ansible_basedir}"
	mkdirp += " #{docker_basedir}"
	mkdirp += " #{kubernetes_basedir}"
	mkdirp += " #{ktemplates_basedir}"
	mkdirp += " #{other_basedir}"
	`#{mkdirp}`
end

native = []	# native files
native += `cd "#{puppet_basedir}" && (git status &> /dev/null) && git ls-files`.strip.split("\n")
native += `cd "#{shell_basedir}" && (git status &> /dev/null) && git ls-files`.strip.split("\n")
native += `cd "#{ansible_basedir}" && (git status &> /dev/null) && git ls-files`.strip.split("\n")
native += `cd "#{docker_basedir}" && (git status &> /dev/null) && git ls-files`.strip.split("\n")
native += `cd "#{kubernetes_basedir}" && (git status &> /dev/null) && git ls-files`.strip.split("\n")
native += `cd "#{ktemplates_basedir}" && (git status &> /dev/null) && git ls-files`.strip.split("\n")
native += `cd "#{other_basedir}" && (git status &> /dev/null) && git ls-files`.strip.split("\n")

if extern.length > 0
	extern.each do |i|
		t = i.fetch('type', nil)
		s = i.fetch('system', nil)	# puppet, ansible, and so on...
		if s == 'puppet'
			basedir = puppet_basedir
		elsif s == 'shell'
			basedir = shell_basedir
		elsif s == 'ansible'
			basedir = ansible_basedir
		elsif s == 'docker'
			basedir = docker_basedir
		elsif s == 'kubernetes'
			basedir = kubernetes_basedir
		elsif s == 'ktemplates'
			basedir = ktemplates_basedir
		else
			# NOTE: this one puts code in the root vagrant dir
			basedir = other_basedir
		end
		if t == 'git'
			repository = i.fetch('repository', nil)
			if repository.start_with?('~/')
				repository = File.expand_path(repository)
			end
			directory = i.fetch('directory', nil)
			directory = directory[0, (directory.index('/', 1).nil?? directory.length : directory.index('/', 1))]
			branch = i.fetch('branch', nil)
			if repository.nil? or directory.nil?
				next
			end
			repo_okay = false
			bd = File.join("#{basedir}", "#{directory}")
			if File.exists?(bd)
				r = `cd "#{bd}" && git remote show -n origin | grep 'Fetch URL: ' | awk -F 'Fetch URL: ' '{print $2}'`.strip
				if r == repository
					repo_okay = true
				else
					$stderr.puts "Repository: '#{repository}' doesn't match installed."

					if not(really_use_rm.nil?)	# if it's nil, never bug the user...
						if (really_use_rm_once.nil? and really_use_rm) or (not(really_use_rm_once.nil?) and really_use_rm_once)
							`rm -rf "#{bd}"`
						else
							$stderr.puts `echo please: rm -rf "#{bd}"`	# safer for now
						end
					end
				end
				# pull latest in if it exists
				r = `cd "#{bd}" && [ $(git fetch --dry-run 2>&1 | wc -l) -gt 0 ] || echo already up-to-date`.strip
				if r == ''
					r = `cd "#{bd}" && git checkout master && git pull`.strip
					$stderr.puts r
				end

			else
				r = `cd "#{basedir}" && git clone --recursive #{repository} #{directory}`
				$stderr.puts r
				repo_okay = true
			end
			if repo_okay and not branch.nil?
				r = `cd "#{bd}" && git checkout "#{branch}"`.strip
			end
			native.push("#{directory}")
		else
			$stderr.puts "Can't process extern type: '#{t}'."
		end
	end
end

# clean up any directories or files that shouldn't be present
(
	Dir.glob(puppet_basedir+'*') +
	Dir.glob(shell_basedir+'*') +
	Dir.glob(ansible_basedir+'*') +
	Dir.glob(docker_basedir+'*') +
	Dir.glob(kubernetes_basedir+'*') +
	Dir.glob(ktemplates_basedir+'*') +
[]).uniq.each do |i|
	b = File.basename(i)
	if not native.include?(b)
		if not(really_use_rm.nil?)	# if it's nil, never bug the user...
			if (really_use_rm_once.nil? and really_use_rm) or (not(really_use_rm_once.nil?) and really_use_rm_once)
				`rm -rf "#{i}"`
			else
				$stderr.puts `echo please: rm -rf "#{i}"`	# safer for now
			end
		end
	end
end

# generate the tests to be run with `vtest`
tests_file = File.join(projectdir, '.vagrant', 'tests.sh')
if tests != []
	File.open(tests_file, 'w') {|tests_handle| tests_handle.write("#!/bin/bash -ie\n# generated by omv, do not edit!\n#{tests.join(10.chr)}\n")}
	FileUtils.chmod 'u+x', tests_file
else
	File.exists?(tests_file) and File.delete(tests_file)	# delete
end

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

	# workaround vagrant 1.7.2 insecure key change
	config.ssh.insert_key = false

	#config.landrush.enable	# TODO ?

	#
	#	cache
	#
	# NOTE: you should probably erase the cache between rebuilds if you are
	# installing older package versions. This is because the newer packages
	# will get cached, and then subsequently might get silently installed!!
	if cachier
		# TODO: this doesn't cache metadata, full offline operation not possible
		config.cache.auto_detect = true
		config.cache.enable :yum
		#config.cache.enable :apt
		if not ARGV.include?('--no-parallel')	# when running in parallel,
			config.cache.scope = :machine	# use the per machine cache
		end
		if sync == 'nfs'	# TODO: support other sync types here...
			config.cache.enable_nfs = true	# sets nfs => true on the synced_folder
			# the nolock option is required, otherwise the NFSv3 client will try to
			# access the NLM sideband protocol to lock files needed for /var/cache/
			# all of this can be avoided by using NFSv4 everywhere. die NFSv3, die!
			config.cache.mount_options = ['rw', 'vers=3', 'tcp', 'nolock']
		end
	end

	#
	#	vip
	#
	vip_ip = range[3].to_s
	vip_hostname = namespace
	range[3] = '__reserved_vip_addr'

	#
	#	hostmanager
	#
	# TODO: does this plugin still mess up the real vagrant-libvirt hostname ?
	config.hostmanager.enabled = true
	config.hostmanager.manage_host = false	# don't manage local /etc/hosts
	config.hostmanager.ignore_private_ip = false
	config.hostmanager.include_offline = true	# manage all the hosts!
	config.hostmanager.fqdn_friendly = true		# fqdns need to work...
	config.hostmanager.domain_name = domain		# use this domain name!
	config.hostmanager.extra_hosts = [
		{
			:host => "#{vip_hostname}.#{domain}",
			:ip => "#{vip_ip}",
			:aliases => ["#{vip_hostname}"],
		}
	]

	#
	#	set vms ips and do other pre-processing
	#
	ansible_groups = {}
	kubernetes_master = false	# TODO: in future allow an array of masters ?
	kubernetes_hosts = []	# list of every participating kubernetes host
	kubernetes_templates = ''
	kubernetes_json = []
	vms.each_with_index do |x, i|
		h = x[:name]
		if not x.fetch(:ip, nil) # if vm ip was not set in omv.yaml...
			x[:ip] = range[offset+i].to_s # ...generate it, and set vm ip
			#vms[i][:ip] = range[offset+i].to_s	# equivalent!
		end

		# kubernetes
		vm_kubernetes = x.fetch(:kubernetes, kubernetes)	# get value
		if kubernetes.is_a?(Hash) and vm_kubernetes.is_a?(Hash)
			vm_kubernetes = kubernetes.merge(vm_kubernetes)	# inherit from global
		end
		if vm_kubernetes
			kubernetes_hosts.push(h)
		end

		# build ansible_groups variable now, because needed in big loop
		vm_ansible = x.fetch(:ansible, ansible)	# get value
		ansible_groups[h] = vm_ansible if vm_ansible != []
	end
	if kubernetes_hosts.length > 0
		kubernetes_master = kubernetes_hosts[0]	# default to first host
		if kubernetes.is_a?(Hash)
			# a master key can set which kubernetes host is the master one!
			tmp = kubernetes.fetch('master', nil)
			if kubernetes_hosts.include?(tmp)
				kubernetes_master = tmp
			end

			# kubernetes templates base path inside repo, eg: v0.9.0
			tmp = kubernetes.fetch('ktemplates', 'default/latest/')	# pick!
			if tmp.is_a?(String)
				tmp = File.join(ktemplates_basedir, tmp)
				if File.exists?(tmp)
					kubernetes_templates = tmp
				end
			end

			# json definition file
			kubernetes_json = kubernetes.fetch('applications', [])	# pick!
			if not kubernetes_json.is_a?(Array)
				kubernetes_json = []
			end
		end
	end
	#$stderr.puts "Kubernetes master: #{kubernetes_master}"
	#$stderr.puts "Kubernetes others: #{kubernetes_hosts.join(',')}"

	#
	#	vms mainloop to define all the machines
	#
	vms.each do |x|
		h = x[:name]
		ip = x.fetch(:ip) # get value
		vm_image = x.fetch(:image, image)	# get value
		vm_cpus = x.fetch(:cpus, cpus)		# get value
		vm_memory = x.fetch(:memory, memory)	# get value
		vm_disks = x.fetch(:disks, disks)	# get value
		vm_disksize = x.fetch(:disksize, disksize)	# get value
		vm_sync = x.fetch(:sync, sync)	# get value
		vm_syncdir = x.fetch(:syncdir, syncdir)	# get value
		vm_syncdir_folder = ''
		if vm_syncdir == '/'	# automatic folder name
			vm_syncdir_folder = basename
		elsif vm_syncdir != ''	# pick whatever they asked for
			vm_syncdir_folder = vm_syncdir
		end
		vm_syncsrc = x.fetch(:syncsrc, syncsrc)	# get value
		vm_cd = x.fetch(:cd, cd)	# get value
		vm_docker = x.fetch(:docker, docker)	# get value
		vm_docker = array_values_to_array_of_hashes(vm_docker)
		vm_puppet = x.fetch(:puppet, puppet)	# get value
		vm_classes = x.fetch(:classes, classes)
		vm_shell = x.fetch(:shell, shell)	# get value
		vm_poolid = x.fetch(:poolid, poolid)	# get value
		vm_repos = x.fetch(:repos, repos)	# get value
		vm_playbook = x.fetch(:playbook, playbook)	# get value
		vm_ansible_extras = x.fetch(:ansible_extras, ansible_extras)	# get value
		vm_update = x.fetch(:update, update) # get value
		vm_reboot = x.fetch(:reboot, reboot) # get value
		vm_unsafe = x.fetch(:unsafe, unsafe) # get value
		vm_nested = x.fetch(:nested, nested) # get value

		# sanity check the user-supplied IP addresses and gracefully fail
		# puppet server is special, so we skip it here
		if h != 'puppet'
			error_check = range.include? IPAddr.new ip
			message = "#{namespace}:#{h}: ERROR: #{ip} is not in the valid range of IPs: #{range[3].to_s}..#{range[-2].to_s}"
			message += "\nConsider changing 'network:' parameter in omv.yaml, or use the correct IP address from the range above!"
			raise Vagrant::Errors::VagrantError.new, message unless error_check
		end

		config.vm.define h.to_sym do |vm|
			vm.vm.hostname = h
			#vm.hostmanager.aliases = ["#{h}.#{domain}"]

			# this is the real network that we'll use...
			vm.vm.network :private_network,
			:ip => ip,
			:libvirt__dhcp_enabled => false,
			:libvirt__network_name => "#{namespace}",
			:virtualbox__intnet => "#{namespace}"

			#
			#	box (pre-built base image)
			#
			vm.vm.box = vm_image	# set vm specific image

			synced_folder = '/vagrant'	# default sync folder
			dot = "#{File.join(projectdir, folder)}"
			if vm_syncsrc.start_with?('/')
				dot = vm_syncsrc
			else
				dot = File.expand_path(File.join(dot, vm_syncsrc))
			end

			# use a specialized vagrant box if it exists :)
			if vm_docker
				if `vagrant box list | awk '{print $1}' | grep -q '^#{vm_image}-docker$' && echo -n found` != ''
					vm.vm.box = "#{vm_image}-docker"
				end
			end

			# family parsing
			is_yum = (vm.vm.box.start_with? 'fedora-' or vm.vm.box.start_with? 'centos-' or vm.vm.box.start_with? 'rhel-')
			is_apt = (vm.vm.box.start_with? 'debian-' or vm.vm.box.start_with? 'ubuntu-')
			is_atomic = vm.vm.box.start_with? 'atomic-'
			is_rhel = (vm.vm.box.start_with? 'rhel-' or vm.vm.box.start_with? 'atomic-rhel-')
			is_family_redhat = (is_yum or is_atomic)
			is_family_debian = is_apt

			# sync type
			# this section is a bit tricky, because if you want a
			# different sync other than /vagrant (which you get by
			# default), then you have to disable it, and then add
			# what you want... cleanups that still work are welcome
			disabled = false
			if is_atomic or vm_sync == ''
				disabled = true
				vm.vm.synced_folder '.', '/vagrant', disabled: true
			else
				synced_folder = vm_syncdir_folder.start_with?('/') ? vm_syncdir_folder : File.join(synced_folder, vm_syncdir_folder)
				vm.vm.synced_folder "#{dot}", synced_folder, create: true, type: vm_sync	# nfs, rsync
			end

			if vm_syncdir_folder != '' and not(disabled)
				vm.vm.synced_folder '.', '/vagrant', disabled: true
			end

			if is_atomic
				synced_folder = '/home/vagrant/sync'
				synced_folder = vm_syncdir_folder.start_with?('/') ? vm_syncdir_folder : File.join(synced_folder, vm_syncdir_folder)
				if vm_sync != ''
					vm.vm.synced_folder "#{dot}", synced_folder, create: true, type: vm_sync
				end
			end

			# box source url's
			# FIXME: boxes should be GPG signed and verified on dl!
			if "#{boxurlprefix}" != ''
				vm.vm.box_url = "#{boxurlprefix}#{vm.vm.box}/#{vm.vm.box}.box"
			end

			# FIXME: speed up or cache (memoize) this check...
			if `vagrant box list | awk '{print $1}' | grep -q '^#{vm.vm.box}$' && echo -n found` != ''
				exists = true
			else
				if vm.vm.box_url.is_a?(String)
					puts "Running wget check on: '#{vm.vm.box_url}'"
					exists = `wget -q --spider #{vm.vm.box_url}`
				else
					exists = false
				end
			end
			if not(exists) and not is_rhel
				vm.vm.box_url = "#{default_boxurlprefix}/#{vm.vm.box}/#{vm.vm.box}.box"
			end

			#vm.landrush.host h, ip	# TODO ?

			fv = File.join(projectdir, '.vagrant', "#{h}-hosts.done")
			if destroy.is_a?(TrueClass) or (destroy.is_a?(Array) and destroy.include?(h))
				if File.exists?(fv)	# safety
					puts "Unlocking shell provisioning for: #{h}..."
					File.delete(fv)	# delete hosts token
				end
			end

			# store the path to the cd directory for vscreen to see
			if vm_cd == '$SYNCDIR' or vm_cd == '-'
				vm_cd = synced_folder
			end
			cd_file = File.join(projectdir, '.vagrant', "#{h}.cd")
			File.open(cd_file, 'w') {|cd_handle| cd_handle.write("#{vm_cd}\n")}

			# should we clean this puppet client machine?
			if puppet and vm_puppet and h != 'puppet' and snoop.include?(h)
				cmd = "puppet cert clean #{h}.#{domain}"
				puts "Running 'puppet cert clean' for: #{h}..."
				`vagrant ssh puppet -c 'sudo #{cmd}'`
				cmd = "puppet node deactivate #{h}.#{domain}"
				puts "Running 'puppet node deactivate' for: #{h}..."
				`vagrant ssh puppet -c 'sudo #{cmd}'`
			end

			# unsubscribe rhel machines on destroy
			if destroy.is_a?(TrueClass) or (destroy.is_a?(Array) and destroy.include?(h))
				if is_rhel
					# check these to know if we registered!
					if username != '' and password != ''
						# ssh into itself and unregister
						cmd = 'subscription-manager unregister'
						puts "Running '#{cmd}' on: #{h}..."
						puts `vagrant ssh #{h} -c 'sudo #{cmd}'`
					end
				end
			end

			#
			#	shell
			#
			first_run = not(File.exists?(fv))
			if first_run	# only run these actions once!
				if provision.is_a?(TrueClass) or (provision.is_a?(Array) and provision.include?(h))
					File.open(fv, 'w') {}	# touch
				end

				# add missing root password where necessary
				if is_family_debian
					vm.vm.provision 'shell', inline: 'usermod -p $(echo vagrant | openssl passwd -1 -stdin) root'
				elsif is_family_redhat
					vm.vm.provision 'shell', inline: 'echo vagrant | passwd --stdin root'
				end

				# fix atomic hosts with missing authorized_keys
				if is_atomic
					vm.vm.provision 'shell', inline: 'test -e /root/.ssh/ || cp -r /home/vagrant/.ssh/ /root/'
				end

				# subscribe rhel machines
				if is_rhel
					if username != '' and password != ''
						vm.vm.provision 'shell', inline: "subscription-manager register --username=#{username} --password=#{password}"

						# attach correct pools
						if vm_poolid.is_a?(Array)
							vm_poolid.each do |j|
								vm.vm.provision 'shell', inline: "subscription-manager attach --pool=#{j}"
							end
						elsif vm_poolid
							# auto attach if value is true
							vm.vm.provision 'shell', inline: 'subscription-manager attach --auto'
						end
					end

					# enable particular repositories
					if vm_repos.is_a?(Array)
						vm_repos.each do |j|
							# if you start the repo name with a #
							# then it will be disabled... this is
							# useful to kill repos from a poolid.
							if j.start_with? '#'
								vm.vm.provision 'shell', inline: "subscription-manager repos --disable=#{j[1,j.length]}"
							else
								vm.vm.provision 'shell', inline: "subscription-manager repos --enable=#{j}"
							end
						end

					elsif vm_repos
						# this enables all repositories
						# NOTE: probably a strong edge
						vm.vm.provision 'shell', inline: "for i in `subscription-manager repos --list | grep '^Repo ID:' | awk '{print $3}'`; do subscription-manager repos --enable=$i; done"
					end

				end

				# update the hosts
				if vm_update
					if is_atomic
						vm.vm.provision 'shell', inline: 'atomic host upgrade'
					elsif is_yum
						vm.vm.provision 'shell', inline: 'yum -y update'
					elsif is_apt
						vm.vm.provision 'shell', inline: 'apt-get update && apt-get -y upgrade'
					end
				end

				# atomic hosts lack the docker group which the
				# docker provisioner expects... workaround this
				if vm_docker
					vm.vm.provision 'shell', inline: 'getent group docker || groupadd --system docker'
				end

				# grow if root xfs file system abnormally small
				# NOTE: empirically, the operating systems that
				# use xfs as / also support the findmnt -b flag
				vm.vm.provision 'shell', inline: '! findmnt -t xfs --target "/" || (( !(findmnt -b 2> /dev/null) || test `findmnt --output "SIZE" -t xfs --target "/" -nb` -lt `expr 1024 \* 1024 \* 1024 \* 20`) && xfs_growfs /)'
			end

			# custom shell provisioner
			# TODO: support running first/last as a setting
			if vm_shell.is_a?(Array)
				vm_shell.each do |sf|

					shell_once = false
					if sf.is_a?(Hash)
						shell_once = sf.fetch('once', shell_once) # get value
					elsif sf.is_a?(String)	# auto-detect
						sx = File.join(shell_basedir, sf)
						if File.exists?(sx) and not(File.directory?(sx)) and File.executable?(sx)
							sf = {'path'=> sf}
						else
							sf = {'script'=> sf}
						end
					else
						next
					end

					# do the running...
					si = sf.fetch('script', nil)
					sp = sf.fetch('path', nil)
					if not(shell_once) or (shell_once and first_run)
						if sp.nil?
							vm.vm.provision 'shell', inline: "#{si}"
						elsif si.nil?
							sx = File.join(shell_basedir, sp)
							vm.vm.provision 'shell', path: "#{sx}"
						else
							next	# optional
						end
					end
				end
			end

			# TODO: remove this chunk?
			# ensure the $namespace module is present for provisioning...
			#if (puppet and vm_puppet and h == 'puppet') and (provision.is_a?(TrueClass) or (provision.is_a?(Array) and provision.include?('puppet')))
			#	cwd = `pwd`
			#	mod = File.join(projectdir, 'puppet', 'modules')
			#	#`cd #{mod} && make MODULENAME=#{namespace} #{namespace} &> /dev/null; cd #{cwd}`
			#end

			#
			#	docker (in vm)
			#
			# TODO: do we need to start the docker daemon first ?
			# NOTE: this docker load from file isn't needed as much
			# because vagrant-builder can now docker pull directly:
			# done in git: b6bd325f3a23b25441050afdd8433457278819fd
			if vm_docker.is_a?(Array)
				vm.vm.provision 'shell', inline: 'systemctl start docker'
				vm_docker.each do |j|
					# if there is a docker image already present, use that
					# otherwise pull one down...
					vm.vm.provision 'shell', inline: "if [ -e '/root/docker/#{j[:name]}.docker' ]; then docker load --input='/root/docker/#{j[:name]}.docker'; else docker pull '#{j[:name]}'; fi"
				end

			elsif vm_docker
				vm.vm.provision 'shell', inline: 'systemctl start docker'
				# pull in all the docker images that the vm has
				vm.vm.provision 'shell', inline: 'for i in /root/docker/*.docker; do [ -e "$i" ] || continue; docker load --input="$i"; done'
			end

			# TODO: should we run the docker pull with vagrant too?
			if vm_docker.is_a?(Array)
				vm.vm.provision :docker, images: vm_docker.map { |y| y.fetch(:name, nil)}.compact
			end

			# docker build
			if vm_docker	# for any value that is not false
				dlist = Dir.glob(docker_basedir+'*')
				if vm_docker.is_a?(Hash)
					dfiles = vm_docker.fetch('files', nil)
					if dfiles.is_a?(Array)
						dlist = vm_docker['files'].map{|dfile| File.join(docker_basedir, dfile)}
					end
				end
				dlist.each do |dd|
					if not File.directory?(dd)
						next	# invalid, skip
					end
					# full Dockerfile path
					df = File.join(dd, 'Dockerfile')
					if not(File.exists?(df)) or File.directory?(df)
						next	# invalid, skip
					end
					# app directory name containing Dockerfile
					b = File.basename(dd)
					vm.vm.provision :docker do |d|
						d.build_image "#{synced_folder}/docker/#{b}", args: "-t #{b}"
						#d.run "#{b}"	# use kube instead
					end
				end
			end

			#
			#	kubernetes
			#
			# NOTE: one could rewrite this kubernetes portion as a
			# vagrant plugin, but until this idea is more baked, i
			# think this is a perfectly fine place to hack this in

			# if we have a master host and if i am an allowed host
			if kubernetes_master and kubernetes_hosts.include?(h)

				# TODO: add support for non yum-based distros
				vm.vm.provision 'shell', inline: 'which kubectl || yum install -y kubernetes'

				# templates are in the kubernetes_templates dir
				ktemplates_host = ((kubernetes_master == h) ? 'master' : 'minion')
				ktemplates = {}
				Dir.glob(File.join(kubernetes_templates, "#{ktemplates_host}/")+'*').each do |ktemplate|
					kbdir = File.basename(ktemplate)
					ktemplates[kbdir] = open(ktemplate, 'r') {|ktf| ktf.read}
				end

				kubernetes_ostruct = OpenStruct.new({
					:master => kubernetes_master,
					:hosts => kubernetes_hosts,
					:diff => (kubernetes_hosts - [kubernetes_master]),
					:me => h,
				})

				ktemplates.each do |kpath, template|
					if kpath.end_with?('.erb')
						kpath = File.join('/etc/kubernetes/', kpath[0, kpath.length-'.erb'.length])
						value = ERB.new(template).result(kubernetes_ostruct.instance_eval {binding})
						b64 = Base64.encode64(value)
					else
						# copy data, don't template it!
						kpath = File.join('/etc/kubernetes/', kpath)
						value = template	# str
						b64 = Base64.encode64(value)
					end
					# FIXME: add a diff check so this doesn't clobber unnecessarily
					vm.vm.provision 'shell' do |s|
						s.inline = "echo '#{b64}' | base64 -d > '#{kpath}'"
					end
				end

				# systemctl changes for kubernetes
				if kubernetes_master == h	# master
					services = ['etcd', 'kube-apiserver', 'kube-controller-manager', 'kube-scheduler']
				else
					# a dirty auth hack that scollier wants
					vm.vm.provision 'shell', inline: 'echo "{}" > /var/lib/kubelet/auth'
					services = ['kube-proxy', 'kubelet']
				end
				services.each do |service|
					# a restart causes a start if stopped :)
					vm.vm.provision 'shell', inline: "systemctl restart #{service}"
					vm.vm.provision 'shell', inline: "systemctl enable #{service}"
				end

				# scollier and purpleidea decided against
				# templating the kubernetes json files...
				# run from an application/ dir instead :)
				if kubernetes_master == h	# master
					kubernetes_json.each do |jsonfile|
						roll = false
						if jsonfile.is_a?(Hash)
							roll = jsonfile.fetch('roll', false)
							roll = (['true', 'yes'].include?(roll.downcase) ? true : false)
							jsonfile = jsonfile.fetch('file', '')

							if jsonfile == ''
								$stderr.puts "Warning: Kubernetes application list element is an invalid hash!"
								next
							end
						end

						if not File.exists?(File.join(kubernetes_basedir, jsonfile))
							$stderr.puts "Warning: Kubernetes .json file: '#{jsonfile}' doesn't exist!"
							next
						end

						# get path to json file inside vm...
						jsonfile = File.join("#{synced_folder}/kubernetes/applications/", jsonfile)
						$script = <<-SCRIPT.unindent
							# the sed strips off the quotes from the python string output
							# unescaped sed is: sed 's/\(^"\|"$\)//g'
							kind="`cat '#{jsonfile}' | python -c 'import json,sys;sys.stdout.write(json.dumps(json.load(sys.stdin)["kind"]))' | sed 's/\\(^"\\|"$\\)//g'`"
							kid="`cat '#{jsonfile}' | python -c 'import json,sys;sys.stdout.write(json.dumps(json.load(sys.stdin)["id"]))' | sed 's/\\(^"\\|"$\\)//g'`"
							roll='false'
							case "$kind" in
							'ReplicationController')
								# remap to command arg name
								kind='replicationControllers'
								roll='#{roll}'
							;;
							'Service')
							;;
							'Pod')
							;;
							*)
							echo "Kubernetes kind: '$kind' not yet supported in Oh-My-Vagrant."
							;;
							esac
							echo Running kubernetes...
							if [ "`kubectl get $kind | tail -n +2 | awk '{print $1}'`" = "$kid" ]; then
								# exists...
								# TODO: can we roll like this?
								if [ "$roll" = 'true' ]; then
									echo Running kubernetes: rollingupdate...
									kubectl rollingupdate "$kid" -f '#{jsonfile}'
								else
									# update from existing json file...
									echo Running kubernetes: update...
									kubectl update -f '#{jsonfile}'
								fi
							else
								# new
								echo Running kubernetes: create...
								kubectl create -f '#{jsonfile}'
							fi
						SCRIPT
						vm.vm.provision 'shell', inline: $script
					end
				end
			end

			#
			#	puppet agent - run on puppet client to set it up
			#
			if puppet and vm_puppet and h != 'puppet'
				vm.vm.provision :puppet_server do |p|
					#p.puppet_node = "#{h}"	# redundant
					#p.puppet_server = "puppet.#{domain}"
					p.puppet_server = 'puppet'
					#p.options = '--verbose --debug'
					p.options = '--test'	# see the output

					# extract a list of puppet classes...
					dynamic = {}
					if vm_classes.is_a?(Array)
						puppet_classes = vm_classes

					elsif vm_classes.is_a?(Hash)
						# here we act as a cheap enc...
						puppet_classes = vm_classes.keys
						puppet_classes.each do |name|
							params = vm_classes.fetch(name, {})
							if params.is_a?(Hash)
								keys = []
								params.keys.sort.each do |key|
									dynamic["vagrant_puppet_classes_values_#{name}_#{key}"] = params[key]
									keys.push(key)
								end
								if keys.length > 0
									dynamic["vagrant_puppet_classes_params_#{name}"] = keys.join(',')
								end
							end
						end
					else
						puppet_classes = []
					end

					facter = {
						'vagrant' => '1',
						'vagrant_vip' => vip_ip,
						'vagrant_vip_fqdn' => "#{vip_hostname}.#{domain}",
						'vagrant_puppet_classes' => puppet_classes.join(','),
					}
					facter = facter.merge(dynamic)
					p.facter = facter
				end

			#
			#	puppet apply - run on puppet master to set it up
			#
			elsif puppet and vm_puppet and h == 'puppet'
				vm.vm.provision :puppet do |p|
					p.module_path = File.join(projectdir, folder+'puppet/modules')
					p.manifests_path = File.join(projectdir, folder+'puppet/manifests')
					p.manifest_file = 'site.pp'
					p.hiera_config_path = File.join(projectdir, folder+'puppet/hiera.yaml')
					# custom fact
					p.facter = {
						'vagrant' => '1',
						'vagrant_allow' => vms.each{|z| z[:ip] },
						'vagrant_puppet_folder' => folder,
					}
					p.synced_folder_type = vm_sync
				end

				# the puppetlabs RPM mistakenly starts the
				# service on installation/upgrade. this is a
				# bug, and it causes our server to break,
				# because we want puppet to run with our
				# environment variables, which it doesn't when
				# started by itself! puppetlabs, fix your RPM
				# already!
				# https://tickets.puppetlabs.com/browse/CPR-128
				vm.vm.provision 'shell', inline: 'service puppet stop'
			end

			#
			#	ansible
			#
			if ansible_groups != {}
				vm_playbook.each do |pb|
					vm.vm.provision 'ansible' do |a|
						# NOTE: this seems to need an absolute path spec
						a.playbook = File.join(projectdir, folder+'ansible/modules/'+pb)
						# reverse the mapping, so that the groups enumerate their hosts
						a.groups = Hash[ansible_groups.values.flatten.uniq.map { |ai| [ai, ansible_groups.select{|_,av| not(av.nil?) and av.include?(ai) }.keys ]}]
						a.host_key_checking = false
						a.limit = 'all'
						ansible_sudo = vm_ansible_extras.fetch(:sudo, nil)
						if ansible_sudo.is_a?(TrueClass) or ansible_sudo.is_a?(FalseClass)
							a.sudo = ansible_sudo
						end
						ansible_extra_vars = vm_ansible_extras.fetch(:extra_vars, {})
						ansible_extra_vars_hash = {}
						if ansible_extra_vars.is_a?(Hash)
							ansible_extra_vars.keys.sort.each do |akey|
								ansible_extra_vars_hash[akey] = ansible_extra_vars[akey]
							end
						end
						# add a default
						if not ansible_extra_vars_hash.has_key?('ansible_ssh_user')
							ansible_extra_vars_hash['ansible_ssh_user'] = 'root'
						end
						a.extra_vars = ansible_extra_vars_hash
					end
				end
			end

			vm.vm.provider :libvirt do |libvirt|
				(0..vm_disks.to_i-1).each do	# if disks is 0, this passes :)
					libvirt.storage :file,
						#:path => '',		# auto!
						#:device => 'vdb',	# auto!
						:size => vm_disksize,
						:type => 'qcow2'
				end

				if vm_cpus.to_i != 0	# handles '', nil, & 0
					libvirt.cpus = vm_cpus.to_i

				# make puppet server a bit fatter by default :P
				elsif (puppet and vm_puppet and h == 'puppet') or vm_nested
					libvirt.cpus = 2
				end

				if vm_memory.to_i != 0
					libvirt.memory = vm_memory.to_i

				# make puppet server a bit fatter by default :P
				elsif (puppet and vm_puppet and h == 'puppet') or vm_nested
					libvirt.memory = 1024
				end

				# use kvm's unsafe mode
				if vm_unsafe
					# Configure the Vagrant environment to use kvm's unsafe cache mode.
					# If you do this, you will trade data integrity on your development
					# environment's filesystem for a noticeable speed boost.
					# See http://libvirt.org/formatdomain.html#elementsDisks
					libvirt.volume_cache = 'unsafe'
				end

				# allow nesting vm's
				if vm_nested
					libvirt.nested = true
					libvirt.cpu_mode = 'host-model'
				end
			end
			vm.vm.provider :virtualbox do |vb|

				if vm_cpus.to_i != 0	# handles '', nil, & 0
					vb.customize ["modifyvm", :id, "--cpus", "#{vm_cpus}"]

				# make puppet server a bit fatter by default :P
				elsif (puppet and vm_puppet and h == 'puppet') or vm_nested
					vb.customize ["modifyvm", :id, "--cpus", "2"]
				end

				if vm_memory.to_i != 0
					vb.customize ["modifyvm", :id, "--memory", "#{vm_memory}"]

				# make puppet server a bit fatter by default :P
				elsif (puppet and vm_puppet and h == 'puppet') or vm_nested
					vb.customize ["modifyvm", :id, "--memory", "1024"]
				end
			end

			# reboot the hosts
			if vm_reboot and first_run
				vm.vm.provision :reload
			end
		end
	end

	#
	#	libvirt
	#
	config.vm.provider :libvirt do |libvirt|
		libvirt.driver = 'kvm'	# needed for kvm performance benefits !
		libvirt.graphics_type = 'spice'
		libvirt.video_type = 'qxl'
		# leave out to connect directly with qemu:///system
		#libvirt.host = 'localhost'
		libvirt.connect_via_ssh = false
		libvirt.username = 'root'
		libvirt.storage_pool_name = 'default'
		#libvirt.default_network = 'default'	# XXX: this does nothing
		libvirt.default_prefix = "#{namespace}"	# set a prefix for your vm's...
	end

	#
	#	virtualbox
	#
	config.vm.provider :virtualbox do |vb|
		# ref params: http://www.virtualbox.org/manual/ch08.html#vboxmanage-modifyvm
		vb.customize ["modifyvm", :id, "--hwvirtex", "on"]	# needed for kvm performance benefits !
		# TODO: Find corresponding params in Virtual Box
		# libvirt.connect_via_ssh = false
		# libvirt.username = 'root'
		# libvirt.storage_pool_name = 'default'
		# #libvirt.default_network = 'default'	# XXX: this does nothing
		# libvirt.default_prefix = "#{namespace}"	# set a prefix for your vm's...
	end

end
