Added target and terminal support.
[src/xds/xds-server.git] / lib / xdsserver / targets.go
diff --git a/lib/xdsserver/targets.go b/lib/xdsserver/targets.go
new file mode 100644 (file)
index 0000000..663233d
--- /dev/null
@@ -0,0 +1,466 @@
+/*
+ * 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 xdsserver
+
+import (
+       "encoding/xml"
+       "fmt"
+       "log"
+       "os"
+       "strings"
+
+       common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib"
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xdsconfig"
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1"
+       socketio "github.com/googollee/go-socket.io"
+       "github.com/syncthing/syncthing/lib/sync"
+)
+
+// Targets Represent a XDS targets
+type Targets struct {
+       *Context
+       fileOnDisk string
+       tgts       map[string]*ITARGET
+       terminals  map[string]*Terminals
+}
+
+// Mutex to make add/delete atomic
+var tcMutex = sync.NewMutex()
+
+/***
+ * Targets
+ ***/
+
+// TargetsConstructor Create a new instance of Model Target
+func TargetsConstructor(ctx *Context) *Targets {
+       file, _ := xdsconfig.TargetsConfigFilenameGet()
+       return &Targets{
+               Context:    ctx,
+               fileOnDisk: file,
+               tgts:       make(map[string]*ITARGET),
+               terminals:  make(map[string]*Terminals),
+       }
+}
+
+// LoadConfig Load targets configuration from disk
+func (t *Targets) LoadConfig() error {
+       var tgts []xsapiv1.TargetConfig
+
+       if t.fileOnDisk != "" {
+               t.Log.Infof("Use target config file: %s", t.fileOnDisk)
+               err := targetsConfigRead(t.fileOnDisk, &tgts)
+               if err != nil {
+                       if strings.HasPrefix(err.Error(), "EOF") {
+                               t.Log.Warnf("Empty target config file")
+                       } else if strings.HasPrefix(err.Error(), "No target config") {
+                               t.Log.Warnf(err.Error())
+                       } else {
+                               return err
+                       }
+               }
+       } else {
+               t.Log.Warnf("Targets config filename not set")
+       }
+
+       // Update targets
+       t.Log.Infof("Loading initial targets config: %d targets found", len(tgts))
+       for _, tc := range tgts {
+               if _, err := t.createUpdate(tc, false, true); err != nil {
+                       return err
+               }
+       }
+
+       return nil
+}
+
+// SaveConfig Save targets configuration to disk
+func (t *Targets) SaveConfig() error {
+       if t.fileOnDisk == "" {
+               return fmt.Errorf("Targets config filename not set")
+       }
+
+       // FIXME: buffered save or avoid to write on disk each time
+       return targetsConfigWrite(t.fileOnDisk, t.getConfigArrUnsafe())
+}
+
+// ResolveID Complete a Target ID (helper for user that can use partial ID value)
+func (t *Targets) ResolveID(id string) (string, error) {
+       if id == "" {
+               return "", nil
+       }
+
+       match := []string{}
+       for iid := range t.tgts {
+               if strings.HasPrefix(iid, id) {
+                       match = append(match, iid)
+               }
+       }
+
+       if len(match) == 1 {
+               return match[0], nil
+       } else if len(match) == 0 {
+               return id, fmt.Errorf("Unknown id")
+       }
+       return id, fmt.Errorf("Multiple IDs found %v", match)
+}
+
+// Get returns the target config or nil if not existing
+func (t *Targets) Get(id string) *ITARGET {
+       if id == "" {
+               return nil
+       }
+       tc, exist := t.tgts[id]
+       if !exist {
+               return nil
+       }
+       return tc
+}
+
+// GetConfigArr returns the config of all targets as an array
+func (t *Targets) GetConfigArr() []xsapiv1.TargetConfig {
+       tcMutex.Lock()
+       defer tcMutex.Unlock()
+
+       return t.getConfigArrUnsafe()
+}
+
+// getConfigArrUnsafe Same as GetConfigArr without mutex protection
+func (t *Targets) getConfigArrUnsafe() []xsapiv1.TargetConfig {
+       conf := []xsapiv1.TargetConfig{}
+       for _, v := range t.tgts {
+               conf = append(conf, (*v).GetConfig())
+       }
+       return conf
+}
+
+// Add adds a new target
+func (t *Targets) Add(newT xsapiv1.TargetConfig) (*xsapiv1.TargetConfig, error) {
+       return t.createUpdate(newT, true, false)
+}
+
+// CreateUpdate creates or update a target
+func (t *Targets) createUpdate(newT xsapiv1.TargetConfig, create bool, initial bool) (*xsapiv1.TargetConfig, error) {
+       var err error
+
+       tcMutex.Lock()
+       defer tcMutex.Unlock()
+
+       // Sanity check
+       if _, exist := t.tgts[newT.ID]; exist {
+               return nil, fmt.Errorf("ID already exists")
+       }
+
+       var tgt ITARGET
+       switch newT.Type {
+       case xsapiv1.TypeTgtStandard:
+               tgt = NewTargetStandard(t.Context)
+       default:
+               return nil, fmt.Errorf("Unsupported target type")
+       }
+
+       // Allocate a new UUID
+       if create {
+               newT.ID = tgt.NewUID("")
+       }
+       if !create && newT.ID == "" {
+               return nil, fmt.Errorf("Cannot update target with null ID")
+       }
+
+       if newT.Name == "" {
+               newT.Name = "Target"
+               if len(newT.ID) > 8 {
+                       newT.Name += "_" + newT.ID[0:8]
+               } else {
+                       newT.Name += "_" + newT.ID
+               }
+       }
+
+       // Call terminals constructor the first time
+       var terms *Terminals
+       if _, exist := t.terminals[newT.ID]; !exist {
+               terms = TerminalsConstructor(t.Context)
+               t.terminals[newT.ID] = terms
+       } else {
+               terms = t.terminals[newT.ID]
+       }
+
+       var newTarget *xsapiv1.TargetConfig
+       if create {
+               // Add target
+               if newTarget, err = tgt.Add(newT, terms); err != nil {
+                       newT.Status = xsapiv1.StatusTgtErrorConfig
+                       log.Printf("ERROR Adding target: %v\n", err)
+                       return newTarget, err
+               }
+       } else {
+               // Just update target config
+               if newTarget, err = tgt.Setup(newT, terms); err != nil {
+                       newT.Status = xsapiv1.StatusTgtErrorConfig
+                       log.Printf("ERROR Updating target: %v\n", err)
+                       return newTarget, err
+               }
+       }
+
+       // Create terminals
+       for _, tc := range newT.Terms {
+               _, err := t.CreateUpdateTerminal(newT.ID, tc, initial)
+               if err != nil {
+                       return newTarget, err
+               }
+       }
+
+       // Add to folders list
+       t.tgts[newT.ID] = &tgt
+
+       // Save config on disk
+       if !initial {
+               if err := t.SaveConfig(); err != nil {
+                       return newTarget, err
+               }
+       }
+
+       newTgt := tgt.GetConfig()
+       return &newTgt, nil
+}
+
+// Delete deletes a specific target
+func (t *Targets) Delete(id string) (xsapiv1.TargetConfig, error) {
+       var err error
+
+       tcMutex.Lock()
+       defer tcMutex.Unlock()
+
+       tgc := xsapiv1.TargetConfig{}
+       tc, exist := t.tgts[id]
+       if !exist {
+               return tgc, fmt.Errorf("unknown id")
+       }
+
+       tgc = (*tc).GetConfig()
+
+       if err = (*tc).Delete(); err != nil {
+               return tgc, err
+       }
+
+       delete(t.tgts, id)
+
+       // Save config on disk
+       err = t.SaveConfig()
+
+       return tgc, err
+}
+
+/***
+ * Terminals
+ ***/
+
+// GetTerminalsArr Return list of existing terminals
+func (t *Targets) GetTerminalsArr(targetID string) ([]xsapiv1.TerminalConfig, error) {
+       arr := []xsapiv1.TerminalConfig{}
+
+       tm, exist := t.terminals[targetID]
+       if !exist {
+               return arr, fmt.Errorf("unknown target id")
+       }
+
+       for _, tt := range (*tm).terms {
+               arr = append(arr, (*tt).GetConfig())
+       }
+       return arr, nil
+}
+
+// GetTerminal Return info of a specific terminal
+func (t *Targets) GetTerminal(targetID, termID string) (*ITERMINAL, error) {
+       tm, exist := t.terminals[targetID]
+       if !exist {
+               return nil, fmt.Errorf("unknown target id")
+       }
+       term, exist := (*tm).terms[termID]
+       if !exist {
+               return nil, fmt.Errorf("unknown terminal id")
+       }
+       return term, nil
+}
+
+// ResolveTerminalID Complete a Terminal ID (helper for user that can use partial ID value)
+func (t *Targets) ResolveTerminalID(termID string) (string, error) {
+       if termID == "" {
+               return "", fmt.Errorf("unknown terminal id")
+       }
+
+       match := []string{}
+       for _, tm := range t.terminals {
+               for tid := range tm.terms {
+                       if strings.HasPrefix(tid, termID) {
+                               match = append(match, tid)
+                       }
+               }
+       }
+
+       if len(match) == 1 {
+               return match[0], nil
+       } else if len(match) == 0 {
+               return termID, fmt.Errorf("Unknown id")
+       }
+       return termID, fmt.Errorf("Multiple IDs found %v", match)
+}
+
+// CreateUpdateTerminal Create or Update a target terminal definition
+func (t *Targets) CreateUpdateTerminal(targetID string, tmCfg xsapiv1.TerminalConfig, initial bool) (*xsapiv1.TerminalConfig, error) {
+
+       var term *xsapiv1.TerminalConfig
+
+       iTerm, err := t.GetTerminal(targetID, tmCfg.ID)
+       if err != nil && strings.Contains(err.Error(), "unknown target") {
+               return nil, err
+       }
+
+       if iTerm != nil {
+               // Update terminal config
+               term = (*iTerm).UpdateConfig(tmCfg)
+       } else {
+               // Auto create a new terminal when needed
+               var err error
+               if term, err = t.terminals[targetID].New(tmCfg, targetID); err != nil {
+                       return nil, err
+               }
+       }
+
+       term.Status = xsapiv1.StatusTermEnable
+
+       // Save config on disk
+       if !initial {
+               if err := t.SaveConfig(); err != nil {
+                       return term, err
+               }
+       }
+
+       return term, nil
+}
+
+// DeleteTerminal Delete a target terminal definition
+func (t *Targets) DeleteTerminal(targetID, termID string) (*xsapiv1.TerminalConfig, error) {
+       terms, exist := t.terminals[targetID]
+       if !exist {
+               return nil, fmt.Errorf("unknown target id")
+       }
+
+       term, err := (*terms).Free(termID)
+       if err != nil {
+               return term, err
+       }
+
+       // Save config on disk
+       if err := t.SaveConfig(); err != nil {
+               return term, err
+       }
+
+       return term, nil
+}
+
+// OpenTerminal Open a target terminal
+func (t *Targets) OpenTerminal(targetID, termID string, sock *socketio.Socket, sessID string) (*xsapiv1.TerminalConfig, error) {
+       terms, exist := t.terminals[targetID]
+       if !exist {
+               return nil, fmt.Errorf("unknown target id")
+       }
+       return (*terms).Open(termID, sock, sessID)
+}
+
+// CloseTerminal Close a target terminal
+func (t *Targets) CloseTerminal(targetID, termID string) (*xsapiv1.TerminalConfig, error) {
+       terms, exist := t.terminals[targetID]
+       if !exist {
+               return nil, fmt.Errorf("unknown target id")
+       }
+       return (*terms).Close(termID)
+}
+
+// ResizeTerminal Set size (row+col) of a target terminal
+func (t *Targets) ResizeTerminal(targetID, termID string, cols, rows uint16) (*xsapiv1.TerminalConfig, error) {
+       terms, exist := t.terminals[targetID]
+       if !exist {
+               return nil, fmt.Errorf("unknown target id")
+       }
+       return (*terms).Resize(termID, cols, rows)
+}
+
+// SignalTerminal Send a signal to a target terminal
+func (t *Targets) SignalTerminal(targetID, termID, sigNum string) error {
+       terms, exist := t.terminals[targetID]
+       if !exist {
+               return fmt.Errorf("unknown target id")
+       }
+       return (*terms).Signal(termID, sigNum)
+}
+
+/**
+ * Private functions
+ **/
+
+// Use XML format and not json to be able to save/load all fields including
+// ones that are masked in json (IOW defined with `json:"-"`)
+type xmlTargets struct {
+       XMLName xml.Name               `xml:"targets"`
+       Version string                 `xml:"version,attr"`
+       Targets []xsapiv1.TargetConfig `xml:"targets"`
+}
+
+// targetsConfigRead reads targets config from disk
+func targetsConfigRead(file string, targets *[]xsapiv1.TargetConfig) error {
+       if !common.Exists(file) {
+               return fmt.Errorf("No target config file found (%s)", file)
+       }
+
+       ffMutex.Lock()
+       defer ffMutex.Unlock()
+
+       fd, err := os.Open(file)
+       defer fd.Close()
+       if err != nil {
+               return err
+       }
+
+       data := xmlTargets{}
+       err = xml.NewDecoder(fd).Decode(&data)
+       if err == nil {
+               *targets = data.Targets
+       }
+       return err
+}
+
+// targetsConfigWrite writes targets config on disk
+func targetsConfigWrite(file string, targets []xsapiv1.TargetConfig) error {
+       ffMutex.Lock()
+       defer ffMutex.Unlock()
+
+       fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
+       defer fd.Close()
+       if err != nil {
+               return err
+       }
+
+       data := &xmlTargets{
+               Version: "1",
+               Targets: targets,
+       }
+
+       enc := xml.NewEncoder(fd)
+       enc.Indent("", "  ")
+       return enc.Encode(data)
+}