#!/usr/bin/env oo-ruby
#--
# Copyright 2010 Red Hat, Inc.
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#    http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#++

require 'rubygems'
require 'parseconfig'
require 'yaml'
require 'fileutils'
require 'logger'
require 'open4'

$log = Logger.new(STDOUT)
$log.level = Logger::INFO

def find_and_replace(file, find, replace)
  $log.debug("Replacing #{find} with #{replace} in file #{file}")
  data = File.open(file).read
  File.open(file, "w") do |f|
    data = data.gsub(find, replace)
    f.write(data)
  end
end

def insert_if_not_exist(file, find, to_insert)
  $log.debug("Checking if #{find} exists in file #{file}")
  data = File.open(file).read
  return if data.match(find)
  $log.debug("...inserting #{to_insert} in file #{file}")
  File.open(file, "w") do |f|
    f.write(data)
    f.write(to_insert)
  end
end

def run(cmd)
  $log.debug("Running command:")
  $log.debug(cmd)
  error_str = ""
  status = Open4.popen4(cmd) do |pid, stdin, stdout, stderr|
    $log.debug(stdout.read)
    error_str = stderr.read
  end
  $log.error(error_str) if (status.to_i != 0 and !error_str.empty?)
  $log.debug("Exit: #{status}")
  return status.to_i
end

def enable_se_booleans(bool_list)
  bool_list.delete_if{ |bool| `getsebool #{bool}`.match(/--> on/) }
  semanage_commands = bool_list.map { |bool| "boolean -m --on #{bool}" }.join("\n")

run <<-EOOF
/usr/sbin/semanage -i - <<_EOF
#{semanage_commands}
_EOF
EOOF
end

def usage
  puts <<USAGE
== Synopsis

oo-setup-node: Configure node hostname and broker IP
  This command must be run as root.

== List of arguments
  -b  |--with-broker-ip	                IP address of the broker (required)
  -h  |--with-node-hostname	        Hostname for this node (required)
  -d  |--domain <domain>                Domain name for this node (optional, default: example.com)

 --eip|--external-ip <IP/PREFIX>	Sets up the VM to use a static IP on the external ethernet device. (Defaults to DHCP)
 --egw|--external-gw <IP>		Gateway for external IP (only for non-dhcp address)
 --ed |--external-device		Sets up the VM to use specified ethernet device. Default: eth0


 --iip|--internal-ip <IP/PREFIX>	Sets up the VM to use a static IP on the internal ethernet device. (Defaults to DHCP)
 --id |--internal-device		Sets up the VM to use specified ethernet device. (Defaults to same as external)

  -n  |--static-dns <IP>[,<IP>]		Comma seperated list of IP addresses to use for DNS forwarding
  -?  |--help                           Print this message

USAGE
  exit 255
end

require 'openshift-origin-node'
opts = GetoptLong.new(
    ["--external-ip",           "--eip", GetoptLong::OPTIONAL_ARGUMENT],
    ["--external-gw",           "--egw", GetoptLong::OPTIONAL_ARGUMENT],
    ["--external-device",       "--ed" , GetoptLong::OPTIONAL_ARGUMENT],

    ["--internal-ip",           "--iip", GetoptLong::OPTIONAL_ARGUMENT],
    ["--internal-gw",           "--igw", GetoptLong::OPTIONAL_ARGUMENT],
    ["--internal-device",       "--id" , GetoptLong::OPTIONAL_ARGUMENT],

    ["--static-dns",            "-n"  , GetoptLong::OPTIONAL_ARGUMENT],
    ["--help",                  "-?"  , GetoptLong::NO_ARGUMENT],
    ["--with-broker-ip",        "-b"  , GetoptLong::REQUIRED_ARGUMENT],
    ["--with-node-hostname",    "-h"  , GetoptLong::REQUIRED_ARGUMENT],
    ["--debug",                         GetoptLong::NO_ARGUMENT],
    ["--domain",                "-d"  , GetoptLong::OPTIONAL_ARGUMENT]
)

args = {}
begin
  opts.each{ |k,v| args[k]=v }
rescue GetoptLong::Error => e
  usage
end

broker_ip = args["--with-broker-ip"]
node_hostname = args["--with-node-hostname"]
node_domain = args["--domain"] || "example.com"

if args["--help"] || (broker_ip.nil? || broker_ip.empty? || node_hostname.nil? || node_hostname.empty?)
  usage
end

if args["--debug"]
  $log.level = Logger::DEBUG
end

ext_eth_device = args["--external-device"] || "eth0"
ext_address    = args["--external-ip"]
ext_address,ext_prefix = ext_address.split("/") unless ext_address.nil?
ext_gw         = args["--external-gw"]

int_eth_device = args["--internal-device"] || ext_eth_device
int_address    = args["--internal-ip"]
int_address,int_prefix = int_address.split("/") unless int_address.nil?

dns            = args["--static-dns"]
dns_address    = dns.split(/,/) unless dns.nil?
ext_dhcp       = false
int_dhcp       = false
use_systemd    = File.exist?('/bin/systemd')
use_nm         = (use_systemd and File.exist?('/lib/systemd/system/NetworkManager.service'))

if ext_address.nil? #DHCP
  ext_address = `/sbin/ip addr show dev #{ext_eth_device} | awk '/inet / { split($2,a, "/") ; print a[1];}'`
  ext_dhcp = true
end 

if int_address.nil? #DHCP
  int_address = `/sbin/ip addr show dev #{int_eth_device} | awk '/inet / { split($2,a, "/") ; print a[1];}'`
  int_dhcp = true
end

if !ext_dhcp && (ext_address.nil? || ext_address.empty? || ext_prefix.nil? || ext_prefix.empty? || ext_gw.nil? || ext_gw.empty?)
  puts "Must provide --external-ip <IP/PREFIX> and --external-gw <IP> for statically configuring external ethernet device."
  usage
end

if !int_dhcp && (int_eth_device.nil? || int_eth_device.empty? || int_eth_device == ext_eth_device || int_address.nil? || int_address.empty? || int_prefix.nil? || int_prefix.empty?)
  puts "Must provide --internal-device <DEV> --internal-ip <IP/PREFIX> and --internal-gw <IP> for statically configuring internal ethernet device."
  usage
end

ext_hw_address = `/sbin/ip addr show dev #{ext_eth_device} | grep 'link/ether' | awk '{ print $2 }'`
int_hw_address = `/sbin/ip addr show dev #{int_eth_device} | grep 'link/ether' | awk '{ print $2 }'`

if dns_address.nil?
  if ext_dhcp
    dns_address = `awk '/domain-name-servers/ {print $3}'  /var/lib/dhclient/dhclient-*#{ext_eth_device}.lease* | sort -u`.split(";\n").map{ |ips| ips.split(",") }.flatten
    dns_address.delete '127.0.0.1'
  else
    dns_address = ["8.8.8.8", "8.8.4.4"]
  end
end

if dns_address.nil? || dns_address.length == 0
  puts "Error: Unable to determine DNS servers.\n"
  usage
end

if args["--help"]
  usage
end

$log.info "Configuring networking"
$log.info "...configuring external network"
File.open("/etc/sysconfig/network-scripts/ifcfg-#{ext_eth_device}","w") do |f|
  f.write "DEVICE=#{ext_eth_device}\n"
  f.write "ONBOOT=yes\n"
  f.write "HWADDR=#{ext_hw_address}\n"
if ext_dhcp
    f.write "BOOTPROTO=dhcp\n"
else
    f.write "BOOTPROTO=static\n"
    f.write "IPADDR=#{ext_address}\n"
    f.write "PREFIX=#{ext_prefix}\n"
    f.write "GATEWAY=#{ext_gw}\n"
end
  f.write "DNS1=#{broker_ip}\n"
  dns_address.each_index do |idx|
    f.write "DNS#{idx+2}=#{dns_address[idx]}\n"
  end

  f.write "TYPE=Ethernet\n"
  f.write "DEFROUTE=yes\n"
  f.write "PEERDNS=no\n" if use_nm
  f.write "PEERROUTES=yes\n"
end

if int_eth_device != ext_eth_device
  $log.info "...configuring internal network"
  File.open("/etc/sysconfig/network-scripts/ifcfg-#{int_eth_device}","w") do |f|
    f.write "DEVICE=#{int_eth_device}\n"
    f.write "ONBOOT=yes\n"
    f.write "HWADDR=#{int_hw_address}\n"
    if int_dhcp
      f.write "BOOTPROTO=dhcp\n"
    else
      f.write "BOOTPROTO=static\n"
      f.write "IPADDR=#{int_address}\n"
      f.write "PREFIX=#{int_prefix}\n"
    end
  end
end

File.open("/etc/sysconfig/network", "w") do |f|
  f.write("NETWORKING=yes\n")
  f.write("HOSTNAME=#{node_hostname}.#{node_domain}\n")
end
run "/sbin/chkconfig network on"

$log.info "Setting node SELinux booleans\n"
enable_se_booleans(["httpd_run_stickshift", "httpd_verify_dns", "allow_polyinstantiation"])

$log.info "Updating node configuration"
find_and_replace("/etc/openshift/node.conf", /^PUBLIC_IP=.*$/, "PUBLIC_IP=#{ext_address}")
find_and_replace("/etc/openshift/node.conf", /^CLOUD_DOMAIN=.*$/, "CLOUD_DOMAIN=#{node_domain}")
find_and_replace("/etc/openshift/node.conf", /^PUBLIC_HOSTNAME=.*$/, "PUBLIC_HOSTNAME=#{node_hostname}.#{node_domain}")
find_and_replace("/etc/openshift/node.conf", /^(# )?EXTERNAL_ETH_DEV=.*$/, "EXTERNAL_ETH_DEV=#{ext_eth_device}")
find_and_replace("/etc/openshift/node.conf", /^(# )?INTERNAL_ETH_DEV=.*$/, "INTERNAL_ETH_DEV=#{int_eth_device}")
find_and_replace("/etc/openshift/node.conf", /^BROKER_HOST=.*$/, "BROKER_HOST=\"#{broker_ip}\"")


$log.info "Opening required ports\n"
run "/usr/sbin/lokkit --service=ssh"
run "/usr/sbin/lokkit --service=http"
run "/usr/sbin/lokkit --service=https"
run "/usr/sbin/lokkit -p 8000:tcp"
run "/usr/sbin/lokkit -p 8443:tcp"

$log.info "Setting up m-collective to use qpid"
$log.info "...configuring mcollective client to use qpid"
File.open("/etc/mcollective/client.cfg","w") do |f|
  f.write <<-EOF
topicprefix = /topic/
main_collective = mcollective
collectives = mcollective
libdir = /usr/libexec/mcollective
loglevel = debug
logfile = /var/log/mcollective-client.log

# Plugins
securityprovider = psk
plugin.psk = unset
connector = qpid
plugin.qpid.host=broker.#{node_domain}
plugin.qpid.secure=false
plugin.qpid.timeout=5

# Facts
factsource = yaml
plugin.yaml = /etc/mcollective/facts.yaml
EOF
end

$log.info "...configuring mcollective server to use qpid"
File.open("/etc/mcollective/server.cfg", "w") do |f|
  f.write <<-EOF
topicprefix = /topic/
main_collective = mcollective
collectives = mcollective
libdir = /usr/libexec/mcollective
logfile = /var/log/mcollective.log
loglevel = debug
daemonize = 1 
direct_addressing = n

# Plugins
securityprovider = psk
plugin.psk = unset
connector = qpid
plugin.qpid.host=broker.#{node_domain}
plugin.qpid.secure=false
plugin.qpid.timeout=5

# Facts
factsource = yaml
plugin.yaml = /etc/mcollective/facts.yaml
EOF
end

$log.info "Generating mcollective facts"
run "/etc/cron.minutely/openshift-facts"

oo_device=%x[/bin/df -P /var/lib/openshift | tail -1].split[0]
oo_mount=%x[/bin/df -P /var/lib/openshift | tail -1 | tr -s ' '].split[5]
$log.info "Enabling quota on filesystem #{oo_mount}"
unless %x[/sbin/quotaon -u -p #{oo_mount} 2>&1].strip == "user quota on #{oo_mount} (#{oo_device}) is on"
  $log.info "... updating fstab and remounting #{oo_mount}"
  run "/bin/awk '{ $2==\"#{oo_mount}\" ? $4=\"defaults,usrjquota=aquota.user,jqfmt=vfsv0\" : $4=$4 ; print $0 }' /etc/fstab > /etc/fstab.new"
  run "/bin/mv /etc/fstab.new /etc/fstab"
  run "/bin/mount -o remount /"
  run "/sbin/quotacheck -cmug /"
end

quota_db_file=File.join(oo_mount,"/aquota.user")
if File.exists? quota_db_file
  quota_db_type = %x[secon -f #{quota_db_file} | grep type: ]
  if quota_db_type !~ /quota_db_t/
    run "restorecon /aquota.*"
  end
else
  $log.info "... switching quota on\n"
  run "/sbin/restorecon /aquota.*"
  run "/sbin/quotaon #{oo_mount}"
end

$log.info "Enabling CGroups"
FileUtils.mkdir_p "/cgroup"
run "/bin/cp -r /usr/share/doc/rubygem-openshift-origin-node-*/cgconfig.conf /etc/cgconfig.conf"

run "/sbin/restorecon -v /etc/cgconfig.conf"
run "/sbin/restorecon -v /cgroup"
run "/sbin/chkconfig cgconfig on"
run "/sbin/chkconfig cgred on"

#if use_systemd
#  run "/bin/systemctl enable openshift-cgroups.service"
#else
  run "/sbin/chkconfig --add openshift-cgroups"
  run "/sbin/chkconfig openshift-cgroups on"
#end

$log.info "Restore SELinux default security contexts."
run "/sbin/restorecon /var/lib/openshift || :"
run "/sbin/restorecon /var/lib/openshift/.httpd.d/ || :"

$log.info "Enable pam_cgroup on sshd"
insert_if_not_exist("/etc/pam.d/sshd", "pam_cgroup.so", "session\t\optional\tpam_cgroup.so\n")
                                   
$log.info "Set pam-openshift and polyinstantiation"
find_and_replace("/etc/pam.d/sshd", "pam_selinux", "pam_openshift")
["runuser", "runuser-l", "sshd", "su", "system-auth-ac"].each do |pamf|
  begin
    insert_if_not_exist("/etc/pam.d/#{pamf}", "pam_namespace.so", "session\t\trequired\tpam_namespace.so no_unmount_on_close\n")
  rescue Errno::ENOENT
  end
end
['/tmp','/var/tmp'].each do |dir|
  File.open('/etc/security/namespace.d/openshift_' + dir.gsub('/','') + '.conf', 'w') do |f|
    f.write("# file create by oo-setup-node for openshift directory polyinstanciation, see http://selinuxproject.org/page/NB_Poly\n")
    f.write(dir + "$HOME/.tmp/   user:iscript=/usr/sbin/oo-namespace-init root,adm,apache\n")
  end
end

$log.info "Increasing kernel port, connection and semaphore limits"
insert_if_not_exist("/etc/sysctl.conf", "net.ipv4.ip_local_port_range = 15000 35530", "net.ipv4.ip_local_port_range = 15000 35530\n")
insert_if_not_exist("/etc/sysctl.conf", "net.netfilter.nf_conntrack_max = 1048576", "net.netfilter.nf_conntrack_max = 1048576\n")
insert_if_not_exist("/etc/sysctl.conf", "kernel.sem = 250  32000 32  4096", "kernel.sem = 250  32000 32  4096\n")

$log.info "Updating system wide OpenShift CLI configuration to use local broker"
File.open("/etc/openshift/express.conf", "w") do |f|
  f.write("libra_server=broker.#{node_domain}\n")
end

$log.info "Updating root user's OpenShift configuration to use 'admin' user"
FileUtils.mkdir_p "/root/.openshift"
File.open("/root/.openshift/express.conf", "w") do |f|
  f.write("username=admin\n")
end


["httpd", "sshd", "mcollective", "openshift-port-proxy", "crond", ].each do |service|
  run "/sbin/chkconfig #{service} on"
end

if use_systemd
  run "/bin/systemctl enable openshift-gears.service"
  run "/bin/systemctl enable openshift-node-web-proxy.service"
else
  run "/sbin/chkconfig openshift-gears on"
  run "/sbin/chkconfig openshift-node-web-proxy on"
end

$log.warn "NOTE: Please ensure that the clocks between broker and node are in sync."
$log.warn "NOTE: Please reboot this node to pick up cgroups, quota and service changes"
