Added target and terminal support
[src/xds/xds-cli.git] / cmd-target.go
diff --git a/cmd-target.go b/cmd-target.go
new file mode 100644 (file)
index 0000000..688aef6
--- /dev/null
@@ -0,0 +1,546 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author Sebastien Douheret <sebastien@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package main
+
+import (
+       "bufio"
+       "encoding/json"
+       "fmt"
+       "os"
+       "sort"
+       "strings"
+       "syscall"
+       "time"
+
+       "github.com/golang/crypto/ssh/terminal"
+
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xaapiv1"
+       "github.com/urfave/cli"
+)
+
+func initCmdTargets(cmdDef *[]cli.Command) {
+       *cmdDef = append(*cmdDef, cli.Command{
+               Name:     "targets",
+               Aliases:  []string{"tgt"},
+               HideHelp: true,
+               Usage:    "targets commands group",
+               Subcommands: []cli.Command{
+                       {
+                               Name:    "add",
+                               Aliases: []string{"a"},
+                               Usage:   "Add a new target",
+                               Action:  targetsAdd,
+                               Flags: []cli.Flag{
+                                       cli.StringFlag{
+                                               Name:  "name, n",
+                                               Usage: "target name (free form string)",
+                                       },
+                                       cli.StringFlag{
+                                               Name:  "ip",
+                                               Usage: "IP address",
+                                       },
+                                       cli.BoolFlag{
+                                               Name:  "short, s",
+                                               Usage: "short output, only print create target id (useful from scripting)",
+                                       },
+                                       cli.StringFlag{
+                                               Name:  "type, t",
+                                               Usage: "target type (standard|std)",
+                                       },
+                               },
+                       },
+                       {
+                               Name:   "get",
+                               Usage:  "Get properties of a target",
+                               Action: targetsGet,
+                               Flags: []cli.Flag{
+                                       cli.StringFlag{
+                                               Name:   "id",
+                                               Usage:  "target id",
+                                               EnvVar: "XDS_TARGET_ID",
+                                       },
+                               },
+                       },
+                       {
+                               Name:    "list",
+                               Aliases: []string{"ls"},
+                               Usage:   "List existing targets",
+                               Action:  targetsList,
+                               Flags: []cli.Flag{
+                                       cli.BoolFlag{
+                                               Name:  "verbose, v",
+                                               Usage: "display verbose output",
+                                       },
+                               },
+                       },
+                       {
+                               Name:    "remove",
+                               Aliases: []string{"rm"},
+                               Usage:   "Remove an existing target",
+                               Action:  targetsRemove,
+                               Flags: []cli.Flag{
+                                       cli.StringFlag{
+                                               Name:   "id",
+                                               Usage:  "target id",
+                                               EnvVar: "XDS_TARGET_ID",
+                                       },
+                                       cli.BoolFlag{
+                                               Name:  "force, f",
+                                               Usage: "remove confirmation prompt before removal",
+                                       },
+                               },
+                       },
+                       {
+                               Name:    "terminal",
+                               Aliases: []string{"term"},
+                               Usage:   "Open a target terminal",
+                               Action:  terminalOpen,
+                               Flags: []cli.Flag{
+                                       cli.StringFlag{
+                                               Name:   "id",
+                                               Usage:  "target id",
+                                               EnvVar: "XDS_TARGET_ID",
+                                       },
+                                       cli.StringSliceFlag{
+                                               Name:  "options, o",
+                                               Usage: "passthrough options set to command line used to start terminal",
+                                       },
+                                       cli.StringFlag{
+                                               Name:   "termId, tid",
+                                               Usage:  "terminal id",
+                                               EnvVar: "XDS_TERMINAL_ID",
+                                       },
+                                       cli.StringFlag{
+                                               Name:   "user, u",
+                                               Usage:  "user name used to connect terminal",
+                                               EnvVar: "XDS_TERMINAL_USER",
+                                       },
+                               },
+                       },
+                       {
+                               Name:    "terminal-remove",
+                               Aliases: []string{"term-rm"},
+                               Usage:   "Remove a target terminal",
+                               Action:  terminalRemove,
+                               Flags: []cli.Flag{
+                                       cli.StringFlag{
+                                               Name:   "id",
+                                               Usage:  "target id",
+                                               EnvVar: "XDS_TARGET_ID",
+                                       },
+                                       cli.StringFlag{
+                                               Name:   "termId, tid",
+                                               Usage:  "terminal id",
+                                               EnvVar: "XDS_TERMINAL_ID",
+                                       },
+                               },
+                       },
+               },
+       })
+}
+
+func targetsList(ctx *cli.Context) error {
+       // Get targets list
+       tgts := []xaapiv1.TargetConfig{}
+       if err := TargetsListGet(&tgts); err != nil {
+               return cli.NewExitError(err.Error(), 1)
+       }
+       _displayTargets(tgts, ctx.Bool("verbose"))
+       return nil
+}
+
+func targetsGet(ctx *cli.Context) error {
+       id := GetID(ctx)
+       if id == "" {
+               return cli.NewExitError("id parameter or option must be set", 1)
+       }
+       tgts := make([]xaapiv1.TargetConfig, 1)
+       url := XdsServerComputeURL("/targets/" + id)
+       if err := HTTPCli.Get(url, &tgts[0]); err != nil {
+               return cli.NewExitError(err, 1)
+       }
+       _displayTargets(tgts, true)
+       return nil
+}
+
+func _displayTargets(tgts []xaapiv1.TargetConfig, verbose bool) {
+       // Display result
+       first := true
+       writer := NewTableWriter()
+       for _, tgt := range tgts {
+               if verbose {
+                       if !first {
+                               fmt.Fprintln(writer)
+                       }
+                       fmt.Fprintln(writer, "ID:\t", tgt.ID)
+                       fmt.Fprintln(writer, "Name:\t", tgt.Name)
+                       fmt.Fprintln(writer, "Type:\t", tgt.Type)
+                       fmt.Fprintln(writer, "IP:\t", tgt.IP)
+                       fmt.Fprintln(writer, "Status:\t", tgt.Status)
+                       if len(tgt.Terms) > 0 {
+                               tmNfo := "\t\n"
+                               for _, tt := range tgt.Terms {
+                                       tmNfo += "\t ID:\t" + tt.ID + "\n"
+                                       tmNfo += "\t  Name:\t" + tt.Name + "\n"
+                                       tmNfo += "\t  Status:\t" + tt.Status + "\n"
+                                       tmNfo += "\t  User:\t" + tt.User + "\n"
+                                       tmNfo += "\t  Options:\t" + strings.Join(tt.Options, " ") + "\n"
+                                       tmNfo += fmt.Sprintf("\t  Size:\t%v x %v\n", tt.Cols, tt.Rows)
+                               }
+                               fmt.Fprintln(writer, "Terminals:", tmNfo)
+                       } else {
+                               fmt.Fprintln(writer, "Terminals:\t None")
+                       }
+
+               } else {
+                       if first {
+                               fmt.Fprintln(writer, "ID\t Name\t IP\t Terminals #")
+                       }
+                       fmt.Fprintln(writer, tgt.ID[0:8], "\t", tgt.Name, "\t", tgt.IP, "\t", len(tgt.Terms))
+               }
+               first = false
+       }
+       writer.Flush()
+}
+
+func targetsAdd(ctx *cli.Context) error {
+
+       // Decode target type
+       var tType xaapiv1.TargetType
+       switch strings.ToLower(ctx.String("type")) {
+       case "standard", "std":
+               tType = xaapiv1.TypeTgtStandard
+       default:
+               tType = xaapiv1.TypeTgtStandard
+       }
+
+       tgt := xaapiv1.TargetConfig{
+               Name: ctx.String("name"),
+               Type: tType,
+               IP:   ctx.String("ip"),
+       }
+
+       Log.Infof("POST /target %v", tgt)
+       newTgt := xaapiv1.TargetConfig{}
+       err := HTTPCli.Post(XdsServerComputeURL("/targets"), tgt, &newTgt)
+       if err != nil {
+               return cli.NewExitError(err, 1)
+       }
+
+       if ctx.Bool("short") {
+               fmt.Println(newTgt.ID)
+       } else {
+               fmt.Printf("New target '%s' (id %v) successfully created.\n", newTgt.Name, newTgt.ID)
+       }
+
+       return nil
+}
+
+func targetsRemove(ctx *cli.Context) error {
+       var res xaapiv1.TargetConfig
+       id := GetID(ctx)
+       if id == "" {
+               return cli.NewExitError("id parameter or option must be set", 1)
+       }
+
+       if !ctx.Bool("force") {
+               if !Confirm("Do you permanently remove target id '" + id + "' [yes/No] ? ") {
+                       return nil
+               }
+       }
+
+       if err := HTTPCli.Delete(XdsServerComputeURL("/targets/"+id), &res); err != nil {
+               return cli.NewExitError(err, 1)
+       }
+
+       fmt.Println("Target ID " + res.ID + " successfully deleted.")
+       return nil
+}
+
+func terminalOpen(ctx *cli.Context) error {
+
+       tgt, term, err := GetTargetAndTerminalIDs(ctx, true)
+       if err != nil {
+               return cli.NewExitError(err.Error(), 1)
+       }
+       if tgt == nil {
+               return cli.NewExitError("cannot identify target", 1)
+       }
+
+       if term == nil {
+               // Create a new terminal when needed
+               newTerm := xaapiv1.TerminalConfig{
+                       Name:    "ssh session from xds-cli",
+                       Type:    xaapiv1.TypeTermSSH,
+                       User:    ctx.String("user"),
+                       Options: ctx.StringSlice("options"),
+               }
+               term = &newTerm
+               url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals")
+               if err := HTTPCli.Post(url, &newTerm, term); err != nil {
+                       return cli.NewExitError(err.Error(), 1)
+               }
+               Log.Debugf("New terminal created: %v", term)
+       } else {
+               // Update terminal config when needed
+               needUp := false
+               if ctx.String("user") != "" {
+                       term.User = ctx.String("user")
+                       needUp = true
+               }
+               if len(ctx.StringSlice("options")) > 0 {
+                       term.Options = ctx.StringSlice("options")
+                       needUp = true
+               }
+               if needUp {
+                       url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID)
+                       if err := HTTPCli.Put(url, &term, term); err != nil {
+                               return cli.NewExitError(err.Error(), 1)
+                       }
+                       Log.Debugf("Update terminal config: %v", term)
+               }
+       }
+
+       // Process Socket IO events
+       type exitResult struct {
+               error error
+               code  int
+       }
+       exitChan := make(chan exitResult, 1)
+
+       IOsk.On("disconnection", func(err error) {
+               Log.Debugf("WS disconnection event with err: %v\n", err)
+               exitChan <- exitResult{err, 2}
+       })
+
+       IOsk.On(xaapiv1.TerminalOutEvent, func(ev xaapiv1.TerminalOutMsg) {
+               if ev.Stdout != "" {
+                       fmt.Printf(ev.Stdout)
+               }
+               if ev.Stderr != "" {
+                       fmt.Fprintf(os.Stderr, ev.Stderr)
+               }
+       })
+
+       IOsk.On(xaapiv1.TerminalExitEvent, func(ev xaapiv1.TerminalExitMsg) {
+               exitChan <- exitResult{ev.Error, ev.Code}
+       })
+
+       /* FIXME - use raw mode to support escape keys, arrows keys, control char...
+       // import       "github.com/golang/crypto/ssh/terminal"
+
+       oldState, err := terminal.MakeRaw(int(os.Stdin.Fd()))
+       if err == nil {
+               defer terminal.Restore(int(os.Stdin.Fd()), oldState)
+       }
+       */
+
+       // Send stdin though WS
+       go func() {
+               paranoia := 600
+               reader := bufio.NewReader(os.Stdin)
+               for {
+                       sc := bufio.NewScanner(reader)
+                       for sc.Scan() {
+                               command := sc.Text()
+                               Log.Debugf("Terminal Send command <%v>", command)
+                               IOsk.Emit(xaapiv1.TerminalInEvent, command+"\n")
+                       }
+                       if sc.Err() != nil {
+                               exitChan <- exitResult{sc.Err(), 3}
+                       }
+
+                       // CTRL-D exited scanner, so send it explicitly
+                       IOsk.Emit(xaapiv1.TerminalInEvent, "\x04\n")
+                       time.Sleep(time.Millisecond * 100)
+
+                       if paranoia--; paranoia <= 0 {
+                               msg := "Abnormal loop detected on stdin"
+                               Log.Errorf("Abnormal loop detected on stdin")
+
+                               // Send signal to gently exit terminal session
+                               TerminalSendSignal(tgt, term, syscall.SIGTERM)
+
+                               exitChan <- exitResult{fmt.Errorf(msg), int(syscall.ELOOP)}
+                       }
+               }
+       }()
+
+       // Handle signals
+       err = OnSignals(func(sig os.Signal) {
+               Log.Debugf("Send signal %v", sig)
+               if IsWinResizeSignal(sig) {
+                       TerminalResize(tgt, term)
+               } else if IsInterruptSignal(sig) {
+                       IOsk.Emit(xaapiv1.TerminalInEvent, "\x03\n")
+               } else {
+                       TerminalSendSignal(tgt, term, sig)
+               }
+       })
+       if err != nil {
+               return cli.NewExitError(err.Error(), 1)
+       }
+
+       // Send open command
+       url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/open")
+       LogPost("POST %v", url)
+       if err := HTTPCli.Post(url, nil, term); err != nil {
+               return cli.NewExitError(err.Error(), 1)
+       }
+
+       // Wait exit - blocking
+       select {
+       case res := <-exitChan:
+               errStr := ""
+               if res.code == 0 {
+                       Log.Debugln("Exit Target Terminal successfully")
+               }
+               if res.error != nil {
+                       Log.Debugln("Exit Target Terminal with ERROR: ", res.error.Error())
+                       errStr = res.error.Error()
+               }
+               return cli.NewExitError(errStr, res.code)
+       }
+}
+
+func terminalRemove(ctx *cli.Context) error {
+
+       tgt, term, err := GetTargetAndTerminalIDs(ctx, false)
+       if err != nil {
+               return cli.NewExitError(err.Error(), 1)
+       }
+       if tgt == nil || tgt.ID == "" {
+               return cli.NewExitError("cannot identify target id", 1)
+       }
+       if term == nil || term.ID == "" {
+               return cli.NewExitError("cannot identify terminal id", 1)
+       }
+
+       // Send delete command
+       url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID)
+       LogPost("DELETE %v", url)
+       if err := HTTPCli.Delete(url, term); err != nil {
+               return cli.NewExitError(err.Error(), 1)
+       }
+
+       return nil
+}
+
+/**
+ * utils functions
+ */
+
+// TerminalResize Send command to resize target terminal
+func TerminalResize(tgt *xaapiv1.TargetConfig, term *xaapiv1.TerminalConfig) {
+       col, row, err := terminal.GetSize(int(os.Stdin.Fd()))
+       if err != nil {
+               Log.Errorf("Error cannot get terminal size: %v", err)
+       }
+       Log.Debugf("Terminal resizing rows %v, cols %v", row, col)
+       sz := xaapiv1.TerminalResizeArgs{Rows: uint16(row), Cols: uint16(col)}
+       url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/resize")
+       if err := HTTPCli.Post(url, &sz, nil); err != nil {
+               Log.Errorf("Error while resizing terminal (term %v): %v", sz, err)
+       }
+}
+
+// TerminalSendSignal Send a signal to a target terminal
+func TerminalSendSignal(tgt *xaapiv1.TargetConfig, term *xaapiv1.TerminalConfig, sig os.Signal) {
+       url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/signal/" + sig.String())
+       if err := HTTPCli.Post(url, nil, nil); err != nil {
+               Log.Errorf("Error to send signal %v: %v", sig, err)
+       }
+}
+
+// GetTargetAndTerminalIDs Retrieve Target and Terminal definition from IDs
+func GetTargetAndTerminalIDs(ctx *cli.Context, useFirstFree bool) (*xaapiv1.TargetConfig, *xaapiv1.TerminalConfig, error) {
+
+       idArg := ctx.String("id")
+       tidArg := GetIDName(ctx, "termId")
+       if tidArg == "" {
+               tidArg = GetIDName(ctx, "tid")
+       }
+       if idArg == "" && tidArg == "" {
+               return nil, nil, fmt.Errorf("id or termId argument must be set")
+       }
+
+       tgts := []xaapiv1.TargetConfig{}
+       if err := TargetsListGet(&tgts); err != nil {
+               return nil, nil, err
+       }
+
+       matching := 0
+       ti := 0
+       tj := 0
+       for ii, tt := range tgts {
+               for jj, ttm := range tt.Terms {
+                       if idArg == "" && compareID(ttm.ID, tidArg) {
+                               ti = ii
+                               tj = jj
+                               matching++
+                       }
+                       if idArg != "" && compareID(tt.ID, idArg) && compareID(ttm.ID, tidArg) {
+                               ti = ii
+                               tj = jj
+                               matching++
+                       }
+               }
+       }
+       if matching > 1 {
+               return nil, nil, fmt.Errorf("Multiple IDs found, please set -id and -tid with full ID notation")
+       } else if matching == 1 {
+               return &tgts[ti], &tgts[ti].Terms[tj], nil
+       }
+
+       // Allow to create a new terminal when only target id is set
+       idArg = GetIDName(ctx, "id")
+       if idArg != "" {
+               for _, tt := range tgts {
+                       if compareID(tt.ID, idArg) {
+                               return &tt, nil, nil
+                       }
+               }
+       }
+
+       return nil, nil, fmt.Errorf("No matching id found")
+}
+
+// Sort targets by Name
+type _TgtByName []xaapiv1.TargetConfig
+
+func (s _TgtByName) Len() int           { return len(s) }
+func (s _TgtByName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
+func (s _TgtByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
+
+// TargetsListGet Get the list of existing targets
+func TargetsListGet(tgts *[]xaapiv1.TargetConfig) error {
+       var data []byte
+       if err := HTTPCli.HTTPGet(XdsServerComputeURL("/targets"), &data); err != nil {
+               return err
+       }
+       Log.Debugf("Result of /targets: %v", string(data[:]))
+
+       if err := json.Unmarshal(data, &tgts); err != nil {
+               return err
+       }
+
+       sort.Sort(_TgtByName(*tgts))
+
+       return nil
+}