From 0787ebb6659bdcceb1368bbeb149f86b0a2aae51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Can=C3=A9vet?= Date: Thu, 9 Jul 2026 18:34:16 +0200 Subject: [PATCH 1/2] feat: add -read flag to print current META config to stdout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add readConfig which loads the ADV from any io.ReaderAt and returns the raw bytes stored under FixedTag. Wire it up behind a -read flag so the tool can inspect what is currently written without modifying the partition. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Mickaël Canévet --- README.md | 5 ++++- main.go | 49 ++++++++++++++++++++++++++++++++++++------------- main_test.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 525e6db..cc5a1ff 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Talos metadata writer tool -Tool for writing network metadata in the Talos META partition. +Tool for reading and writing network metadata in the Talos META partition. Doc: https://docs.siderolabs.com/talos/v1.13/platform-specific-installations/bare-metal-platforms/metal-network-configuration @@ -12,6 +12,9 @@ GOOS=linux GOARCH=amd64 go build -o talos-meta-tool . Usage: ```bash +# read current config +talos-meta-tool -device /dev/sda -read + # write config from file talos-meta-tool -device /dev/sda -config config.yaml diff --git a/main.go b/main.go index c26731d..1c71073 100644 --- a/main.go +++ b/main.go @@ -89,6 +89,18 @@ func writeConfig(dev interface{ io.ReaderAt; io.WriterAt }, configData []byte) e return nil } +func readConfig(dev interface{ io.ReaderAt }) ([]byte, error) { + adv, err := talos.NewADV(io.NewSectionReader(dev, 0, int64(talos.Size))) + if adv == nil { + return nil, fmt.Errorf("loading ADV: %w", err) + } + data, ok := adv.ReadTagBytes(FixedTag) + if !ok { + return nil, fmt.Errorf("tag %#x not found", FixedTag) + } + return data, nil +} + func loadConfig(path, envVar string, b64 bool) ([]byte, error) { if envVar != "" { val, ok := os.LookupEnv(envVar) @@ -119,6 +131,7 @@ func loadConfig(path, envVar string, b64 bool) ([]byte, error) { func main() { devicePath := flag.String("device", "", "Path to the disk device (e.g., /dev/sda)") + read := flag.Bool("read", false, "Read and print the current configuration from the META partition") configPath := flag.String("config", "", "Path to the configuration file (e.g., config.yaml)") configEnv := flag.String("config-env", "", "Name of the environment variable containing the configuration") configEnvBase64 := flag.Bool("config-env-base64", false, "Decode the -config-env value as base64 before use") @@ -126,19 +139,7 @@ func main() { flag.Parse() if *devicePath == "" { - fmt.Fprintln(os.Stderr, "Usage: talos-meta-tool -device (-config | -config-env [-config-env-base64])") - os.Exit(1) - } - if *configPath == "" && *configEnv == "" { - fmt.Fprintln(os.Stderr, "Usage: talos-meta-tool -device (-config | -config-env [-config-env-base64])") - os.Exit(1) - } - if *configPath != "" && *configEnv != "" { - fmt.Fprintln(os.Stderr, "Error: -config and -config-env are mutually exclusive") - os.Exit(1) - } - if *configEnvBase64 && *configEnv == "" { - fmt.Fprintln(os.Stderr, "Error: -config-env-base64 requires -config-env") + fmt.Fprintln(os.Stderr, "Usage: talos-meta-tool -device (-read | -config | -config-env [-config-env-base64])") os.Exit(1) } @@ -153,6 +154,28 @@ func main() { log.Fatalf("Error: %v", err) } + if *read { + data, err := readConfig(meta) + if err != nil { + log.Fatalf("Error reading config: %v", err) + } + fmt.Print(string(data)) + return + } + + if *configPath == "" && *configEnv == "" { + fmt.Fprintln(os.Stderr, "Usage: talos-meta-tool -device (-read | -config | -config-env [-config-env-base64])") + os.Exit(1) + } + if *configPath != "" && *configEnv != "" { + fmt.Fprintln(os.Stderr, "Error: -config and -config-env are mutually exclusive") + os.Exit(1) + } + if *configEnvBase64 && *configEnv == "" { + fmt.Fprintln(os.Stderr, "Error: -config-env-base64 requires -config-env") + os.Exit(1) + } + configData, err := loadConfig(*configPath, *configEnv, *configEnvBase64) if err != nil { log.Fatalf("loading configuration: %v", err) diff --git a/main_test.go b/main_test.go index de05676..1ef0676 100644 --- a/main_test.go +++ b/main_test.go @@ -390,3 +390,36 @@ func TestWriteConfigFullDisk(t *testing.T) { t.Fatalf("tag value: got %q, want %q", got, payload) } } + +func TestReadConfigRoundTrip(t *testing.T) { + f := newTestFile(t) + + payload := []byte("key: value\n") + + if err := writeConfig(f, payload); err != nil { + t.Fatalf("writeConfig: %v", err) + } + + got, err := readConfig(f) + if err != nil { + t.Fatalf("readConfig: %v", err) + } + + if !bytes.Equal(got, payload) { + t.Fatalf("readConfig: got %q, want %q", got, payload) + } +} + +func TestReadConfigEmpty(t *testing.T) { + f := newTestFile(t) + + if _, err := readConfig(f); err == nil { + t.Fatal("expected error reading from empty ADV, got nil") + } +} + +func TestReadConfigBadDevice(t *testing.T) { + if _, err := readConfig(errDevice{}); err == nil { + t.Fatal("expected error for bad device, got nil") + } +} From 88a877915b6ad8e51bbb04e4c8ce1366c6da0f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Can=C3=A9vet?= Date: Thu, 9 Jul 2026 18:44:59 +0200 Subject: [PATCH 2/2] fix: address review feedback on -read flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move flag validation immediately after flag.Parse(), before opening the device or reading the GPT table, and make -read mutually exclusive with -config/-config-env. - Open the device O_RDONLY for -read instead of O_RDWR. - readConfig: take io.ReaderAt directly instead of an inline interface, and check err != nil rather than adv == nil so ADV corruption is reported instead of masked as "tag not found". - README: fix title, still said "writer tool" after -read was added. Signed-off-by: Mickaël Canévet --- README.md | 2 +- main.go | 40 ++++++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index cc5a1ff..def42c8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Talos metadata writer tool +# Talos metadata tool Tool for reading and writing network metadata in the Talos META partition. diff --git a/main.go b/main.go index 1c71073..3e47220 100644 --- a/main.go +++ b/main.go @@ -89,9 +89,9 @@ func writeConfig(dev interface{ io.ReaderAt; io.WriterAt }, configData []byte) e return nil } -func readConfig(dev interface{ io.ReaderAt }) ([]byte, error) { +func readConfig(dev io.ReaderAt) ([]byte, error) { adv, err := talos.NewADV(io.NewSectionReader(dev, 0, int64(talos.Size))) - if adv == nil { + if err != nil { return nil, fmt.Errorf("loading ADV: %w", err) } data, ok := adv.ReadTagBytes(FixedTag) @@ -142,8 +142,29 @@ func main() { fmt.Fprintln(os.Stderr, "Usage: talos-meta-tool -device (-read | -config | -config-env [-config-env-base64])") os.Exit(1) } + if !*read && *configPath == "" && *configEnv == "" { + fmt.Fprintln(os.Stderr, "Usage: talos-meta-tool -device (-read | -config | -config-env [-config-env-base64])") + os.Exit(1) + } + if *read && (*configPath != "" || *configEnv != "") { + fmt.Fprintln(os.Stderr, "Error: -read is mutually exclusive with -config and -config-env") + os.Exit(1) + } + if *configPath != "" && *configEnv != "" { + fmt.Fprintln(os.Stderr, "Error: -config and -config-env are mutually exclusive") + os.Exit(1) + } + if *configEnvBase64 && *configEnv == "" { + fmt.Fprintln(os.Stderr, "Error: -config-env-base64 requires -config-env") + os.Exit(1) + } - device, err := os.OpenFile(*devicePath, os.O_RDWR, 0) + openMode := os.O_RDWR + if *read { + openMode = os.O_RDONLY + } + + device, err := os.OpenFile(*devicePath, openMode, 0) if err != nil { log.Fatalf("Error opening device: %v", err) } @@ -163,19 +184,6 @@ func main() { return } - if *configPath == "" && *configEnv == "" { - fmt.Fprintln(os.Stderr, "Usage: talos-meta-tool -device (-read | -config | -config-env [-config-env-base64])") - os.Exit(1) - } - if *configPath != "" && *configEnv != "" { - fmt.Fprintln(os.Stderr, "Error: -config and -config-env are mutually exclusive") - os.Exit(1) - } - if *configEnvBase64 && *configEnv == "" { - fmt.Fprintln(os.Stderr, "Error: -config-env-base64 requires -config-env") - os.Exit(1) - } - configData, err := loadConfig(*configPath, *configEnv, *configEnvBase64) if err != nil { log.Fatalf("loading configuration: %v", err)