blob: 3808efd299969631b55f67a3bfb6725706dde72f [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package system_ota
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
tuf_data "github.com/flynn/go-tuf/data"
)
var (
fuchsiaBuildDir = flag.String("fuchsia-build-dir", os.Getenv("FUCHSIA_BUILD_DIR"), "fuchsia build dir")
sshConfig = flag.String("ssh-config", "", "ssh config file")
repoDir = flag.String("repo-dir", "", "amber repository dir")
zirconToolsDir = flag.String("zircon-tools-dir", os.Getenv("ZIRCON_TOOLS_DIR"), "zircon tools dir")
localHostname = flag.String("local-hostname", "", "local hostname")
deviceName = flag.String("device", "", "device name")
deviceHostname = flag.String("device-hostname", "", "device hostname")
localDevmgr []byte
noSuchFile = regexp.MustCompile("(:No such file or directory$)|(^error: cannot stat )")
)
const (
rebootFile = "/tmp/ota-test-waiting-for-reboot"
remoteDevmgrPath = "/boot/config/devmgr"
)
func needFuchsiaBuildDir() {
if *fuchsiaBuildDir == "" {
log.Fatalf("either pass -fuchsia-build-dir or set $FUCHSIA_BUILD_DIR")
}
}
func needZirconToolsDir() {
if *zirconToolsDir == "" {
log.Fatalf("either pass -zircon-tools-dir or set $ZIRCON_TOOLS_DIR")
}
}
func init() {
flag.Parse()
if *deviceName != "" && *deviceHostname != "" {
log.Fatalf("-device and -device-hostname are incompatible")
}
if *sshConfig == "" && *fuchsiaBuildDir != "" {
*sshConfig = filepath.Join(*fuchsiaBuildDir, "ssh-keys", "ssh_config")
}
if *repoDir == "" {
needFuchsiaBuildDir()
*repoDir = filepath.Join(*fuchsiaBuildDir, "amber-files", "repository")
}
var err error
if *localHostname == "" {
needZirconToolsDir()
*localHostname, err = netaddr("--local", *deviceName)
if err != nil {
log.Fatalf("ERROR: netaddr failed: %s", err)
}
if *localHostname == "" {
log.Fatalf("unable to determine the local hostname")
}
}
if *deviceHostname == "" {
needZirconToolsDir()
*deviceHostname, err = netaddr("--nowait", "--timeout=1000", "--fuchsia", *deviceName)
if err != nil {
log.Fatalf("ERROR: netaddr failed: %s", err)
}
if *deviceHostname == "" {
log.Fatalf("unable to determine the device hostname")
}
}
needFuchsiaBuildDir()
// We want to make sure that /boot/config/devmgr config file changed to the
// value we expect. First, read the file from the build.
localDevmgrPath := filepath.Join(*fuchsiaBuildDir, "obj", "build", "images", "devmgr_config.txt")
localDevmgr, err = ioutil.ReadFile(localDevmgrPath)
if err != nil {
log.Fatalf("failed to read %q: %s", localDevmgrPath, err)
}
// Serve the repository before the test begins.
serveRepository(*repoDir)
// Tell the device to connect to our repository.
registerAmberSource()
}
// Prepare the device for an OTA.
func PrepareOTA(t *testing.T) {
// Make sure we can ping the device.
waitForDeviceToPing(t)
// Make sure that the device does not have the /boot/config/devmgr we
// are OTA-ing to. In addition, make sure our package and system
// package do not exist on the system.
remoteDevmgr := ReadRemotePath(t, remoteDevmgrPath)
if bytes.Equal(localDevmgr, remoteDevmgr) {
t.Fatalf("%q should not be:\n\n%s", remoteDevmgrPath, remoteDevmgr)
}
}
// Trigger an OTA and verify the device successfully came up.
func TriggerOTA(t *testing.T) {
// We need a way to tell if a reboot happened. We'll do this by writing a dummy file to /tmp, which will
// be wiped on reboot.
touchRebootFile(t)
log.Printf("triggering OTA")
stdout, stderr, err := ssh("amber_ctl", "system_update")
log.Printf("%s\n%s", stdout, stderr)
if err != nil {
t.Fatalf("failed to trigger OTA: %s", err)
}
// Wait until our reboot file is removed, and for a few file systems to mount.
waitForDeviceToReboot(t)
waitForDevicePath(t, "/boot")
waitForDevicePath(t, "/system")
waitForDevicePath(t, "/pkgfs")
// Verify that we are now running the expected /boot/config/devmgr.
remoteDevmgr := ReadRemotePath(t, remoteDevmgrPath)
if !bytes.Equal(localDevmgr, remoteDevmgr) {
t.Fatalf("expected %q to be:\n\n%s\n\nbut instead got:\n\n%s", remoteDevmgrPath, localDevmgr, remoteDevmgr)
}
}
// Read a file off the remote device.
func ReadRemotePath(t *testing.T, path string) []byte {
stdout, stderr, err := ssh(fmt.Sprintf("while read line; do echo \"$line\"; done < %s", path))
if err != nil {
t.Fatalf("failed to read %q: %s: %s", path, err, string(stderr))
}
return stdout
}
// Check if a file exists on the remote device.
func RemoteFileExists(t *testing.T, path string) bool {
_, stderr, err := ssh("ls", path)
if err == nil {
return true
}
if !noSuchFile.Match(stderr) {
t.Fatalf("error reading %q: %s", path, stderr)
}
return false
}
type loggingWriter struct {
http.ResponseWriter
status int
}
func (lw *loggingWriter) WriteHeader(status int) {
lw.status = status
lw.ResponseWriter.WriteHeader(status)
}
func serveRepository(repoDir string) {
go func() {
http.Handle("/", http.FileServer(http.Dir(repoDir)))
err := http.ListenAndServe(":8083", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lw := &loggingWriter{w, 0}
http.DefaultServeMux.ServeHTTP(lw, r)
log.Printf("%s [pm serve] %d %s\n",
time.Now().Format("2006-01-02 15:04:05"), lw.status, r.RequestURI)
}))
if err != nil {
log.Fatal(err)
}
}()
time.Sleep(1 * time.Second)
}
func runOutput(name string, arg ...string) ([]byte, []byte, error) {
log.Printf("running: %s %q", name, arg)
c := exec.Command(name, arg...)
var o bytes.Buffer
var e bytes.Buffer
c.Stdout = &o
c.Stderr = &e
err := c.Run()
stdout := o.Bytes()
stderr := e.Bytes()
log.Printf("stdout: %s", stdout)
log.Printf("stderr: %s", stderr)
return stdout, stderr, err
}
func ssh(arg ...string) ([]byte, []byte, error) {
var a []string
a = append(a, "-o", "LogLevel=quiet", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null")
if *sshConfig == "" {
a = append(append(a, *deviceHostname), arg...)
} else {
a = append(append(a, "-F", *sshConfig, *deviceHostname), arg...)
}
return runOutput("/usr/bin/ssh", a...)
}
func netaddr(arg ...string) (string, error) {
stdout, stderr, err := runOutput(filepath.Join(*zirconToolsDir, "netaddr"), arg...)
if err != nil {
if len(stderr) != 0 {
return "", fmt.Errorf("netaddr failed: %s: %s", err, string(stderr))
} else {
return "", fmt.Errorf("netaddr failed: %s", err)
}
}
return strings.TrimRight(string(stdout), "\n"), nil
}
func waitForDeviceToPing(t *testing.T) {
log.Printf("waiting for device %q to ping", *deviceHostname)
path, err := exec.LookPath("/bin/ping")
if err != nil {
t.Fatal(err)
}
for {
out, err := exec.Command(path, "-c", "1", "-W", "1", *deviceHostname).Output()
if err == nil {
break
}
log.Printf("%s - %s", err, out)
time.Sleep(1 * time.Second)
}
log.Printf("device up")
}
func touchRebootFile(t *testing.T) {
log.Printf("touching %q", rebootFile)
_, _, err := ssh(fmt.Sprintf("echo > %s", rebootFile))
if err != nil {
t.Fatal(err)
}
}
type keyConfig struct {
Type string
Value string
}
type statusConfig struct {
Enabled bool
}
type sourceConfig struct {
Id string `json:"id"`
RepoUrl string `json:"repoUrl"`
BlobRepoUrl string `json:"blobRepoUrl"`
RootKeys []keyConfig `json:"rootKeys"`
StatusConfig statusConfig `json:"statusConfig"`
}
func registerAmberSource() {
log.Printf("registering devhost as update source")
f, err := os.Open(filepath.Join(*repoDir, "root.json"))
if err != nil {
log.Fatal(err)
}
defer f.Close()
var signed tuf_data.Signed
if err := json.NewDecoder(f).Decode(&signed); err != nil {
log.Fatal(err)
}
var root tuf_data.Root
if err := json.Unmarshal(signed.Signed, &root); err != nil {
log.Fatal(err)
}
var rootKeys []keyConfig
for _, keyId := range root.Roles["root"].KeyIDs {
key := root.Keys[keyId]
rootKeys = append(rootKeys, keyConfig{
Type: key.Type,
Value: key.Value.Public.String(),
})
}
hostname := strings.SplitN(*localHostname, "%", 2)[0]
repoUrl := fmt.Sprintf("http://[%s]:8083", hostname)
configUrl := fmt.Sprintf("%s/devhost/config.json", repoUrl)
config, err := json.Marshal(&sourceConfig{
Id: "devhost",
RepoUrl: repoUrl,
BlobRepoUrl: fmt.Sprintf("%s/blobs", repoUrl),
RootKeys: rootKeys,
StatusConfig: statusConfig{
Enabled: true,
},
})
if err != nil {
log.Fatal(err)
}
configHash := sha256.Sum256(config)
configHashString := hex.EncodeToString(configHash[:])
configDir := filepath.Join(*repoDir, "devhost")
if err := os.MkdirAll(configDir, 0755); err != nil {
log.Fatal(err)
}
configPath := filepath.Join(configDir, "config.json")
log.Printf("writing %q", configPath)
if err := ioutil.WriteFile(configPath, config, 0644); err != nil {
log.Fatal(err)
}
stdout, stderr, err := ssh("amber_ctl", "add_src", "-f", configUrl, "-h", configHashString)
log.Printf("%s\n%s", stdout, stderr)
if err != nil {
log.Fatal(err)
}
}
func waitForDeviceToReboot(t *testing.T) {
for {
log.Printf("waiting for Device to reboot")
if !RemoteFileExists(t, rebootFile) {
break
}
time.Sleep(1 * time.Second)
}
}
func waitForDevicePath(t *testing.T, path string) {
for {
log.Printf("waiting for %s to mount", path)
_, _, err := ssh("ls", path)
if err == nil {
break
}
time.Sleep(1 * time.Second)
}
}