Skip to content

Commit 60472f8

Browse files
Add arduino-flasher-cli tool (#376)
1 parent c5bea04 commit 60472f8

File tree

12 files changed

+553
-0
lines changed

12 files changed

+553
-0
lines changed

Taskfile.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,40 @@ tasks:
320320
321321
EOF
322322
cat .licenses/arduino-router/NOTICE >> debian/arduino-router/usr/share/doc/arduino-router/copyright
323+
324+
arduino-flasher-cli:build:
325+
desc: "Build the arduino-flasher-cli locally"
326+
dir: arduino-flasher-cli
327+
vars:
328+
VERSION: "{{.VERSION }}"
329+
cmds:
330+
- cmd: go build -ldflags "-X main.Version={{.VERSION}}" -v -o ../build/arduino-flasher-cli .
331+
platforms: [linux, darwin]
332+
- cmd: go build -ldflags "-X main.Version={{.VERSION}}" -v -o ../build/arduino-flasher-cli.exe .
333+
platforms: [windows]
334+
335+
debian:release-server:start:
336+
dir: arduino-flasher-cli
337+
cmds:
338+
- docker rm -f debian-static-server
339+
- docker run --rm -d -v "$PWD/public:/usr/share/nginx/html:ro" -p 3001:80 --name debian-static-server nginx:alpine
340+
341+
flash:staging:
342+
cmds:
343+
- task: arduino-flasher-cli:build
344+
vars:
345+
VERSION: "{{.VERSION}}"
346+
- task: flash:internal
347+
vars:
348+
CF_ACCESS_CLIENT_SECRET: "$(export AWS_PROFILE=arduino-staging && aws ssm get-parameter --with-decryption --name /devops/downloads/cloudflare_access_client_secret --query Parameter.Value --output text)"
349+
CF_ACCESS_CLIENT_ID: "$(export AWS_PROFILE=arduino-staging && aws ssm get-parameter --with-decryption --name /devops/downloads/cloudflare_access_client_id --query Parameter.Value --output text)"
350+
TARGET: latest
351+
352+
flash:internal:
353+
internal: true
354+
dir: build
355+
cmds:
356+
- cmd: CF_ACCESS_CLIENT_ID="{{.CF_ACCESS_CLIENT_ID}}" CF_ACCESS_CLIENT_SECRET="{{.CF_ACCESS_CLIENT_SECRET}}" ./arduino-flasher-cli flash {{.TARGET}}
357+
platforms: [linux, darwin]
358+
- cmd: CF_ACCESS_CLIENT_ID="{{.CF_ACCESS_CLIENT_ID}}" CF_ACCESS_CLIENT_SECRET="{{.CF_ACCESS_CLIENT_SECRET}}" ./arduino-flasher-cli.exe flash {{.TARGET}}
359+
platforms: [windows]

arduino-flasher-cli/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
public/

arduino-flasher-cli/flash.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package main
2+
3+
import (
4+
"arduino-flasher/updater"
5+
"context"
6+
"fmt"
7+
"net/url"
8+
"os"
9+
"strings"
10+
11+
"github.com/arduino/go-paths-helper"
12+
"github.com/bcmi-labs/orchestrator/cmd/feedback"
13+
"github.com/bcmi-labs/orchestrator/cmd/i18n"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
func newFlashCmd() *cobra.Command {
18+
var forceYes bool
19+
appCmd := &cobra.Command{
20+
Use: "flash",
21+
Short: "Flash a Debian image on the board",
22+
Long: "Dowload the specified Debian image version and flash it on the board",
23+
Example: " " + os.Args[0] + " flash latest\n" +
24+
" " + os.Args[0] + " flash 20250915-173\n",
25+
Args: cobra.ExactArgs(1),
26+
Run: func(cmd *cobra.Command, args []string) {
27+
runFlashCommand(cmd.Context(), args, forceYes)
28+
},
29+
}
30+
appCmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "Automatically confirm all prompts")
31+
// TODO: add --clean-install flag or something similar to distinguish between keeping and purging the /home directory
32+
33+
return appCmd
34+
}
35+
36+
func runFlashCommand(ctx context.Context, args []string, forceYes bool) {
37+
version := args[0]
38+
39+
updateURL := os.Getenv("UPDATE_URL")
40+
if updateURL == "" {
41+
// TODO: change to prod
42+
updateURL = "https://downloads.oniudra.cc"
43+
}
44+
45+
parsedURL, err := url.Parse(updateURL)
46+
if err != nil {
47+
feedback.Fatal(i18n.Tr("invalid UPDATE_URL:", err), feedback.ErrBadArgument)
48+
}
49+
50+
headers := map[string]string{}
51+
clientID := os.Getenv("CF_ACCESS_CLIENT_ID")
52+
clientSecret := os.Getenv("CF_ACCESS_CLIENT_SECRET")
53+
if clientID != "" && clientSecret != "" {
54+
headers["CF-Access-Client-Id"] = clientID
55+
headers["CF-Access-Client-Secret"] = clientSecret
56+
}
57+
58+
var client *updater.Client
59+
if len(headers) == 2 {
60+
client = updater.NewClient(parsedURL, "debian-im/Stable", updater.WithHeaders(headers))
61+
} else {
62+
client = updater.NewClient(parsedURL, "debian-im/Stable")
63+
}
64+
65+
downloadedImagePath, updatedVersion, err := updater.DownloadImage(client, version, func(target string) (bool, error) {
66+
feedback.Printf("Found Debian image version: %s", target)
67+
feedback.Printf("Do you want to download it and flash it on the board? (yes/no)")
68+
69+
var yesInput string
70+
_, err := fmt.Scanf("%s\n", &yesInput)
71+
if err != nil {
72+
return false, err
73+
}
74+
yes := strings.ToLower(yesInput) == "yes" || strings.ToLower(yesInput) == "y"
75+
return yes, nil
76+
}, forceYes)
77+
78+
if err != nil {
79+
feedback.Fatal(i18n.Tr("error downloading the image: %v", err), feedback.ErrBadArgument)
80+
}
81+
82+
if downloadedImagePath != "" {
83+
defer paths.New(downloadedImagePath).RemoveAll()
84+
85+
if err := updater.FlashBoard(ctx, downloadedImagePath, updatedVersion); err != nil {
86+
feedback.Fatal(i18n.Tr("error flashing the board: %v", err), feedback.ErrBadArgument)
87+
}
88+
}
89+
}

arduino-flasher-cli/go.mod

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module arduino-flasher
2+
3+
go 1.25
4+
5+
require (
6+
github.com/arduino/go-paths-helper v1.14.0
7+
github.com/bcmi-labs/orchestrator v0.3.0
8+
github.com/codeclysm/extract/v4 v4.0.0
9+
github.com/spf13/cobra v1.9.1
10+
go.bug.st/cleanup v1.0.0
11+
)
12+
13+
require (
14+
github.com/h2non/filetype v1.1.3 // indirect
15+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
16+
github.com/juju/errors v1.0.0 // indirect
17+
github.com/klauspost/compress v1.18.0 // indirect
18+
github.com/sirupsen/logrus v1.9.3 // indirect
19+
github.com/spf13/pflag v1.0.7 // indirect
20+
github.com/ulikunitz/xz v0.5.12 // indirect
21+
golang.org/x/sys v0.35.0 // indirect
22+
)

arduino-flasher-cli/go.sum

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
github.com/arduino/go-paths-helper v1.14.0 h1:b4C8KJa7CNz2XnzTWg97M3LAmzWSWrj+m/o5/skzv3Y=
2+
github.com/arduino/go-paths-helper v1.14.0/go.mod h1:dDodKn2ZX4iwuoBMapdDO+5d0oDLBeM4BS0xS4i40Ak=
3+
github.com/bcmi-labs/orchestrator v0.3.0 h1:lCsGiz1OlM8Bzflavaz1+9y1roUUkGfA4IKntm8HXl4=
4+
github.com/bcmi-labs/orchestrator v0.3.0/go.mod h1:D2NWi/sJtJ9/hDxaeotfftHi+dXbXXwDWsySXR8k3Pg=
5+
github.com/codeclysm/extract/v4 v4.0.0 h1:H87LFsUNaJTu2e/8p/oiuiUsOK/TaPQ5wxsjPnwPEIY=
6+
github.com/codeclysm/extract/v4 v4.0.0/go.mod h1:SFju1lj6as7FvUgalpSct7torJE0zttbJUWtryPRG6s=
7+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
8+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
11+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12+
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
13+
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
14+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
15+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
16+
github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=
17+
github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8=
18+
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
19+
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
20+
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
21+
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
22+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
23+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
24+
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
25+
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
26+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
27+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
28+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
29+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
30+
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
31+
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
32+
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
33+
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
34+
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
35+
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
36+
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
37+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
38+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
39+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
40+
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
41+
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
42+
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
43+
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
44+
go.bug.st/cleanup v1.0.0 h1:XVj1HZxkBXeq3gMT7ijWUpHyIC1j8XAoNSyQ06CskgA=
45+
go.bug.st/cleanup v1.0.0/go.mod h1:EqVmTg2IBk4znLbPD28xne3abjsJftMdqqJEjhn70bk=
46+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
47+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
48+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
49+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
50+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
51+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
52+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
53+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
54+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

arduino-flasher-cli/main.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
8+
"github.com/bcmi-labs/orchestrator/cmd/feedback"
9+
"github.com/bcmi-labs/orchestrator/cmd/i18n"
10+
"github.com/spf13/cobra"
11+
"go.bug.st/cleanup"
12+
)
13+
14+
// Version will be set a build time with -ldflags
15+
var Version string = "0.0.0-dev"
16+
var format string
17+
18+
func main() {
19+
rootCmd := &cobra.Command{
20+
Use: "arduino-flasher-cli",
21+
Short: "A CLI to update and flash the Debian image",
22+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
23+
format, ok := feedback.ParseOutputFormat(format)
24+
if !ok {
25+
feedback.Fatal(i18n.Tr("Invalid output format: %s", format), feedback.ErrBadArgument)
26+
}
27+
feedback.SetFormat(format)
28+
},
29+
SilenceUsage: true,
30+
SilenceErrors: true,
31+
}
32+
33+
rootCmd.PersistentFlags().StringVar(&format, "format", "text", "Output format (text, json)")
34+
35+
rootCmd.AddCommand(
36+
newFlashCmd(),
37+
&cobra.Command{
38+
Use: "version",
39+
Short: "Print the version number of Arduino Flasher CLI",
40+
Run: func(cmd *cobra.Command, args []string) {
41+
fmt.Println("Arduino Flasher CLI v" + Version)
42+
},
43+
})
44+
45+
ctx := context.Background()
46+
ctx, _ = cleanup.InterruptableContext(ctx)
47+
if err := rootCmd.ExecuteContext(ctx); err != nil {
48+
slog.Error(err.Error())
49+
}
50+
}
1.47 MB
Binary file not shown.
4.98 MB
Binary file not shown.
6.09 MB
Binary file not shown.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package updater
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/md5"
7+
"encoding/hex"
8+
"fmt"
9+
"io"
10+
"log/slog"
11+
12+
"github.com/arduino/go-paths-helper"
13+
"github.com/bcmi-labs/orchestrator/cmd/feedback"
14+
"github.com/codeclysm/extract/v4"
15+
)
16+
17+
// TODO: add more fields to download other image versions
18+
type Manifest struct {
19+
Latest struct {
20+
Version string `json:"version"`
21+
Url string `json:"url"`
22+
Md5sum string `json:"md5sum"`
23+
} `json:"latest"`
24+
}
25+
26+
// DownloadConfirmCB is a function that is called when a Debian image is ready to be downloaded.
27+
type DownloadConfirmCB func(target string) (bool, error)
28+
29+
func DownloadImage(client *Client, targetVersion string, upgradeConfirmCb DownloadConfirmCB, forceYes bool) (string, string, error) {
30+
var err error
31+
32+
slog.Info("Checking for Debian image releases")
33+
manifest, err := client.GetInfoManifest()
34+
if err != nil {
35+
return "", "", err
36+
}
37+
38+
if targetVersion == "latest" {
39+
targetVersion = manifest.Latest.Version
40+
}
41+
42+
if !forceYes {
43+
res, err := upgradeConfirmCb(targetVersion)
44+
if err != nil {
45+
return "", "", err
46+
}
47+
if !res {
48+
slog.Info("Download not confirmed by user, exiting")
49+
return "", "", nil
50+
}
51+
}
52+
53+
// Download the Debian image
54+
var download io.ReadCloser
55+
if targetVersion == manifest.Latest.Version {
56+
slog.Info("Downloading Debian image", "version", manifest.Latest.Version)
57+
download, err = client.FetchZip(manifest.Latest.Url)
58+
if err != nil {
59+
return "", "", fmt.Errorf("could not fetch Debian image: %w", err)
60+
}
61+
} else {
62+
// TODO: check the json for the specific version and download it
63+
return "", "", nil
64+
}
65+
defer download.Close()
66+
67+
// Download the zip
68+
temp, err := paths.MkTempDir("", "flasher-updater-")
69+
if err != nil {
70+
return "", "", fmt.Errorf("could not create temporary download directory: %w", err)
71+
}
72+
tmpZip := temp.Join("update.tar.xz")
73+
defer func() {
74+
if err := tmpZip.Remove(); err != nil {
75+
slog.Warn("Could not remove temp zip", "zip", tmpZip.String(), "error", err)
76+
}
77+
}()
78+
79+
tmpZipFile, err := tmpZip.Create()
80+
if err != nil {
81+
return "", "", err
82+
}
83+
defer tmpZipFile.Close()
84+
85+
md5 := md5.New()
86+
if _, err := io.Copy(io.MultiWriter(md5, tmpZipFile), download); err != nil {
87+
return "", "", err
88+
}
89+
tmpZipFile.Close()
90+
91+
// Check the hash
92+
if md5Byte, err := hex.DecodeString(manifest.Latest.Md5sum); err != nil {
93+
return "", "", fmt.Errorf("could not convert md5 from hex to bytes: %w", err)
94+
} else if s := md5.Sum(nil); !bytes.Equal(s, md5Byte) {
95+
return "", "", fmt.Errorf("bad hash: %x (expected %x)", s, md5Byte)
96+
}
97+
98+
// Unzip the Debian image
99+
slog.Info("Unzipping Debian image", "tmpDir", temp)
100+
tmpZipFile, err = tmpZip.Open()
101+
if err != nil {
102+
return "", "", fmt.Errorf("could not open archive for unzip: %w", err)
103+
}
104+
defer tmpZipFile.Close()
105+
106+
if err := extract.Archive(context.Background(), tmpZipFile, temp.String(), func(s string) string {
107+
feedback.Print(s)
108+
return s
109+
}); err != nil {
110+
return "", "", fmt.Errorf("extracting archive: %w", err)
111+
}
112+
113+
slog.Info("Download of Debian image completed", "path", temp)
114+
115+
return temp.String(), targetVersion, nil
116+
}

0 commit comments

Comments
 (0)