Skip to content

Commit 9e817ea

Browse files
committed
Ota Upload on multiple devices
1 parent 0bae4ec commit 9e817ea

File tree

3 files changed

+126
-16
lines changed

3 files changed

+126
-16
lines changed

cli/ota/upload.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ import (
2828
)
2929

3030
var uploadFlags struct {
31-
deviceID string
32-
file string
33-
deferred bool
31+
deviceIDs []string
32+
tags map[string]string
33+
file string
34+
deferred bool
35+
fqbn string
3436
}
3537

3638
func initUploadCommand() *cobra.Command {
@@ -41,23 +43,33 @@ func initUploadCommand() *cobra.Command {
4143
Run: runUploadCommand,
4244
}
4345

44-
uploadCommand.Flags().StringVarP(&uploadFlags.deviceID, "device-id", "d", "", "Device ID")
46+
uploadCommand.Flags().StringSliceVarP(&uploadFlags.deviceIDs, "device-ids", "d", []string{},
47+
"Comma-separated list of device IDs to update")
48+
uploadCommand.Flags().StringToStringVar(&uploadFlags.tags, "tags", nil,
49+
"Comma-separated list of tags with format <key>=<value>.\n"+
50+
"Perform and OTA upload on all devices that match the provided tags.\n"+
51+
"Mutually exclusive with `--device-id`.",
52+
)
4553
uploadCommand.Flags().StringVarP(&uploadFlags.file, "file", "", "", "Binary file (.bin) to be uploaded")
4654
uploadCommand.Flags().BoolVar(&uploadFlags.deferred, "deferred", false, "Perform a deferred OTA. It can take up to 1 week.")
55+
uploadCommand.Flags().StringVarP(&uploadFlags.fqbn, "fqbn", "b", "", "FQBN of the devices to update")
4756

48-
uploadCommand.MarkFlagRequired("device-id")
4957
uploadCommand.MarkFlagRequired("file")
58+
uploadCommand.MarkFlagRequired("fqbn")
5059
return uploadCommand
5160
}
5261

5362
func runUploadCommand(cmd *cobra.Command, args []string) {
54-
logrus.Infof("Uploading binary %s to device %s", uploadFlags.file, uploadFlags.deviceID)
63+
logrus.Infof("Uploading binary %s", uploadFlags.file)
5564

5665
params := &ota.UploadParams{
57-
DeviceID: uploadFlags.deviceID,
58-
File: uploadFlags.file,
59-
Deferred: uploadFlags.deferred,
66+
DeviceIDs: uploadFlags.deviceIDs,
67+
Tags: uploadFlags.tags,
68+
File: uploadFlags.file,
69+
Deferred: uploadFlags.deferred,
70+
FQBN: uploadFlags.fqbn,
6071
}
72+
6173
err := ota.Upload(params)
6274
if err != nil {
6375
feedback.Errorf("Error during ota upload: %v", err)

command/ota/upload.go

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
package ota
1919

2020
import (
21+
"errors"
2122
"fmt"
2223
"io/ioutil"
2324
"os"
2425
"path/filepath"
26+
"strings"
2527

2628
"github.com/arduino/arduino-cloud-cli/internal/config"
2729
"github.com/arduino/arduino-cloud-cli/internal/iot"
@@ -32,19 +34,29 @@ const (
3234
otaExpirationMins = 10
3335
// deferred ota can take up to 1 week (equal to 10080 minutes)
3436
otaDeferredExpirationMins = 10080
37+
38+
numConcurrentUploads = 10
3539
)
3640

3741
// UploadParams contains the parameters needed to
3842
// perform an OTA upload.
3943
type UploadParams struct {
40-
DeviceID string
41-
File string
42-
Deferred bool
44+
DeviceIDs []string
45+
Tags map[string]string
46+
File string
47+
Deferred bool
48+
FQBN string
4349
}
4450

4551
// Upload command is used to upload a firmware OTA,
4652
// on a device of Arduino IoT Cloud.
4753
func Upload(params *UploadParams) error {
54+
if params.DeviceIDs == nil && params.Tags == nil {
55+
return errors.New("provide either DeviceID or Tags")
56+
} else if params.DeviceIDs != nil && params.Tags != nil {
57+
return errors.New("cannot use both DeviceID and Tags. only one of them should be not nil")
58+
}
59+
4860
conf, err := config.Retrieve()
4961
if err != nil {
5062
return err
@@ -54,10 +66,14 @@ func Upload(params *UploadParams) error {
5466
return err
5567
}
5668

57-
dev, err := iotClient.DeviceShow(params.DeviceID)
69+
d, err := idsGivenTags(iotClient, params.Tags)
5870
if err != nil {
5971
return err
6072
}
73+
devs := append(params.DeviceIDs, d...)
74+
if len(devs) == 0 {
75+
return errors.New("no device found")
76+
}
6177

6278
otaDir, err := ioutil.TempDir("", "")
6379
if err != nil {
@@ -66,7 +82,7 @@ func Upload(params *UploadParams) error {
6682
otaFile := filepath.Join(otaDir, "temp.ota")
6783
defer os.RemoveAll(otaDir)
6884

69-
err = Generate(params.File, otaFile, dev.Fqbn)
85+
err = Generate(params.File, otaFile, params.FQBN)
7086
if err != nil {
7187
return fmt.Errorf("%s: %w", "cannot generate .ota file", err)
7288
}
@@ -81,10 +97,56 @@ func Upload(params *UploadParams) error {
8197
expiration = otaDeferredExpirationMins
8298
}
8399

84-
err = iotClient.DeviceOTA(params.DeviceID, file, expiration)
100+
return run(iotClient, devs, file, expiration)
101+
}
102+
103+
func idsGivenTags(iotClient iot.Client, tags map[string]string) ([]string, error) {
104+
if tags == nil {
105+
return nil, nil
106+
}
107+
devs, err := iotClient.DeviceList(tags)
85108
if err != nil {
86-
return err
109+
return nil, fmt.Errorf("%s: %w", "cannot retrieve devices from cloud", err)
110+
}
111+
devices := make([]string, 0, len(devs))
112+
for _, d := range devs {
113+
devices = append(devices, d.Id)
114+
}
115+
return devices, nil
116+
}
117+
118+
func run(iotClient iot.Client, ids []string, file *os.File, expiration int) error {
119+
idsToProcess := make(chan string, 2000)
120+
idsFailed := make(chan string, 2000)
121+
for _, id := range ids {
122+
idsToProcess <- id
123+
}
124+
close(idsToProcess)
125+
126+
for i := 0; i < numConcurrentUploads; i++ {
127+
go func() {
128+
for id := range idsToProcess {
129+
err := iotClient.DeviceOTA(id, file, expiration)
130+
fail := ""
131+
if err != nil {
132+
fail = id
133+
}
134+
idsFailed <- fail
135+
}
136+
}()
137+
}
138+
139+
failMsg := ""
140+
for range ids {
141+
i := <-idsFailed
142+
if i != "" {
143+
failMsg = strings.Join([]string{i, failMsg}, ",")
144+
}
87145
}
88146

147+
if failMsg != "" {
148+
failMsg = strings.TrimRight(failMsg, ",")
149+
return fmt.Errorf("failed to update these devices: %s", failMsg)
150+
}
89151
return nil
90152
}

command/ota/upload_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package ota
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"strings"
8+
"testing"
9+
"time"
10+
11+
"github.com/arduino/arduino-cloud-cli/internal/iot/mocks"
12+
"github.com/stretchr/testify/mock"
13+
)
14+
15+
func TestRun(t *testing.T) {
16+
mockClient := &mocks.Client{}
17+
mockDeviceOTA := func(id string, file *os.File, expireMins int) error {
18+
time.Sleep(3 * time.Second)
19+
if strings.Split(id, "-")[0] == "fail" {
20+
return errors.New("err")
21+
}
22+
return nil
23+
}
24+
mockClient.On("DeviceOTA", mock.Anything, mock.Anything, mock.Anything).Return(mockDeviceOTA, nil)
25+
26+
err := run(mockClient, []string{"dont-fail", "fail-1", "dont-fail", "fail-2"}, nil, 0)
27+
if err == nil {
28+
t.Error("should return error")
29+
}
30+
fmt.Println(err.Error())
31+
failed := strings.Split(err.Error(), ",")
32+
if len(failed) != 2 {
33+
fmt.Println(len(failed), failed)
34+
t.Error("two updates should have failed")
35+
}
36+
}

0 commit comments

Comments
 (0)