2 * Copyright (C) 2018 "IoT.bzh"
3 * Author Sebastien Douheret <sebastien@iot.bzh>
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
30 "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xaapiv1"
31 "github.com/creack/goselect"
32 "github.com/golang/crypto/ssh/terminal"
33 "github.com/urfave/cli"
36 func initCmdTargets(cmdDef *[]cli.Command) {
37 *cmdDef = append(*cmdDef, cli.Command{
39 Aliases: []string{"tgt"},
41 Usage: "targets commands group",
42 Subcommands: []cli.Command{
45 Aliases: []string{"a"},
46 Usage: "Add a new target",
51 Usage: "target name (free form string)",
59 Usage: "short output, only print create target id (useful from scripting)",
63 Usage: "target type (standard|std)",
69 Usage: "Get properties of a target",
75 EnvVar: "XDS_TARGET_ID",
81 Aliases: []string{"ls"},
82 Usage: "List existing targets",
87 Usage: "display verbose output",
93 Aliases: []string{"rm"},
94 Usage: "Remove an existing target",
95 Action: targetsRemove,
100 EnvVar: "XDS_TARGET_ID",
104 Usage: "remove confirmation prompt before removal",
110 Aliases: []string{"term"},
111 Usage: "Open a target terminal",
112 Action: terminalOpen,
117 EnvVar: "XDS_TARGET_ID",
121 Usage: "passthrough options set to command line used to start terminal",
125 Usage: "terminal id",
126 EnvVar: "XDS_TERMINAL_ID",
130 Usage: "user name used to connect terminal",
131 EnvVar: "XDS_TERMINAL_USER",
136 Name: "terminal-remove",
137 Aliases: []string{"term-rm"},
138 Usage: "Remove a target terminal",
139 Action: terminalRemove,
144 EnvVar: "XDS_TARGET_ID",
148 Usage: "terminal id",
149 EnvVar: "XDS_TERMINAL_ID",
157 func targetsList(ctx *cli.Context) error {
159 tgts := []xaapiv1.TargetConfig{}
160 if err := TargetsListGet(&tgts); err != nil {
161 return cli.NewExitError(err.Error(), 1)
163 _displayTargets(tgts, ctx.Bool("verbose"))
167 func targetsGet(ctx *cli.Context) error {
168 id := GetID(ctx, "XDS_TARGET_ID")
170 return cli.NewExitError("id parameter or option must be set", 1)
172 tgts := make([]xaapiv1.TargetConfig, 1)
173 url := XdsServerComputeURL("/targets/" + id)
174 if err := HTTPCli.Get(url, &tgts[0]); err != nil {
175 return cli.NewExitError(err, 1)
177 _displayTargets(tgts, true)
181 func _displayTargets(tgts []xaapiv1.TargetConfig, verbose bool) {
184 writer := NewTableWriter()
185 for _, tgt := range tgts {
190 fmt.Fprintln(writer, "ID:\t", tgt.ID)
191 fmt.Fprintln(writer, "Name:\t", tgt.Name)
192 fmt.Fprintln(writer, "Type:\t", tgt.Type)
193 fmt.Fprintln(writer, "IP:\t", tgt.IP)
194 fmt.Fprintln(writer, "Status:\t", tgt.Status)
195 if len(tgt.Terms) > 0 {
197 for _, tt := range tgt.Terms {
198 tmNfo += "\t ID:\t" + tt.ID + "\n"
199 tmNfo += "\t Name:\t" + tt.Name + "\n"
200 tmNfo += "\t Type:\t" + string(tt.Type) + "\n"
201 tmNfo += "\t Status:\t" + tt.Status + "\n"
202 tmNfo += "\t User:\t" + tt.User + "\n"
203 tmNfo += "\t Options:\t" + strings.Join(tt.Options, " ") + "\n"
204 tmNfo += fmt.Sprintf("\t Size:\t%v x %v\n", tt.Cols, tt.Rows)
206 fmt.Fprintln(writer, "Terminals:", tmNfo)
208 fmt.Fprintln(writer, "Terminals:\t None")
213 fmt.Fprintln(writer, "ID\t Name\t IP\t Terminals #")
215 fmt.Fprintln(writer, tgt.ID[0:8], "\t", tgt.Name, "\t", tgt.IP, "\t", len(tgt.Terms))
222 func targetsAdd(ctx *cli.Context) error {
224 // Decode target type
225 var tType xaapiv1.TargetType
226 switch strings.ToLower(ctx.String("type")) {
227 case "standard", "std":
228 tType = xaapiv1.TypeTgtStandard
230 tType = xaapiv1.TypeTgtStandard
233 tgt := xaapiv1.TargetConfig{
234 Name: ctx.String("name"),
236 IP: ctx.String("ip"),
239 Log.Infof("POST /target %v", tgt)
240 newTgt := xaapiv1.TargetConfig{}
241 err := HTTPCli.Post(XdsServerComputeURL("/targets"), tgt, &newTgt)
243 return cli.NewExitError(err, 1)
246 if ctx.Bool("short") {
247 fmt.Println(newTgt.ID)
249 fmt.Printf("New target '%s' (id %v) successfully created.\n", newTgt.Name, newTgt.ID)
255 func targetsRemove(ctx *cli.Context) error {
256 var res xaapiv1.TargetConfig
257 id := GetID(ctx, "XDS_TARGET_ID")
259 return cli.NewExitError("id parameter or option must be set", 1)
262 if !ctx.Bool("force") {
263 if !Confirm("Do you permanently remove target id '" + id + "' [yes/No] ? ") {
268 if err := HTTPCli.Delete(XdsServerComputeURL("/targets/"+id), &res); err != nil {
269 return cli.NewExitError(err, 1)
272 fmt.Println("Target ID " + res.ID + " successfully deleted.")
276 func terminalOpen(ctx *cli.Context) error {
278 tgt, term, err := GetTargetAndTerminalIDs(ctx, true)
280 return cli.NewExitError(err.Error(), 1)
283 return cli.NewExitError("cannot identify target", 1)
287 // Create a new terminal when needed
288 newTerm := xaapiv1.TerminalConfig{
289 Name: "ssh session from xds-cli",
290 Type: xaapiv1.TypeTermSSH,
291 User: ctx.String("user"),
292 Options: ctx.StringSlice("options"),
295 url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals")
296 if err := HTTPCli.Post(url, &newTerm, term); err != nil {
297 return cli.NewExitError(err.Error(), 1)
299 Log.Debugf("New terminal created: %v", term)
301 // Update terminal config when needed
303 if ctx.String("user") != "" {
304 term.User = ctx.String("user")
307 if len(ctx.StringSlice("options")) > 0 {
308 term.Options = ctx.StringSlice("options")
312 url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID)
313 if err := HTTPCli.Put(url, &term, term); err != nil {
314 return cli.NewExitError(err.Error(), 1)
316 Log.Debugf("Update terminal config: %v", term)
320 // Process Socket IO events
321 type exitResult struct {
325 exitChan := make(chan exitResult, 1)
327 IOSkClient.On("disconnection", func(err error) {
328 Log.Debugf("WS disconnection event with err: %v\n", err)
329 exitChan <- exitResult{err, 2}
332 IOSkClient.On(xaapiv1.TerminalOutEvent, func(ev xaapiv1.TerminalOutMsg) {
333 if len(ev.Stdout) > 0 {
334 os.Stdout.Write(ev.Stdout)
336 if len(ev.Stderr) > 0 {
337 os.Stderr.Write(ev.Stdout)
341 IOSkClient.On(xaapiv1.TerminalExitEvent, func(ev xaapiv1.TerminalExitMsg) {
342 exitChan <- exitResult{ev.Error, ev.Code}
345 // Setup terminal (raw mode to handle escape and control keys)
346 if !terminal.IsTerminal(0) || !terminal.IsTerminal(1) {
347 return cli.NewExitError("stdin/stdout should be terminal", 1)
349 oldState, err := terminal.MakeRaw(int(os.Stdin.Fd()))
351 return cli.NewExitError(err.Error(), 1)
353 defer terminal.Restore(int(os.Stdin.Fd()), oldState)
355 // Send stdin though WS
357 type exposeFd interface {
360 buff := make([]byte, 128)
361 rdfs := &goselect.FDSet{}
362 reader := io.ReadCloser(os.Stdin)
367 rdfs.Set(reader.(exposeFd).Fd())
368 err := goselect.Select(1, rdfs, nil, nil, 50*time.Millisecond)
370 terminal.Restore(int(os.Stdin.Fd()), oldState)
371 exitChan <- exitResult{err, 3}
374 if rdfs.IsSet(reader.(exposeFd).Fd()) {
375 size, err := reader.Read(buff)
378 Log.Debugf("Read error %v; err %v", size, err)
380 // CTRL-D exited scanner, so send it explicitly
381 err := IOSkClient.Emit(xaapiv1.TerminalInEvent, "\x04\n")
384 terminal.Restore(int(os.Stdin.Fd()), oldState)
385 exitChan <- exitResult{err, 4}
388 time.Sleep(time.Millisecond * 100)
391 terminal.Restore(int(os.Stdin.Fd()), oldState)
392 exitChan <- exitResult{err, 5}
402 LogSillyf("Terminal Send data <%v> (%s)", data, data)
403 err = IOSkClient.Emit(xaapiv1.TerminalInEvent, data)
405 terminal.Restore(int(os.Stdin.Fd()), oldState)
406 exitChan <- exitResult{err, 6}
414 err = OnSignals(func(sig os.Signal) {
415 Log.Debugf("Send signal %v", sig)
416 if IsWinResizeSignal(sig) {
417 TerminalResize(tgt, term)
418 } else if IsInterruptSignal(sig) {
419 IOSkClient.Emit(xaapiv1.TerminalInEvent, "\x03\n")
421 TerminalSendSignal(tgt, term, sig)
425 return cli.NewExitError(err.Error(), 1)
429 url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/open")
430 LogPost("POST %v", url)
431 if err := HTTPCli.Post(url, nil, term); err != nil {
432 return cli.NewExitError(err.Error(), 1)
436 TerminalResize(tgt, term)
438 // Wait exit - blocking
440 case res := <-IOSkClient.ServerDiscoChan:
441 Log.Debugf("XDS Server disconnected %v", res)
442 return cli.NewExitError(res.error, res.code)
444 case res := <-exitChan:
447 Log.Debugln("Exit Target Terminal successfully")
449 if res.error != nil {
450 Log.Debugln("Exit Target Terminal with ERROR: ", res.error.Error())
451 errStr = res.error.Error()
453 return cli.NewExitError(errStr, res.code)
457 func terminalRemove(ctx *cli.Context) error {
459 tgt, term, err := GetTargetAndTerminalIDs(ctx, false)
461 return cli.NewExitError(err.Error(), 1)
463 if tgt == nil || tgt.ID == "" {
464 return cli.NewExitError("cannot identify target id", 1)
466 if term == nil || term.ID == "" {
467 return cli.NewExitError("cannot identify terminal id", 1)
470 // Send delete command
471 url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID)
472 LogPost("DELETE %v", url)
473 if err := HTTPCli.Delete(url, term); err != nil {
474 return cli.NewExitError(err.Error(), 1)
484 // TerminalResize Send command to resize target terminal
485 func TerminalResize(tgt *xaapiv1.TargetConfig, term *xaapiv1.TerminalConfig) {
486 col, row, err := terminal.GetSize(int(os.Stdin.Fd()))
488 Log.Errorf("Error cannot get terminal size: %v", err)
491 LogSillyf("Terminal resizing rows %v, cols %v", row, col)
492 sz := xaapiv1.TerminalResizeArgs{Rows: uint16(row), Cols: uint16(col)}
493 url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/resize")
494 if err := HTTPCli.Post(url, &sz, nil); err != nil {
495 Log.Errorf("Error while resizing terminal (term %v): %v", sz, err)
499 // TerminalSendSignal Send a signal to a target terminal
500 func TerminalSendSignal(tgt *xaapiv1.TargetConfig, term *xaapiv1.TerminalConfig, sig os.Signal) {
501 url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/signal/" + sig.String())
502 if err := HTTPCli.Post(url, nil, nil); err != nil {
503 Log.Errorf("Error to send signal %v: %v", sig, err)
507 // GetTargetAndTerminalIDs Retrieve Target and Terminal definition from IDs
508 func GetTargetAndTerminalIDs(ctx *cli.Context, useFirstFree bool) (*xaapiv1.TargetConfig, *xaapiv1.TerminalConfig, error) {
510 tgts := []xaapiv1.TargetConfig{}
511 if err := TargetsListGet(&tgts); err != nil {
515 idArg := ctx.String("id")
516 tidArg := ctx.String("termId")
518 tidArg = ctx.String("tid")
520 if idArg != "" || tidArg != "" {
524 for ii, tt := range tgts {
525 for jj, ttm := range tt.Terms {
526 if idArg == "" && compareID(ttm.ID, tidArg) {
531 if idArg != "" && compareID(tt.ID, idArg) && compareID(ttm.ID, tidArg) {
539 return nil, nil, fmt.Errorf("Multiple IDs found, please set -id and -tid with full ID notation")
540 } else if matching == 1 {
541 return &tgts[ti], &tgts[ti].Terms[tj], nil
545 // Allow to create a new terminal when only target id is set
546 idArg = GetIDName(ctx, "id", "XDS_TARGET_ID")
548 return nil, nil, fmt.Errorf("id or termId argument must be set")
551 for _, tt := range tgts {
552 if compareID(tt.ID, idArg) {
554 for _, ttm := range tt.Terms {
555 if ttm.Type == xaapiv1.TypeTermSSH &&
556 (ttm.Status == xaapiv1.StatusTermEnable || ttm.Status == xaapiv1.StatusTermClose) {
557 return &tt, &ttm, nil
565 return nil, nil, fmt.Errorf("No matching id found")
568 // Sort targets by Name
569 type _TgtByName []xaapiv1.TargetConfig
571 func (s _TgtByName) Len() int { return len(s) }
572 func (s _TgtByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
573 func (s _TgtByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
575 // TargetsListGet Get the list of existing targets
576 func TargetsListGet(tgts *[]xaapiv1.TargetConfig) error {
578 if err := HTTPCli.HTTPGet(XdsServerComputeURL("/targets"), &data); err != nil {
581 Log.Debugf("Result of /targets: %v", string(data[:]))
583 if err := json.Unmarshal(data, &tgts); err != nil {
587 sort.Sort(_TgtByName(*tgts))