add optional webui restart

This commit is contained in:
Greg T. Wallace 2024-02-05 18:25:55 -05:00
parent 29cd44077b
commit 2b46f33af8
6 changed files with 103 additions and 28 deletions

View file

@ -11,6 +11,9 @@ const createDefaultOutFilePath = "apctool.p15"
// cmdCreate is the app's command to create an apc p15 file from key and cert
// pem files
func (app *app) cmdCreate(_ context.Context, args []string) error {
// done
defer app.stdLogger.Println("create: done")
// extra args == error
if len(args) != 0 {
return fmt.Errorf("create: failed, %w (%d)", ErrExtraArgs, len(args))

View file

@ -16,6 +16,9 @@ import (
// cmdInstall is the app's command to create apc p15 file content from key and cert
// pem files and upload the p15 to the specified APC UPS
func (app *app) cmdInstall(cmdCtx context.Context, args []string) error {
// done
defer app.stdLogger.Println("install: done")
// extra args == error
if len(args) != 0 {
return fmt.Errorf("install: failed, %w (%d)", ErrExtraArgs, len(args))
@ -129,7 +132,7 @@ func (app *app) cmdInstall(cmdCtx context.Context, args []string) error {
HostKeyCallback: hk,
// reasonable timeout for file copy
Timeout: scpTimeout,
Timeout: sshScpTimeout,
}
// connect to ups over SSH
@ -137,15 +140,37 @@ func (app *app) cmdInstall(cmdCtx context.Context, args []string) error {
if err != nil {
return fmt.Errorf("install: failed to connect to host (%w)", err)
}
defer client.Close()
// send file to UPS
err = scpSendFileToUPS(client, apcFile)
err = sshScpSendFileToUPS(client, apcFile)
if err != nil {
return fmt.Errorf("install: failed to send p15 file to ups over scp (%w)", err)
}
// done
// installed
app.stdLogger.Printf("install: apc p15 file installed on %s", *app.config.install.hostAndPort)
// restart UPS webUI
if app.config.install.restartWebUI != nil && *app.config.install.restartWebUI {
app.stdLogger.Println("install: sending restart command")
// connect to ups over SSH
// opening a second session doesn't seem to work with my NMC2 for some reason, so make
// a new connection instead
client, err = ssh.Dial("tcp", *app.config.install.hostAndPort, config)
if err != nil {
return fmt.Errorf("install: failed to reconnect to host to send webui restart command (%w)", err)
}
defer client.Close()
err = sshResetUPSWebUI(client)
if err != nil {
return fmt.Errorf("install: failed to send webui restart command (%w)", err)
}
app.stdLogger.Println("install: sent webui restart command")
}
return nil
}

View file

@ -36,6 +36,7 @@ type config struct {
fingerprint *string
username *string
password *string
restartWebUI *bool
insecureCipher *bool
}
}
@ -93,6 +94,7 @@ func (app *app) getConfig(args []string) error {
cfg.install.fingerprint = installFlags.StringLong("fingerprint", "", "the SHA256 fingerprint value of the ups' ssh server")
cfg.install.username = installFlags.StringLong("username", "", "username to login to the apc ups")
cfg.install.password = installFlags.StringLong("password", "", "password to login to the apc ups")
cfg.install.restartWebUI = installFlags.BoolLong("restartwebui", "some devices may need a webui restart to begin using the new cert, enabling this option sends the restart command after the p15 is installed")
cfg.install.insecureCipher = installFlags.BoolLong("insecurecipher", "allows the use of insecure ssh ciphers (NOT recommended)")
installCmd := &ff.Command{

45
pkg/app/ssh_resetwebui.go Normal file
View file

@ -0,0 +1,45 @@
package app
import (
"errors"
"fmt"
"golang.org/x/crypto/ssh"
)
// sshResetUPSWebUI sends a command to the UPS to restart the WebUI. This
// command is supposed to be required to load the new cert, but that
// doesn't seem to be true (at least it isn't on my UPS). Adding the
// option though, in case other UPS might need it.
func sshResetUPSWebUI(client *ssh.Client) error {
// make session to use for restart command
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("ssh: restart: failed to create session (%w)", err)
}
defer session.Close()
// start shell
err = session.Shell()
if err != nil {
return fmt.Errorf("ssh: restart: failed to start shell (%w)", err)
}
// execure reboot via SendRequest
payload := []byte("reboot -Y")
payloadLen := uint8(len(payload))
payload = append([]byte{0, 0, 0, payloadLen}, payload...)
ok, err := session.SendRequest("exec", true, payload)
if err != nil {
return fmt.Errorf("ssh: scp: failed to execute scp cmd (%w)", err)
}
if !ok {
return errors.New("ssh: scp: execute scp cmd not ok")
}
// don't read remote output, as nothing interesting actually outputs
// done
return nil
}

View file

@ -6,13 +6,13 @@ import (
"io"
)
// scpCheckResponse reads the output from the remote and returns an error
// sshCheckResponse reads the output from the remote and returns an error
// if the remote output was not 0
func scpCheckResponse(remoteOutPipe io.Reader) error {
func sshCheckResponse(remoteOutPipe io.Reader) error {
buffer := make([]uint8, 1)
_, err := remoteOutPipe.Read(buffer)
if err != nil {
return fmt.Errorf("scp: failed to make read output buffer (%w)", err)
return fmt.Errorf("ssh: failed to make read output buffer (%w)", err)
}
responseType := buffer[0]
@ -21,13 +21,13 @@ func scpCheckResponse(remoteOutPipe io.Reader) error {
bufferedReader := bufio.NewReader(remoteOutPipe)
message, err = bufferedReader.ReadString('\n')
if err != nil {
return fmt.Errorf("scp: failed to read output buffer (%w)", err)
return fmt.Errorf("ssh: failed to read output buffer (%w)", err)
}
}
// if not 0 (aka OK)
if responseType != 0 {
return fmt.Errorf("scp: remote returned error (%d: %s)", responseType, message)
return fmt.Errorf("ssh: remote returned error (%d: %s)", responseType, message)
}
return nil

View file

@ -16,20 +16,20 @@ import (
// command instead of relying on something like github.com/bramvdbogaerde/go-scp
const (
scpP15Destination = "/ssl/defaultcert.p15"
scpP15PermissionsStr = "0600"
sshScpP15Destination = "/ssl/defaultcert.p15"
sshScpP15PermissionsStr = "0600"
scpTimeout = 90 * time.Second
sshScpTimeout = 90 * time.Second
)
// scpSendFileToUPS sends the p15File to the APC UPS via the SCP protocol. it is
// sshScpSendFileToUPS sends the p15File to the APC UPS via the SCP protocol. it is
// automatically placed in the correct directory and will overwrite any existing
// file
func scpSendFileToUPS(client *ssh.Client, p15File []byte) error {
func sshScpSendFileToUPS(client *ssh.Client, p15File []byte) error {
// make session to use for SCP
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("scp: failed to create session (%w)", err)
return fmt.Errorf("ssh: scp: failed to create session (%w)", err)
}
defer session.Close()
@ -49,55 +49,55 @@ func scpSendFileToUPS(client *ssh.Client, p15File []byte) error {
// Go implementation sends additional 0x22 bytes when using Run() (as
// compared to putty's scp tool). these additional bytes seem to cause the
// apc ups to fail execution of the command
payload := []byte(fmt.Sprintf("scp -q -t %s", scpP15Destination))
payload := []byte(fmt.Sprintf("scp -q -t %s", sshScpP15Destination))
payloadLen := uint8(len(payload))
payload = append([]byte{0, 0, 0, payloadLen}, payload...)
ok, err := session.SendRequest("exec", true, payload)
if err != nil {
return fmt.Errorf("scp: failed to execute scp cmd (%w)", err)
return fmt.Errorf("ssh: scp: failed to execute scp cmd (%w)", err)
}
if !ok {
return errors.New("scp: execute scp cmd not ok")
return errors.New("ssh: scp: execute scp cmd not ok")
}
// check remote response
// Note: File upload may not work if the client doesn't actually read from
// the remote output.
err = scpCheckResponse(out)
err = sshCheckResponse(out)
if err != nil {
return fmt.Errorf("scp: failed to send scp cmd (bad remote response) (%w)", err)
return fmt.Errorf("ssh: scp: failed to send scp cmd (bad remote response) (%w)", err)
}
// just file name (without path)
filename := path.Base(scpP15Destination)
filename := path.Base(sshScpP15Destination)
// send file header
_, err = fmt.Fprintln(w, "C"+scpP15PermissionsStr, len(p15File), filename)
_, err = fmt.Fprintln(w, "C"+sshScpP15PermissionsStr, len(p15File), filename)
if err != nil {
return fmt.Errorf("scp: failed to send file info (%w)", err)
return fmt.Errorf("ssh: scp: failed to send file info (%w)", err)
}
err = scpCheckResponse(out)
err = sshCheckResponse(out)
if err != nil {
return fmt.Errorf("scp: failed to send file info (bad remote response) (%w)", err)
return fmt.Errorf("ssh: scp: failed to send file info (bad remote response) (%w)", err)
}
// send actual file
_, err = io.Copy(w, bytes.NewReader(p15File))
if err != nil {
return fmt.Errorf("scp: failed to send file(%w)", err)
return fmt.Errorf("ssh: scp: failed to send file(%w)", err)
}
// send file end
_, err = fmt.Fprint(w, "\x00")
if err != nil {
return fmt.Errorf("scp: failed to send final 00 byte (%w)", err)
return fmt.Errorf("ssh: scp: failed to send final 00 byte (%w)", err)
}
err = scpCheckResponse(out)
err = sshCheckResponse(out)
if err != nil {
return fmt.Errorf("scp: failed to send file (bad remote response) (%w)", err)
return fmt.Errorf("ssh: scp: failed to send file (bad remote response) (%w)", err)
}
// done