Skip to content

Commit 3b67c2b

Browse files
committed
Introduce new ota mass-upload command
1 parent a92e17c commit 3b67c2b

File tree

6 files changed

+331
-177
lines changed

6 files changed

+331
-177
lines changed

cli/ota/massupload.go

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// This file is part of arduino-cloud-cli.
2+
//
3+
// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU Affero General Public License as published
7+
// by the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU Affero General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU Affero General Public License
16+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
18+
package ota
19+
20+
import (
21+
"os"
22+
"strings"
23+
24+
"github.com/arduino/arduino-cli/cli/errorcodes"
25+
"github.com/arduino/arduino-cli/cli/feedback"
26+
"github.com/arduino/arduino-cloud-cli/command/ota"
27+
"github.com/sirupsen/logrus"
28+
"github.com/spf13/cobra"
29+
)
30+
31+
var massUploadFlags struct {
32+
deviceIDs []string
33+
tags map[string]string
34+
file string
35+
deferred bool
36+
fqbn string
37+
}
38+
39+
func initMassUploadCommand() *cobra.Command {
40+
massUploadCommand := &cobra.Command{
41+
Use: "mass-upload",
42+
Short: "Mass OTA upload",
43+
Long: "Mass OTA upload on devices of Arduino IoT Cloud",
44+
Run: runMassUploadCommand,
45+
}
46+
47+
massUploadCommand.Flags().StringSliceVarP(&massUploadFlags.deviceIDs, "device-ids", "d", []string{},
48+
"Comma-separated list of device IDs to update")
49+
massUploadCommand.Flags().StringToStringVar(&massUploadFlags.tags, "tags", nil,
50+
"Comma-separated list of tags with format <key>=<value>.\n"+
51+
"Perform and OTA upload on all devices that match the provided tags.\n"+
52+
"Mutually exclusive with `--device-id`.",
53+
)
54+
massUploadCommand.Flags().StringVarP(&massUploadFlags.file, "file", "", "", "Binary file (.bin) to be uploaded")
55+
massUploadCommand.Flags().BoolVar(&massUploadFlags.deferred, "deferred", false, "Perform a deferred OTA. It can take up to 1 week.")
56+
massUploadCommand.Flags().StringVarP(&massUploadFlags.fqbn, "fqbn", "b", "", "FQBN of the devices to update")
57+
58+
massUploadCommand.MarkFlagRequired("file")
59+
massUploadCommand.MarkFlagRequired("fqbn")
60+
return massUploadCommand
61+
}
62+
63+
func runMassUploadCommand(cmd *cobra.Command, args []string) {
64+
logrus.Infof("Uploading binary %s", massUploadFlags.file)
65+
66+
params := &ota.MassUploadParams{
67+
DeviceIDs: massUploadFlags.deviceIDs,
68+
Tags: massUploadFlags.tags,
69+
File: massUploadFlags.file,
70+
Deferred: massUploadFlags.deferred,
71+
FQBN: massUploadFlags.fqbn,
72+
}
73+
74+
resp, err := ota.MassUpload(params)
75+
if err != nil {
76+
feedback.Errorf("Error during ota upload: %v", err)
77+
os.Exit(errorcodes.ErrGeneric)
78+
}
79+
80+
success := strings.Join(resp.Updated, ",")
81+
success = strings.TrimRight(success, ",")
82+
feedback.Printf("\nSuccessfully sent OTA request to: %s", success)
83+
84+
invalid := strings.Join(resp.Invalid, ",")
85+
invalid = strings.TrimRight(invalid, ",")
86+
feedback.Printf("Cannot send OTA request to: %s", invalid)
87+
88+
fail := strings.Join(resp.Failed, ",")
89+
fail = strings.TrimRight(fail, ",")
90+
feedback.Printf("Failed to send OTA request to: %s", fail)
91+
92+
det := strings.Join(resp.Errors, "\n")
93+
feedback.Printf("\nDetails:\n%s", det)
94+
}

cli/ota/ota.go

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func NewCommand() *cobra.Command {
2929
}
3030

3131
otaCommand.AddCommand(initUploadCommand())
32+
otaCommand.AddCommand(initMassUploadCommand())
3233

3334
return otaCommand
3435
}

cli/ota/upload.go

+11-37
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package ota
1919

2020
import (
2121
"os"
22-
"strings"
2322

2423
"github.com/arduino/arduino-cli/cli/errorcodes"
2524
"github.com/arduino/arduino-cli/cli/feedback"
@@ -29,11 +28,9 @@ import (
2928
)
3029

3130
var uploadFlags struct {
32-
deviceIDs []string
33-
tags map[string]string
34-
file string
35-
deferred bool
36-
fqbn string
31+
deviceID string
32+
file string
33+
deferred bool
3734
}
3835

3936
func initUploadCommand() *cobra.Command {
@@ -44,51 +41,28 @@ func initUploadCommand() *cobra.Command {
4441
Run: runUploadCommand,
4542
}
4643

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

48+
uploadCommand.MarkFlagRequired("device-id")
5849
uploadCommand.MarkFlagRequired("file")
59-
uploadCommand.MarkFlagRequired("fqbn")
6050
return uploadCommand
6151
}
6252

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

6656
params := &ota.UploadParams{
67-
DeviceIDs: uploadFlags.deviceIDs,
68-
Tags: uploadFlags.tags,
69-
File: uploadFlags.file,
70-
Deferred: uploadFlags.deferred,
71-
FQBN: uploadFlags.fqbn,
57+
DeviceID: uploadFlags.deviceID,
58+
File: uploadFlags.file,
59+
Deferred: uploadFlags.deferred,
7260
}
73-
74-
resp, err := ota.Upload(params)
61+
err := ota.Upload(params)
7562
if err != nil {
7663
feedback.Errorf("Error during ota upload: %v", err)
7764
os.Exit(errorcodes.ErrGeneric)
7865
}
7966

80-
success := strings.Join(resp.Updated, ",")
81-
success = strings.TrimRight(success, ",")
82-
feedback.Printf("\nSuccessfully sent OTA request to: %s", success)
83-
84-
invalid := strings.Join(resp.Invalid, ",")
85-
invalid = strings.TrimRight(invalid, ",")
86-
feedback.Printf("Cannot send OTA request to: %s", invalid)
87-
88-
fail := strings.Join(resp.Failed, ",")
89-
fail = strings.TrimRight(fail, ",")
90-
feedback.Printf("Failed to send OTA request to: %s", fail)
91-
92-
det := strings.Join(resp.Errors, "\n")
93-
feedback.Printf("\nDetails:\n%s", det)
67+
logrus.Info("Upload successfully started")
9468
}

command/ota/massupload.go

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// This file is part of arduino-cloud-cli.
2+
//
3+
// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU Affero General Public License as published
7+
// by the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU Affero General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU Affero General Public License
16+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
18+
package ota
19+
20+
import (
21+
"errors"
22+
"fmt"
23+
"io/ioutil"
24+
"os"
25+
"path/filepath"
26+
27+
"github.com/arduino/arduino-cloud-cli/internal/config"
28+
"github.com/arduino/arduino-cloud-cli/internal/iot"
29+
iotclient "github.com/arduino/iot-client-go"
30+
)
31+
32+
const (
33+
numConcurrentUploads = 10
34+
)
35+
36+
// MassUploadParams contains the parameters needed to
37+
// perform a Mass OTA upload.
38+
type MassUploadParams struct {
39+
DeviceIDs []string
40+
Tags map[string]string
41+
File string
42+
Deferred bool
43+
FQBN string
44+
}
45+
46+
// MassUploadResp contains the results of the mass ota upload
47+
type MassUploadResp struct {
48+
Updated []string // Ids of devices updated
49+
Invalid []string // Ids of device not valid (mismatched fqbn)
50+
Failed []string // Ids of device failed
51+
Errors []string // Contains detailed errors for each failure
52+
}
53+
54+
// MassUpload command is used to mass upload a firmware OTA,
55+
// on devices of Arduino IoT Cloud.
56+
func MassUpload(params *MassUploadParams) (*MassUploadResp, error) {
57+
if params.DeviceIDs == nil && params.Tags == nil {
58+
return nil, errors.New("provide either DeviceIDs or Tags")
59+
} else if params.DeviceIDs != nil && params.Tags != nil {
60+
return nil, errors.New("cannot use both DeviceIDs and Tags. only one of them should be not nil")
61+
}
62+
63+
// Generate .ota file
64+
otaDir, err := ioutil.TempDir("", "")
65+
if err != nil {
66+
return nil, fmt.Errorf("%s: %w", "cannot create temporary folder", err)
67+
}
68+
otaFile := filepath.Join(otaDir, "temp.ota")
69+
defer os.RemoveAll(otaDir)
70+
71+
err = Generate(params.File, otaFile, params.FQBN)
72+
if err != nil {
73+
return nil, fmt.Errorf("%s: %w", "cannot generate .ota file", err)
74+
}
75+
76+
conf, err := config.Retrieve()
77+
if err != nil {
78+
return nil, err
79+
}
80+
iotClient, err := iot.NewClient(conf.Client, conf.Secret)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
d, err := idsGivenTags(iotClient, params.Tags)
86+
if err != nil {
87+
return nil, err
88+
}
89+
d = append(params.DeviceIDs, d...)
90+
valid, invalid, details, err := validateDevices(iotClient, d, params.FQBN)
91+
if err != nil {
92+
return nil, fmt.Errorf("failed to validate devices: %w", err)
93+
}
94+
if len(valid) == 0 {
95+
return &MassUploadResp{Invalid: invalid}, nil
96+
}
97+
98+
expiration := otaExpirationMins
99+
if params.Deferred {
100+
expiration = otaDeferredExpirationMins
101+
}
102+
103+
good, fail, ers := run(iotClient, valid, otaFile, expiration)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
// Merge the failure details with the details of invalid devices
109+
ers = append(details, ers...)
110+
111+
return &MassUploadResp{Updated: good, Invalid: invalid, Failed: fail, Errors: ers}, nil
112+
}
113+
114+
func idsGivenTags(iotClient iot.Client, tags map[string]string) ([]string, error) {
115+
if tags == nil {
116+
return nil, nil
117+
}
118+
devs, err := iotClient.DeviceList(tags)
119+
if err != nil {
120+
return nil, fmt.Errorf("%s: %w", "cannot retrieve devices from cloud", err)
121+
}
122+
devices := make([]string, 0, len(devs))
123+
for _, d := range devs {
124+
devices = append(devices, d.Id)
125+
}
126+
return devices, nil
127+
}
128+
129+
func validateDevices(iotClient iot.Client, ids []string, fqbn string) (valid, invalid, details []string, err error) {
130+
devs, err := iotClient.DeviceList(nil)
131+
if err != nil {
132+
return nil, nil, nil, fmt.Errorf("%s: %w", "cannot retrieve devices from cloud", err)
133+
}
134+
135+
for _, id := range ids {
136+
var found *iotclient.ArduinoDevicev2
137+
for _, d := range devs {
138+
if d.Id == id {
139+
found = &d
140+
break
141+
}
142+
}
143+
// Device not found on the cloud
144+
if found == nil {
145+
invalid = append(invalid, id)
146+
details = append(details, fmt.Sprintf("%s : not found", id))
147+
continue
148+
}
149+
// Device FQBN doesn't match the passed one
150+
if found.Fqbn != fqbn {
151+
invalid = append(invalid, id)
152+
details = append(details, fmt.Sprintf("%s : has FQBN `%s` instead of `%s`", found.Id, found.Fqbn, fqbn))
153+
continue
154+
}
155+
valid = append(valid, id)
156+
}
157+
return valid, invalid, details, nil
158+
}
159+
160+
func run(iotClient iot.Client, ids []string, otaFile string, expiration int) (updated, failed, errors []string) {
161+
type job struct {
162+
id string
163+
file *os.File
164+
}
165+
jobs := make(chan job, len(ids))
166+
167+
type result struct {
168+
id string
169+
err error
170+
}
171+
results := make(chan result, len(ids))
172+
173+
for _, id := range ids {
174+
file, err := os.Open(otaFile)
175+
if err != nil {
176+
failed = append(failed, id)
177+
errors = append(errors, fmt.Sprintf("%s: cannot open ota file", id))
178+
}
179+
jobs <- job{id: id, file: file}
180+
}
181+
close(jobs)
182+
183+
for i := 0; i < numConcurrentUploads; i++ {
184+
go func() {
185+
for job := range jobs {
186+
err := iotClient.DeviceOTA(job.id, job.file, expiration)
187+
results <- result{id: job.id, err: err}
188+
}
189+
}()
190+
}
191+
192+
for range ids {
193+
r := <-results
194+
if r.err != nil {
195+
failed = append(failed, r.id)
196+
errors = append(errors, fmt.Sprintf("%s: %s", r.id, r.err.Error()))
197+
} else {
198+
updated = append(updated, r.id)
199+
}
200+
}
201+
return
202+
}
File renamed without changes.

0 commit comments

Comments
 (0)