From befb10519daefcbee85950a4e574678fe4b7f402 Mon Sep 17 00:00:00 2001 From: Sebastien Douheret Date: Fri, 23 Feb 2018 18:50:31 +0100 Subject: Added target and terminal support. Signed-off-by: Sebastien Douheret --- lib/agent/apiv1-browse.go | 2 +- lib/agent/apiv1-config.go | 2 +- lib/agent/apiv1-targets.go | 172 +++++++++++++++++++++++++++++++++++++++++++++ lib/agent/apiv1.go | 8 +-- lib/agent/webserver.go | 12 ++-- lib/agent/xdsserver.go | 69 ++++++++++++++++++ lib/xaapiv1/targets.go | 124 ++++++++++++++++++++++++++++++++ 7 files changed, 378 insertions(+), 11 deletions(-) create mode 100644 lib/agent/apiv1-targets.go create mode 100644 lib/xaapiv1/targets.go (limited to 'lib') diff --git a/lib/agent/apiv1-browse.go b/lib/agent/apiv1-browse.go index dcfe79b..f1b25e8 100644 --- a/lib/agent/apiv1-browse.go +++ b/lib/agent/apiv1-browse.go @@ -37,7 +37,7 @@ func (s *APIService) browseFS(c *gin.Context) { response := apiDirectory{ Dir: []directory{ - directory{Name: "TODO SEB"}, + directory{Name: "TODO"}, }, } diff --git a/lib/agent/apiv1-config.go b/lib/agent/apiv1-config.go index b24dc21..cfecd82 100644 --- a/lib/agent/apiv1-config.go +++ b/lib/agent/apiv1-config.go @@ -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 index 0000000..5a7862a --- /dev/null +++ b/lib/agent/apiv1-targets.go @@ -0,0 +1,172 @@ +/* + * 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 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) +} diff --git a/lib/agent/apiv1.go b/lib/agent/apiv1.go index 1921f98..3ca84b9 100644 --- a/lib/agent/apiv1.go +++ b/lib/agent/apiv1.go @@ -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 { diff --git a/lib/agent/webserver.go b/lib/agent/webserver.go index 2f24c6c..4deb738 100644 --- a/lib/agent/webserver.go +++ b/lib/agent/webserver.go @@ -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) }) }) diff --git a/lib/agent/xdsserver.go b/lib/agent/xdsserver.go index 1c715f6..c08bfb1 100644 --- a/lib/agent/xdsserver.go +++ b/lib/agent/xdsserver.go @@ -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 index 0000000..9cea6ff --- /dev/null +++ b/lib/xaapiv1/targets.go @@ -0,0 +1,124 @@ +/* + * 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 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 + } +) -- cgit 1.2.3-korg