Added target and terminal support. v1.1.0
authorSebastien Douheret <sebastien.douheret@iot.bzh>
Fri, 23 Feb 2018 17:50:31 +0000 (18:50 +0100)
committerSebastien Douheret <sebastien.douheret@iot.bzh>
Fri, 23 Feb 2018 18:15:02 +0000 (19:15 +0100)
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
.vscode/settings.json
glide.yaml
lib/agent/apiv1-browse.go
lib/agent/apiv1-config.go
lib/agent/apiv1-targets.go [new file with mode: 0644]
lib/agent/apiv1.go
lib/agent/webserver.go
lib/agent/xdsserver.go
lib/xaapiv1/targets.go [new file with mode: 0644]

index 2e081d9..47bcd75 100644 (file)
@@ -90,7 +90,8 @@
     "Sillyf",
     "tabindex",
     "EVTSDK",
-    "gerrit"
+    "gerrit",
+    "tgts"
   ],
   // codelyzer
   "tslint.rulesDirectory": "./webapp/node_modules/codelyzer",
index e7c6bf0..8ddc3e1 100644 (file)
@@ -22,11 +22,11 @@ import:
 - package: github.com/satori/go.uuid
   version: ^1.1.0
 - package: gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git
-  version: ^0.1.0
+  version: ~0.2.0
   subpackages:
   - golib/common
 - package: gerrit.automotivelinux.org/gerrit/src/xds/xds-server.git
-  version: ~1.0.0
+  version: ~1.1.0
   subpackages:
   - lib/xsapiv1
 - package: github.com/franciscocpg/reflectme
index dcfe79b..f1b25e8 100644 (file)
@@ -37,7 +37,7 @@ func (s *APIService) browseFS(c *gin.Context) {
 
        response := apiDirectory{
                Dir: []directory{
-                       directory{Name: "TODO SEB"},
+                       directory{Name: "TODO"},
                },
        }
 
index b24dc21..cfecd82 100644 (file)
@@ -66,7 +66,7 @@ func (s *APIService) setConfig(c *gin.Context) {
                }
        }
 
-       // Add new XDS Server
+       // Add new / unconnected XDS Server
        for _, svr := range cfgArg.Servers {
                if svr.Connected && svr.ID != "" {
                        continue
diff --git a/lib/agent/apiv1-targets.go b/lib/agent/apiv1-targets.go
new file mode 100644 (file)
index 0000000..5a7862a
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * 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 agent
+
+import (
+       "fmt"
+       "net/http"
+
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-agent/lib/xaapiv1"
+       common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib"
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-server.git/lib/xsapiv1"
+       "github.com/franciscocpg/reflectme"
+       "github.com/gin-gonic/gin"
+       uuid "github.com/satori/go.uuid"
+)
+
+// targetsPassthroughInit Declare passthrough routes for targets
+func (s *APIService) targetsPassthroughInit(svr *XdsServer) error {
+       svr.PassthroughGet("/targets")
+       svr.PassthroughGet("/targets/:id")
+       svr.PassthroughPost("/targets")
+       svr.PassthroughDelete("/targets/:id")
+
+       svr.PassthroughGet("/targets/:id/terminals")
+       svr.PassthroughGet("/targets/:id/terminals/:tid")
+       svr.PassthroughPost("/targets/:id/terminals")
+       svr.PassthroughPut("/targets/:id/terminals/:tid")
+       svr.PassthroughDelete("/targets/:id/terminals/:tid")
+
+       svr.apiRouter.POST("/targets/:id/terminals/:tid/open", s.TargetTerminalOpen)
+
+       svr.PassthroughPost("/targets/:id/terminals/:tid/close")
+       svr.PassthroughPost("/targets/:id/terminals/:tid/resize")
+       svr.PassthroughPost("/targets/:id/terminals/:tid/signal")
+       svr.PassthroughPost("/targets/:id/terminals/:tid/signal/:sig")
+
+       return nil
+}
+
+// GetServerFromTargetID Retrieve XDS Server definition from a target ID
+func (s *APIService) GetServerFromTargetID(targetID, termID string) (*XdsServer, string, error) {
+
+       // FIXME add cache (but take care to support partial term ID)
+       for _, svr := range s.xdsServers {
+               term := xsapiv1.TerminalConfig{}
+               if err := svr.CommandTgtTerminalGet(targetID, termID, &term); err == nil {
+                       return svr, term.ID, nil
+               }
+       }
+       return nil, "", fmt.Errorf("Cannot identify XDS Server")
+}
+
+// TargetTerminalOpen Open a target terminal/console
+func (s *APIService) TargetTerminalOpen(c *gin.Context) {
+
+       // First retrieve Server ID and send command to right server
+       targetID := c.Param("id")
+       svr, termID, err := s.GetServerFromTargetID(targetID, c.Param("tid"))
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       // Retrieve session info
+       sess := s.sessions.Get(c)
+       if sess == nil {
+               common.APIError(c, "Unknown sessions")
+               return
+       }
+       sock := sess.IOSocket
+       if sock == nil {
+               common.APIError(c, "Websocket not established")
+               return
+       }
+
+       // Forward input events from client to XDSServer through WS
+       err = (*sock).On(xsapiv1.TerminalInEvent, func(stdin string) {
+               s.LogSillyf("TARGET TERMINAL EVENT IN (%s) <<%v>>", xsapiv1.TerminalInEvent, stdin)
+               svr.EventEmit(xaapiv1.TerminalInEvent, stdin)
+       })
+       if err != nil {
+               msgErr := "Error while registering WS for " + xsapiv1.TerminalInEvent + " event"
+               s.Log.Errorf(msgErr, ", err: %v", err)
+               common.APIError(c, msgErr)
+               return
+       }
+
+       // Forward output events from XDSServer to client through WS
+       var outFwdFuncID uuid.UUID
+       outFwdFunc := func(pData interface{}, evData interface{}) error {
+               sid := pData.(string)
+               // IO socket can be nil when disconnected
+               so := s.sessions.IOSocketGet(sid)
+               if so == nil {
+                       s.Log.Infof("%s not emitted: WS closed (sid:%s)", xaapiv1.TerminalOutEvent, sid)
+                       return nil
+               }
+
+               // Add sessionID to event Data
+               reflectme.SetField(evData, "sessionID", sid)
+
+               s.LogSillyf("TARGET TERMINAL EVENT OUT (%s) <<%v>>", xaapiv1.TerminalOutEvent, evData)
+
+               // Forward event to Client/Dashboard
+               (*so).Emit(xaapiv1.TerminalOutEvent, evData)
+               return nil
+       }
+       outFwdFuncID, err = svr.EventOn(xsapiv1.TerminalOutEvent, sess.ID, outFwdFunc)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       // Handle Exit event separately to cleanup registered listener
+       var exitFuncID uuid.UUID
+       exitFunc := func(privD interface{}, evData interface{}) error {
+               evN := xaapiv1.TerminalExitEvent
+
+               pData := privD.(map[string]string)
+               sid := pData["sessID"]
+
+               // Add sessionID to event Data
+               reflectme.SetField(evData, "sessionID", sid)
+
+               // IO socket can be nil when disconnected
+               so := s.sessions.IOSocketGet(sid)
+               if so != nil {
+                       (*so).Emit(evN, evData)
+               } else {
+                       s.Log.Infof("%s not emitted: WS closed (sid:%s)", evN, sid)
+               }
+
+               // cleanup listener
+               svr.EventOff(xaapiv1.TerminalOutEvent, outFwdFuncID)
+               svr.EventOff(evN, exitFuncID)
+
+               return nil
+       }
+
+       privData := map[string]string{
+               "sessID": sess.ID,
+       }
+       exitFuncID, err = svr.EventOn(xaapiv1.TerminalExitEvent, privData, exitFunc)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       // Forward back command to right server
+       res := xsapiv1.TerminalConfig{}
+       if err := svr.CommandTgtTerminalOpen(targetID, termID, &res); err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       c.JSON(http.StatusOK, res)
+}
index 1921f98..3ca84b9 100644 (file)
@@ -49,7 +49,7 @@ func NewAPIV1(ctx *Context) *APIService {
        s.apiRouter.GET("/config", s.getConfig)
        s.apiRouter.POST("/config", s.setConfig)
 
-       s.apiRouter.GET("/browse", s.browseFS)
+       // s.apiRouter.GET("/browse", s.browseFS)
 
        s.apiRouter.GET("/projects", s.getProjects)
        s.apiRouter.GET("/projects/:id", s.getProject)
@@ -118,6 +118,7 @@ func (s *APIService) AddXdsServer(cfg xdsconfig.XDSServerConf) (*XdsServer, erro
 
                // Declare passthrough routes
                s.sdksPassthroughInit(svr)
+               s.targetsPassthroughInit(svr)
 
                // Register callback on Connection
                svr.ConnectOn(func(server *XdsServer) error {
@@ -125,9 +126,9 @@ func (s *APIService) AddXdsServer(cfg xdsconfig.XDSServerConf) (*XdsServer, erro
                        // Add server to list
                        s.xdsServers[server.ID] = svr
 
-                       // Register event forwarder
+                       // Register events forwarder
                        if err := s.sdksEventsForwardInit(server); err != nil {
-                               s.Log.Errorf("XDS Server %v - sdk event forwarding error: %v", server.ID, err)
+                               s.Log.Errorf("XDS Server %v - sdk events forwarding error: %v", server.ID, err)
                        }
 
                        // Load projects
@@ -164,7 +165,6 @@ func (s *APIService) DelXdsServer(id string) error {
        s.xdsServers[id].Close()
        return nil
 }
-}
 
 // UpdateXdsServer Update XDS Server configuration settings
 func (s *APIService) UpdateXdsServer(cfg xaapiv1.ServerCfg) error {
index 2f24c6c..4deb738 100644 (file)
@@ -246,13 +246,15 @@ func (s *WebServer) socketHandler(c *gin.Context) {
        }
 
        s.sIOServer.On("connection", func(so socketio.Socket) {
-               s.Log.Debugf("WS Connected (WSID=%s, SID=%s)", so.Id(), sess.ID)
-               s.sessions.UpdateIOSocket(sess.ID, &so)
+               sessID := sess.ID
+
+               s.Log.Debugf("WS Connected (sessID=%s, WS SID=%s)", sessID, so.Id())
+               s.sessions.UpdateIOSocket(sessID, &so)
 
                so.On("disconnection", func() {
-                       s.Log.Debugf("WS disconnected (WSID=%s, SID=%s)", so.Id(), sess.ID)
-                       s.events.UnRegister(xaapiv1.EVTAll, sess.ID)
-                       s.sessions.UpdateIOSocket(sess.ID, nil)
+                       s.Log.Debugf("WS disconnected (sessID=%s, WS SID=%s)", sessID, so.Id())
+                       s.events.UnRegister(xaapiv1.EVTAll, sessID)
+                       s.sessions.UpdateIOSocket(sessID, nil)
                })
        })
 
index 1c715f6..c08bfb1 100644 (file)
@@ -208,6 +208,22 @@ func (xs *XdsServer) CommandSignal(args *xsapiv1.ExecSignalArgs, res *xsapiv1.Ex
        return xs.client.Post("/signal", args, res)
 }
 
+// CommandTgtTerminalGet Send GET request to retrieve info of a target terminals
+func (xs *XdsServer) CommandTgtTerminalGet(targetID, termID string, res *xsapiv1.TerminalConfig) error {
+       return xs.client.Get("/targets/"+targetID+"/terminals/"+termID, res)
+}
+
+// CommandTgtTerminalOpen Send POST request to open a target terminal
+func (xs *XdsServer) CommandTgtTerminalOpen(targetID string, termID string, res *xsapiv1.TerminalConfig) error {
+       var empty interface{}
+       return xs.client.Post("/targets/"+targetID+"/terminals/"+termID+"/open", &empty, res)
+}
+
+// CommandTgtTerminalSignal Send POST request to send a signal to a target terminal
+func (xs *XdsServer) CommandTgtTerminalSignal(args *xsapiv1.TerminalSignalArgs, res *xsapiv1.TerminalConfig) error {
+       return xs.client.Post("/signal", args, res)
+}
+
 // SetAPIRouterGroup .
 func (xs *XdsServer) SetAPIRouterGroup(r *gin.RouterGroup) {
        xs.apiRouter = r
@@ -290,6 +306,56 @@ func (xs *XdsServer) PassthroughPost(url string) {
        })
 }
 
+// PassthroughPut Used to declare a route that sends directly a PUT request to XDS Server
+func (xs *XdsServer) PassthroughPut(url string) {
+       if xs.apiRouter == nil {
+               xs.Log.Errorf("apiRouter not set !")
+               return
+       }
+
+       xs.apiRouter.PUT(url, func(c *gin.Context) {
+               var err error
+               var data interface{}
+
+               // Get raw body
+               body, err := c.GetRawData()
+               if err != nil {
+                       common.APIError(c, err.Error())
+                       return
+               }
+
+               // Take care of param (eg. id in /projects/:id)
+               nURL := url
+               if strings.Contains(url, ":") {
+                       nURL = strings.TrimPrefix(c.Request.URL.Path, xs.APIURL)
+               }
+
+               // Send Put request
+               response, err := xs.client.HTTPPutWithRes(nURL, string(body))
+               if err != nil {
+                       goto httpError
+               }
+               if response.StatusCode != 200 {
+                       err = fmt.Errorf(response.Status)
+                       return
+               }
+               err = json.Unmarshal(xs.client.ResponseToBArray(response), &data)
+               if err != nil {
+                       goto httpError
+               }
+
+               c.JSON(http.StatusOK, data)
+               return
+
+               /* Handle error case */
+       httpError:
+               if strings.Contains(err.Error(), "connection refused") {
+                       xs._Disconnected()
+               }
+               common.APIError(c, err.Error())
+       })
+}
+
 // PassthroughDelete Used to declare a route that sends directly a Delete request to XDS Server
 func (xs *XdsServer) PassthroughDelete(url string) {
        if xs.apiRouter == nil {
@@ -660,6 +726,9 @@ func (xs *XdsServer) _SocketConnect() error {
 // _Disconnected Set XDS Server as disconnected
 func (xs *XdsServer) _Disconnected() error {
        // Clear all register events as socket is closed
+       xs.sockEventsLock.Lock()
+       defer xs.sockEventsLock.Unlock()
+
        for k := range xs.sockEvents {
                delete(xs.sockEvents, k)
        }
diff --git a/lib/xaapiv1/targets.go b/lib/xaapiv1/targets.go
new file mode 100644 (file)
index 0000000..9cea6ff
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * 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 xaapiv1
+
+/**
+ * Target
+ **/
+
+// TargetType definition
+type TargetType string
+
+const (
+       // TypeTgtStandard Standard target type
+       TypeTgtStandard = "standard"
+)
+
+// Target Status definition
+const (
+       StatusTgtErrorConfig = "ErrorConfig"
+       StatusTgtDisable     = "Disable"
+       StatusTgtEnable      = "Enable"
+)
+
+// TargetConfig config of a target / board
+type TargetConfig struct {
+       ID     string           `json:"id"`
+       Name   string           `json:"name"`
+       Type   TargetType       `json:"type"`
+       IP     string           `json:"ip"`
+       Status string           `json:"status"`
+       Terms  []TerminalConfig `json:"terms"`
+}
+
+/**
+ * Terminal
+ **/
+
+// TerminalType definition
+type TerminalType string
+
+const (
+       // TypeTermSSH ssh terminal type
+       TypeTermSSH = "ssh"
+
+       // StatusTermErrorConfig Configuration error
+       StatusTermErrorConfig = "ErrorConfig"
+       // StatusTermEnable Enable state
+       StatusTermEnable = "Enable"
+       // StatusTermOpen Open state
+       StatusTermOpen = "Open"
+       // StatusTermClose Close state
+       StatusTermClose = "Close"
+
+       // TerminalInEvent Event send in WS when characters are sent (stdin)
+       TerminalInEvent = "term:input"
+       // TerminalOutEvent Event send in WS when characters are received (stdout or stderr)
+       TerminalOutEvent = "term:output"
+       // TerminalExitEvent Event send in WS on terminal/console exit
+       TerminalExitEvent = "term:exit"
+)
+
+type (
+
+       // TerminalConfig config of a board terminal
+       TerminalConfig struct {
+               ID      string       `json:"id"`
+               Name    string       `json:"name"`
+               Type    TerminalType `json:"type"`
+               User    string       `json:"user"`
+               Options []string     `json:"options"`
+               Status  string       `json:"status"`
+               Cols    uint16       `json:"cols"`
+               Rows    uint16       `json:"rows"`
+       }
+
+       // TerminalInMsg Message used to received input characters (stdin)
+       TerminalInMsg struct {
+               TermID    string `json:"termID"`
+               Timestamp string `json:"timestamp"`
+               Stdin     string `json:"stdin"`
+       }
+
+       // TerminalOutMsg Message used to send output characters (stdout+stderr)
+       TerminalOutMsg struct {
+               TermID    string `json:"termID"`
+               Timestamp string `json:"timestamp"`
+               Stdout    string `json:"stdout"`
+               Stderr    string `json:"stderr"`
+       }
+
+       // TerminalExitMsg Message sent on terminal/console exit
+       TerminalExitMsg struct {
+               TermID    string `json:"termID"`
+               Timestamp string `json:"timestamp"`
+               Code      int    `json:"code"`
+               Error     error  `json:"error"`
+       }
+
+       // TerminalResizeArgs JSON parameters of /terminal/:tid/resize command
+       TerminalResizeArgs struct {
+               Cols uint16 `json:"cols"`
+               Rows uint16 `json:"rows"`
+       }
+
+       // TerminalSignalArgs JSON parameters of /terminal/:tid/signal command
+       TerminalSignalArgs struct {
+               Signal string `json:"signal" binding:"required"` // signal number
+       }
+)