Skip to content

Commit 7b6f189

Browse files
add support command for automated firmware update (#55)
1 parent 82477b0 commit 7b6f189

File tree

10 files changed

+226
-4
lines changed

10 files changed

+226
-4
lines changed

.github/workflows/main.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
/tmp/setup-hw
2626
/tmp/monitor-hw
2727
/tmp/collector
28+
/tmp/setup-apply-firmware
2829
retention-days: 1
2930
build_image:
3031
name: Build images
@@ -40,6 +41,7 @@ jobs:
4041
cp setup-hw docker
4142
cp monitor-hw docker
4243
cp collector docker
44+
cp setup-apply-firmware docker
4345
- run: |
4446
docker build -t quay.io/cybozu/setup-hw:latest --target stage1 docker
4547
docker build -t quay.io/cybozu/setup-hw-secret:latest docker

Makefile

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
BIN_PKGS = ./pkg/idrac-passwd-hash ./pkg/setup-hw ./pkg/monitor-hw ./pkg/collector
1+
# binaries to be included in the image
2+
BINS_IMAGE = setup-hw monitor-hw collector setup-apply-firmware
3+
4+
# binaries not to be included in the image
5+
BINS_NOIMAGE = idrac-passwd-hash
6+
7+
BINS = $(BINS_IMAGE) $(BINS_NOIMAGE)
28
GENERATED = redfish/rendered_rules.go
39
GENERATE_SRC = $(shell find redfish/rules)
410

@@ -29,14 +35,14 @@ install: generate
2935
ifdef GOBIN
3036
mkdir -p $(GOBIN)
3137
endif
32-
GOBIN=$(GOBIN) go install $(BIN_PKGS)
38+
GOBIN=$(GOBIN) go install $(foreach f, $(BINS), ./pkg/$(f))
3339

3440
build-image: install
3541
ifdef GOBIN
3642
mkdir -p $(GOBIN)
37-
cp $(GOBIN)/setup-hw $(GOBIN)/monitor-hw $(GOBIN)/collector ./docker/
43+
cp $(foreach f, $(BINS_IMAGE), $(GOBIN)/$(f)) ./docker/
3844
else
39-
cp $(GOPATH)/bin/setup-hw $(GOPATH)/bin/monitor-hw $(GOPATH)/bin/collector ./docker/
45+
cp $(foreach f, $(BINS_IMAGE), $(GOPATH)/bin/$(f)) ./docker/
4046
endif
4147
cd docker && docker build -t quay.io/cybozu/setup-hw:dev .
4248

docker/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
setup-hw
22
monitor-hw
33
collector
4+
setup-apply-firmware

docker/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ FROM centos:7 as stage1
33
COPY setup-hw /usr/local/bin/setup-hw
44
COPY monitor-hw /usr/local/sbin/monitor-hw
55
COPY collector /usr/local/sbin/collector
6+
COPY setup-apply-firmware /usr/local/sbin/setup-apply-firmware
67

78
CMD ["/usr/local/sbin/monitor-hw"]
89

docs/setup-apply-firmware.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Automated Firmware Update Support Tool
2+
======================================
3+
4+
`setup-apply-firmware` is a tool to configure BMC to apply firmware update.
5+
6+
Synopsis
7+
--------
8+
9+
```console
10+
$ setup-apply-firmware UPDATER_URL...
11+
```
12+
13+
`UPDATER_URL` should be those supported by `curl`.
14+
15+
Description
16+
-----------
17+
18+
`setup-apply-firmware` is a tool to configure BMC to apply firmware update.
19+
20+
It downloads the firmware updaters specified by `UPDATER_URL`s and sends them to BMC.
21+
22+
Caveat
23+
------
24+
25+
- The order to send each updater is indefinite.
26+
- This tool does not initiate reboot.

pkg/setup-apply-firmware/dell.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"errors"
7+
"fmt"
8+
"os/exec"
9+
"strings"
10+
"time"
11+
12+
"github.com/cybozu-go/log"
13+
"github.com/cybozu-go/well"
14+
)
15+
16+
func setupDell(ctx context.Context, files []string) error {
17+
for _, f := range files {
18+
cmd := well.CommandContext(ctx, "/opt/dell/srvadmin/bin/idracadm7", "update", "-f", f)
19+
buf := bytes.Buffer{}
20+
cmd.Stdout = &buf
21+
cmd.Stderr = &buf
22+
err := cmd.Run()
23+
// we cannot use exit status to detect errors because `idracadm7 update` returns nonzero status even in case of successful update initiation.
24+
var exitError *exec.ExitError
25+
if err != nil && !errors.As(err, &exitError) {
26+
return fmt.Errorf("racadm update failed at file %s: %w", f, err)
27+
}
28+
msg := buf.String()
29+
if err = checkRacadmOutput(msg, f); err != nil {
30+
return err
31+
}
32+
log.Info("racadm update succeeded", map[string]interface{}{
33+
"file": f,
34+
})
35+
36+
// if the next `idracadm7 update` is executed immediately after the previous one, it will fail.
37+
time.Sleep(time.Second * 10)
38+
}
39+
40+
return nil
41+
}
42+
43+
func checkRacadmOutput(msg, f string) error {
44+
if !strings.Contains(msg, "\nRAC987: ") {
45+
return fmt.Errorf("racadm update failed at file %s: msg: %s", f, msg)
46+
}
47+
return nil
48+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestCheckRacadmOutput(t *testing.T) {
8+
t.Parallel()
9+
10+
err := checkRacadmOutput(`Applying...
11+
RAC987: Update initiated.
12+
`, "filename.dat")
13+
if err != nil {
14+
t.Error(err)
15+
}
16+
17+
err = checkRacadmOutput(`ERROR: Invalid File Type
18+
`, "some_file_name.dat")
19+
if err == nil || err.Error() != "racadm update failed at file some_file_name.dat: msg: ERROR: Invalid File Type\n" {
20+
t.Fatal(err)
21+
}
22+
23+
err = checkRacadmOutput(``, "some_file_name.dat")
24+
if err == nil || err.Error() != "racadm update failed at file some_file_name.dat: msg: " {
25+
t.Fatal(err)
26+
}
27+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/cybozu-go/well"
9+
)
10+
11+
func downloadUpdaters(ctx context.Context, urls, files []string) error {
12+
if len(urls) != len(files) {
13+
return fmt.Errorf("length of urls and files are defferent")
14+
}
15+
16+
for i, u := range urls {
17+
f := files[i]
18+
cmd := well.CommandContext(ctx, "/usr/bin/curl", "-sSfo", f, u)
19+
cmd.Stdout = os.Stdout
20+
cmd.Stderr = os.Stderr
21+
err := cmd.Run()
22+
if err != nil {
23+
return err
24+
}
25+
}
26+
27+
return nil
28+
}

pkg/setup-apply-firmware/main.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/url"
7+
"os"
8+
"path"
9+
"path/filepath"
10+
11+
"github.com/cybozu-go/log"
12+
"github.com/cybozu-go/setup-hw/lib"
13+
"github.com/cybozu-go/well"
14+
)
15+
16+
func main() {
17+
well.LogConfig{}.Apply()
18+
ctx := context.Background()
19+
20+
vendor, err := lib.DetectVendor()
21+
if err != nil {
22+
log.ErrorExit(err)
23+
}
24+
25+
var setup func(context.Context, []string) error
26+
switch vendor {
27+
case lib.QEMU:
28+
setup = setupQEMU
29+
case lib.Dell:
30+
setup = setupDell
31+
default:
32+
log.ErrorExit(errors.New("unsupported vendor hardware"))
33+
}
34+
35+
tmpdir, err := os.MkdirTemp("/tmp", "setup-apply-firmware-")
36+
if err != nil {
37+
log.ErrorExit(err)
38+
}
39+
defer os.RemoveAll(tmpdir)
40+
41+
urls := os.Args[1:]
42+
files := make([]string, len(urls))
43+
for i, u := range urls {
44+
parsedUrl, err := url.Parse(u)
45+
if err != nil {
46+
log.ErrorExit(err)
47+
}
48+
f := path.Base(parsedUrl.Path)
49+
files[i] = filepath.Join(tmpdir, f)
50+
}
51+
52+
err = downloadUpdaters(ctx, urls, files)
53+
if err != nil {
54+
log.ErrorExit(err)
55+
}
56+
57+
err = setup(ctx, files)
58+
if err != nil {
59+
log.ErrorExit(err)
60+
}
61+
}

pkg/setup-apply-firmware/qemu.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
)
8+
9+
func setupQEMU(ctx context.Context, files []string) error {
10+
// nothing to do... just check file existence
11+
for _, f := range files {
12+
info, err := os.Stat(f)
13+
if err != nil {
14+
return err
15+
}
16+
if !info.Mode().IsRegular() {
17+
return fmt.Errorf("file %s is not a regular file", f)
18+
}
19+
}
20+
21+
return nil
22+
}

0 commit comments

Comments
 (0)