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

Robust Download with reattempt and retry #27

Merged
merged 4 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/software-update/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ func main() {
loggerOut := logger.SetupLogger(logConfig)
defer loggerOut.Close()

if err := suConfig.Validate(); err != nil {
logger.Errorf("failed to validate script-based software updatable configuration: %v\n", err)
os.Exit(1)
}

// Create new Script-Based software updatable
edgeCtr, err := feature.InitScriptBasedSU(suConfig)
if err != nil {
Expand Down
55 changes: 55 additions & 0 deletions internal/duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) 2022 Contributors to the Eclipse Foundation
//
// See the NOTICE file(s) distributed with this work for additional
// information regarding copyright ownership.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0
//
// SPDX-License-Identifier: EPL-2.0

package feature

import (
"encoding/json"
"errors"
"time"
)

//durationTime is custom type of type time.durationTime in order to add json unmarshal support
gboyvalenkov-bosch marked this conversation as resolved.
Show resolved Hide resolved
type durationTime time.Duration

//UnmarshalJSON unmarshal durationTime type
func (d *durationTime) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {

case string:
duration, err := time.ParseDuration(value)
if err != nil {
return err
}
*d = durationTime(duration)
default:
return errors.New("invalid duration")
}
return nil
}

//Set durationTime from string, used for flag set
func (d *durationTime) Set(s string) error {
v, err := time.ParseDuration(s)
if err != nil {
err = errors.New("parse error")
}
*d = durationTime(v)
return err
}

func (d durationTime) String() string {
return time.Duration(d).String()
}
56 changes: 37 additions & 19 deletions internal/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
package feature

import (
"fmt"
"sync"
"time"

Expand Down Expand Up @@ -39,28 +40,32 @@ type operationFunc func() bool

// ScriptBasedSoftwareUpdatableConfig provides the Script-Based SoftwareUpdatable configuration.
type ScriptBasedSoftwareUpdatableConfig struct {
Broker string
Username string
Password string
StorageLocation string
FeatureID string
ModuleType string
ArtifactType string
ServerCert string
InstallCommand command
Broker string
Username string
Password string
StorageLocation string
FeatureID string
ModuleType string
ArtifactType string
ServerCert string
DownloadRetryCount int
DownloadRetryInterval durationTime
InstallCommand command
}

// ScriptBasedSoftwareUpdatable is the Script-Based SoftwareUpdatable actual implementation.
type ScriptBasedSoftwareUpdatable struct {
lock sync.Mutex
queue chan operationFunc
store *storage.Storage
su *hawkbit.SoftwareUpdatable
dittoClient *ditto.Client
mqttClient MQTT.Client
artifactType string
serverCert string
installCommand *command
lock sync.Mutex
queue chan operationFunc
store *storage.Storage
su *hawkbit.SoftwareUpdatable
dittoClient *ditto.Client
mqttClient MQTT.Client
artifactType string
serverCert string
downloadRetryCount int
downloadRetryInterval time.Duration
installCommand *command
}

// InitScriptBasedSU creates a new Script-Based SoftwareUpdatable instance, listening for edge configuration.
Expand All @@ -78,7 +83,12 @@ func InitScriptBasedSU(scriptSUPConfig *ScriptBasedSoftwareUpdatableConfig) (*Ed
store: localStorage,
// Build install script command
installCommand: &scriptSUPConfig.InstallCommand,
serverCert: scriptSUPConfig.ServerCert,
// Server download certificate
serverCert: scriptSUPConfig.ServerCert,
// Number of download reattempts
downloadRetryCount: scriptSUPConfig.DownloadRetryCount,
// Interval between download reattempts
downloadRetryInterval: time.Duration(scriptSUPConfig.DownloadRetryInterval),
// Define the module artifact(s) type: archive or plane
artifactType: scriptSUPConfig.ArtifactType,
// Create queue with size 10
Expand Down Expand Up @@ -133,3 +143,11 @@ func (f *ScriptBasedSoftwareUpdatable) Disconnect(closeStorage bool) {
logger.Info("ditto client disconnected")
f.dittoClient.Disconnect()
}

// Validate the software updatable configuration
func (scriptSUPConfig *ScriptBasedSoftwareUpdatableConfig) Validate() error {
if scriptSUPConfig.DownloadRetryCount < 0 {
return fmt.Errorf("negative download retry count value - %d", scriptSUPConfig.DownloadRetryCount)
}
return nil
}
2 changes: 1 addition & 1 deletion internal/feature_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Started:
Downloading:
if opError = f.store.DownloadModule(toDir, module, func(percent int) {
setLastOS(su, newOS(cid, module, hawkbit.StatusDownloading).WithProgress(percent))
}, f.serverCert); opError != nil {
}, f.serverCert, f.downloadRetryCount, f.downloadRetryInterval); opError != nil {
opErrorMsg = errDownload
return opError == storage.ErrCancel
}
Expand Down
2 changes: 1 addition & 1 deletion internal/feature_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ Started:
Downloading:
if opError = f.store.DownloadModule(dir, module, func(progress int) {
setLastOS(su, newOS(cid, module, hawkbit.StatusDownloading).WithProgress(progress))
}, f.serverCert); opError != nil {
}, f.serverCert, f.downloadRetryCount, f.downloadRetryInterval); opError != nil {
opErrorMsg = errDownload
return opError == storage.ErrCancel
}
Expand Down
55 changes: 39 additions & 16 deletions internal/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const (
flagLogFileSize = "logFileSize"
flagLogFileCount = "logFileCount"
flagLogFileMaxAge = "logFileMaxAge"
flagRetryCount = "downloadRetryCount"
gboyvalenkov-bosch marked this conversation as resolved.
Show resolved Hide resolved
flagRetryInterval = "downloadRetryInterval"
)

var (
Expand All @@ -51,20 +53,22 @@ var (
)

type cfg struct {
Broker string `json:"broker" def:"tcp://localhost:1883" descr:"Local MQTT broker address"`
Username string `json:"username" descr:"Username for authorized local client"`
Password string `json:"password" descr:"Password for authorized local client"`
StorageLocation string `json:"storageLocation" def:"." descr:"Location of the storage"`
FeatureID string `json:"featureId" def:"SoftwareUpdatable" descr:"Feature identifier of SoftwareUpdatable"`
ModuleType string `json:"moduleType" def:"software" descr:"Module type of SoftwareUpdatable"`
ArtifactType string `json:"artifactType" def:"archive" descr:"Defines the module artifact type: archive or plane"`
Install []string `json:"install" descr:"Defines the absolute path to install script"`
ServerCert string `json:"serverCert" descr:"A PEM encoded certificate \"file\" for secure artifact download"`
LogFile string `json:"logFile" def:"log/software-update.log" descr:"Log file location in storage directory"`
LogLevel string `json:"logLevel" def:"INFO" descr:"Log levels are ERROR, WARN, INFO, DEBUG, TRACE"`
LogFileSize int `json:"logFileSize" def:"2" descr:"Log file size in MB before it gets rotated"`
LogFileCount int `json:"logFileCount" def:"5" descr:"Log file max rotations count"`
LogFileMaxAge int `json:"logFileMaxAge" def:"28" descr:"Log file rotations max age in days"`
Broker string `json:"broker" def:"tcp://localhost:1883" descr:"Local MQTT broker address"`
Username string `json:"username" descr:"Username for authorized local client"`
Password string `json:"password" descr:"Password for authorized local client"`
StorageLocation string `json:"storageLocation" def:"." descr:"Location of the storage"`
FeatureID string `json:"featureId" def:"SoftwareUpdatable" descr:"Feature identifier of SoftwareUpdatable"`
ModuleType string `json:"moduleType" def:"software" descr:"Module type of SoftwareUpdatable"`
ArtifactType string `json:"artifactType" def:"archive" descr:"Defines the module artifact type: archive or plane"`
Install []string `json:"install" descr:"Defines the absolute path to install script"`
ServerCert string `json:"serverCert" descr:"A PEM encoded certificate \"file\" for secure artifact download"`
DownloadRetryCount int `json:"downloadRetryCount" def:"0" descr:"The number of retries, in case of a failed download.\n By default no retries are supported."`
gboyvalenkov-bosch marked this conversation as resolved.
Show resolved Hide resolved
DownloadRetryInterval durationTime `json:"downloadRetryInterval" def:"5s" descr:"The interval between retries, in case of a failed download.\n Should be a sequence of decimal numbers, each with optional fraction and a unit suffix, such as '300ms', '1.5h', '10m30s', etc. Valid time units are 'ns', 'us' (or 'µs'), 'ms', 's', 'm', 'h'."`
gboyvalenkov-bosch marked this conversation as resolved.
Show resolved Hide resolved
LogFile string `json:"logFile" def:"log/software-update.log" descr:"Log file location in storage directory"`
LogLevel string `json:"logLevel" def:"INFO" descr:"Log levels are ERROR, WARN, INFO, DEBUG, TRACE"`
LogFileSize int `json:"logFileSize" def:"2" descr:"Log file size in MB before it gets rotated"`
LogFileCount int `json:"logFileCount" def:"5" descr:"Log file max rotations count"`
LogFileMaxAge int `json:"logFileMaxAge" def:"28" descr:"Log file rotations max age in days"`
}

// InitFlags tries to initialize Script-Based SoftwareUpdatable and Log configurations.
Expand All @@ -80,7 +84,6 @@ func InitFlags(version string) (*ScriptBasedSoftwareUpdatableConfig, *logger.Log

initFlagsWithDefaultValues(flgConfig)
flag.Parse()

if *printVersion {
fmt.Println(version)
os.Exit(0)
Expand Down Expand Up @@ -112,6 +115,13 @@ func initFlagsWithDefaultValues(config interface{}) {
log.Printf("error parsing integer argument %v with value %v", fieldType.Name, defaultValue)
}
flag.IntVar(pointer.(*int), flagName, value, description)
case durationTime:
v, ok := pointer.(flag.Value)
if ok {
flag.Var(v, flagName, description)
} else {
log.Println("custom type Duration must implement reflect.Value interface")
}
}
}
}
Expand All @@ -123,6 +133,8 @@ func loadDefaultValues() *cfg {
for i := 0; i < typeOf.NumField(); i++ {
fieldType := typeOf.Field(i)
defaultValue := fieldType.Tag.Get("def")
fieldValue := valueOf.FieldByName(fieldType.Name)
pointer := fieldValue.Addr().Interface()
if len(defaultValue) > 0 {
fieldValue := valueOf.FieldByName(fieldType.Name)
switch fieldValue.Interface().(type) {
Expand All @@ -134,6 +146,17 @@ func loadDefaultValues() *cfg {
log.Printf("error parsing integer argument %v with value %v", fieldType.Name, defaultValue)
}
fieldValue.Set(reflect.ValueOf(value))
case durationTime:
v, ok := pointer.(flag.Value)
if ok {
if err := v.Set(defaultValue); err == nil {
fieldValue.Set(reflect.ValueOf(v).Elem())
} else {
log.Printf("error parsing argument %v with value %v - %v", fieldType.Name, defaultValue, err)
}
} else {
log.Println("custom type Duration must implement reflect.Value interface")
}
}

}
Expand Down Expand Up @@ -163,7 +186,7 @@ func applyFlags(flagsConfig interface{}) {
func applyConfigurationFile(configFile string) error {
def := loadDefaultValues()

// Load configuration file (if posible)
// Load configuration file (if possible)
if len(configFile) > 0 {
if jf, err := os.Open(configFile); err == nil {
defer jf.Close()
Expand Down
4 changes: 4 additions & 0 deletions internal/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ func TestFlagsHasHigherPriority(t *testing.T) {
expectedFeatureID := "TestFeature"
expectedInstall := "TestInstall"
expectedServerCert := "TestCert"
expectedDownloadRetryCount := 3
expectedDownloadRetryInterval := "5s"
expectedLogFile := ""
expectedLogFileCount := 4
expectedLogFileMaxAge := 13
Expand All @@ -129,6 +131,8 @@ func TestFlagsHasHigherPriority(t *testing.T) {
c(flagFeatureID, expectedFeatureID),
c(flagInstall, expectedInstall),
c(flagCert, expectedServerCert),
c(flagRetryCount, strconv.Itoa(expectedDownloadRetryCount)),
c(flagRetryInterval, expectedDownloadRetryInterval),
c(flagLogFile, expectedLogFile),
c(flagLogFileCount, strconv.Itoa(expectedLogFileCount)),
c(flagLogFileMaxAge, strconv.Itoa(expectedLogFileMaxAge)),
Expand Down
Loading