/* * Copyright (C) 2018 "IoT.bzh" * Author Sebastien Douheret * * 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 ( "encoding/json" "fmt" "io" "os" "sort" "strings" "time" "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xaapiv1" "github.com/creack/goselect" "github.com/golang/crypto/ssh/terminal" "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, "XDS_TARGET_ID") 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 Type:\t" + string(tt.Type) + "\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, "XDS_TARGET_ID") 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) IOSkClient.On("disconnection", func(err error) { Log.Debugf("WS disconnection event with err: %v\n", err) exitChan <- exitResult{err, 2} }) IOSkClient.On(xaapiv1.TerminalOutEvent, func(ev xaapiv1.TerminalOutMsg) { if len(ev.Stdout) > 0 { os.Stdout.Write(ev.Stdout) } if len(ev.Stderr) > 0 { os.Stderr.Write(ev.Stdout) } }) IOSkClient.On(xaapiv1.TerminalExitEvent, func(ev xaapiv1.TerminalExitMsg) { exitChan <- exitResult{ev.Error, ev.Code} }) // Setup terminal (raw mode to handle escape and control keys) if !terminal.IsTerminal(0) || !terminal.IsTerminal(1) { return cli.NewExitError("stdin/stdout should be terminal", 1) } oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) if err != nil { return cli.NewExitError(err.Error(), 1) } defer terminal.Restore(int(os.Stdin.Fd()), oldState) // Send stdin though WS go func() { type exposeFd interface { Fd() uintptr } buff := make([]byte, 128) rdfs := &goselect.FDSet{} reader := io.ReadCloser(os.Stdin) defer reader.Close() for { rdfs.Zero() rdfs.Set(reader.(exposeFd).Fd()) err := goselect.Select(1, rdfs, nil, nil, 50*time.Millisecond) if err != nil { terminal.Restore(int(os.Stdin.Fd()), oldState) exitChan <- exitResult{err, 3} return } if rdfs.IsSet(reader.(exposeFd).Fd()) { size, err := reader.Read(buff) if err != nil { Log.Debugf("Read error %v; err %v", size, err) if err == io.EOF { // CTRL-D exited scanner, so send it explicitly err := IOSkClient.Emit(xaapiv1.TerminalInEvent, "\x04\n") if err != nil { terminal.Restore(int(os.Stdin.Fd()), oldState) exitChan <- exitResult{err, 4} return } time.Sleep(time.Millisecond * 100) continue } else { terminal.Restore(int(os.Stdin.Fd()), oldState) exitChan <- exitResult{err, 5} return } } if size <= 0 { continue } data := buff[:size] LogSillyf("Terminal Send data <%v> (%s)", data, data) err = IOSkClient.Emit(xaapiv1.TerminalInEvent, data) if err != nil { terminal.Restore(int(os.Stdin.Fd()), oldState) exitChan <- exitResult{err, 6} return } } } }() // Handle signals err = OnSignals(func(sig os.Signal) { Log.Debugf("Send signal %v", sig) if IsWinResizeSignal(sig) { TerminalResize(tgt, term) } else if IsInterruptSignal(sig) { IOSkClient.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) } // Send init size TerminalResize(tgt, term) // Wait exit - blocking select { case res := <-IOSkClient.ServerDiscoChan: Log.Debugf("XDS Server disconnected %v", res) return cli.NewExitError(res.error, res.code) 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) } LogSillyf("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) { tgts := []xaapiv1.TargetConfig{} if err := TargetsListGet(&tgts); err != nil { return nil, nil, err } idArg := ctx.String("id") tidArg := ctx.String("termId") if tidArg == "" { tidArg = ctx.String("tid") } if idArg != "" || tidArg != "" { 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", "XDS_TARGET_ID") if idArg == "" { return nil, nil, fmt.Errorf("id or termId argument must be set") } for _, tt := range tgts { if compareID(tt.ID, idArg) { if useFirstFree { for _, ttm := range tt.Terms { if ttm.Type == xaapiv1.TypeTermSSH && (ttm.Status == xaapiv1.StatusTermEnable || ttm.Status == xaapiv1.StatusTermClose) { return &tt, &ttm, nil } } } 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 }