/* * Copyright (C) 2017-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/json" "fmt" "os/exec" "path" "strconv" "strings" "time" common "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib" "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib/eows" "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1" "github.com/Sirupsen/logrus" uuid "github.com/satori/go.uuid" ) // Definition of scripts used to managed SDKs const ( scriptAdd = "add" scriptDbDump = "db-dump" scriptDbUpdate = "db-update" scriptGetFamConfig = "get-family-config" scriptGetSdkInfo = "get-sdk-info" scriptRemove = "remove" ) var scriptsAll = []string{ scriptAdd, scriptDbDump, scriptDbUpdate, scriptGetFamConfig, scriptGetSdkInfo, scriptRemove, } var sdkCmdID = 0 // CrossSDK Hold SDK config type CrossSDK struct { *Context sdk xsapiv1.SDK scripts map[string]string installCmd *eows.ExecOverWS removeCmd *eows.ExecOverWS } // ListCrossSDK List all available and installed SDK (call "db-dump" script) func ListCrossSDK(scriptDir string, update bool, log *logrus.Logger) ([]xsapiv1.SDK, error) { sdksList := []xsapiv1.SDK{} // First update sdk DB when requested if update { out, err := UpdateSDKDb(scriptDir, log) if err != nil { log.Errorf("SDK DB update failure (%v): %v", err, out) return sdksList, fmt.Errorf("Error while updating SDK DB (%v)", err) } } // Retrieve SDKs list and info cmd := exec.Command(path.Join(scriptDir, scriptDbDump)) stdout, err := cmd.CombinedOutput() if err != nil { return sdksList, fmt.Errorf("Cannot get sdks list: %v", err) } if err = json.Unmarshal(stdout, &sdksList); err != nil { log.Errorf("SDK %s script output:\n%v\n", scriptDbDump, string(stdout)) return sdksList, fmt.Errorf("Cannot decode sdk list %v", err) } return sdksList, nil } // GetSDKInfo Used get-sdk-info script to extract SDK get info from a SDK file/tarball func GetSDKInfo(scriptDir, url, filename, md5sum string, log *logrus.Logger) (xsapiv1.SDK, error) { sdk := xsapiv1.SDK{} args := []string{} if url != "" { args = append(args, "--url", url) } else if filename != "" { args = append(args, "--file", filename) if md5sum != "" { args = append(args, "--md5", md5sum) } } else { return sdk, fmt.Errorf("url of filename must be set") } cmd := exec.Command(path.Join(scriptDir, scriptGetSdkInfo), args...) stdout, err := cmd.CombinedOutput() if err != nil { return sdk, fmt.Errorf("%v %v", string(stdout), err) } if err = json.Unmarshal(stdout, &sdk); err != nil { log.Errorf("SDK %s script output:\n%v\n", scriptGetSdkInfo, string(stdout)) return sdk, fmt.Errorf("Cannot decode sdk info %v", err) } return sdk, nil } // UpdateSDKDb Used db-update script to update SDK database func UpdateSDKDb(scriptDir string, log *logrus.Logger) (string, error) { args := []string{} cmd := exec.Command(path.Join(scriptDir, scriptDbUpdate), args...) stdout, err := cmd.CombinedOutput() return string(stdout), err } // NewCrossSDK creates a new instance of CrossSDK func NewCrossSDK(ctx *Context, sdk xsapiv1.SDK, scriptDir string) (*CrossSDK, error) { s := CrossSDK{ Context: ctx, sdk: sdk, scripts: make(map[string]string), } // Execute get-config script to retrieve SDK configuration getConfFile := path.Join(scriptDir, scriptGetFamConfig) if !common.Exists(getConfFile) { return &s, fmt.Errorf("'%s' script file not found in %s", scriptGetFamConfig, scriptDir) } cmd := exec.Command(getConfFile) stdout, err := cmd.CombinedOutput() if err != nil { return &s, fmt.Errorf("Cannot get sdk config using %s: %v", getConfFile, err) } err = json.Unmarshal(stdout, &s.sdk.FamilyConf) if err != nil { s.Log.Errorf("SDK config script output:\n%v\n", string(stdout)) return &s, fmt.Errorf("Cannot decode sdk config %v", err) } famName := s.sdk.FamilyConf.FamilyName // Sanity check if s.sdk.FamilyConf.RootDir == "" { return &s, fmt.Errorf("SDK config not valid (rootDir not set)") } if s.sdk.FamilyConf.EnvSetupFile == "" { return &s, fmt.Errorf("SDK config not valid (envSetupFile not set)") } // Check that other mandatory scripts are present for _, scr := range scriptsAll { s.scripts[scr] = path.Join(scriptDir, scr) if !common.Exists(s.scripts[scr]) { return &s, fmt.Errorf("Script named '%s' missing in SDK family '%s'", scr, famName) } } // Fixed default fields value sdk.LastError = "" if sdk.Status == "" { sdk.Status = xsapiv1.SdkStatusNotInstalled } // Sanity check errMsg := "Invalid SDK definition " if sdk.Name == "" { return &s, fmt.Errorf(errMsg + "(name not set)") } else if sdk.Profile == "" { return &s, fmt.Errorf(errMsg + "(profile not set)") } else if sdk.Version == "" { return &s, fmt.Errorf(errMsg + "(version not set)") } else if sdk.Arch == "" { return &s, fmt.Errorf(errMsg + "(arch not set)") } if sdk.Status == xsapiv1.SdkStatusInstalled { if sdk.SetupFile == "" { return &s, fmt.Errorf(errMsg + "(setupFile not set)") } else if !common.Exists(sdk.SetupFile) { return &s, fmt.Errorf(errMsg + "(setupFile not accessible)") } if sdk.Path == "" { return &s, fmt.Errorf(errMsg + "(path not set)") } else if !common.Exists(sdk.Path) { return &s, fmt.Errorf(errMsg + "(path not accessible)") } } // Use V3 to ensure that we get same uuid on restart nm := s.sdk.Name if nm == "" { nm = s.sdk.Profile + "_" + s.sdk.Arch + "_" + s.sdk.Version } s.sdk.ID = uuid.NewV3(uuid.FromStringOrNil("sdks"), nm).String() s.LogSillyf("New SDK: ID=%v, Family=%s, Name=%v", s.sdk.ID[:8], s.sdk.FamilyConf.FamilyName, s.sdk.Name) return &s, nil } // Install a SDK (non blocking command, IOW run in background) func (s *CrossSDK) Install(file string, force bool, timeout int, args []string, sess *ClientSession) error { if s.sdk.Status == xsapiv1.SdkStatusInstalled { return fmt.Errorf("already installed") } if s.sdk.Status == xsapiv1.SdkStatusInstalling { return fmt.Errorf("installation in progress") } // Compute command args cmdArgs := []string{} if file != "" { cmdArgs = append(cmdArgs, "--file", file) } else { cmdArgs = append(cmdArgs, "--url", s.sdk.URL) } if force { cmdArgs = append(cmdArgs, "--force") } // Append additional args (passthrough arguments) if len(args) > 0 { cmdArgs = append(cmdArgs, args...) } // Unique command id sdkCmdID++ cmdID := "sdk-install-" + strconv.Itoa(sdkCmdID) // Create new instance to execute command and sent output over WS s.installCmd = eows.New(s.scripts[scriptAdd], cmdArgs, sess.IOSocket, sess.ID, cmdID) s.installCmd.Log = s.Log // TODO: enable Term s.installCmd.PtyMode = true s.installCmd.LineTimeSpan = 500 * time.Millisecond.Nanoseconds() if timeout > 0 { s.installCmd.CmdExecTimeout = timeout } else { s.installCmd.CmdExecTimeout = 30 * 60 // default 30min } // Define callback for output (stdout+stderr) s.installCmd.OutputCB = func(e *eows.ExecOverWS, bStdout, bStderr []byte) { stdout := string(bStdout) stderr := string(bStderr) // paranoia data := e.UserData sdkID := (*data)["SDKID"].(string) if sdkID != s.sdk.ID { s.Log.Errorln("BUG: sdk ID differs: %v != %v", sdkID, s.sdk.ID) } // IO socket can be nil when disconnected so := s.sessions.IOSocketGet(e.Sid) if so == nil { s.Log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", xsapiv1.EVTSDKManagement, e.Sid, e.CmdID) return } if s.LogLevelSilly { s.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - SDK ID:%s:", xsapiv1.EVTSDKManagement, e.Sid[4:], e.CmdID, sdkID[:16]) if stdout != "" { s.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1)) } if stderr != "" { s.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1)) } } err := (*so).Emit(xsapiv1.EVTSDKManagement, xsapiv1.SDKManagementMsg{ CmdID: e.CmdID, Timestamp: time.Now().String(), Action: xsapiv1.SdkMgtActionInstall, Sdk: s.sdk, Progress: 0, // TODO add progress Exited: false, Stdout: stdout, Stderr: stderr, }) if err != nil { s.Log.Errorf("WS Emit : %v", err) } } // Define callback for output s.installCmd.ExitCB = func(e *eows.ExecOverWS, code int, exitError error) { // paranoia data := e.UserData sdkID := (*data)["SDKID"].(string) if sdkID != s.sdk.ID { s.Log.Errorln("BUG: sdk ID differs: %v != %v", sdkID, s.sdk.ID) } s.Log.Infof("Command SDK ID %s [Cmd ID %s] exited: code %d, exitError: %v", sdkID[:16], e.CmdID, code, exitError) // IO socket can be nil when disconnected so := s.sessions.IOSocketGet(e.Sid) if so == nil { s.Log.Infof("%s (exit) not emitted - WS closed (id:%s)", xsapiv1.EVTSDKManagement, e.CmdID) return } // Update SDK status if code == 0 && exitError == nil { s.sdk.LastError = "" s.sdk.Status = xsapiv1.SdkStatusInstalled // FIXME: better update it using monitoring install dir (inotify) // (see sdks.go / monitorSDKInstallation ) // Update SetupFile when n if s.sdk.SetupFile == "" { sdkDef, err := GetSDKInfo(s.sdk.FamilyConf.ScriptsDir, s.sdk.URL, "", "", s.Log) if err != nil || sdkDef.SetupFile == "" { code = 1 s.sdk.LastError = "Installation failed (cannot init SetupFile path)" s.sdk.Status = xsapiv1.SdkStatusNotInstalled } else { s.sdk.SetupFile = sdkDef.SetupFile } } } else { s.sdk.LastError = "Installation failed (code " + strconv.Itoa(code) + ")" if exitError != nil { s.sdk.LastError = ". Error: " + exitError.Error() } s.sdk.Status = xsapiv1.SdkStatusNotInstalled } emitErr := "" if exitError != nil { emitErr = exitError.Error() } if emitErr == "" && s.sdk.LastError != "" { emitErr = s.sdk.LastError } // Emit event errSoEmit := (*so).Emit(xsapiv1.EVTSDKManagement, xsapiv1.SDKManagementMsg{ CmdID: e.CmdID, Timestamp: time.Now().String(), Action: xsapiv1.SdkMgtActionInstall, Sdk: s.sdk, Progress: 100, Exited: true, Code: code, Error: emitErr, }) if errSoEmit != nil { s.Log.Errorf("WS Emit EVTSDKManagement : %v", errSoEmit) } errSoEmit = s.events.Emit(xsapiv1.EVTSDKStateChange, s.sdk, e.Sid) if errSoEmit != nil { s.Log.Errorf("WS Emit EVTSDKStateChange : %v", errSoEmit) } // Cleanup command for the next time s.installCmd = nil } // User data (used within callbacks) data := make(map[string]interface{}) data["SDKID"] = s.sdk.ID s.installCmd.UserData = &data // Start command execution s.Log.Infof("Install SDK %s: cmdID=%v, cmd=%v, args=%v", s.sdk.Name, s.installCmd.CmdID, s.installCmd.Cmd, s.installCmd.Args) s.sdk.Status = xsapiv1.SdkStatusInstalling s.sdk.LastError = "" err := s.installCmd.Start() return err } // AbortInstallRemove abort an install or remove command func (s *CrossSDK) AbortInstallRemove(timeout int) error { if s.installCmd == nil { return fmt.Errorf("no installation in progress for this sdk") } s.sdk.Status = xsapiv1.SdkStatusNotInstalled return s.installCmd.Signal("SIGKILL") } // Remove Used to remove/uninstall a SDK func (s *CrossSDK) Remove(timeout int, sess *ClientSession) error { if s.sdk.Status != xsapiv1.SdkStatusInstalled { return fmt.Errorf("this sdk is not installed") } // IO socket can be nil when disconnected so := s.sessions.IOSocketGet(sess.ID) if so == nil { return fmt.Errorf("Cannot retrieve socket ") } s.sdk.Status = xsapiv1.SdkStatusUninstalling // Notify state change if err := s.events.Emit(xsapiv1.EVTSDKStateChange, s.sdk, sess.ID); err != nil { s.Log.Warningf("Cannot notify SDK remove: %v", err) } script := s.scripts[scriptRemove] args := s.sdk.Path s.Log.Infof("Uninstall SDK %s: script=%v args=%v", s.sdk.Name, script, args) // Notify start removing evData := xsapiv1.SDKManagementMsg{ Timestamp: time.Now().String(), Action: xsapiv1.SdkMgtActionRemove, Sdk: s.sdk, Progress: 0, Exited: false, Code: 0, Error: "", } if errEmit := (*so).Emit(xsapiv1.EVTSDKManagement, evData); errEmit != nil { s.Log.Warningf("Cannot notify EVTSDKManagement end: %v", errEmit) } // Run command to remove SDK cmd := exec.Command(script, args) stdout, err := cmd.CombinedOutput() s.sdk.Status = xsapiv1.SdkStatusNotInstalled s.Log.Debugf("SDK uninstall err %v, output:\n %v", err, string(stdout)) // Emit end of removing process evData = xsapiv1.SDKManagementMsg{ Timestamp: time.Now().String(), Action: xsapiv1.SdkMgtActionRemove, Sdk: s.sdk, Progress: 100, Exited: true, Code: 0, Error: "", } // Update error code on error if err != nil { evData.Code = 1 evData.Error = err.Error() } if errEmit := (*so).Emit(xsapiv1.EVTSDKManagement, evData); errEmit != nil { s.Log.Warningf("Cannot notify EVTSDKManagement end: %v", errEmit) } // Notify state change if errEmit := s.events.Emit(xsapiv1.EVTSDKStateChange, s.sdk, sess.ID); errEmit != nil { s.Log.Warningf("Cannot notify EVTSDKStateChange end: %v", errEmit) } if err != nil { return fmt.Errorf("Error while uninstalling sdk: %v", err) } return nil } // Get Return SDK definition func (s *CrossSDK) Get() *xsapiv1.SDK { return &s.sdk } // GetEnvCmd returns the command used to initialized the environment func (s *CrossSDK) GetEnvCmd() []string { if s.sdk.SetupFile == "" { return []string{} } return []string{"source", s.sdk.SetupFile} }