add breakglass SSH wrapper tool (for convenience)
This commit is contained in:
parent
6a8318bdb5
commit
8157f8ee60
224
cmd/breakglass/breakglass.go
Normal file
224
cmd/breakglass/breakglass.go
Normal file
@ -0,0 +1,224 @@
|
||||
// Binary breakglass is a wrapper around SSH, starting breakglass on the
|
||||
// destination gokrazy installation <hostname> first.
|
||||
//
|
||||
// Example:
|
||||
// breakglass gokrazy
|
||||
// breakglass -debug_tarball_pattern=$HOME/gokrazy/debug-\${GOARCH}.tar gokrazy
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type bg struct {
|
||||
// config
|
||||
hostname string
|
||||
pw string
|
||||
forceRestart bool
|
||||
|
||||
// state
|
||||
GOARCH string
|
||||
}
|
||||
|
||||
func (bg *bg) startBreakglass() error {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := &http.Client{Jar: jar}
|
||||
urlPrefix := "http://gokrazy:" + bg.pw + "@" + bg.hostname
|
||||
form, err := client.Get(urlPrefix + "/status?path=/user/breakglass")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if form.StatusCode == http.StatusNotFound {
|
||||
fmt.Fprintf(os.Stderr, "Hint: have you installed Go package github.com/gokrazy/breakglass on your gokrazy installation %q?\n", bg.hostname)
|
||||
}
|
||||
if got, want := form.StatusCode, http.StatusOK; got != want {
|
||||
b, _ := ioutil.ReadAll(form.Body)
|
||||
return fmt.Errorf("starting breakglass: unexpected HTTP status: got %v (%s), want %v",
|
||||
form.Status,
|
||||
strings.TrimSpace(string(b)),
|
||||
want)
|
||||
}
|
||||
var xsrfToken string
|
||||
for _, c := range form.Cookies() {
|
||||
if c.Name != "gokrazy_xsrf" {
|
||||
continue
|
||||
}
|
||||
xsrfToken = c.Value
|
||||
break
|
||||
}
|
||||
if xsrfToken == "" {
|
||||
return fmt.Errorf("no gokrazy_xsrf cookie received")
|
||||
}
|
||||
|
||||
bg.GOARCH = form.Header.Get("X-Gokrazy-Goarch")
|
||||
|
||||
if form.Header.Get("X-Gokrazy-Status") == "started" && !bg.forceRestart {
|
||||
return nil // breakglass already running
|
||||
}
|
||||
|
||||
resp, err := client.Post(urlPrefix+"/restart?path=/user/breakglass&xsrftoken="+xsrfToken, "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if got, want := resp.StatusCode, http.StatusOK; got != want {
|
||||
return fmt.Errorf("restarting breakglass: unexpected HTTP status code: got %d, want %d", got, want)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pollPort(ctx context.Context, hostname, port string) error {
|
||||
var d net.Dialer
|
||||
for ctx.Err() == nil {
|
||||
conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(hostname, port))
|
||||
if err != nil {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (bg *bg) uploadDebugTarball(debugTarballPattern string) error {
|
||||
if debugTarballPattern == "" {
|
||||
return nil // nothing to do
|
||||
}
|
||||
debugTarball := strings.ReplaceAll(
|
||||
debugTarballPattern,
|
||||
"${GOARCH}",
|
||||
bg.GOARCH)
|
||||
st, err := os.Stat(debugTarball)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var contents []string
|
||||
f, err := os.Open(debugTarball)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
tr := tar.NewReader(f)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break // end of archive
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contents = append(contents, fmt.Sprintf("%s (%v bytes)", hdr.Name, hdr.Size))
|
||||
}
|
||||
log.Printf("uploading debug tarball:\n\t%s\n\t(last modified: %v, %v ago)\n\t\t%s",
|
||||
debugTarball,
|
||||
st.ModTime().Format("2006-01-02 15:04:05 -0700"),
|
||||
time.Since(st.ModTime()).Round(1*time.Second),
|
||||
strings.Join(contents, "\n\t\t"))
|
||||
|
||||
scp := exec.Command("scp", debugTarball, bg.hostname+":")
|
||||
scp.Stderr = os.Stderr
|
||||
if err := scp.Run(); err != nil {
|
||||
return fmt.Errorf("%v: %v", scp.Args, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func breakglass() error {
|
||||
var (
|
||||
forceRestart = flag.Bool(
|
||||
"force_restart",
|
||||
false,
|
||||
"restart breakglass if it is already running")
|
||||
|
||||
debugTarballPattern = flag.String(
|
||||
"debug_tarball_pattern",
|
||||
"",
|
||||
"If non-empty, a pattern resulting in the path to a debug.tar archive that should be copied to breakglass before starting a shell. This can be used to make additional tools available for debugging. All occurrences of ${GOARCH} will be replaced with the runtime.GOARCH of the remote gokrazy installation.")
|
||||
)
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, " breakglass gokrazy\n")
|
||||
fmt.Fprintf(os.Stderr, " breakglass -debug_tarball_pattern=$HOME/gokrazy/debug-\\${GOARCH}.tar gokrazy\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\nOptions:\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
if flag.NArg() < 1 {
|
||||
log.Fatalf("syntax: breakglass <hostname> [command]")
|
||||
}
|
||||
hostname := flag.Arg(0)
|
||||
|
||||
dir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := ioutil.ReadFile(filepath.Join(dir, "gokrazy", "http-password.txt"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pw := strings.TrimSpace(string(b))
|
||||
bg := &bg{
|
||||
hostname: hostname,
|
||||
pw: pw,
|
||||
forceRestart: *forceRestart,
|
||||
}
|
||||
|
||||
log.Printf("checking breakglass status on gokrazy installation %q", hostname)
|
||||
if err := bg.startBreakglass(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(250 * time.Millisecond) // give gokrazy some time to restart
|
||||
|
||||
log.Printf("polling SSH port to become available")
|
||||
ctx, canc := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer canc()
|
||||
if err := pollPort(ctx, hostname, "ssh"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := bg.uploadDebugTarball(*debugTarballPattern); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ssh := exec.Command("ssh", hostname)
|
||||
if args := flag.Args()[1:]; len(args) > 0 {
|
||||
ssh.Args = append(ssh.Args, args...)
|
||||
}
|
||||
log.Printf("%v", ssh.Args)
|
||||
ssh.Stdin = os.Stdin
|
||||
ssh.Stdout = os.Stdout
|
||||
ssh.Stderr = os.Stderr
|
||||
if err := ssh.Run(); err != nil {
|
||||
return fmt.Errorf("%v: %v", ssh.Args, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := breakglass(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user