#!/usr/bin/expect --
#
# hmc -- handle consoles and rebooting of HMC based systems
#
# Allow connecting to the console of and rebooting of HMC connected
# machines.  Machines are identified by the HMC IP address, connected
# system name, and partition name.
#
# usage:
#  hmc open-term|reboot -h <hmc IP> -m <system> -p <partition> \
#	-U <user> -P <password> -V <hmc version>
#
# example:
#  hmc open-term -m elm3b70 -p FullSystemPartition -U hscroot -P passwd \
#	-V 2.6 -h 1.2.3.4 
#  hmc reboot -m elm3b70 -p FullSystemPartition -U hscroot -P passwd \
#	-V 2.6 -h 1.2.3.4
#
# (C) Copyright IBM Corp. 2004, 2005, 2006
# Author: Andy Whitcroft <andyw@uk.ibm.com>
#
# The Console Multiplexor is released under the GNU Public License V2
#
set P "hmc"

# See interactions.
log_user 0
#exp_internal 1

proc note {m} {
	global P
	puts "$P: $m"
}
proc warn {m} {
	global P
	puts "$P: WARNING: $m"
}
proc winge {m} {
	global P
	puts "$P: MACHINE ERROR: $m"
}

#
# OPTIONS: options parser.
#
proc shift {_list} {
	upvar $_list list

	set res [lindex $list 0]
	set list [lreplace $list 0 0]

	return $res
}
proc arg {_list arg} {
	upvar $_list list

	if {[llength $list] < 1} {
		winge "$arg: required argument missing"
		exit 1
	}
	
	return [shift list]
}

set user		{hscroot}
set host		{}
set system		{}
set lpar		{}
set profile		{default}
set mode		{norm}
set passwd		{}
set version		{}

set cmd [lindex $argv 0]
shift argv
while {[llength $argv] > 0} {
	switch -- [shift argv] {
		-h	{ set host [arg argv h]}
		hmc	{ set host [arg argv h]}
		-m	{ set system [arg argv m]}
		-l   	{ set lpar [arg argv l]}
		-p   	{ set lpar [arg argv p]}
		-f   	{ set profile [arg argv f]}
		-b   	{ set mode [arg argv b]}
		-U	{ set user [arg argv P]}
		-P	{ set passwd [arg argv P]}
		-V	{ set version [arg argv P]}
	}
}

if {[llength $argv] > 0} {
	puts stderr "Usage: $P <cmd> -h <hmc> -m <system> -p <lpar> -f <profile> -b <mode>"
	exit 1
}
if {[string compare $host ""] == 0 ||
    [string compare $system ""] == 0 ||
    [string compare $lpar ""] == 0} \
{
	winge "hmc (-h), machine (-m) and lpar (-p) required"
	exit 1
}

set prompt		{___EOF___HMC___EOF___}
set hscpath		{/opt/hsc/bin}

#log_file -a "$logfile"

set elapsed_time 0
set timeout 30

# Ensure we have a terminal so we don't get prompted for one.
if {![info exists env(TERM)]} {
	set env(TERM) {vt100}
}

# If we have a password don't use any ssh-keys we may have been offered.
if {[string compare $passwd ""] == 0} {
	set command "ssh $env(SSH_OPTIONS) $user@$host"
} else {
	set command "ssh $user@$host"
}

# CONNECT: connect to the remote console.  Set the prompt up so we
#          know when a command we execute has completed.
note "Logging into HMC console with command \"$command\" ..."
eval spawn $command
send "PS1=$prompt\r"
expect {
	-re {[pP]assword:} {
		if {[string compare $passwd ""] == 0} {
			winge "$host: requesting password - not hacked"
			exit 2
		}
		send "$passwd\r"
		after 500
		send "PS1=$prompt\r"
		exp_continue
	}
	"Connection timed out" {
		winge "$host: cannot connect to console"
		exit 2
	}
	"Name or service not known" {
		winge "$host: HMC name invalid"
		exit 1
	}
	"Permission denied" {
		winge "$host: authentication failure, permission denied"
		exit 2
	}
	timeout {
		winge "$host: HMC did not prompt us, timed out"
		exit 2
	}
	eof {
		winge "$host: HMC disconnected without prompting"
		exit 2
	}

	"Terminal type?" {
		send "vt100\r"
		send "PS1=$prompt\r"
		exp_continue
	}

	"PS1=$prompt" {
		# Ignore us typing the change to the prompt.
		exp_continue
	}
	"$prompt" {
		# Prompt see, we are in and working, drop out.
		## note "prompt seen"
	}

	-re "^\[^\n]*\n" {
		# We need to absorb any login information, motd etc.
		## puts "NOISE<$expect_out(buffer)>"
		exp_continue
	}
}

proc runit {cmd _out} {
	upvar $_out out

	global prompt

	send "$cmd"

	set text ""
	set line 0

	set timeout 120
	expect {
		
		-re "^\[^\n]*\n" {
			##note "LINE: $expect_out(buffer)"
			if { $line > 0 } {
				append text $expect_out(buffer)
			}
			incr line
			exp_continue
		}

		-re "$prompt" {
			##note "prompt seen"
		}

		timeout {
			set out "hmc: command timeout"
			return 255
		}
	}
	set out [string trimright $text]

	send "echo \$?\r"
	set text ""
	set line 0
	set timeout 30
	expect {
		-re "^\[^\n]*\n" {
			##note "LINE: $expect_out(buffer)"
			if { $line > 0 } {
				append text $expect_out(buffer)
			}
			incr line
			exp_continue
		}

		-re "$prompt" {
			##note "prompt seen"
		}

		timeout {
			set out "hmc: command status timeout"
			return 255
		}
	}
	##note "EXIT: $text"

	return [string trimright $text]
}

proc extract_names {out} {
	set res {}
	foreach sys [split $out] {
		if {[string compare $sys ""] != 0} {
			lappend res $sys
		}
	}

	return $res
}

# Find out the current version of the HMC software.
if {[string compare $version ""] == 0} {
	runit "rpm -q IBMhsc.coreserver --qf '%{VERSION}'; echo ''\r" version
}
if {[string compare $version ""] == 0} {
	winge "unable to obtain HMC code version"
	exit 1
}
note "HMC revision v$version"

# Based on the version of the HMC software select payload.
if {$version < 4.0} {
	#
	# VERSION 2.6/3.0 specific support.
	#
	proc system_list {} {
		global hscpath

		# Ask for a list of systems.
		if {[runit "lssyscfg -r sys --all -F name\r" out] != 0} {
			winge "unable to obtain list of systems"
			exit 1
		}

		return [extract_names $out]
	}
	proc system_state {system} {
		global hscpath

		# Ask for the system status.
		if {[runit "lssyscfg -r sys -n $system -F state\r" out] != 0} {
			winge "$system: failed to get status"
			exit 1
		}

		return $out
	}
	proc lpar_list {system} {
		global hscpath

		# Ask for a list of lpars for this system.
		if {[runit "lssyscfg -r lpar -m $system --all -F name\r" out] != 0} {
			winge "unable to obtain list of lpars"
			exit 1
		}

		return [extract_names $out]
	}
	proc state {system lpar} {
		global hscpath

		# Ask for the lpar status.
		if {[runit "lssyscfg -r lpar -m $system -n $lpar -F state\r" out] != 0} {
			winge "$system/$lpar: failed to get lpar status"
			exit 1
		}

		return $out
	}
	proc reboot_metal {system lpar profile mode} {
		global hscpath

		# If we have no connection wait for the machine to appear.
		# XXX: timeout?
		set was {}
		set when [clock seconds]
		if {[string compare [system_state $system] "Ready"] != 0} {
			while {([clock seconds] - $when) < 60} {
				sleep 15
				set state [system_state $system]
				
				note "waiting for stable connected state ($state)"
				if {$state != $was || [string compare $state "No Connection"] == 0} {
					set was $state
					set when [clock seconds]
				}
			}
		}

		# See if the system is is in Error, if so attempt to
		# power it on.
		if {[string compare [system_state $system] "Error"] == 0} {
			note "starting full system (from Error)"
			if {[runit "chsysstate -r sys -m $system -n $system -b $mode -o on -c full\r" out] == 0} {
				note "started"
				return
			}
			note "start failed - attempting shutdown"
		}

		# See if the system is up, if so shut it down.
		if {[string compare [system_state $system] "No Power"] != 0} {
			note "shutting down full system"
			if {[runit "chsysstate -r sys -m $system -n $system -o off -c full\r" out] != 0} {
				winge "$system: power off failed\n$out"
				exit 2
			}
		}

		while {[string compare [system_state $system] "No Power"] != 0} {
			note "waiting for shutdown"
			sleep 15
		}

		note "starting full system"
		if {[runit "chsysstate -r sys -m $system -n $system -b $mode -o on -c full\r" out] != 0} {
			winge "$system: power on failed"
			exit 2
		}
		note "started"
	}
	proc reboot {system lpar profile mode} {
		global hscpath

		# Handle the bare metal separatly.
		if {[string compare $lpar "FullSystemPartition"] == 0} {
			return [reboot_metal $system $lpar $profile $mode]
		}

		# Partitions in Error state are tricky, you _may_ either
		# have to shut them down and then start them, or you may
		# just have to start them.  So, if we are in Error start
		# the partition, if it fails we can just drop through
		# to the regular stop/start cycle.
		if {[string compare [state $system $lpar] "Error"] == 0} {
			note "starting lpar (from Error)"
			if {[runit "chsysstate -r lpar -m $system -n $lpar -b $mode -o on\r" out] == 0} {
				note "started"
				return
			}
			note "start failed - attempting shutdown"
		}

		# See if the lpar is up, if so shut it down.
		if {[string compare [state $system $lpar] "Ready"] != 0} {
			note "shutting down lpar"
			if {[runit "chsysstate -r lpar -m $system -n $lpar -o off\r" out] != 0} {
				winge "$system/$lpar: power off failed\n$out"
				exit 2
			}
		}

		while {[string compare [state $system $lpar] "Ready"] != 0} {
			note "waiting for shutdown"
			sleep 15
		}

		note "starting lpar"
		if {[runit "chsysstate -r lpar -m $system -n $lpar -b $mode -o on\r" out] != 0} {
			winge "$system/$lpar: power on failed\n$out"
			exit 2
		}
		note "started"
	}

}

if {$version >= 4.0} {
	#
	# VERSION 4.x specific support.
	#
	proc system_list {} {
		global hscpath

		# Ask for a list of systems.
		if {[runit "lssyscfg -r sys -F name\r" out] != 0} {
			winge "unable to obtain list of systems"
			exit 1
		}

		return [extract_names $out]
	}
	proc system_state {system} {
		global hscpath

		# Ask for the system status.
		if {[runit "lssyscfg -r sys -m $system -F state\r" out] != 0} {
			winge "$system: failed to get system status"
			exit 1
		}

		return $out
	}
	proc lpar_list {system} {
		global hscpath

		# Ask for a list of lpars for this system.
		if {[runit "lssyscfg -r lpar -m $system -F name\r" out] != 0} {
			winge "unable to obtain list of lpars"
			exit 1
		}

		return [extract_names $out]
	}
	proc state {system lpar} {
		global hscpath

		# Ask for the lpar status.
		if {[runit "lssyscfg -r lpar -m $system --filter lpar_names=$lpar -F state\r" out] != 0} {
			winge "$system/$lpar: failed to get lpar status"
			exit 1
		}

		return $out
	}

	proc reboot {system lpar profile mode} {
		global hscpath

		# Partitions in Error state are tricky, you _may_ either
		# have to shut them down and then start them, or you may
		# just have to start them.  So, if we are in Error start
		# the partition, if it fails we can just drop through
		# to the regular stop/start cycle.
		if {[string compare [state $system $lpar] "Error"] == 0} {
			note "starting lpar (from Error)"
			if {[runit "chsysstate -r lpar -m $system -n $lpar -f $profile -b $mode -o on\r" out] == 0} {
				note "started"
				return
			}
			note "start failed - attempting shutdown"
		}
				
		# See if the lpar is up, if so shut it down.
		if {[string compare [state $system $lpar] "Not Activated"] != 0} {
			note "shutting down lpar"
			if {[runit "chsysstate -r lpar -m $system -n $lpar -o shutdown --immed\r" out] != 0} {
				winge "$system/$lpar: power off failed\n$out"
				exit 2
			}
		}

		while {[string compare [state $system $lpar] "Not Activated"] != 0} {
			note "waiting for shutdown"
			sleep 15
		}

		note "starting lpar"
		if {[runit "chsysstate -r lpar -m $system -n $lpar -f $profile -b $mode -o on\r" out] != 0} {
			winge "$system/$lpar: power on failed\n$out"
			exit 2
		}
		note "started"
	}
}

#
# VERSION: common
#
proc open-term {system lpar} {
	global prompt
	global hscpath
	global spawn_id

	note "opening console ..."
	send "ulimit -t 3600; \[ -f cleandown ] && sudo /home/hscroot/cleandown $system $lpar; mkvterm -m $system -p $lpar\r"
	expect {
		"NVTS" { }
		"Open in progress.." { }
		"$prompt" {
			winge "$system/$lpar: open console failed"
			exit 2
		}
	}

	note "console open"
	interact {
		-i $spawn_id
		" Connection has closed" {
			winge "$system/$lpar: console connection closed"
			exit 2
		}
		{Error in communication path to the partition} {
			winge "$system/$lpar: lost contact with lpar"
			exit 2
		}
		"$prompt" {
			winge "$system/$lpar: open console failed"
			exit 2
		}
		eof {
			winge "$system/$lpar: payload lost"
			exit 2
		}
		\000 {
		}
			
	}
	note "console closed"
}

proc close-term {system lpar} {
	global hscpath

	note "closing console ..."
	if {[runit "rmvterm -m $system -p $lpar\r" out] != 0} {
		winge "$system/$lpar: close console failed"
		exit 2
	}
}

# Look and see if this system exists.
set systems [system_list]
if {[lsearch -exact $systems $system] < 0} {
	winge "$system: system not known; console knows: $systems"
	exit 1
}
set lpars [lpar_list $system]
if {[lsearch -exact $lpars $lpar] < 0} {
	winge "$system/$lpar: lpar not known; console knows: $lpars"
	exit 1
}

# Check for system in Operating/Ready.
set state [system_state $system]
if {[string compare $lpar "FullSystemPartition"] != 0 &&
    [string compare $state "Operating"] != 0 &&
    [string compare $state "Ready"] != 0} \
{
	winge "$system: unusable - $state"
	exit 1
}

# Ask for the lpar status, to see if it exists.
set lstate [state $system $lpar]
note "$system/$lpar: found ($state/$lstate)"

#
# COMMANDS: command.
#
switch -- $cmd {
	{open-term}	{
		close-term $system $lpar
		open-term $system $lpar
	}
	{close-term}	{
		close-term $system $lpar
	}
	{reboot}	{
		reboot $system $lpar $profile $mode
	}
	default		{
		winge "$cmd: unknown command"
	}
}

exit 0
