Added bash completion support.
[src/xds/xds-cli.git] / cmd-target.go
1 /*
2  * Copyright (C) 2018 "IoT.bzh"
3  * Author Sebastien Douheret <sebastien@iot.bzh>
4  *
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
8  *
9  *   http://www.apache.org/licenses/LICENSE-2.0
10  *
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.
16  *
17  */
18
19 package main
20
21 import (
22         "bufio"
23         "encoding/json"
24         "fmt"
25         "os"
26         "sort"
27         "strings"
28         "syscall"
29         "time"
30
31         "github.com/golang/crypto/ssh/terminal"
32
33         "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xaapiv1"
34         "github.com/urfave/cli"
35 )
36
37 func initCmdTargets(cmdDef *[]cli.Command) {
38         *cmdDef = append(*cmdDef, cli.Command{
39                 Name:     "targets",
40                 Aliases:  []string{"tgt"},
41                 HideHelp: true,
42                 Usage:    "targets commands group",
43                 Subcommands: []cli.Command{
44                         {
45                                 Name:    "add",
46                                 Aliases: []string{"a"},
47                                 Usage:   "Add a new target",
48                                 Action:  targetsAdd,
49                                 Flags: []cli.Flag{
50                                         cli.StringFlag{
51                                                 Name:  "name, n",
52                                                 Usage: "target name (free form string)",
53                                         },
54                                         cli.StringFlag{
55                                                 Name:  "ip",
56                                                 Usage: "IP address",
57                                         },
58                                         cli.BoolFlag{
59                                                 Name:  "short, s",
60                                                 Usage: "short output, only print create target id (useful from scripting)",
61                                         },
62                                         cli.StringFlag{
63                                                 Name:  "type, t",
64                                                 Usage: "target type (standard|std)",
65                                         },
66                                 },
67                         },
68                         {
69                                 Name:   "get",
70                                 Usage:  "Get properties of a target",
71                                 Action: targetsGet,
72                                 Flags: []cli.Flag{
73                                         cli.StringFlag{
74                                                 Name:   "id",
75                                                 Usage:  "target id",
76                                                 EnvVar: "XDS_TARGET_ID",
77                                         },
78                                 },
79                         },
80                         {
81                                 Name:    "list",
82                                 Aliases: []string{"ls"},
83                                 Usage:   "List existing targets",
84                                 Action:  targetsList,
85                                 Flags: []cli.Flag{
86                                         cli.BoolFlag{
87                                                 Name:  "verbose, v",
88                                                 Usage: "display verbose output",
89                                         },
90                                 },
91                         },
92                         {
93                                 Name:    "remove",
94                                 Aliases: []string{"rm"},
95                                 Usage:   "Remove an existing target",
96                                 Action:  targetsRemove,
97                                 Flags: []cli.Flag{
98                                         cli.StringFlag{
99                                                 Name:   "id",
100                                                 Usage:  "target id",
101                                                 EnvVar: "XDS_TARGET_ID",
102                                         },
103                                         cli.BoolFlag{
104                                                 Name:  "force, f",
105                                                 Usage: "remove confirmation prompt before removal",
106                                         },
107                                 },
108                         },
109                         {
110                                 Name:    "terminal",
111                                 Aliases: []string{"term"},
112                                 Usage:   "Open a target terminal",
113                                 Action:  terminalOpen,
114                                 Flags: []cli.Flag{
115                                         cli.StringFlag{
116                                                 Name:   "id",
117                                                 Usage:  "target id",
118                                                 EnvVar: "XDS_TARGET_ID",
119                                         },
120                                         cli.StringSliceFlag{
121                                                 Name:  "options, o",
122                                                 Usage: "passthrough options set to command line used to start terminal",
123                                         },
124                                         cli.StringFlag{
125                                                 Name:   "termId, tid",
126                                                 Usage:  "terminal id",
127                                                 EnvVar: "XDS_TERMINAL_ID",
128                                         },
129                                         cli.StringFlag{
130                                                 Name:   "user, u",
131                                                 Usage:  "user name used to connect terminal",
132                                                 EnvVar: "XDS_TERMINAL_USER",
133                                         },
134                                 },
135                         },
136                         {
137                                 Name:    "terminal-remove",
138                                 Aliases: []string{"term-rm"},
139                                 Usage:   "Remove a target terminal",
140                                 Action:  terminalRemove,
141                                 Flags: []cli.Flag{
142                                         cli.StringFlag{
143                                                 Name:   "id",
144                                                 Usage:  "target id",
145                                                 EnvVar: "XDS_TARGET_ID",
146                                         },
147                                         cli.StringFlag{
148                                                 Name:   "termId, tid",
149                                                 Usage:  "terminal id",
150                                                 EnvVar: "XDS_TERMINAL_ID",
151                                         },
152                                 },
153                         },
154                 },
155         })
156 }
157
158 func targetsList(ctx *cli.Context) error {
159         // Get targets list
160         tgts := []xaapiv1.TargetConfig{}
161         if err := TargetsListGet(&tgts); err != nil {
162                 return cli.NewExitError(err.Error(), 1)
163         }
164         _displayTargets(tgts, ctx.Bool("verbose"))
165         return nil
166 }
167
168 func targetsGet(ctx *cli.Context) error {
169         id := GetID(ctx)
170         if id == "" {
171                 return cli.NewExitError("id parameter or option must be set", 1)
172         }
173         tgts := make([]xaapiv1.TargetConfig, 1)
174         url := XdsServerComputeURL("/targets/" + id)
175         if err := HTTPCli.Get(url, &tgts[0]); err != nil {
176                 return cli.NewExitError(err, 1)
177         }
178         _displayTargets(tgts, true)
179         return nil
180 }
181
182 func _displayTargets(tgts []xaapiv1.TargetConfig, verbose bool) {
183         // Display result
184         first := true
185         writer := NewTableWriter()
186         for _, tgt := range tgts {
187                 if verbose {
188                         if !first {
189                                 fmt.Fprintln(writer)
190                         }
191                         fmt.Fprintln(writer, "ID:\t", tgt.ID)
192                         fmt.Fprintln(writer, "Name:\t", tgt.Name)
193                         fmt.Fprintln(writer, "Type:\t", tgt.Type)
194                         fmt.Fprintln(writer, "IP:\t", tgt.IP)
195                         fmt.Fprintln(writer, "Status:\t", tgt.Status)
196                         if len(tgt.Terms) > 0 {
197                                 tmNfo := "\t\n"
198                                 for _, tt := range tgt.Terms {
199                                         tmNfo += "\t ID:\t" + tt.ID + "\n"
200                                         tmNfo += "\t  Name:\t" + tt.Name + "\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)
205                                 }
206                                 fmt.Fprintln(writer, "Terminals:", tmNfo)
207                         } else {
208                                 fmt.Fprintln(writer, "Terminals:\t None")
209                         }
210
211                 } else {
212                         if first {
213                                 fmt.Fprintln(writer, "ID\t Name\t IP\t Terminals #")
214                         }
215                         fmt.Fprintln(writer, tgt.ID[0:8], "\t", tgt.Name, "\t", tgt.IP, "\t", len(tgt.Terms))
216                 }
217                 first = false
218         }
219         writer.Flush()
220 }
221
222 func targetsAdd(ctx *cli.Context) error {
223
224         // Decode target type
225         var tType xaapiv1.TargetType
226         switch strings.ToLower(ctx.String("type")) {
227         case "standard", "std":
228                 tType = xaapiv1.TypeTgtStandard
229         default:
230                 tType = xaapiv1.TypeTgtStandard
231         }
232
233         tgt := xaapiv1.TargetConfig{
234                 Name: ctx.String("name"),
235                 Type: tType,
236                 IP:   ctx.String("ip"),
237         }
238
239         Log.Infof("POST /target %v", tgt)
240         newTgt := xaapiv1.TargetConfig{}
241         err := HTTPCli.Post(XdsServerComputeURL("/targets"), tgt, &newTgt)
242         if err != nil {
243                 return cli.NewExitError(err, 1)
244         }
245
246         if ctx.Bool("short") {
247                 fmt.Println(newTgt.ID)
248         } else {
249                 fmt.Printf("New target '%s' (id %v) successfully created.\n", newTgt.Name, newTgt.ID)
250         }
251
252         return nil
253 }
254
255 func targetsRemove(ctx *cli.Context) error {
256         var res xaapiv1.TargetConfig
257         id := GetID(ctx)
258         if id == "" {
259                 return cli.NewExitError("id parameter or option must be set", 1)
260         }
261
262         if !ctx.Bool("force") {
263                 if !Confirm("Do you permanently remove target id '" + id + "' [yes/No] ? ") {
264                         return nil
265                 }
266         }
267
268         if err := HTTPCli.Delete(XdsServerComputeURL("/targets/"+id), &res); err != nil {
269                 return cli.NewExitError(err, 1)
270         }
271
272         fmt.Println("Target ID " + res.ID + " successfully deleted.")
273         return nil
274 }
275
276 func terminalOpen(ctx *cli.Context) error {
277
278         tgt, term, err := GetTargetAndTerminalIDs(ctx, true)
279         if err != nil {
280                 return cli.NewExitError(err.Error(), 1)
281         }
282         if tgt == nil {
283                 return cli.NewExitError("cannot identify target", 1)
284         }
285
286         if term == nil {
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"),
293                 }
294                 term = &newTerm
295                 url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals")
296                 if err := HTTPCli.Post(url, &newTerm, term); err != nil {
297                         return cli.NewExitError(err.Error(), 1)
298                 }
299                 Log.Debugf("New terminal created: %v", term)
300         } else {
301                 // Update terminal config when needed
302                 needUp := false
303                 if ctx.String("user") != "" {
304                         term.User = ctx.String("user")
305                         needUp = true
306                 }
307                 if len(ctx.StringSlice("options")) > 0 {
308                         term.Options = ctx.StringSlice("options")
309                         needUp = true
310                 }
311                 if needUp {
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)
315                         }
316                         Log.Debugf("Update terminal config: %v", term)
317                 }
318         }
319
320         // Process Socket IO events
321         type exitResult struct {
322                 error error
323                 code  int
324         }
325         exitChan := make(chan exitResult, 1)
326
327         IOsk.On("disconnection", func(err error) {
328                 Log.Debugf("WS disconnection event with err: %v\n", err)
329                 exitChan <- exitResult{err, 2}
330         })
331
332         IOsk.On(xaapiv1.TerminalOutEvent, func(ev xaapiv1.TerminalOutMsg) {
333                 if ev.Stdout != "" {
334                         fmt.Printf(ev.Stdout)
335                 }
336                 if ev.Stderr != "" {
337                         fmt.Fprintf(os.Stderr, ev.Stderr)
338                 }
339         })
340
341         IOsk.On(xaapiv1.TerminalExitEvent, func(ev xaapiv1.TerminalExitMsg) {
342                 exitChan <- exitResult{ev.Error, ev.Code}
343         })
344
345         /* FIXME - use raw mode to support escape keys, arrows keys, control char...
346         // import       "github.com/golang/crypto/ssh/terminal"
347
348         oldState, err := terminal.MakeRaw(int(os.Stdin.Fd()))
349         if err == nil {
350                 defer terminal.Restore(int(os.Stdin.Fd()), oldState)
351         }
352         */
353
354         // Send stdin though WS
355         go func() {
356                 paranoia := 600
357                 reader := bufio.NewReader(os.Stdin)
358                 for {
359                         sc := bufio.NewScanner(reader)
360                         for sc.Scan() {
361                                 command := sc.Text()
362                                 Log.Debugf("Terminal Send command <%v>", command)
363                                 IOsk.Emit(xaapiv1.TerminalInEvent, command+"\n")
364                         }
365                         if sc.Err() != nil {
366                                 exitChan <- exitResult{sc.Err(), 3}
367                         }
368
369                         // CTRL-D exited scanner, so send it explicitly
370                         IOsk.Emit(xaapiv1.TerminalInEvent, "\x04\n")
371                         time.Sleep(time.Millisecond * 100)
372
373                         if paranoia--; paranoia <= 0 {
374                                 msg := "Abnormal loop detected on stdin"
375                                 Log.Errorf("Abnormal loop detected on stdin")
376
377                                 // Send signal to gently exit terminal session
378                                 TerminalSendSignal(tgt, term, syscall.SIGTERM)
379
380                                 exitChan <- exitResult{fmt.Errorf(msg), int(syscall.ELOOP)}
381                         }
382                 }
383         }()
384
385         // Handle signals
386         err = OnSignals(func(sig os.Signal) {
387                 Log.Debugf("Send signal %v", sig)
388                 if IsWinResizeSignal(sig) {
389                         TerminalResize(tgt, term)
390                 } else if IsInterruptSignal(sig) {
391                         IOsk.Emit(xaapiv1.TerminalInEvent, "\x03\n")
392                 } else {
393                         TerminalSendSignal(tgt, term, sig)
394                 }
395         })
396         if err != nil {
397                 return cli.NewExitError(err.Error(), 1)
398         }
399
400         // Send open command
401         url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/open")
402         LogPost("POST %v", url)
403         if err := HTTPCli.Post(url, nil, term); err != nil {
404                 return cli.NewExitError(err.Error(), 1)
405         }
406
407         // Wait exit - blocking
408         select {
409         case res := <-exitChan:
410                 errStr := ""
411                 if res.code == 0 {
412                         Log.Debugln("Exit Target Terminal successfully")
413                 }
414                 if res.error != nil {
415                         Log.Debugln("Exit Target Terminal with ERROR: ", res.error.Error())
416                         errStr = res.error.Error()
417                 }
418                 return cli.NewExitError(errStr, res.code)
419         }
420 }
421
422 func terminalRemove(ctx *cli.Context) error {
423
424         tgt, term, err := GetTargetAndTerminalIDs(ctx, false)
425         if err != nil {
426                 return cli.NewExitError(err.Error(), 1)
427         }
428         if tgt == nil || tgt.ID == "" {
429                 return cli.NewExitError("cannot identify target id", 1)
430         }
431         if term == nil || term.ID == "" {
432                 return cli.NewExitError("cannot identify terminal id", 1)
433         }
434
435         // Send delete command
436         url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID)
437         LogPost("DELETE %v", url)
438         if err := HTTPCli.Delete(url, term); err != nil {
439                 return cli.NewExitError(err.Error(), 1)
440         }
441
442         return nil
443 }
444
445 /**
446  * utils functions
447  */
448
449 // TerminalResize Send command to resize target terminal
450 func TerminalResize(tgt *xaapiv1.TargetConfig, term *xaapiv1.TerminalConfig) {
451         col, row, err := terminal.GetSize(int(os.Stdin.Fd()))
452         if err != nil {
453                 Log.Errorf("Error cannot get terminal size: %v", err)
454         }
455         Log.Debugf("Terminal resizing rows %v, cols %v", row, col)
456         sz := xaapiv1.TerminalResizeArgs{Rows: uint16(row), Cols: uint16(col)}
457         url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/resize")
458         if err := HTTPCli.Post(url, &sz, nil); err != nil {
459                 Log.Errorf("Error while resizing terminal (term %v): %v", sz, err)
460         }
461 }
462
463 // TerminalSendSignal Send a signal to a target terminal
464 func TerminalSendSignal(tgt *xaapiv1.TargetConfig, term *xaapiv1.TerminalConfig, sig os.Signal) {
465         url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/signal/" + sig.String())
466         if err := HTTPCli.Post(url, nil, nil); err != nil {
467                 Log.Errorf("Error to send signal %v: %v", sig, err)
468         }
469 }
470
471 // GetTargetAndTerminalIDs Retrieve Target and Terminal definition from IDs
472 func GetTargetAndTerminalIDs(ctx *cli.Context, useFirstFree bool) (*xaapiv1.TargetConfig, *xaapiv1.TerminalConfig, error) {
473
474         tgts := []xaapiv1.TargetConfig{}
475         if err := TargetsListGet(&tgts); err != nil {
476                 return nil, nil, err
477         }
478
479         idArg := ctx.String("id")
480         tidArg := ctx.String("termId")
481         if tidArg == "" {
482                 tidArg = ctx.String("tid")
483         }
484         if idArg != "" || tidArg != "" {
485                 matching := 0
486                 ti := 0
487                 tj := 0
488                 for ii, tt := range tgts {
489                         for jj, ttm := range tt.Terms {
490                                 if idArg == "" && compareID(ttm.ID, tidArg) {
491                                         ti = ii
492                                         tj = jj
493                                         matching++
494                                 }
495                                 if idArg != "" && compareID(tt.ID, idArg) && compareID(ttm.ID, tidArg) {
496                                         ti = ii
497                                         tj = jj
498                                         matching++
499                                 }
500                         }
501                 }
502                 if matching > 1 {
503                         return nil, nil, fmt.Errorf("Multiple IDs found, please set -id and -tid with full ID notation")
504                 } else if matching == 1 {
505                         return &tgts[ti], &tgts[ti].Terms[tj], nil
506                 }
507         }
508
509         // Allow to create a new terminal when only target id is set
510         idArg = GetIDName(ctx, "id")
511         if idArg == "" {
512                 return nil, nil, fmt.Errorf("id or termId argument must be set")
513         }
514
515         for _, tt := range tgts {
516                 if compareID(tt.ID, idArg) {
517                         return &tt, nil, nil
518                 }
519         }
520
521         return nil, nil, fmt.Errorf("No matching id found")
522 }
523
524 // Sort targets by Name
525 type _TgtByName []xaapiv1.TargetConfig
526
527 func (s _TgtByName) Len() int           { return len(s) }
528 func (s _TgtByName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
529 func (s _TgtByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
530
531 // TargetsListGet Get the list of existing targets
532 func TargetsListGet(tgts *[]xaapiv1.TargetConfig) error {
533         var data []byte
534         if err := HTTPCli.HTTPGet(XdsServerComputeURL("/targets"), &data); err != nil {
535                 return err
536         }
537         Log.Debugf("Result of /targets: %v", string(data[:]))
538
539         if err := json.Unmarshal(data, &tgts); err != nil {
540                 return err
541         }
542
543         sort.Sort(_TgtByName(*tgts))
544
545         return nil
546 }