incus-mirror/cmd/incus/low_level.go
Benjamin Somers fd63243512
client: Make GetRawInstanceNVRAMGUIDVar return attributes
Signed-off-by: Benjamin Somers <benjamin.somers@imt-atlantique.fr>
2026-07-30 11:36:30 -04:00

957 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"os"
"sort"
"strings"
"sync"
"github.com/spf13/cobra"
"go.yaml.in/yaml/v4"
incus "github.com/lxc/incus/v7/client"
"github.com/lxc/incus/v7/cmd/incus/color"
u "github.com/lxc/incus/v7/cmd/incus/usage"
"github.com/lxc/incus/v7/internal/i18n"
"github.com/lxc/incus/v7/shared/api"
cli "github.com/lxc/incus/v7/shared/cmd"
"github.com/lxc/incus/v7/shared/termios"
"github.com/lxc/incus/v7/shared/uefi"
"github.com/lxc/incus/v7/shared/util"
)
type cmdLowLevel struct {
global *cmdGlobal
}
func (c *cmdLowLevel) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("low-level")
cmd.Aliases = []string{"debug"}
cmd.Short = i18n.G("Low-level commands")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Low-level commands for instances`))
lowLevelBitmapsCmd := cmdLowLevelBitmaps{global: c.global}
cmd.AddCommand(lowLevelBitmapsCmd.command())
lowLevelAttachCmd := cmdLowLevelMemory{global: c.global, lowLevel: c}
cmd.AddCommand(lowLevelAttachCmd.command())
lowLevelNBDCmd := cmdLowLevelNBD{global: c.global, lowLevel: c}
cmd.AddCommand(lowLevelNBDCmd.command())
lowLevelNVRAMCmd := cmdLowLevelNVRAM{global: c.global}
cmd.AddCommand(lowLevelNVRAMCmd.command())
lowLevelRepairCmd := cmdLowLevelRepair{global: c.global}
cmd.AddCommand(lowLevelRepairCmd.command())
return cmd
}
type cmdLowLevelBitmaps struct {
global *cmdGlobal
}
func (c *cmdLowLevelBitmaps) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("bitmaps")
cmd.Short = i18n.G("Manage dirty bitmaps on virtual machines")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Manage dirty bitmaps on virtual machines`))
// Create.
lowLevelBitmapsCreateCmd := cmdLowLevelBitmapsCreate{global: c.global}
cmd.AddCommand(lowLevelBitmapsCreateCmd.command())
// Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706.
cmd.Args = cobra.NoArgs
cmd.Run = func(cmd *cobra.Command, _ []string) { _ = cmd.Usage() }
return cmd
}
// Create.
type cmdLowLevelBitmapsCreate struct {
global *cmdGlobal
flagGranularity int
flagPersistent bool
flagDisabled bool
}
var cmdLowLevelBitmapsCreateUsage = u.Usage{u.Instance.Remote(), u.NewName(u.Placeholder(i18n.G("bitmap")))}
func (c *cmdLowLevelBitmapsCreate) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("create", cmdLowLevelBitmapsCreateUsage...)
cmd.Short = i18n.G("Create a dirty bitmap on a virtual machine")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
`Create a dirty bitmap on all the disks of a running virtual machine`,
))
cmd.RunE = c.run
cli.AddIntFlag(cmd.Flags(), &c.flagGranularity, "granularity", i18n.G("Granularity of the dirty bitmap in bytes"))
cli.AddBoolFlag(cmd.Flags(), &c.flagPersistent, "persistent", i18n.G("Store the bitmap on disk"))
cli.AddBoolFlag(cmd.Flags(), &c.flagDisabled, "disabled", i18n.G("Create the bitmap in the disabled state"))
// completion for instance.
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpInstances(toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
return cmd
}
func (c *cmdLowLevelBitmapsCreate) run(cmd *cobra.Command, args []string) error {
parsed, err := c.global.Parse(cmdLowLevelBitmapsCreateUsage, cmd, args)
if err != nil {
return err
}
d := parsed[0].RemoteServer
instanceName := parsed[0].RemoteObject.String
bitmap := api.StorageVolumeBitmapsPost{
Name: parsed[1].String,
Granularity: c.flagGranularity,
Persistent: c.flagPersistent,
Disabled: c.flagDisabled,
}
err = d.CreateInstanceBitmap(instanceName, bitmap)
if err != nil {
return fmt.Errorf(i18n.G("Failed to create bitmap: %w"), err)
}
return nil
}
type cmdLowLevelRepair struct {
global *cmdGlobal
}
var cmdLowLevelRepairUsage = u.Usage{u.Instance.Remote(), u.Placeholder(i18n.G("action"))}
func (c *cmdLowLevelRepair) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("repair", cmdLowLevelRepairUsage...)
cmd.Short = i18n.G("Run a repair action on an instance")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
`Run a low-level repair action on an instance.
Supported actions:
rebuild-config-volume Rebuild the config volume of a stopped QCOW2 backed virtual machine
rebuild-nvram Rebuild the virtual machine's UEFI NVRAM`,
))
cmd.RunE = c.run
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpInstances(toComplete)
}
if len(args) == 1 {
return []string{"rebuild-config-volume"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
return cmd
}
func (c *cmdLowLevelRepair) run(cmd *cobra.Command, args []string) error {
parsed, err := c.global.Parse(cmdLowLevelRepairUsage, cmd, args)
if err != nil {
return err
}
d := parsed[0].RemoteServer
instanceName := parsed[0].RemoteObject.String
err = d.RepairInstance(instanceName, api.InstanceDebugRepairPost{Action: parsed[1].String})
if err != nil {
return fmt.Errorf(i18n.G("Failed to repair instance: %w"), err)
}
return nil
}
type cmdLowLevelMemory struct {
global *cmdGlobal
lowLevel *cmdLowLevel
flagFormat string
}
var cmdLowLevelMemoryUsage = u.Usage{u.Instance.Remote(), u.Target(u.File)}
func (c *cmdLowLevelMemory) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("dump-memory", cmdLowLevelMemoryUsage...)
cmd.Short = i18n.G("Export a virtual machine's memory state")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
`Export the current memory state of a running virtual machine into a dump file.
This can be useful for debugging or analysis purposes.`,
))
cmd.Example = cli.FormatSection("", i18n.G(
`incus low-level dump-memory vm1 memory-dump.elf --format=elf
Creates an ELF format memory dump of the vm1 instance.`,
))
cmd.RunE = c.run
cli.AddStringFlag(cmd.Flags(), &c.flagFormat, "format|f", "elf", "", i18n.G("Format of memory dump (e.g. elf, win-dmp, kdump-zlib, kdump-raw-zlib, ...)"))
return cmd
}
func (c *cmdLowLevelMemory) run(cmd *cobra.Command, args []string) error {
parsed, err := c.global.Parse(cmdLowLevelMemoryUsage, cmd, args)
if err != nil {
return err
}
d := parsed[0].RemoteServer
instanceName := parsed[0].RemoteObject.String
path := parsed[1].String
target, err := os.Create(path)
if err != nil {
return err
}
rc, err := d.GetInstanceDebugMemory(instanceName, c.flagFormat)
if err != nil {
return fmt.Errorf(i18n.G("Failed to dump instance memory: %w"), err)
}
_, err = util.SafeCopy(target, rc)
if err != nil {
return err
}
return nil
}
type cmdLowLevelNBD struct {
global *cmdGlobal
lowLevel *cmdLowLevel
flagAddress string
}
var cmdLowLevelNBDUsage = u.Usage{u.Instance.Remote()}
func (c *cmdLowLevelNBD) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("nbd", cmdLowLevelNBDUsage...)
cmd.Short = i18n.G("NBD access to all of a virtual machine's disks")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
`NBD access to all of a virtual machine's disks
This exposes all the disks of a running virtual machine over a local NBD
server, with each disk reachable as an NBD export named after its Incus
device name.`,
))
cli.AddStringFlag(cmd.Flags(), &c.flagAddress, "address", "", "", i18n.G("Specific address to listen on"))
cmd.RunE = c.run
// completion for instance.
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpInstances(toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
return cmd
}
func (c *cmdLowLevelNBD) run(cmd *cobra.Command, args []string) error {
parsed, err := c.global.Parse(cmdLowLevelNBDUsage, cmd, args)
if err != nil {
return err
}
d := parsed[0].RemoteServer
instanceName := parsed[0].RemoteObject.String
// Check that the instance exists before starting the NBD server.
_, _, err = d.GetInstance(instanceName)
if err != nil {
return err
}
// Proxy to a local listener.
listenAddr := c.flagAddress
if listenAddr == "" {
listenAddr = "127.0.0.1:0" // Listen on a random local port if not specified.
}
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
return fmt.Errorf(i18n.G("Failed to listen for connection: %w"), err)
}
fmt.Printf(i18n.G("NBD listening on %v")+"\n", listener.Addr())
// Track the active connections, the first one starts the NBD session and the
// following ones attach to it. The server stops the session when all of its
// connections are closed.
var connMu sync.Mutex
activeConns := 0
for {
// Wait for a connection.
nConn, err := listener.Accept()
if err != nil {
return fmt.Errorf(i18n.G("Failed to accept incoming connection: %w"), err)
}
go func() {
defer func() { _ = nConn.Close() }()
fmt.Printf(i18n.G("NBD client connected %q")+"\n", nConn.RemoteAddr())
defer fmt.Printf(i18n.G("NBD client disconnected %q")+"\n", nConn.RemoteAddr())
connMu.Lock()
reuse := activeConns > 0
activeConns++
connMu.Unlock()
defer func() {
connMu.Lock()
activeConns--
connMu.Unlock()
}()
// Get a connection to the NBD session.
conn, err := d.GetInstanceNBDConn(instanceName, incus.InstanceNBDArgs{Reuse: reuse})
if err != nil {
fmt.Printf(i18n.G("NBD connection failed: %v")+"\n", err)
return
}
defer func() { _ = conn.Close() }()
// Proxy the traffic.
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
_, _ = util.SafeCopy(conn, nConn)
_ = conn.Close()
_ = nConn.Close()
}()
go func() {
defer wg.Done()
_, _ = util.SafeCopy(nConn, conn)
_ = conn.Close()
_ = nConn.Close()
}()
wg.Wait()
}()
}
}
type cmdLowLevelNVRAM struct {
global *cmdGlobal
}
type nvramColumn struct {
Name string
Data func(string, string, *api.InstanceNVRAMVariable) string
}
func (c *cmdLowLevelNVRAM) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("nvram")
cmd.Short = i18n.G("Manage NVRAM on virtual machines")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Manage NVRAM on virtual machines`))
// Edit.
lowLevelNVRAMEditCmd := cmdLowLevelNVRAMEdit{global: c.global}
cmd.AddCommand(lowLevelNVRAMEditCmd.command())
// Get.
lowLevelNVRAMGetCmd := cmdLowLevelNVRAMGet{global: c.global}
cmd.AddCommand(lowLevelNVRAMGetCmd.command())
// List.
lowLevelNVRAMListCmd := cmdLowLevelNVRAMList{global: c.global}
cmd.AddCommand(lowLevelNVRAMListCmd.command())
// Set.
lowLevelNVRAMSetCmd := cmdLowLevelNVRAMSet{global: c.global}
cmd.AddCommand(lowLevelNVRAMSetCmd.command())
// Unset.
lowLevelNVRAMUnsetCmd := cmdLowLevelNVRAMUnset{global: c.global}
cmd.AddCommand(lowLevelNVRAMUnsetCmd.command())
// Workaround for subcommand usage errors. See: https://github.com/spf13/cobra/issues/706.
cmd.Args = cobra.NoArgs
cmd.Run = func(cmd *cobra.Command, _ []string) { _ = cmd.Usage() }
return cmd
}
// nvramGuessVar guesses a GUID and a variable name from the user input.
func nvramGuessVar(name string) (string, string, error) {
// First, try the GUID:name syntax, which also allows aliases. Any errors here are fatal, as we
// have no other sane way to parse colons.
parts := strings.SplitN(name, ":", 2)
if len(parts) == 2 {
guid, err := uefi.ParseGUIDOrName(parts[0])
if err != nil {
return "", "", err
}
return guid, parts[1], nil
}
// Then, try both GUID-name and name-GUID combinations, which dont allow aliases.
parts = strings.Split(name, "-")
n := len(parts)
// If there is no dash, no namespace is given, so we use EFI_GLOBAL_VARIABLE as a sane default.
if n == 1 {
return uefi.EfiGlobalVariableGuid, name, nil
}
// People can go wild in how they represent GUIDs, so we dumbly try parsing up to the last dash,
// then if it fails, from the first dash on.
guid, err := uefi.ParseGUID(strings.Join(parts[:n-1], "-"))
if err == nil {
return guid, parts[n-1], nil
}
guid, err = uefi.ParseGUID(strings.Join(parts[1:], "-"))
if err == nil {
return guid, parts[0], nil
}
// If anything fails, we assume that no namespace is given. Dashes are allowed in UEFI variable
// names, so this last safety net covers this unlikely case.
return uefi.EfiGlobalVariableGuid, name, nil
}
// Edit.
type cmdLowLevelNVRAMEdit struct {
global *cmdGlobal
}
var cmdLowLevelNVRAMEditUsage = u.Usage{u.Instance.Remote(), u.Variable}
func (c *cmdLowLevelNVRAMEdit) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("edit", cmdLowLevelNVRAMEditUsage...)
cmd.Short = i18n.G("Edit instance UEFI variables")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Edit instance UEFI variables`))
cmd.RunE = c.run
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpInstances(toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
return cmd
}
// helpTemplate returns a sample YAML configuration and guidelines for editing UEFI variables.
func (c *cmdLowLevelNVRAMEdit) helpTemplate() string {
return i18n.G(
`### This is a YAML representation of the UEFI variable.
### Any line starting with a '# will be ignored.`,
)
}
func (c *cmdLowLevelNVRAMEdit) run(cmd *cobra.Command, args []string) error {
parsed, err := c.global.Parse(cmdLowLevelNVRAMEditUsage, cmd, args)
if err != nil {
return err
}
d := parsed[0].RemoteServer
instanceName := parsed[0].RemoteObject.String
guid, varName, err := nvramGuessVar(parsed[1].String)
if err != nil {
return err
}
// If stdin isn't a terminal, read text from it
if !termios.IsTerminal(getStdinFd()) {
loader, err := yaml.NewLoader(os.Stdin)
if err != nil {
return err
}
newData := api.InstanceNVRAMVariablePut{}
err = loader.Load(&newData)
if err != nil && !errors.Is(err, io.EOF) {
return err
}
err = d.UpdateInstanceNVRAMGUIDVar(instanceName, guid, varName, newData, "")
if err != nil {
return err
}
return nil
}
// Extract the current value
v, etag, err := d.GetInstanceNVRAMGUIDVar(instanceName, guid, varName)
if err != nil {
return err
}
// If the variable couldn't be dissected, then there's really nothing to edit.
if v.Data == nil {
return fmt.Errorf(i18n.G("Incus does not know how to dissect %s:%s"), guid, varName)
}
// Empty binary representation so it isn't shown in edit screen (relies on omitempty tag).
v.Binary = nil
data, err := yaml.Dump(&v, yaml.WithV2Defaults())
if err != nil {
return err
}
// Spawn the editor
content, err := cli.TextEditor("", []byte(c.helpTemplate()+"\n\n"+string(data)))
if err != nil {
return err
}
for {
// Parse the text received from the editor
newData := api.InstanceNVRAMVariablePut{}
err = yaml.Load(content, &newData)
if err == nil {
err = d.UpdateInstanceNVRAMGUIDVar(instanceName, guid, varName, newData, etag)
}
// Respawn the editor
if err != nil {
fmt.Fprintf(os.Stderr, i18n.G("Failed to set UEFI variable %s:%s: %s")+"\n", guid, varName, err)
fmt.Println(i18n.G("Press enter to open the editor again or ctrl+c to abort change"))
_, err := os.Stdin.Read(make([]byte, 1))
if err != nil {
return err
}
content, err = cli.TextEditor("", content)
if err != nil {
return err
}
continue
}
break
}
return nil
}
// Get.
type cmdLowLevelNVRAMGet struct {
global *cmdGlobal
flagRaw bool
}
var cmdLowLevelNVRAMGetUsage = u.Usage{u.Instance.Remote(), u.Variable}
func (c *cmdLowLevelNVRAMGet) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("get", cmdLowLevelNVRAMGetUsage...)
cmd.Short = i18n.G("Get values for UEFI variables")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Get values for UEFI variables`))
cmd.RunE = c.run
cli.AddBoolFlag(cmd.Flags(), &c.flagRaw, "raw", i18n.G("Get the raw binary variable value"))
// completion for instance.
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpInstances(toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
return cmd
}
func (c *cmdLowLevelNVRAMGet) run(cmd *cobra.Command, args []string) error {
parsed, err := c.global.Parse(cmdLowLevelNVRAMGetUsage, cmd, args)
if err != nil {
return err
}
d := parsed[0].RemoteServer
instanceName := parsed[0].RemoteObject.String
name := parsed[1].String
guid, varName, err := nvramGuessVar(name)
if err != nil {
return err
}
if c.flagRaw {
resp, _, err := d.GetRawInstanceNVRAMGUIDVar(instanceName, guid, varName)
if err != nil {
return fmt.Errorf(i18n.G("Failed to get instance UEFI variable: %w"), err)
}
fmt.Print(string(resp))
return nil
}
v, _, err := d.GetInstanceNVRAMGUIDVar(instanceName, guid, varName)
if err != nil {
return fmt.Errorf(i18n.G("Failed to get instance UEFI variable: %w"), err)
}
data, err := yaml.Dump(v, yaml.WithV2Defaults())
if err != nil {
return err
}
print(string(data))
return nil
}
// List.
type cmdLowLevelNVRAMList struct {
global *cmdGlobal
flagFormat string
flagColumns string
}
var cmdLowLevelNVRAMListUsage = u.Usage{u.Instance.Remote(), u.Placeholder(i18n.G("GUID")).Optional()}
func (c *cmdLowLevelNVRAMList) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("list", cmdLowLevelNVRAMListUsage...)
cmd.Aliases = []string{"ls"}
cmd.Short = i18n.G("List UEFI GUIDs and variables")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`List UEFI GUIDs and variables`))
cmd.RunE = c.run
cli.AddStringFlag(cmd.Flags(), &c.flagFormat, "format|f", c.global.defaultListFormat(), "", i18n.G(`Format (csv|json|table|yaml|compact|markdown), use suffix ",noheader" to disable headers and ",header" to enable it if missing, e.g. csv,header`))
cli.AddStringFlag(cmd.Flags(), &c.flagColumns, "columns|c", defaultNVRAMColumns, "", i18n.G("Columns"))
// completion for instance.
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpInstances(toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
return cmd
}
const defaultNVRAMColumns = "Gnv"
func (c *cmdLowLevelNVRAMList) parseColumns() ([]nvramColumn, error) {
columnsShorthandMap := map[rune]nvramColumn{
'a': {i18n.G("ATTRIBUTES"), c.attributesColumnData},
'g': {i18n.G("GUID"), c.guidColumnData},
'G': {i18n.G("GUID NAME"), c.guidNameColumnData},
'n': {i18n.G("VARIABLE NAME"), c.nameColumnData},
'r': {i18n.G("RAW VALUE"), c.rawColumnData},
't': {i18n.G("TIMESTAMP"), c.timestampColumnData},
'v': {i18n.G("INTERPRETED VALUE"), c.valueColumnData},
}
columnList := strings.Split(c.flagColumns, ",")
columns := []nvramColumn{}
for _, columnEntry := range columnList {
if columnEntry == "" {
return nil, fmt.Errorf(i18n.G("Empty column entry (redundant, leading or trailing command) in '%s'"), c.flagColumns)
}
for _, columnRune := range columnEntry {
column, ok := columnsShorthandMap[columnRune]
if !ok {
return nil, fmt.Errorf(i18n.G("Unknown column shorthand char '%c' in '%s'"), columnRune, columnEntry)
}
columns = append(columns, column)
}
}
return columns, nil
}
func (c *cmdLowLevelNVRAMList) attributesColumnData(guid string, name string, v *api.InstanceNVRAMVariable) string {
return strings.Join(v.Attributes, " + ")
}
func (c *cmdLowLevelNVRAMList) guidColumnData(guid string, name string, v *api.InstanceNVRAMVariable) string {
return guid
}
func (c *cmdLowLevelNVRAMList) guidNameColumnData(guid string, name string, v *api.InstanceNVRAMVariable) string {
return uefi.GUIDName(guid)
}
func (c *cmdLowLevelNVRAMList) nameColumnData(guid string, name string, v *api.InstanceNVRAMVariable) string {
return name
}
func (c *cmdLowLevelNVRAMList) rawColumnData(guid string, name string, v *api.InstanceNVRAMVariable) string {
return base64.StdEncoding.EncodeToString(v.Binary)
}
func (c *cmdLowLevelNVRAMList) timestampColumnData(guid string, name string, v *api.InstanceNVRAMVariable) string {
if v.Timestamp == nil {
return i18n.G("(no timestamp)")
}
return v.Timestamp.String()
}
func (c *cmdLowLevelNVRAMList) valueColumnData(guid string, name string, v *api.InstanceNVRAMVariable) string {
if v.Data == nil {
return i18n.G("(binary data)")
}
repr, err := yaml.Dump(v.Data, yaml.WithV2Defaults())
if err != nil {
return fmt.Sprintf(i18n.G("(error: %w)"), err)
}
return string(repr)
}
func (c *cmdLowLevelNVRAMList) run(cmd *cobra.Command, args []string) error {
parsed, err := c.global.Parse(cmdLowLevelNVRAMListUsage, cmd, args)
if err != nil {
return err
}
d := parsed[0].RemoteServer
instanceName := parsed[0].RemoteObject.String
var uefiVars map[string]map[string]*api.InstanceNVRAMVariable
if parsed[1].Skipped {
uefiVars, err = d.GetInstanceNVRAM(instanceName)
if err != nil {
return fmt.Errorf(i18n.G("Failed to get instance UEFI variables: %w"), err)
}
} else {
guid := parsed[1].String
parsedGUID, err := uefi.ParseGUIDOrName(guid)
if err != nil {
return fmt.Errorf(i18n.G("Invalid GUID: %s"), guid)
}
vars, err := d.GetInstanceNVRAMGUID(instanceName, parsedGUID)
if err != nil {
return err
}
uefiVars = map[string]map[string]*api.InstanceNVRAMVariable{guid: vars}
}
// Parse column flags.
columns, err := c.parseColumns()
if err != nil {
return err
}
// Render the table
data := [][]string{}
for guid, vars := range uefiVars {
for name, v := range vars {
line := []string{}
for _, column := range columns {
line = append(line, column.Data(guid, name, v))
}
data = append(data, line)
}
}
sort.Sort(cli.SortColumnsNaturally(data))
header := []string{}
for _, column := range columns {
header = append(header, column.Name)
}
return cli.RenderTable(os.Stdout, c.flagFormat, header, data, uefiVars)
}
// Set.
type cmdLowLevelNVRAMSet struct {
global *cmdGlobal
flagAttributes uint32
flagTimestamp int64
flagRaw bool
}
var cmdLowLevelNVRAMSetUsage = u.Usage{u.Instance.Remote(), u.MakeKV(u.Variable, u.Value)}
func (c *cmdLowLevelNVRAMSet) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("set", cmdLowLevelNVRAMSetUsage...)
cmd.Short = i18n.G("Set values for UEFI variables")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Set values for UEFI variables.`))
cmd.RunE = c.run
cli.AddUint32Flag(cmd.Flags(), &c.flagAttributes, "attributes", i18n.G("Set the variable attributes (requires `--raw`)"), 7)
cli.AddInt64Flag(cmd.Flags(), &c.flagTimestamp, "timestamp", i18n.G("Set the variable timestamp (requires `--raw`)"))
cli.AddBoolFlag(cmd.Flags(), &c.flagRaw, "raw", i18n.G("Set the raw binary variable value"))
// completion for instance.
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpInstances(toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
return cmd
}
func (c *cmdLowLevelNVRAMSet) run(cmd *cobra.Command, args []string) error {
// We deliberately only accept a single definition at a time because we dont want to give users
// the impression that they are hitting any kind of optimized path. Setting 100 variables leads to
// 100 full NVRAM rewrites.
parsed, err := c.global.Parse(cmdLowLevelNVRAMSetUsage, cmd, args)
if err != nil {
return err
}
d := parsed[0].RemoteServer
instanceName := parsed[0].RemoteObject.String
keys, err := kvToMap(u.AsSingleton(parsed[1]))
if err != nil {
return err
}
var errs []error
for k, v := range keys {
guid, varName, err := nvramGuessVar(k)
if err != nil {
errs = append(errs, err)
continue
}
if c.flagRaw {
err = d.UpdateRawInstanceNVRAMGUIDVar(instanceName, guid, varName, []byte(v), c.flagAttributes, c.flagTimestamp)
} else {
if cmd.Flags().Changed("attributes") {
return errors.New(i18n.G("--attributes cannot be used without --raw"))
}
if cmd.Flags().Changed("timestamp") {
return errors.New(i18n.G("--timestamp cannot be used without --raw"))
}
data := api.InstanceNVRAMVariablePut{}
err = yaml.Load([]byte(v), &data)
if err != nil {
errs = append(errs, fmt.Errorf(i18n.G("Failed to parse variable %s:%s: %w"), guid, varName, err))
continue
}
err = d.UpdateInstanceNVRAMGUIDVar(instanceName, guid, varName, data, "")
}
if err != nil {
errs = append(errs, fmt.Errorf(i18n.G("Failed to set UEFI variable %s:%s: %w"), guid, varName, err))
}
}
return errors.Join(errs...)
}
// Unset.
type cmdLowLevelNVRAMUnset struct {
global *cmdGlobal
}
var cmdLowLevelNVRAMUnsetUsage = u.Usage{u.Instance.Remote(), u.Variable}
func (c *cmdLowLevelNVRAMUnset) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("unset", cmdLowLevelNVRAMUnsetUsage...)
cmd.Short = i18n.G("Unset UEFI variables")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G("Unset UEFI variables"))
cmd.RunE = c.run
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpInstances(toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
return cmd
}
func (c *cmdLowLevelNVRAMUnset) run(cmd *cobra.Command, args []string) error {
// We deliberately only accept a single deletion at a time because we dont want to give users
// the impression that they are hitting any kind of optimized path. Deleting 100 variables leads
// to 100 full NVRAM rewrites.
parsed, err := c.global.Parse(cmdLowLevelNVRAMUnsetUsage, cmd, args)
if err != nil {
return err
}
d := parsed[0].RemoteServer
instanceName := parsed[0].RemoteObject.String
guid, varName, err := nvramGuessVar(parsed[1].String)
if err != nil {
return err
}
// Delete the UEFI variable.
err = d.DeleteInstanceNVRAMGUIDVar(instanceName, guid, varName)
if err != nil {
return err
}
if !c.global.flagQuiet {
fmt.Printf(i18n.G("UEFI variable %s:%s deleted on %s")+"\n", guid, varName, formatRemote(c.global.conf, parsed[0]))
}
return nil
}