Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide local artifacts for software installation #36

Merged
merged 7 commits into from
Sep 12, 2022
1 change: 1 addition & 0 deletions hawkbit/lib_protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ const (
HTTPS Protocol = "HTTPS"
FTP Protocol = "FTP"
SFTP Protocol = "SFTP"
FILE Protocol = "FILE"
)
27 changes: 27 additions & 0 deletions internal/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package feature

import (
"fmt"
"strings"
"sync"
"time"

Expand All @@ -27,6 +28,10 @@ import (
const (
defaultDisconnectTimeout = 250 * time.Millisecond
defaultKeepAlive = 20 * time.Second

modeStrict = "strict"
modeScoped = "scoped"
modeLax = "lax"
)

var (
Expand All @@ -50,6 +55,8 @@ type ScriptBasedSoftwareUpdatableConfig struct {
ServerCert string
DownloadRetryCount int
DownloadRetryInterval durationTime
InstallPath string
Mode string
InstallCommand command
}

Expand All @@ -65,6 +72,8 @@ type ScriptBasedSoftwareUpdatable struct {
serverCert string
downloadRetryCount int
downloadRetryInterval time.Duration
installPath []string
accessMode string
installCommand *command
}

Expand All @@ -89,6 +98,10 @@ func InitScriptBasedSU(scriptSUPConfig *ScriptBasedSoftwareUpdatableConfig) (*Ed
downloadRetryCount: scriptSUPConfig.DownloadRetryCount,
// Interval between download reattempts
downloadRetryInterval: time.Duration(scriptSUPConfig.DownloadRetryInterval),
// Install locations for local artifacts
installPath: parseInstallPaths(scriptSUPConfig.InstallPath),
// Access mode for local artifacts
accessMode: initAccessMode(scriptSUPConfig.Mode),
// Define the module artifact(s) type: archive or plane
artifactType: scriptSUPConfig.ArtifactType,
// Create queue with size 10
Expand Down Expand Up @@ -149,5 +162,19 @@ func (scriptSUPConfig *ScriptBasedSoftwareUpdatableConfig) Validate() error {
if scriptSUPConfig.DownloadRetryCount < 0 {
return fmt.Errorf("negative download retry count value - %d", scriptSUPConfig.DownloadRetryCount)
}
if !strings.EqualFold(modeStrict, scriptSUPConfig.Mode) && !strings.EqualFold(modeScoped, scriptSUPConfig.Mode) && !strings.EqualFold(modeLax, scriptSUPConfig.Mode) {
return fmt.Errorf("invalid mode value, must be either strict, scoped or lax")
}
return nil
}

func parseInstallPaths(installPaths string) []string {
return strings.FieldsFunc(installPaths, storage.SplitArtifacts)
}

func initAccessMode(accessMode string) string {
if accessMode == "" {
return modeStrict
}
return accessMode
}
8 changes: 5 additions & 3 deletions internal/feature_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (f *ScriptBasedSoftwareUpdatable) downloadHandler(
// returns true if canceled!
func (f *ScriptBasedSoftwareUpdatable) downloadModules(
toDir string, updatable *storage.Updatable, su *hawkbit.SoftwareUpdatable) bool {
// Process download operation.
// Process downlaod operation.
logger.Debugf("Process download operation with id: %s", updatable.CorrelationID)

// Download all modules.
Expand Down Expand Up @@ -68,7 +68,7 @@ func (f *ScriptBasedSoftwareUpdatable) downloadModules(
// downloadModule returns true if canceled!
func (f *ScriptBasedSoftwareUpdatable) downloadModule(
cid string, module *storage.Module, toDir string, su *hawkbit.SoftwareUpdatable) bool {
// Download module to direcotry.
// Downlaod module to direcotry.
logger.Infof("Download module [%s.%s] to directory: %s", module.Name, module.Version, toDir)
// Create few useful variables.
id := module.Name + ":" + module.Version
Expand Down Expand Up @@ -123,7 +123,9 @@ Started:
Downloading:
if opError = f.store.DownloadModule(toDir, module, func(percent int) {
setLastOS(su, newOS(cid, module, hawkbit.StatusDownloading).WithProgress(percent))
}, f.serverCert, f.downloadRetryCount, f.downloadRetryInterval); opError != nil {
}, f.serverCert, f.downloadRetryCount, f.downloadRetryInterval, func() error {
return f.validateLocalArtifacts(module)
}); opError != nil {
opErrorMsg = errDownload
return opError == storage.ErrCancel
}
Expand Down
70 changes: 49 additions & 21 deletions internal/feature_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"

"github.com/eclipse-kanto/software-update/hawkbit"
Expand Down Expand Up @@ -63,14 +64,16 @@ func (f *ScriptBasedSoftwareUpdatable) installModules(
// installModule returns true if canceled!
func (f *ScriptBasedSoftwareUpdatable) installModule(
cid string, module *storage.Module, dir string, su *hawkbit.SoftwareUpdatable) bool {
// Install module to direcotry.
// Install module to directory.
logger.Infof("Install module [%s.%s] from directory: %s", module.Name, module.Version, dir)
// Create few useful variables.
id := module.Name + ":" + module.Version
s := filepath.Join(dir, storage.InternalStatusName)
var opError error
opErrorMsg := errRuntime

execInstallScriptDir := dir

// Process final operation status in defer to also catch potential panic calls.
defer func() {
if opError == storage.ErrCancel {
Expand All @@ -84,15 +87,15 @@ func (f *ScriptBasedSoftwareUpdatable) installModule(
if exiterr, ok := opError.(*exec.ExitError); ok {
logger.Errorf("failed to install module [%s.%s][ExitCode: %v]: %v",
module.Name, module.Version, exiterr.ExitCode(), opError)
setLastOS(su, newFileOS(dir, cid, module, hawkbit.StatusFinishedError).
setLastOS(su, newFileOS(execInstallScriptDir, cid, module, hawkbit.StatusFinishedError).
WithStatusCode(strconv.Itoa(exiterr.ExitCode())).
WithMessage(opErrorMsg))
} else {
logger.Errorf("failed to install module [%s.%s]: %v", module.Name, module.Version, opError)
setLastOS(su, newOS(cid, module, hawkbit.StatusFinishedError).WithMessage(opErrorMsg))
}
} else { // Success
setLastOS(su, newFileOS(dir, cid, module, hawkbit.StatusFinishedSuccess))
setLastOS(su, newFileOS(execInstallScriptDir, cid, module, hawkbit.StatusFinishedSuccess))
}
}()

Expand Down Expand Up @@ -130,7 +133,9 @@ Started:
Downloading:
if opError = f.store.DownloadModule(dir, module, func(progress int) {
setLastOS(su, newOS(cid, module, hawkbit.StatusDownloading).WithProgress(progress))
}, f.serverCert, f.downloadRetryCount, f.downloadRetryInterval); opError != nil {
}, f.serverCert, f.downloadRetryCount, f.downloadRetryInterval, func() error {
return f.validateLocalArtifacts(module)
}); opError != nil {
opErrorMsg = errDownload
return opError == storage.ErrCancel
}
Expand All @@ -147,17 +152,6 @@ Downloaded:
storage.WriteLn(s, string(hawkbit.StatusInstalling))
Installing:

// Monitor install progress
monitor, err := (&monitor{
status: hawkbit.StatusInstalling,
su: su,
cid: cid,
module: &hawkbit.SoftwareModuleID{Name: module.Name, Version: module.Version},
}).waitFor(dir)
if err != nil {
logger.Debugf("fail to start progress monitor: %v", err)
}

// Get artifact type
artifactType := f.artifactType
if module.Metadata != nil && module.Metadata["artifact-type"] != "" {
Expand All @@ -166,19 +160,53 @@ Installing:
if artifactType == "archive" { // Extract if needed
if len(module.Artifacts) > 1 { // Only one archive/artifact is allowed in archive modules
opErrorMsg = errMultiArchives
opError = fmt.Errorf("archive modules cannot have multiples artifacts")
opError = fmt.Errorf(opErrorMsg)
return false
}
logger.Debugf("[%s.%s] Extract module archive(s) to: ", module.Name, module.Version)
if opError = storage.ExtractArchive(dir); opError != nil {
opErrorMsg = errExtractArchive
return false
}
} else {
var installScriptExtLocation string
for _, sa := range module.Artifacts {
if runtime.GOOS == "windows" {
if sa.FileName == "install.bat" && sa.Local && !sa.Copy {
installScriptExtLocation = sa.Link
break
}
} else if sa.FileName == "install.sh" && sa.Local && !sa.Copy {
installScriptExtLocation = sa.Link
break
}
}
if installScriptExtLocation != "" {
absExecPath, err := filepath.Abs(installScriptExtLocation)
if err != nil {
opErrorMsg = errDetermineAbsolutePath
opError = fmt.Errorf(opErrorMsg, err)
return false
}
execInstallScriptDir = filepath.Dir(absExecPath)
logger.Debugf("install script %s will be ran in its original folder", installScriptExtLocation)
}
}

// Monitor install progress
monitor, err := (&monitor{
status: hawkbit.StatusInstalling,
su: su,
cid: cid,
module: &hawkbit.SoftwareModuleID{Name: module.Name, Version: module.Version},
}).waitFor(execInstallScriptDir)
if err != nil {
logger.Debugf("fail to start progress monitor: %v", err)
}

// Start install script
logger.Debugf("[%s.%s] Run module install script", module.Name, module.Version)
if opError = f.installCommand.run(dir, "install"); opError != nil {
logger.Debugf("[%s.%s] Run module install script in %s", module.Name, module.Version, execInstallScriptDir)
if opError = f.installCommand.run(execInstallScriptDir, "install"); opError != nil {
opErrorMsg = errInstallScript
return false
}
Expand All @@ -189,14 +217,14 @@ Installing:
}

// Move the predefined installed dependencies
if opError = f.store.MoveInstalledDeps(dir, module.Metadata); opError != nil {
opErrorMsg = errInstalledDepsSsave
if opError = f.store.MoveInstalledDeps(execInstallScriptDir, module.Metadata); opError != nil {
opErrorMsg = errInstalledDepsSave
return false
}

// Installed
logger.Debugf("[%s.%s] Module installed", module.Name, module.Version)
setLastOS(su, newFileOS(dir, cid, module, hawkbit.StatusInstalled))
setLastOS(su, newFileOS(execInstallScriptDir, cid, module, hawkbit.StatusInstalled))

// Update installed dependencies
deps, err := f.store.LoadInstalledDeps()
Expand Down
35 changes: 27 additions & 8 deletions internal/feature_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ import (
)

const (
errRuntime = "internal runtime error"
errMultiArchives = "archive modules cannot have multiples artifacts"
errDownload = "fail to download module"
errExtractArchive = "fail to extract module archive"
errInstallScript = "fail to execute install script"
errInstalledDepsSsave = "fail to save installed dependencies"
errInstalledDepsRefresh = "fail to refresh installed dependencies"
errRuntime = "internal runtime error"
errMultiArchives = "archive modules cannot have multiple artifacts"
errDownload = "fail to download module"
errExtractArchive = "fail to extract module archive"
errInstallScript = "fail to execute install script"
errInstalledDepsSave = "fail to save installed dependencies"
errInstalledDepsRefresh = "fail to refresh installed dependencies"
errDetermineAbsolutePath = "fail to determine absolute path of install script %s - %v"
)

// opw is an operation wrapper function.
Expand Down Expand Up @@ -200,9 +201,27 @@ func newFileOS(dir string, cid string, module *storage.Module, status hawkbit.St
return ops
}

// setLastOS sets the last operatino status and log an error on error.
// setLastOS sets the last operation status and log an error on error.
func setLastOS(su *hawkbit.SoftwareUpdatable, os *hawkbit.OperationStatus) {
if err := su.SetLastOperation(os); err != nil {
logger.Errorf("fail to send last operation status: %v", err)
}
}

func (f *ScriptBasedSoftwareUpdatable) validateLocalArtifacts(module *storage.Module) error {
logger.Debugf("validating local artifacts of module - %v", module)
for _, sa := range module.Artifacts {
if !sa.Local {
continue
}
location := f.locateArtifact(sa.Link)
if location == "" {
msg := fmt.Sprintf("could not locate local artifact [%s]", sa.Link)
logger.Error(msg)
return fmt.Errorf(msg)
}
logger.Infof("resolved local artifact location - %s", location)
sa.Link = location
}
return nil
}
2 changes: 1 addition & 1 deletion internal/feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func testDisconnectWhileRunningOperation(feature *ScriptBasedSoftwareUpdatable,

statuses = append(statuses, pullStatusChanges(mc, postDisconnectEventCount)...)
waitDisconnect.Wait()
defer feature.Connect(mc, supConfig, edgeCfg)
defer connectFeature(t, mc, feature, getDefaultFlagValue(t, flagFeatureID))
if install {
checkInstallStatusEvents(statuses, t)
} else {
Expand Down
Loading