Adds a CLI subcommand that downloads a signed Tailscale appliance image (Gokrazy archive format, GAF) from pkgs.tailscale.com, constructs a fresh GPT-partitioned disk from it (mbr.img + a synthesized partition table + boot.img + root.img), formats /perm as ext4 in pure Go via go-diskfs, and ejects the disk so a user running on a regular workstation can flash an SD card or homelab VM disk in one command without installing e2fsprogs. On macOS the target disk is auto-discovered via diskutil, skipping the boot disk and anything bigger than 256 GB out of paranoia. On Linux the user passes --disk=/dev/sdX explicitly. Windows is not supported yet and the command returns an error. The GPT layout matches monogok's full-disk layout via the new public github.com/bradfitz/monogok/disklayout package; a drift- guard test inside monogok asserts the two implementations stay byte-identical so OTA updates against monogok-built images keep working. Behind a ts_omit_flashappliance build tag (on by default). Updates #1866 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: Ic1a8cd185e7039edccb7702ab4104544fcb58d29
99 lines
2.6 KiB
Go
99 lines
2.6 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !ts_omit_flashappliance
|
|
|
|
package cli
|
|
|
|
import "testing"
|
|
|
|
const diskutilListSample = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>AllDisks</key>
|
|
<array>
|
|
<string>disk4</string>
|
|
<string>disk4s1</string>
|
|
</array>
|
|
<key>WholeDisks</key>
|
|
<array>
|
|
<string>disk4</string>
|
|
</array>
|
|
</dict>
|
|
</plist>`
|
|
|
|
const diskutilInfoSample = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>DeviceIdentifier</key>
|
|
<string>disk4</string>
|
|
<key>DeviceModel</key>
|
|
<string>Generic STORAGE DEVICE</string>
|
|
<key>MediaName</key>
|
|
<string>Generic STORAGE DEVICE Media</string>
|
|
<key>Size</key>
|
|
<integer>62512365568</integer>
|
|
<key>Removable</key>
|
|
<true/>
|
|
</dict>
|
|
</plist>`
|
|
|
|
const diskutilInfoRootSample = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>DeviceIdentifier</key>
|
|
<string>disk3s1s1</string>
|
|
<key>ParentWholeDisk</key>
|
|
<string>disk3</string>
|
|
<key>APFSPhysicalStores</key>
|
|
<array>
|
|
<dict>
|
|
<key>APFSPhysicalStore</key>
|
|
<string>disk0s2</string>
|
|
</dict>
|
|
</array>
|
|
</dict>
|
|
</plist>`
|
|
|
|
func TestParseDiskutilListPlist(t *testing.T) {
|
|
ids, err := parseDiskutilListPlist([]byte(diskutilListSample))
|
|
if err != nil {
|
|
t.Fatalf("parseDiskutilListPlist: %v", err)
|
|
}
|
|
if len(ids) != 1 || ids[0] != "disk4" {
|
|
t.Errorf("ids = %v; want [disk4]", ids)
|
|
}
|
|
}
|
|
|
|
func TestParseDiskutilInfoPlist(t *testing.T) {
|
|
info, err := parseDiskutilInfoPlist([]byte(diskutilInfoSample))
|
|
if err != nil {
|
|
t.Fatalf("parseDiskutilInfoPlist: %v", err)
|
|
}
|
|
if info.Model != "Generic STORAGE DEVICE" {
|
|
t.Errorf("Model = %q; want %q", info.Model, "Generic STORAGE DEVICE")
|
|
}
|
|
if info.MediaName != "Generic STORAGE DEVICE Media" {
|
|
t.Errorf("MediaName = %q", info.MediaName)
|
|
}
|
|
if info.Size != 62512365568 {
|
|
t.Errorf("Size = %d; want 62512365568", info.Size)
|
|
}
|
|
}
|
|
|
|
func TestParseDiskutilInfoPlistRoot(t *testing.T) {
|
|
info, err := parseDiskutilInfoPlist([]byte(diskutilInfoRootSample))
|
|
if err != nil {
|
|
t.Fatalf("parseDiskutilInfoPlist: %v", err)
|
|
}
|
|
if info.ParentWholeDisk != "disk3" {
|
|
t.Errorf("ParentWholeDisk = %q; want disk3", info.ParentWholeDisk)
|
|
}
|
|
if len(info.APFSPhysicalStores) != 1 || info.APFSPhysicalStores[0] != "disk0s2" {
|
|
t.Errorf("APFSPhysicalStores = %v; want [disk0s2]", info.APFSPhysicalStores)
|
|
}
|
|
}
|