From 2b46f33af826062c2963151990992fff2687510a Mon Sep 17 00:00:00 2001 From: "Greg T. Wallace" Date: Mon, 5 Feb 2024 18:25:55 -0500 Subject: [PATCH] add optional webui restart --- pkg/app/cmd_create.go | 3 ++ pkg/app/cmd_install.go | 31 ++++++++++++-- pkg/app/config.go | 2 + pkg/app/ssh_resetwebui.go | 45 ++++++++++++++++++++ pkg/app/{scp_response.go => ssh_response.go} | 10 ++--- pkg/app/{scp.go => ssh_scp.go} | 40 ++++++++--------- 6 files changed, 103 insertions(+), 28 deletions(-) create mode 100644 pkg/app/ssh_resetwebui.go rename pkg/app/{scp_response.go => ssh_response.go} (59%) rename pkg/app/{scp.go => ssh_scp.go} (57%) diff --git a/pkg/app/cmd_create.go b/pkg/app/cmd_create.go index e0e0291..51cf1cc 100644 --- a/pkg/app/cmd_create.go +++ b/pkg/app/cmd_create.go @@ -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)) diff --git a/pkg/app/cmd_install.go b/pkg/app/cmd_install.go index fd60efc..5d826ec 100644 --- a/pkg/app/cmd_install.go +++ b/pkg/app/cmd_install.go @@ -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 } diff --git a/pkg/app/config.go b/pkg/app/config.go index ed6cd9a..893bb9c 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -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{ diff --git a/pkg/app/ssh_resetwebui.go b/pkg/app/ssh_resetwebui.go new file mode 100644 index 0000000..52f90d9 --- /dev/null +++ b/pkg/app/ssh_resetwebui.go @@ -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 +} diff --git a/pkg/app/scp_response.go b/pkg/app/ssh_response.go similarity index 59% rename from pkg/app/scp_response.go rename to pkg/app/ssh_response.go index 9968366..3b44523 100644 --- a/pkg/app/scp_response.go +++ b/pkg/app/ssh_response.go @@ -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 diff --git a/pkg/app/scp.go b/pkg/app/ssh_scp.go similarity index 57% rename from pkg/app/scp.go rename to pkg/app/ssh_scp.go index 9fe55e4..54f216c 100644 --- a/pkg/app/scp.go +++ b/pkg/app/ssh_scp.go @@ -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