M8: add command to append a single device to config
New 'arca add <device>' subcommand detects a LUKS device via udisks2 and appends it to the config with passphrase as default method. Supports --alias/-a to override the generated name. Skips if UUID already configured. Adds Config.Save() and Config.HasUUID() to config package. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
87
cmd/add.go
Normal file
87
cmd/add.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/arca/internal/config"
|
||||||
|
"git.wntrmute.dev/kyle/arca/internal/udisks"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var addAlias string
|
||||||
|
|
||||||
|
var addCmd = &cobra.Command{
|
||||||
|
Use: "add <device>",
|
||||||
|
Short: "Add a device to the config",
|
||||||
|
Long: "Detects a LUKS device via udisks2 and adds it to the config file with a default passphrase method.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runAdd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
addCmd.Flags().StringVarP(&addAlias, "alias", "a", "", "alias name (default: first 8 chars of UUID)")
|
||||||
|
rootCmd.AddCommand(addCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAdd(cmd *cobra.Command, args []string) error {
|
||||||
|
target := args[0]
|
||||||
|
|
||||||
|
client, err := udisks.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connecting to udisks2: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Find the device to get its UUID.
|
||||||
|
dev, err := client.FindDevice("", target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dev.HasEncrypted {
|
||||||
|
return fmt.Errorf("%s is not a LUKS-encrypted device", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dev.UUID == "" {
|
||||||
|
return fmt.Errorf("%s has no UUID", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
// Check if already configured.
|
||||||
|
if existing := cfg.AliasFor(dev.UUID); existing != "" {
|
||||||
|
fmt.Printf("Device %s (UUID %s) already configured as %q\n", dev.DevicePath, dev.UUID, existing)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
alias := addAlias
|
||||||
|
if alias == "" {
|
||||||
|
alias = aliasFromUUID(dev.UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for alias collision.
|
||||||
|
if _, exists := cfg.Devices[alias]; exists {
|
||||||
|
return fmt.Errorf("alias %q already in use — choose a different name with --alias", alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Devices[alias] = config.DeviceConfig{
|
||||||
|
UUID: dev.UUID,
|
||||||
|
Methods: []string{"passphrase"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Save(); err != nil {
|
||||||
|
return fmt.Errorf("saving config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Added %s (UUID %s) as %q\n", dev.DevicePath, dev.UUID, alias)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func aliasFromUUID(uuid string) string {
|
||||||
|
clean := strings.ReplaceAll(uuid, "-", "")
|
||||||
|
if len(clean) > 8 {
|
||||||
|
clean = clean[:8]
|
||||||
|
}
|
||||||
|
return clean
|
||||||
|
}
|
||||||
11
cmd/init.go
11
cmd/init.go
@@ -4,8 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
|
||||||
"git.wntrmute.dev/kyle/arca/internal/config"
|
"git.wntrmute.dev/kyle/arca/internal/config"
|
||||||
"git.wntrmute.dev/kyle/arca/internal/udisks"
|
"git.wntrmute.dev/kyle/arca/internal/udisks"
|
||||||
@@ -102,12 +100,3 @@ func isRootBacking(path dbus.ObjectPath, rootDevices []dbus.ObjectPath) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func aliasFromUUID(uuid string) string {
|
|
||||||
// Use first 8 chars of UUID as a stable alias.
|
|
||||||
// "b8b2f8e3-4cde-4aca-a96e-df9274019f9f" -> "b8b2f8e3"
|
|
||||||
clean := strings.ReplaceAll(uuid, "-", "")
|
|
||||||
if len(clean) > 8 {
|
|
||||||
clean = clean[:8]
|
|
||||||
}
|
|
||||||
return clean
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
@@ -76,6 +77,31 @@ func resolvedFrom(dev DeviceConfig) ResolvedDevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasUUID returns true if a device with the given UUID is already configured.
|
||||||
|
func (c *Config) HasUUID(uuid string) bool {
|
||||||
|
for _, dev := range c.Devices {
|
||||||
|
if dev.UUID == uuid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save writes the config to the config file.
|
||||||
|
func (c *Config) Save() error {
|
||||||
|
path := configPath()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("creating config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := yaml.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshaling config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(path, data, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
// AliasFor returns the config alias for a given UUID, or "" if none.
|
// AliasFor returns the config alias for a given UUID, or "" if none.
|
||||||
func (c *Config) AliasFor(uuid string) string {
|
func (c *Config) AliasFor(uuid string) string {
|
||||||
for name, dev := range c.Devices {
|
for name, dev := range c.Devices {
|
||||||
|
|||||||
Reference in New Issue
Block a user