Update .gitreview file
[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         "encoding/json"
23         "fmt"
24         "io"
25         "os"
26         "sort"
27         "strings"
28         "time"
29
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"
34 )
35
36 func initCmdTargets(cmdDef *[]cli.Command) {
37         *cmdDef = append(*cmdDef, cli.Command{
38                 Name:     "targets",
39                 Aliases:  []string{"tgt"},
40                 HideHelp: true,
41                 Usage:    "targets commands group",
42                 Subcommands: []cli.Command{
43                         {
44                                 Name:    "add",
45                                 Aliases: []string{"a"},
46                                 Usage:   "Add a new target",
47                                 Action:  targetsAdd,
48                                 Flags: []cli.Flag{
49                                         cli.StringFlag{
50                                                 Name:  "name, n",
51                                                 Usage: "target name (free form string)",
52                                         },
53                                         cli.StringFlag{
54                                                 Name:  "ip",
55                                                 Usage: "IP address",
56                                         },
57                                         cli.BoolFlag{
58                                                 Name:  "short, s",
59                                                 Usage: "short output, only print create target id (useful from scripting)",
60                                         },
61                                         cli.StringFlag{
62                                                 Name:  "type, t",
63                                                 Usage: "target type (standard|std)",
64                                         },
65                                 },
66                         },
67                         {
68                                 Name:   "get",
69                                 Usage:  "Get properties of a target",
70                                 Action: targetsGet,
71                                 Flags: []cli.Flag{
72                                         cli.StringFlag{
73                                                 Name:   "id",
74                                                 Usage:  "target id",
75                                                 EnvVar: "XDS_TARGET_ID",
76                                         },
77                                 },
78                         },
79                         {
80                                 Name:    "list",
81                                 Aliases: []string{"ls"},
82                                 Usage:   "List existing targets",
83                                 Action:  targetsList,
84                                 Flags: []cli.Flag{
85                                         cli.BoolFlag{
86                                                 Name:  "verbose, v",
87                                                 Usage: "display verbose output",
88                                         },
89                                 },
90                         },
91                         {
92                                 Name:    "remove",
93                                 Aliases: []string{"rm"},
94                                 Usage:   "Remove an existing target",
95                                 Action:  targetsRemove,
96                                 Flags: []cli.Flag{
97                                         cli.StringFlag{
98                                                 Name:   "id",
99                                                 Usage:  "target id",
100                                                 EnvVar: "XDS_TARGET_ID",
101                                         },
102                                         cli.BoolFlag{
103                                                 Name:  "force, f",
104                                                 Usage: "remove confirmation prompt before removal",
105                                         },
106                                 },
107                         },
108                         {
109                                 Name:    "terminal",
110                                 Aliases: []string{"term"},
111                                 Usage:   "Open a target terminal",
112                                 Action:  terminalOpen,
113                                 Flags: []cli.Flag{
114                                         cli.StringFlag{
115                                                 Name:   "id",
116                                                 Usage:  "target id",
117                                                 EnvVar: "XDS_TARGET_ID",
118                                         },
119                                         cli.StringSliceFlag{
120                                                 Name:  "options, o",
121                                                 Usage: "passthrough options set to command line used to start terminal",
122                                         },
123                                         cli.StringFlag{
124                                                 Name:   "termId, tid",
125                                                 Usage:  "terminal id",
126                                                 EnvVar: "XDS_TERMINAL_ID",
127                                         },
128                                         cli.StringFlag{
129                                                 Name:   "user, u",
130                                                 Usage:  "user name used to connect terminal",
131                                                 EnvVar: "XDS_TERMINAL_USER",
132                                         },
133                                 },
134                         },
135                         {
136                                 Name:    "terminal-remove",
137                                 Aliases: []string{"term-rm"},
138                                 Usage:   "Remove a target terminal",
139                                 Action:  terminalRemove,
140                                 Flags: []cli.Flag{
141                                         cli.StringFlag{
142                                                 Name:   "id",
143                                                 Usage:  "target id",
144                                                 EnvVar: "XDS_TARGET_ID",
145                                         },
146                                         cli.StringFlag{
147                                                 Name:   "termId, tid",
148                                                 Usage:  "terminal id",
149                                                 EnvVar: "XDS_TERMINAL_ID",
150                                         },
151                                 },
152                         },
153                 },
154         })
155 }
156
157 func targetsList(ctx *cli.Context) error {
158         // Get targets list
159         tgts := []xaapiv1.TargetConfig{}
160         if err := TargetsListGet(&tgts); err != nil {
161                 return cli.NewExitError(err.Error(), 1)
162         }
163         _displayTargets(tgts, ctx.Bool("verbose"))
164         return nil
165 }
166
167 func targetsGet(ctx *cli.Context) error {
168         id := GetID(ctx, "XDS_TARGET_ID")
169         if id == "" {
170                 return cli.NewExitError("id parameter or option must be set", 1)
171         }
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)
176         }
177         _displayTargets(tgts, true)
178         return nil
179 }
180
181 func _displayTargets(tgts []xaapiv1.TargetConfig, verbose bool) {
182         // Display result
183         first := true
184         writer := NewTableWriter()
185         for _, tgt := range tgts {
186                 if verbose {
187                         if !first {
188                                 fmt.Fprintln(writer)
189                         }
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 {
196                                 tmNfo := "\t\n"
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)
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, "XDS_TARGET_ID")
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         IOSkClient.On("disconnection", func(err error) {
328                 Log.Debugf("WS disconnection event with err: %v\n", err)
329                 exitChan <- exitResult{err, 2}
330         })
331
332         IOSkClient.On(xaapiv1.TerminalOutEvent, func(ev xaapiv1.TerminalOutMsg) {
333                 if len(ev.Stdout) > 0 {
334                         os.Stdout.Write(ev.Stdout)
335                 }
336                 if len(ev.Stderr) > 0 {
337                         os.Stderr.Write(ev.Stdout)
338                 }
339         })
340
341         IOSkClient.On(xaapiv1.TerminalExitEvent, func(ev xaapiv1.TerminalExitMsg) {
342                 exitChan <- exitResult{ev.Error, ev.Code}
343         })
344
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)
348         }
349         oldState, err := terminal.MakeRaw(int(os.Stdin.Fd()))
350         if err != nil {
351                 return cli.NewExitError(err.Error(), 1)
352         }
353         defer terminal.Restore(int(os.Stdin.Fd()), oldState)
354
355         // Send stdin though WS
356         go func() {
357                 type exposeFd interface {
358                         Fd() uintptr
359                 }
360                 buff := make([]byte, 128)
361                 rdfs := &goselect.FDSet{}
362                 reader := io.ReadCloser(os.Stdin)
363                 defer reader.Close()
364
365                 for {
366                         rdfs.Zero()
367                         rdfs.Set(reader.(exposeFd).Fd())
368                         err := goselect.Select(1, rdfs, nil, nil, 50*time.Millisecond)
369                         if err != nil {
370                                 terminal.Restore(int(os.Stdin.Fd()), oldState)
371                                 exitChan <- exitResult{err, 3}
372                                 return
373                         }
374                         if rdfs.IsSet(reader.(exposeFd).Fd()) {
375                                 size, err := reader.Read(buff)
376
377                                 if err != nil {
378                                         Log.Debugf("Read error %v; err %v", size, err)
379                                         if err == io.EOF {
380                                                 // CTRL-D exited scanner, so send it explicitly
381                                                 err := IOSkClient.Emit(xaapiv1.TerminalInEvent, "\x04\n")
382
383                                                 if err != nil {
384                                                         terminal.Restore(int(os.Stdin.Fd()), oldState)
385                                                         exitChan <- exitResult{err, 4}
386                                                         return
387                                                 }
388                                                 time.Sleep(time.Millisecond * 100)
389                                                 continue
390                                         } else {
391                                                 terminal.Restore(int(os.Stdin.Fd()), oldState)
392                                                 exitChan <- exitResult{err, 5}
393                                                 return
394                                         }
395                                 }
396
397                                 if size <= 0 {
398                                         continue
399                                 }
400
401                                 data := buff[:size]
402                                 LogSillyf("Terminal Send data <%v> (%s)", data, data)
403                                 err = IOSkClient.Emit(xaapiv1.TerminalInEvent, data)
404                                 if err != nil {
405                                         terminal.Restore(int(os.Stdin.Fd()), oldState)
406                                         exitChan <- exitResult{err, 6}
407                                         return
408                                 }
409                         }
410                 }
411         }()
412
413         // Handle signals
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")
420                 } else {
421                         TerminalSendSignal(tgt, term, sig)
422                 }
423         })
424         if err != nil {
425                 return cli.NewExitError(err.Error(), 1)
426         }
427
428         // Send open command
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)
433         }
434
435         // Send init size
436         TerminalResize(tgt, term)
437
438         // Wait exit - blocking
439         select {
440         case res := <-IOSkClient.ServerDiscoChan:
441                 Log.Debugf("XDS Server disconnected %v", res)
442                 return cli.NewExitError(res.error, res.code)
443
444         case res := <-exitChan:
445                 errStr := ""
446                 if res.code == 0 {
447                         Log.Debugln("Exit Target Terminal successfully")
448                 }
449                 if res.error != nil {
450                         Log.Debugln("Exit Target Terminal with ERROR: ", res.error.Error())
451                         errStr = res.error.Error()
452                 }
453                 return cli.NewExitError(errStr, res.code)
454         }
455 }
456
457 func terminalRemove(ctx *cli.Context) error {
458
459         tgt, term, err := GetTargetAndTerminalIDs(ctx, false)
460         if err != nil {
461                 return cli.NewExitError(err.Error(), 1)
462         }
463         if tgt == nil || tgt.ID == "" {
464                 return cli.NewExitError("cannot identify target id", 1)
465         }
466         if term == nil || term.ID == "" {
467                 return cli.NewExitError("cannot identify terminal id", 1)
468         }
469
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)
475         }
476
477         return nil
478 }
479
480 /**
481  * utils functions
482  */
483
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()))
487         if err != nil {
488                 Log.Errorf("Error cannot get terminal size: %v", err)
489         }
490
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)
496         }
497 }
498
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)
504         }
505 }
506
507 // GetTargetAndTerminalIDs Retrieve Target and Terminal definition from IDs
508 func GetTargetAndTerminalIDs(ctx *cli.Context, useFirstFree bool) (*xaapiv1.TargetConfig, *xaapiv1.TerminalConfig, error) {
509
510         tgts := []xaapiv1.TargetConfig{}
511         if err := TargetsListGet(&tgts); err != nil {
512                 return nil, nil, err
513         }
514
515         idArg := ctx.String("id")
516         tidArg := ctx.String("termId")
517         if tidArg == "" {
518                 tidArg = ctx.String("tid")
519         }
520         if idArg != "" || tidArg != "" {
521                 matching := 0
522                 ti := 0
523                 tj := 0
524                 for ii, tt := range tgts {
525                         for jj, ttm := range tt.Terms {
526                                 if idArg == "" && compareID(ttm.ID, tidArg) {
527                                         ti = ii
528                                         tj = jj
529                                         matching++
530                                 }
531                                 if idArg != "" && compareID(tt.ID, idArg) && compareID(ttm.ID, tidArg) {
532                                         ti = ii
533                                         tj = jj
534                                         matching++
535                                 }
536                         }
537                 }
538                 if matching > 1 {
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
542                 }
543         }
544
545         // Allow to create a new terminal when only target id is set
546         idArg = GetIDName(ctx, "id", "XDS_TARGET_ID")
547         if idArg == "" {
548                 return nil, nil, fmt.Errorf("id or termId argument must be set")
549         }
550
551         for _, tt := range tgts {
552                 if compareID(tt.ID, idArg) {
553                         if useFirstFree {
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
558                                         }
559                                 }
560                         }
561                         return &tt, nil, nil
562                 }
563         }
564
565         return nil, nil, fmt.Errorf("No matching id found")
566 }
567
568 // Sort targets by Name
569 type _TgtByName []xaapiv1.TargetConfig
570
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 }
574
575 // TargetsListGet Get the list of existing targets
576 func TargetsListGet(tgts *[]xaapiv1.TargetConfig) error {
577         var data []byte
578         if err := HTTPCli.HTTPGet(XdsServerComputeURL("/targets"), &data); err != nil {
579                 return err
580         }
581         Log.Debugf("Result of /targets: %v", string(data[:]))
582
583         if err := json.Unmarshal(data, &tgts); err != nil {
584                 return err
585         }
586
587         sort.Sort(_TgtByName(*tgts))
588
589         return nil
590 }