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


require "yaml"
require "fileutils"
load "dvmtools.rb"
CS_VM_ADDRESS="192.168.33.100"
DB_VM_ADDRESS="192.168.33.150"
BE_VM_ADDRESS="192.168.33.151"
LDAP_VM_ADDRESS="192.168.33.152"
REPORTING_DB_VM_ADDRESS="192.168.33.155"
DB_SUPERUSER="bofh"
DB_SUPERPASS="i1uvd3v0ps"
LDAP_PASSWORD='H0\/\/!|\/|3tY0ur|\/|0th3r'

nodes_dir = File.join(File.expand_path(File.dirname(__FILE__)), 'nodes')
unless File.directory?(nodes_dir)
  puts "nodes directory is missing...creating it now"
  puts
  FileUtils.mkdir(nodes_dir)
end

Vagrant.configure("2") do |config|
  attributes = load_settings

  # Use the official Ubuntu 14.04 box
  # Vagrant will auto resolve the url to download from Atlas
  config.vm.box = "ubuntu/trusty64"
  config.ssh.forward_agent = true

  # This plugin allows for a much more efficient sync than the vanilla rsync-auto command
  # see https://github.com/smerrill/vagrant-gatling-rsync
  if Vagrant.has_plugin?('vagrant-gatling-rsync')
    config.gatling.rsync_on_startup = false
  end

  if attributes['vm'].has_key? 'postgresql'
    if attributes['vm']['postgresql']['start']
      config.vm.define("database") do |c|
        define_db_server(c, attributes)
      end
    end
  else
    attributes['vm']['postgresql'] = nil
  end

  if attributes['vm'].has_key? 'reporting_postgresql'
    if attributes['vm']['reporting_postgresql']['start']
      config.vm.define("reportingdb") do |c|
        define_db_server_reporting(c, attributes)
      end
    end
  else
    attributes['vm']['reporting_postgresql'] = nil
  end

  if attributes['vm'].has_key? 'ldap'
    if attributes['vm']['ldap']['start']
      config.vm.define('ldap') do |c|
        define_ldap_server(c, attributes)
      end
    end
  end

  if attributes['vm'].has_key? 'chef-backend'
    if attributes['vm']['chef-backend']['start']
      config.vm.define("backend") do |c|
        define_backend_server(c, attributes)
      end
    end
  else
    attributes['vm']['chef-backend'] = nil
  end

  config.vm.define("chef-server", primary: true) do |c|
    define_chef_server(c, attributes)
  end
end


def define_chef_server(config, attributes)
  provisioning, installer, installer_path = prepare('INSTALLER', 'chef-server-core', 'Chef Server 12+')
  m_provisioning, m_installer, m_installer_path = prepare('MANAGE_PKG', 'chef-manage', 'Chef Manage 1.4+') if plugin_active?('chef-manage', attributes)
  ps_provisioning, ps_installer, ps_installer_path = prepare('PUSH_JOBS_PKG', 'opscode-push-jobs-server', 'Push Jobs Server 1.1+') if plugin_active?('push-jobs-server', attributes)
  r_provisioning, r_installer, r_installer_path = prepare('REPORTING_PKG', 'opscode-reporting', 'Chef Reporting 1.6+') if plugin_active?('reporting', attributes)

  config.vm.hostname = "api.chef-server.dev"
  config.vm.network "private_network", ip: CS_VM_ADDRESS

  config.vm.provider :virtualbox do |vb|
    vb.customize ["modifyvm", :id,
                  "--name", "chef-server",
                  "--memory", attributes["vm"]["memory"],
                  "--cpus", attributes["vm"]["cpus"],
                  "--usb", "off",
                  "--usbehci", "off"
    ]
  end
  if provisioning
    json = {
      "install_packages" => attributes["vm"]["packages"],
      "tz" => host_timezone,
      "omnibus-autoload" => attributes["vm"]["omnibus-autoload"]
    }.merge attributes["vm"]["node-attributes"]

    if attributes["vm"]["postgresql"]["start"] and attributes["vm"]["postgresql"]["use-external"]
      # TODO make this stuff common - we have these values in 2-3 places now...
      pg = { "postgresql['external']" => true,
             "postgresql['vip']" => "\"#{DB_VM_ADDRESS}\"",
             "postgresql['port']" => 5432,
             "postgresql['db_superuser']" => "\"#{DB_SUPERUSER}\"",
             "postgresql['db_superuser_password']" => "\"#{DB_SUPERPASS}\"",
             "opscode_erchef['db_pool_size']" => 10,
             "oc_id['db_pool_size']" => 10,
             "oc_bifrost['db_pool_size']" => 10 }
      json = simple_deep_merge(json, { "provisioning" => { "chef-server-config" => pg } })
    end

    if attributes["vm"]["reporting_postgresql"]["start"] and attributes["vm"]["reporting_postgresql"]["use-external"]
      pg = { "postgresql['external']" => true,
             "postgresql['vip']" => "\"#{REPORTING_DB_VM_ADDRESS}\"",
             "postgresql['port']" => 5432,
             "postgresql['db_superuser']" => "\"#{DB_SUPERUSER}\"",
             "postgresql['db_superuser_password']" => "\"#{DB_SUPERPASS}\""
           }
      json = simple_deep_merge(json, { "provisioning" => { "opscode-reporting-config" => pg } })
    end

    if attributes['vm']['ldap']['start']
      backend_compat_message('ldap') if chef_backend_active?(attributes)
      ldap = { "ldap['base_dn']" => '"ou=chefs,dc=chef-server,dc=dev"',
               "ldap['bind_dn']" => '"cn=admin,dc=chef-server,dc=dev"',
               "ldap['bind_password']" => "'#{LDAP_PASSWORD}'",
               "ldap['host']" => "'#{LDAP_VM_ADDRESS}'",
               "ldap['login_attribute']" => '"uid"'
             }
      json = simple_deep_merge(json, { "provisioning" => { "chef-server-config" => ldap } })
    end

    dotfiles_path = attributes["vm"]["dotfile_path"] || "dotfiles"
    config.vm.synced_folder File.absolute_path(File.join(Dir.pwd, "../")), "/host",
      type: "rsync",
      rsync__args: ["--verbose", "--archive", "--delete", "-z", "--no-owner", "--no-group" ],
      rsync__exclude: attributes["vm"]['sync']['exclude-files']
    # We're also going to do a share of the slower vboxsf style, allowing us to auto-checkout dependencies
    # and have them be properly synced to a place that we can use them.
    config.vm.synced_folder installer_path, "/installers"
    config.vm.synced_folder File.expand_path(dotfiles_path), "/dotfiles"

    config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig"
    config.vm.provision "shell", inline: install_hack(installer, 'opscode')
    config.vm.provision "shell", inline: 'echo "PATH=/opt/opscode/embedded/bin:$PATH" > /root/.bashrc'
    config.vm.provision "chef_zero" do |chef|
      chef.install = false
      chef.binary_path = "/opt/opscode/embedded/bin"
      chef.node_name = config.vm.hostname
      chef.cookbooks_path = "cookbooks"
      chef.add_recipe("provisioning::chef-server")
      # If we're running chef-backend, let _it_ create the chef-server.rb
      chef.add_recipe("provisioning::chef-server-rb") unless chef_backend_active?(attributes)
      chef.add_recipe("dev::system")
      chef.add_recipe("dev::user-env")
      chef.add_recipe("dev::dvm")
      chef.json = json || {}
      chef.nodes_path = "nodes"
    end

    if chef_backend_active?(attributes)
      config.vm.provision "shell", inline: "cp /installers/api.chef-server.dev.rb /etc/opscode/chef-server.rb"
    end

    # Makes more sense here than in a one-off line in the dvm recipe, which
    # has no direct connection...
    config.vm.provision "shell", inline: "chef-server-ctl reconfigure"

    if plugin_active?('chef-manage', attributes)
      config.vm.provision "shell", "inline": install_hack(m_installer, 'chef-manage')
      config.vm.provision "shell", "inline": "chef-manage-ctl reconfigure --accept-license"
    end

    if plugin_active?('push-jobs-server', attributes)
      backend_compat_message('push-jobs-server') if chef_backend_active?(attributes)
      config.vm.provision "shell", "inline": install_hack(ps_installer, 'opscode-push-jobs-server')
      config.vm.provision "shell", "inline": "opscode-push-jobs-server-ctl reconfigure"
    end

    if plugin_active?('reporting', attributes)
      backend_compat_message('reporting') if chef_backend_active?(attributes)
      config.vm.provision "shell", "inline": install_hack(r_installer, 'opscode-reporting')
      config.vm.provision "shell", "inline": "opscode-reporting-ctl reconfigure --accept-license"
    end

  end
end

def define_ldap_server(config, attributes)
  config.vm.hostname = "ldap.chef-server.dev"
  config.vm.network "private_network", ip: LDAP_VM_ADDRESS
  config.vm.provider :virtualbox do |vb|
    vb.customize ["modifyvm", :id,
                  "--name", "ldap",
                  # For basic pedant tests, we're not putting
                  # a lot of load on this:
                  "--memory", 512,
                  "--cpus", 1,
                  "--usb", "off",
                  "--usbehci", "off"
    ]
  end

  config.vm.provision "chef_zero" do |chef|
    chef.node_name = config.vm.hostname
    chef.cookbooks_path = "cookbooks"
    chef.add_recipe("provisioning::ldap-server")
    chef.nodes_path = "nodes"
    chef.json = { 'ldap' => {'password' => LDAP_PASSWORD }}
  end
end

def define_db_server(config, attributes)
  config.vm.hostname = "database.chef-server.dev"
  config.vm.network "private_network", ip: DB_VM_ADDRESS
  config.vm.provider :virtualbox do |vb|
    vb.customize ["modifyvm", :id,
                  "--name", "database",
                  # For basic pedant tests, we're not putting
                  # a lot of load on this:
                  "--memory", 512,
                  "--cpus", 1,
                  "--usb", "off",
                  "--usbehci", "off"
    ]
  end

  # Using shell here to ave the trouble of downloading
  # chef-client for the node.  May reconsider...
  config.vm.provision "shell", inline: configure_postgres
end

def define_backend_server(config, attribute)
  provisioning, installer, installer_path = prepare('BE_INSTALLER', 'chef-backend', 'Chef Backend 1.1+')
  config.vm.hostname = "backend.chef-server.dev"
  config.vm.network "private_network", ip: BE_VM_ADDRESS
  config.vm.provider :virtualbox do |vb|
    vb.customize ["modifyvm", :id,
                  "--name", "backend",
                  # Elasticsearch requires much RAM
                  "--memory", 2048,
                  "--cpus", 2,
                  "--usb", "off",
                  "--usbehci", "off"
    ]
  end

  if provisioning
    config.vm.synced_folder installer_path, "/installers"

    config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig"
    config.vm.provision "shell", inline: install_hack(installer)
    config.vm.provision "shell", inline: configure_backend
  end
end


def define_db_server_reporting(config, attributes)
  config.vm.hostname = "reportingdb.chef-server.dev"
  config.vm.network "private_network", ip: REPORTING_DB_VM_ADDRESS
  config.vm.provider :virtualbox do |vb|
    vb.customize ["modifyvm", :id,
                  "--name", "reportingdb",
                  # For basic pedant tests, we're not putting
                  # a lot of load on this:
                  "--memory", 512,
                  "--cpus", 1,
                  "--usb", "off",
                  "--usbehci", "off"
    ]
  end

  # Using shell here to ave the trouble of downloading
  # chef-client for the node.  May reconsider...
  config.vm.provision "shell", inline: configure_postgres
end


##############
# Internals
##############
# These functions are used for provisioning, and ensuring that the VM has
# what it needs to load up and install chef-server
##############


def prepare(installer_env, package_name, title)
  action = ARGV[0]
  if action =~ /^(provision|up|reload)$/
    installer = prompt_installer(installer_env, package_name)
    raise "Please set #{installer_env} to the path of a .deb package for #{title}." if installer.nil?
    raise "#{installer} does not exist! Please fix this." unless File.file?(installer)
    installer_path = File.dirname(File.expand_path(installer))
    provisioning = true
  end
  [provisioning, installer, installer_path]
end

def prompt_installer(installer_env, package_name)
  puts "Looking in #{Dir.home}/Downloads and #{base_path}/omnibus/pkg for installable #{package_name} package."
  # TODO allow config override of location, multiple locations, search pattern, max count?
  files = Dir.glob("#{Dir.home}/Downloads/#{package_name}*.deb") + Dir.glob("#{base_path}/omnibus/pkg/#{package_name}*.deb")

  if ENV[installer_env]
    if ENV[installer_env] =~ /^.*#{package_name}.*deb$/ and File.file?(ENV[installer_env])
      user_installer = File.expand_path(ENV[installer_env])
    else
      puts "#{installer_env} #{ENV[installer_env]} is not a valid #{package_name} package. Ignoring."
    end
  end

  if files.length == 0 and not user_installer
    return nil
  end

  files = files.sort_by{ |f| File.mtime(f) }.last(10)
  files.reverse!
  files << "[#{installer_env}]: #{user_installer}" if user_installer

  selection = 0

  # For the fantastically lazy, allow an environment variable to specify
  # which package selection to use. Special value of '-1' or 'installer' will
  # use the INSTALLER env var automatically (instead of just putting it in
  # the list to choose from).
  if ENV.has_key? 'AUTOPACKAGE'

    selection = ENV['AUTOPACKAGE']
    if (selection == 'installer' or selection == '-1') and user_installer
      # Auto pick the INSTALLER pacckage
      selection = files.length
    else
      selection = selection.to_i
    end

    if selection <= 0 or selection > files.length
      puts "Invalid AUTOPACKAGE selection of #{selection}."
      selection = get_selection(files)
    else
      puts "Using AUTOPACKAGE selection of #{files[selection - 1]}"
    end

  else
    selection = get_selection(installer_env, files)
  end

  if selection == files.length  and user_installer
    user_installer # we munged the text on this one
  else
    files[selection - 1]
  end

end

def get_selection(env, files)
  selection = 0
  files.each_index do |x|
    puts " #{x+1}) #{files[x]}\n"
  end
  loop do
    print "Select an image, or set the #{env} variable and run again: [1 - #{files.length}]: "
    selection = $stdin.gets.chomp.to_i
    break if selection > 0 and selection <= files.length
  end
  selection
end

def host_timezone
  require "time"
  # Note that we have to reverse the offset sign if we're using Etc/GMT,
  # reference: http://en.wikipedia.org/wiki/Tz_database#Area
  #  offset = (Time.zone_offset(Time.now.zone) / 3600) * -1
  #  zonesuffix = offset >= 0 ? "+#{offset.to_s}" : "#{offset.to_s}"
  #  "Etc/GMT#{zonesuffix}"
  #  Sigh - sqitch doesn't like the above format and dies.
  if /darwin/ =~ RUBY_PLATFORM
    host_timezone_osx
  else # TODO windows if we otherwise check out for windows.
    host_timezone_linux
  end
end

def host_timezone_linux
  File.read("/etc/timezone").chomp
end

def host_timezone_osx
  if File.exists?(".cached_tz")
    puts "Reading timezone from cache(.cached_tz)"
    File.read(".cached_tz")
  else
    puts "Notice: using sudo to get timezone, no updates being made"
    puts "Executing: sudo systemsetup -gettimezone"
    # Time Zone: Blah/Blah
    tz = `sudo systemsetup -gettimezone`.chomp.split(":")[1].strip
    File.write(".cached_tz", tz)
    tz
  end
end



# this is here in order to avoid having to download a chef provisioner -
# we already have a chef-client install included with the server package, and since
# we're going to run in solo mode, it will run for VM provisioning without
# interfering with the server install.
def install_hack(installer, omnibus = "opscode")
  server_installer_name = File.basename(installer)
  return ";" if server_installer_name.nil?
<<SCRIPT
cp /home/vagrant/.gitconfig /root/.gitconfig
if [ -d "/opt/#{omnibus}/embedded" ]
then
  echo "Bypassing server install, it appears done."
else
  sudo dpkg -i "/installers/#{server_installer_name}"
fi
SCRIPT
end

# Quick and dirty postgres configuration that avoids having to download
# a chef installer when we bring a box up.
def configure_postgres
<<BASH
echo "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main" > /etc/apt/sources.list.d/pgdg.list
wget --quiet https://www.postgresql.org/media/keys/ACCC4CF8.asc
apt-key add ACCC4CF8.asc
apt-get update
apt-get install postgresql-9.2 -y
echo "host    all             all             #{CS_VM_ADDRESS}/32         md5" >> /etc/postgresql/9.2/main/pg_hba.conf
echo "listen_addresses='*'" >> /etc/postgresql/9.2/main/postgresql.conf
service postgresql restart
export PATH=/usr/lib/postgresql/9.2/bin:$PATH
sudo -u postgres psql -c "CREATE USER bofh SUPERUSER ENCRYPTED PASSWORD 'i1uvd3v0ps';"
BASH
end

def configure_backend
<<BASH
cat > /etc/chef-backend/chef-backend.rb <<EOF
publish_address "#{BE_VM_ADDRESS}"
EOF
chef-backend-ctl create-cluster --accept-license
chef-backend-ctl gen-server-config api.chef-server.dev > /installers/api.chef-server.dev.rb
BASH
end

def backend_compat_message(plugin)
  puts ""
  puts "WARNING: #{plugin} may not work with Chef Backend"
  puts ""
end

def chef_backend_active?(attributes)
  attributes['vm']['chef-backend']['start']
end

def plugin_active?(plugin, attributes)
  attributes['vm']['plugins'][plugin]
end

def base_path
  File.absolute_path(File.join(Dir.pwd, "../"))
end
