#!/usr/bin/expect --
#*************************************************************************
# File:	libssh
# Description: Library of expect routines for ssh connection, copy, etc.
#
# Routines:
#    SshUsage			- Generic ssh usage (for standalone apps)
#    SshParseCmdLine	- Generic parse subroutine for ssh apps
#    ScpUsage           - Generic scp usage for standalone apps
#    ScpParseCmdLine    - Generic parse subroutine for standalone apps
#    FixKnownHosts      - Fix known hosts for crash-and-burn systems
#                         that were reimaged and have different keys
#    ConnectSsh 		- Connect to a remote host using ssh
#    Scp				- Copy files to/from a remote host
#    CheckScpInfo 		- Generic check routine for reasonable data.
#
#      -*- OpenSAF  -*-
#
# (C) Copyright 2008 The OpenSAF Foundation
#
# 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. This file and program are licensed
# under the GNU Lesser General Public License Version 2.1, February 1999.
# The complete license can be accessed from the following location:
# http://opensource.org/licenses/lgpl-license.php
# See the Copying file included with the OpenSAF distribution for full
# licensing terms.
#
# Author(s):
#           Hewlett-Packard Company
#
#
#*************************************************************************
if {![info exists __LIBSSH_SOURCED] || !$__LIBSSH_SOURCED } {
   set __LIBSSH_SOURCED 1
} \
else {
   return
}


#*************************** Defaults/Globals ****************************
source "$basepath/defaults"
set nodomain 0
set destDir ""
set localDestDir .


# Get access to library routines
source "$basepath/libsm"

#*********************** Usage messages for ssh *********************
#
# SshUsage: usage for scripts that use ssh to log onto a system or
#           to run a command on a remote system.
#           Exits after printing usage message with requested value
# Input:    exitVal   --  Exit value
#    (global) argv0   -- the name of the script
#    (global) user    -- User to log in as (for printing default)
#    (global) passwd  -- Password of user to log in as (for printing default)
#    (global) domain  -- Domain of host.
# Return: void (exits to calling shell)
#
#********************************************************************
proc SshUsage {exitVal} {
    global argv0
    global user passwd domain

    puts "\n\tUsage: $argv0 \[-l user] \[-p passwd] \[-s] sut \[\[-c] cmd]"
    puts ""
    puts "\t  -h|h\[elp] Print out this message"
    puts "\t  -c run command 'cmd' on sut (no login)"
    puts "\t  -l log in as user"
    puts "\t     Default: $user"
    puts "\t  -n Don't append domain .$domain to the sut hostname"
    puts "\t  -p password for user"
    puts "\t     Default: $passwd"
    puts "\t  -s log into system 'sut'"
    puts "\t     Note:  This script appends domain '$domain' to the"
    puts "\t     hostname unless the -n option is used."
    puts ""
    exit $exitVal
}

#********************** Parse ssh-script cmdline ********************
#
# SshParseCmdLine: Parse command line for ssh'ing to a system.
# Input: argvRef -- reference to argv, the user's command line
#        (global) host  -- host to log into.
# Output: Sets variables in the caller's scope
#
#********************************************************************
proc SshParseCmdLine {argvRef} {
   global VERBOSE
   global host
   upvar $argvRef argv

   # Get command in local scope
   global cmd
   # Replace argv with a list split by spaces
   set argv [split $argv { }]

   if {$VERBOSE} {
      send_user "\n\[SM] SshParseCmdLine\n"
      send_user "\targv = $argv\n"
   }

   while {[llength $argv] > 0} {
      set opt [shift argv]
      switch -- $opt {
           -dryrun { uplevel #0 set DRYRUN 1; continue }
           -h -
           -help -
           -\\\? {SshUsage}
           -v { uplevel #0 set VERBOSE 1; continue }
           -cmd {
                  # The command has to be the thing on the end of the line
                  # so that commands can have flags
                  set cmd [lrange $argv 0 end]
                  set argv ""
                  continue
           }
           -dryrun { uplevel #0 set DRYRUN 1 ; continue }
           -l		{ uplevel #0 set user [shift argv] ; continue }
		   -help -
 		   -?    -
	       -h   { SshUsage 0 }
           -nodomain -
           -n		{ uplevel #0 set nodomain 1 ; continue }
           -domain { uplevel #0 set domain [shift argv]; continue }
		   -passwd -
           -p		{ uplevel #0 set passwd [shift argv] ; continue }
		   -system -
		   -host -
		   -sut -
           -s		{ uplevel #0 set host [shift argv] ; continue }
           -u -
           -user    { uplevel #0 set user [shift argv]; continue }
           -timeout		{ uplevel #0 set timeout_setting [shift argv] ; continue }
           -v		{ uplevel #0 set VERBOSE 1 ; continue }
		   default {
		             if {$VERBOSE} {
				        send_user "\tFound flagless option\n"
				     }
				     if [info exists host] {
				       # We already have a hostname, so we add the
				       # rest of the command line to the command
                       #  that we will run.
                       if {$VERBOSE} { 
                          send_user "\tBuilding remote command line\n"
                       }
		               push cmd $opt
                       while {[llength $argv]} {
                          push cmd [shift argv]
                       }
                       set cmd [join $cmd " "]
				       if {$VERBOSE} {
                          send_user "\tcmd = $cmd\n"
                       }
				     } \
				     else  {
                        if {$VERBOSE} { send_user "\tsetting host to $opt\n" }
                        set host $opt
		             }
                     continue
				   }
      }
   }
} ;# End SshParseCmdLine

#************************ Print Usage for scp **********************
#
# ScpUsage: usage for scripts that use scp to copy files to or
#           from a remote system.
# Input:    exitVal   --  Exit value
#    (global) argv0   -- the name of the script
#    (global) user    -- User to log in as (for printing default)
#    (global) passwd  -- Password of user to log in as (for printing default)
#    (global) domain  -- Domain of host.
# Return: void (exits to calling shell)
#
#*******************************************************************
proc ScpUsage {exitVal} {
    global argv0 user passwd domain

    set sname [split $argv0 "/"]
    set script [lindex $sname [expr [llength $sname] - 1]] 
    puts "\nUsage: $script file1 \[file2 ...] \[-l user] \[-p passwd] system"
    puts "Copy files to a system."
    puts ""
    puts "  -h|h\[elp] Print out this message"
    puts "  -l log in as user"
    puts "     Default: $user"
    puts "  -n Don't append domain .$domain to the sut hostname"
    puts "  -p password for user"
    puts "     Default: $passwd"
    puts "     Files are copied to 'system'"
    puts "     Note:  This script appends domain '$domain' to the"
    puts "     hostname unless the -n option is used."
    puts ""
    exit $exitVal
}

#********************** Parse ssh-script cmdline ********************
#
# ScpParseCmdLine: parse command line for an application using
#                  scp to copy files.
# Input: argvRef -- reference to argv, the user's command line
#        (global) host  -- host to log into.
# Output: Sets variables in the caller's scope
#
#********************************************************************
proc ScpParseCmdLine {argvRef} {
   global VERBOSE
   upvar $argvRef argv


   if {$VERBOSE} {
      send_user "\[SM] ScpParseCmdLine:\n"
   }
   # Get variables in local scope for smart command-line parsing
   upvar scpOpts scpOpts
   upvar host host
   upvar copyTo copyTo

   # Replace argv with a list split by '=' signs
   set argv [split $argv { =}]

   while {[llength $argv] > 0} {
      set opt [shift argv]
      if {$VERBOSE} {
         send_user "\tParsing '$opt'\n"
      }
      switch -- $opt {
           -l        { uplevel #0 set user [shift argv] ; continue }
           -help -
           -?    -
           -h   { ScpUsage 0 }
           -passwd -
           -n        { uplevel #0 set nodomain 1 ; continue }
           -p        { uplevel #0 set passwd [shift argv] ; continue }
           -system -
           -sut -
           -s        { uplevel #0 set host [shift argv] ; continue }
           -v      { uplevel #0 set VERBOSE 3; continue }
           default {
 
                    # Check if the argument is a file or directory, if so,
                    # add it to the fileList variable
                    # Otherwise, treat the argument as the host
                    #  to copy to.
                    #
                     if {[regexp {:} $opt]} {
                        if { ![info exists copyTo] } {
                           if {$VERBOSE} {
                              send_user "\tInterpreting $opt as host with path\n"
                              send_user "\tCopying FROM $opt\n"
                           }
                           uplevel #0 set host $opt
                           uplevel #0 set copyTo 0
                           continue
                        } \
                        elseif {[info exists copyTo]} {
                           if {$VERBOSE} {
                              send_user "\tInterpreting $opt as host with path\n"
                              send_user "\tCopying TO $opt\n"
                           }
                           uplevel #0 set host $opt
                           uplevel #0 set copyTo 1
                           continue
                       }
                     }
                     if { ![catch {file exists $opt} fileTestOut] }  {
                        if {$fileTestOut} {
                           if {$VERBOSE} {
                              send_user "\t$opt passes file exists\n"
                           }
                           # The file or directory exists locally.
                           if {$opt == "."} {
                              # Copying to the current directory
                              if {$VERBOSE} {
                                 send_user "\tUsing '$opt' as localDest.\n"
                              }
                              uplevel #0 set localDestDir $opt
                              continue
                           }
                            # Copying a local file to a server
                           if {![info exists host]} {
                              if {$VERBOSE} {
                                 send_user "\tIntepreting $opt as a file to copy to a system\n"
                              }
                              uplevel #0 set copyTo 1
                              uplevel #0 lappend fileList $opt
                              continue
                           }
                            # Copying to a local destination: figure
                            #   out what kind:
                           if { ![catch {file type $opt} fileType] } {
                               if {$VERBOSE} {
                                   send_user "\t$opt file type is $fileType\n"
                               }
                               if {$fileType == "directory"} {
                                  if {$VERBOSE} { send_user "\tSetting localDestDir to $opt\n"}
                                  uplevel #0 set localDestDir $opt
                               } \
                               elseif {$fileType == "file"} {
                                  if {$VERBOSE} {
                                     send_user "\tAdding $opt to fileList\n"
                                     send_user "\tOverwriting file $opt\n"
                                 }
                                  uplevel #0 lappend fileList $opt
                               }
                               continue
                           }
                           # Default: append the option to fileList
                           if {$VERBOSE} {
                              send_user "\tInterpreting $opt as a file\n"
                              send_user "\tAdding $opt to fileList "
                           }
                           uplevel #0 lappend fileList $opt
                           continue
                        } ;# End if $fileTestOut
                     }
                      # Check if this is an option to scp
                     if {[regexp -- {-[A-Za-z0-9]+} $opt]} {
                        if {![info exists scpOpts]} {
                           if {$VERBOSE} {
                              send_user "\t'$opt' is scp option.\n"
                           }
                           set scpOpts "$opt"
                           continue
                        } \
                        else {
                           if {$VERBOSE} {
                              send_user "\tAdding '$opt' to scpOpts.\n"
                           }
                           set scpOpts "$scpOpts $opt"
                        }
                     }
                     if {$VERBOSE} {
                        send_user "\tInterpreting $opt as file to copy\n"
                        send_user "\tDefault: copying TO host\n"
                     }
                     send_user "\nERROR: File $opt doesn't exist on local system.\n"
                     exit 1
                     if {$VERBOSE} {
                        send_user "END of default option\n"
                        send_user "Why are we here?!!\n"
                     }
                 }
      }
   }
}

#************************ Fix Known Hosts File **********************
#
# FixKnownHosts: Fix up the user's .ssh/known_hosts file when the
#                host's public key has changed (occurs often on
#                test systems because they are reinstalled).
#                This routine removes the offending key and lets
#                ssh add the new one.
#
#         Input: Hostname for the system to fix.
#        Output: Changes data in the user's .ssh/known_hosts file
#                This routine saves the old known_hosts file in
#                ~/.ssh with the PID for the caller.
#
#********************************************************************
proc FixKnownHosts {host} {
    global VERBOSE DRYRUN

    set knownHosts "~/.ssh/known_hosts"
    set newHosts ${knownHosts}.new
    set bckup ${knownHosts}.bck.[pid]
    if {![file exists $knownHosts]} {
        send_user "\nERROR:  Can't find $knownHosts to edit!\n"
		Done 1
    }

    # Check to see if the hostname exists in known_hosts
#    set grepRtn [catch {system grep $host $knownHosts} grepOut]
    set sshKeygenRtn [catch {system ssh-keygen -R $host} grepOut]
    if {$sshKeygenRtn} {
       # OK, the hostname is encrypted.  We'll just move known_hosts out of the
       #  way... 
       send_user "Moving old known_hosts out of the way (to $bckup) because of OS reinstall.\n"
       if {$DRYRUN} {
          send_user "mv $knownHosts $bckup\n"
       } \
       else {
         
         set mvRtn [catch {system mv $knownHosts $bckup} mvOut]
         if {$mvRtn} {
            send_user "\nERROR: Couldn't move $knownHosts to $bckup\n"
            send_user "Hostname is encrypted and couldn't move known_hosts out of the way\n"
            Done 1
         }
       }
    }
    return
}

#********************************************************************
#
# ConnectSsh: Connect using ssh.
#             Will run ssh to connect to the desired host
# Input:
#             user - login user name
#             host - host for login
#             rcmd - reference to command to run
#                    If this is set, run the command instead
#                    of logging in.
# Output:     Command output if command is not null
#             (global) spawn_id set to the ssh session
# Return:     0 if not connected
#             1 if connected
#
#********************************************************************
proc ConnectSsh {user passwd host rcmd} {
    global VERBOSE DRYRUN
    global domain nodomain
    global prompt root_prompt user_prompt
    global ilo_prompt oa_prompt
    global atILOprompt atOAprompt
    global passwdRequested
    global spawn_id
    global endl
    upvar $rcmd cmd

    if {![info exists prompt]} {set prompt "$root_prompt"}
    # Check to see if the hostname has a domain extension.
    #  Add domain if not
    if {! $nodomain} {
       set TmpHost [split $host {.}]
       if {[llength $TmpHost] < 2} {
           set host "${host}.${domain}"
       }
    }
    
     # Use this to see if we need to know if authenticationless
     #  connection is already set up.
    set passwdRequested 0

    set sshCmd "ssh ${user}@${host}"
    if [info exists cmd] {
        if {$cmd != "" && ![regexp {{}} $cmd] } {
           set sshCmd "$sshCmd $cmd"
        }
    }

    if {$VERBOSE} {
       send_user "\n\[SM] ConnectSsh:\n"
       send_user "\tsshCmd: $sshCmd\n"
    }
    if {$DRYRUN} {
       send_user "$sshCmd\n"
       return 1
    }

    set connected 0
    set maxTry 3
    set try 0
    while {!$connected && [expr $try < $maxTry]} {
        incr try
         # ssh to the system as the requested user.
		 # sshExit is the return value spawning the ssh command
		 # sshOut is the output of ssh.
        set sshExit [catch {eval spawn -noecho $sshCmd} sshOut]
        
         # test to see if we successfully got an ssh connection
        if {$sshExit} {
           send_user "ERROR: $sshOut"
           return $sshExit
        }
        if {$VERBOSE} {
           send_user "\n\tAfter spawning ssh command, return value is $sshExit\n"
        }
        
        expect {
               timeout {
                   send_user "ERROR: Timed out trying to connect to $host\n"
                   Done 1
		       }
        
               -re "connecting \(yes/no)?" {
                  # Haven't logged into this system before, so we need
                  #  to put the key into our .ssh/known_hosts.
                   send "yes\r"
                   exp_continue
               }
        
               -re "\[Pp]assword:" {
				   incr connected
                   set passwdRequested 1
                   send "$passwd\n"
                   exp_continue
               }
        
               -re "\[Cc]onnection \[Rr]efused" {
                    send_user "ERROR: Can't connect to $host\n"
                    return 0
               }
        
               -re "\[Nn]o \[Rr]oute" {
                    send_user "ERROR: Can't connect to $host\n"
                    return 0
               }
        
               -re "Name or service not known" {
                    if [info exists cmd] {
                       send_user "ERROR: Failed to run $cmd on $host\n"
                    } \
                    else {
                       send_user "ERROR: $host is not DNS resolvable.\n"
                    }
                    return 0
               }

               -re "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!" {
                    if {$VERBOSE} {send_user "\nUpdating \$HOME/.ssh/known_hosts\n"}
                    FixKnownHosts $host
               }
               -re "$ilo_prompt" {
                    set prompt "$ilo_prompt"
                    set connected 1
                    set atILOprompt 1
                }
        
               -re "$oa_prompt" {
                    set prompt "$oa_prompt"
                    set connected 1
                    set atOAprompt 1
               }
        
               -re "$root_prompt" {
                    set connected 1
                    set prompt "$root_prompt"
               }
        
               -re "$user_prompt" {
                    set connected 1
                    set prompt "$user_prompt"
               }
        
               -re "$prompt" {set connected 1}
               -re "$endl" {
                     # Stay alive in the case we're running a remote command
                     exp_continue
               }
               default {
                  # This is the case where we have commands to run
                  #  so return the return value of the command
                  if {$VERBOSE} {
                     send_user "\nConnectSsh: Default case, returning $sshExit\n"
                  }
                  return $sshExit
               }
        }
    }
    if {$VERBOSE && [info exists passwdRequested]} {
       send_user "\n\[ConnectSsh] end\n"
       send_user "\tconnected = $connected\n"
       send_user "\tpasswdRequested = $passwdRequested\n"
    }
    return $connected
} ;# End ConnectSsh

#********************************************************************
# Scp: copy a file to a system
# Input:
#        host       - host to copy to/from
#        copyList   - file or directory list
#        cpFlags   - Copy flags
#     globals:
#        copyTo     - Flag
#                      0 -> copy from host 
#                      1 -> copy to host 
#
# Return: void
#********************************************************************
proc Scp {cpDirection user host fileListRef scpFlagsRef} {
    global VERBOSE DRYRUN
    global domain nodomain
    global copyTo passwd
    global localDestDir

    # Get the flags for scp from the caller
    # This allows us to not have anything set.
    upvar $scpFlagsRef scpFlags
    upvar $fileListRef fileList
     
    if {$VERBOSE} {
       send_user "\n\[SM] SCP:\n"
       send_user "\tcpDirection = $cpDirection\n"
       send_user "\tuser = $user\n"
       send_user "\thost = $host\n"
       if [info exists scpFlags] {
          send_user "\tscpFlags = '$scpFlags'\n"
       } \
       else {
          send_user "\tNo options for scp\n"
       }
       if [info exists fileList] {
          send_user "\tfileList = '$fileList'\n"
       } \
       else {
          send_user "\tNo file to scp $Direction $host\n"
       }
    }
    if {[regexp -nocase {to} $cpDirection]} {
       set copyTo 1
    } \
    else {
       set copyTo 0
    }
    # check for consistency of info
    CheckScpInfo host fileList

    # Get the host name to append the domain
    if {[regexp {:} $host]} {
        set hostParts [split $host {:}]
        if {[llength $hostParts] != 2 } {
           send_user "\nERROR:  Sorry, I don't know what I did wrong....\n"
           send_user "I have the following host name as a host to copy files from,\n"
           send_user "\t$host\n"
           send_user "However, I can't seem to parse info into a host and a file\n"
           Done 1
        }
        set host [lindex $hostParts 0]
        set destDir [lindex $hostParts 1]
    }
    # Check to see if the hostname has a domain extension.
    #  Add $domain if not.
    if {! $nodomain} {
       set TmpHost [split $host {.}]
       if {[llength $TmpHost] < 2} {
           set host "${host}.${domain}"
       }
    }
    
     # scp files to/from the system as the requested user.
    if {$copyTo} {
        if {[info exists targetFile]} { set destDir $destDir/$targetFile }
		if {[info exists scpFlags]} {
           set cmd "scp $scpFlags $fileList ${user}@${host}:$destDir"
		} \
		else {
           set cmd "scp $fileList ${user}@${host}:$destDir"
		}
    } \
    else {
        if {[info exists targetFile]} { set localDestDir $targetFile }
		if {[info exists scpFlags]} {
           set cmd "scp $scpFlags ${user}@${host}:$destDir $localDestDir"
		} \
		else {
           set cmd "scp ${user}@${host}:$destDir $localDestDir"
		}
    }
    
    if {$VERBOSE} {
       send_user "$cmd\n"
    }
    if {$DRYRUN} {
       return
    }
    set scpExit [catch {eval spawn -noecho $cmd} sshOut]
     # test to see if we successfully ssh'ed into the system:
    if {$scpExit} {
       send_user "ERROR: $sshOut"
       Done $scpExit
    }
    
    
    expect {
           timeout {
               send_user "Can't connect to $host"
    		   Done 1
    		}
    
              # Haven't logged into this system before, so we need
    		  #  to put the key into .ssh/known_hosts.
            -re "connecting \(yes/no)?" {
    		   send "yes\r"
    		   exp_continue
    		}
    
    		-re "\[Pp]assword:" {
    		   send "$passwd\n"
    		   exp_continue
    		}
    
            -re "Name or service not known" {
    		    send_user "\n\tERROR: Couldn't connect to $host\n"
    		    Done 1
    		}

            -re "WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!" {
                 if {$VERBOSE} {send_user "\nUpdating \$HOME/.ssh/known_hosts\n"}
                 FixKnownHosts $host
            }
    }
}  ;# End Scp

#********************************************************************
# CheckScpInfo: Verify that all the information required to copy files
#               was provided
# Input:
#        rhost       - reference to host variable
#        rfileList   - reference to file or directory list
#     globals:
#        copyTo     - Flag
#                      0 -> copy from host 
#                      1 -> copy to host 
#
# Return: void
#         Will exit to calling shell if failure.
#********************************************************************
proc CheckScpInfo {rhost rfileList} {
    global VERBOSE failures
    global copyTo

    # Check for complete information:
    upvar $rhost host
    upvar $rfileList fileList

     # Is the host to copy to set?  
    if {![info exists host]} {
       send_user "\nERROR:  You must supply a system copy files to!\n"
       if [info exists fileList] {
           send_user "\tIn case here was a parsing error, the list of files to copy is\n"
           send_user "\t '$fileList'\n"
           exit 1
       } 
       ScpUsage 1
    }
    
     # Do we have a file list?
    if {$copyTo && ![info exists fileList]} {
       send_user "\nERROR:  You must supply a list of files to copy!\n"
       if [info exists host] {
           set verb "to"
           if {!$copyTo} { set verb "from" }
           send_user "\tIn case here was a parsing error, the host to copy $verb is\n"
           send_user "\t '$host'\n"
           exit 1
       } 
       ScpUsage 1
    }
}  ;# End CheckScpInfo

##
# vim: tabstop=4
# -*- tab-width:4 -*-
##
