2024-06-07 02:51:12 +00:00
|
|
|
package apcssh
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2024-06-19 23:56:17 +00:00
|
|
|
"errors"
|
2024-06-07 02:51:12 +00:00
|
|
|
"fmt"
|
|
|
|
"strings"
|
2024-06-19 23:56:17 +00:00
|
|
|
"time"
|
2024-06-07 02:51:12 +00:00
|
|
|
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
)
|
|
|
|
|
2024-06-19 23:56:17 +00:00
|
|
|
// Abort shell connection if UPS doesn't send a recognizable response within
|
|
|
|
// the specified timeouts; Cmd timeout is very long as it is unlikely to be
|
|
|
|
// needed but still exists to avoid an indefinite hang in the unlikely event
|
|
|
|
// something does go wrong at that part of the app
|
|
|
|
const (
|
|
|
|
shellTimeoutLogin = 20 * time.Second
|
|
|
|
shellTimeoutCmd = 5 * time.Minute
|
|
|
|
)
|
|
|
|
|
|
|
|
// upsCmdResult is a structure that holds all of a shell commands results
|
|
|
|
type upsCmdResult struct {
|
2024-06-07 02:51:12 +00:00
|
|
|
command string
|
|
|
|
code string
|
|
|
|
codeText string
|
|
|
|
resultText string
|
|
|
|
}
|
|
|
|
|
|
|
|
// cmd creates an interactive shell and executes the specified command
|
2024-06-19 23:56:17 +00:00
|
|
|
func (cli *Client) cmd(command string) (*upsCmdResult, error) {
|
2024-06-07 02:51:12 +00:00
|
|
|
// connect
|
|
|
|
sshClient, err := ssh.Dial("tcp", cli.hostname, cli.sshCfg)
|
|
|
|
if err != nil {
|
2024-06-19 23:56:17 +00:00
|
|
|
return nil, fmt.Errorf("failed to dial client (%w)", err)
|
2024-06-07 02:51:12 +00:00
|
|
|
}
|
|
|
|
defer sshClient.Close()
|
|
|
|
|
|
|
|
session, err := sshClient.NewSession()
|
|
|
|
if err != nil {
|
2024-06-19 23:56:17 +00:00
|
|
|
return nil, fmt.Errorf("failed to create session (%w)", err)
|
2024-06-07 02:51:12 +00:00
|
|
|
}
|
|
|
|
defer session.Close()
|
|
|
|
|
|
|
|
// pipes to send shell command to; and to receive repsonse
|
|
|
|
sshInput, err := session.StdinPipe()
|
|
|
|
if err != nil {
|
2024-06-19 23:56:17 +00:00
|
|
|
return nil, fmt.Errorf("failed to make shell input pipe (%w)", err)
|
2024-06-07 02:51:12 +00:00
|
|
|
}
|
|
|
|
sshOutput, err := session.StdoutPipe()
|
|
|
|
if err != nil {
|
2024-06-19 23:56:17 +00:00
|
|
|
return nil, fmt.Errorf("failed to make shell output pipe (%w)", err)
|
2024-06-07 02:51:12 +00:00
|
|
|
}
|
|
|
|
|
2024-06-19 23:56:17 +00:00
|
|
|
// make scanner to read shell output continuously
|
2024-06-07 02:51:12 +00:00
|
|
|
scanner := bufio.NewScanner(sshOutput)
|
|
|
|
scanner.Split(scanAPCShell)
|
|
|
|
|
|
|
|
// start interactive shell
|
|
|
|
if err := session.Shell(); err != nil {
|
2024-06-19 23:56:17 +00:00
|
|
|
return nil, fmt.Errorf("failed to start shell (%w)", err)
|
2024-06-07 02:51:12 +00:00
|
|
|
}
|
2024-06-19 23:56:17 +00:00
|
|
|
|
|
|
|
// use a timer to close the session early in case Scan() hangs (which can
|
|
|
|
// happen if the UPS provides output this app does not understand)
|
|
|
|
cancelAbort := make(chan struct{})
|
|
|
|
defer close(cancelAbort)
|
|
|
|
go func() {
|
|
|
|
select {
|
|
|
|
case <-time.After(shellTimeoutLogin):
|
|
|
|
_ = session.Close()
|
|
|
|
|
|
|
|
case <-cancelAbort:
|
|
|
|
// aborted cancel (i.e., succesful Scan())
|
2024-06-07 02:51:12 +00:00
|
|
|
}
|
2024-06-19 23:56:17 +00:00
|
|
|
}()
|
|
|
|
|
|
|
|
// check shell response after connect
|
|
|
|
scannedOk := scanner.Scan()
|
|
|
|
// if failed to scan (e.g., timer closed the session after timeout)
|
|
|
|
if !scannedOk {
|
|
|
|
return nil, errors.New("shell did not return parsable login response")
|
2024-06-07 02:51:12 +00:00
|
|
|
}
|
2024-06-19 23:56:17 +00:00
|
|
|
// success; cancel abort timer
|
|
|
|
cancelAbort <- struct{}{}
|
|
|
|
// discard the initial shell response (login message(s) / initial shell prompt)
|
|
|
|
_ = scanner.Bytes()
|
2024-06-07 02:51:12 +00:00
|
|
|
|
|
|
|
// send command
|
|
|
|
_, err = fmt.Fprint(sshInput, command+"\n")
|
|
|
|
if err != nil {
|
2024-06-19 23:56:17 +00:00
|
|
|
return nil, fmt.Errorf("failed to send shell command (%w)", err)
|
2024-06-07 02:51:12 +00:00
|
|
|
}
|
|
|
|
|
2024-06-19 23:56:17 +00:00
|
|
|
// use a timer to close the session early in case Scan() hangs (which can
|
|
|
|
// happen if the UPS provides output this app does not understand);
|
|
|
|
// since initial login message Scan() was okay, it is relatively unlikely this
|
|
|
|
// will hang
|
|
|
|
go func() {
|
|
|
|
select {
|
|
|
|
case <-time.After(shellTimeoutCmd):
|
|
|
|
_ = session.Close()
|
2024-06-07 02:51:12 +00:00
|
|
|
|
2024-06-19 23:56:17 +00:00
|
|
|
case <-cancelAbort:
|
|
|
|
// aborted cancel (i.e., succesful Scan())
|
|
|
|
}
|
|
|
|
}()
|
2024-06-07 02:51:12 +00:00
|
|
|
|
2024-06-19 23:56:17 +00:00
|
|
|
// check shell response to command
|
|
|
|
scannedOk = scanner.Scan()
|
|
|
|
// if failed to scan (e.g., timer closed the session after timeout)
|
|
|
|
if !scannedOk {
|
|
|
|
return nil, fmt.Errorf("shell did not return parsable response to cmd '%s'", command)
|
|
|
|
}
|
|
|
|
// success; cancel abort timer
|
|
|
|
cancelAbort <- struct{}{}
|
2024-06-07 02:51:12 +00:00
|
|
|
|
2024-06-19 23:56:17 +00:00
|
|
|
// parse the UPS response into result struct and return
|
|
|
|
upsRawResponse := string(scanner.Bytes())
|
|
|
|
result := &upsCmdResult{}
|
2024-06-07 02:51:12 +00:00
|
|
|
|
2024-06-19 23:56:17 +00:00
|
|
|
cmdIndx := strings.Index(upsRawResponse, "\n")
|
|
|
|
result.command = upsRawResponse[:cmdIndx-1]
|
|
|
|
upsRawResponse = upsRawResponse[cmdIndx+1:]
|
|
|
|
|
|
|
|
codeIndx := strings.Index(upsRawResponse, ": ")
|
|
|
|
result.code = upsRawResponse[:codeIndx]
|
|
|
|
upsRawResponse = upsRawResponse[codeIndx+2:]
|
|
|
|
|
|
|
|
codeTxtIndx := strings.Index(upsRawResponse, "\n")
|
|
|
|
result.codeText = upsRawResponse[:codeTxtIndx-1]
|
|
|
|
|
|
|
|
// avoid out of bounds if no result text
|
|
|
|
if codeTxtIndx+1 <= len(upsRawResponse)-2 {
|
|
|
|
result.resultText = upsRawResponse[codeTxtIndx+1 : len(upsRawResponse)-2]
|
2024-06-07 02:51:12 +00:00
|
|
|
}
|
|
|
|
|
2024-06-19 23:56:17 +00:00
|
|
|
return result, nil
|
2024-06-07 02:51:12 +00:00
|
|
|
}
|