mirror of
https://github.com/lxc/incus
synced 2026-08-02 05:26:46 +00:00
1041 lines
26 KiB
Go
1041 lines
26 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"go.yaml.in/yaml/v4"
|
|
|
|
"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/units"
|
|
)
|
|
|
|
type cmdStorage struct {
|
|
global *cmdGlobal
|
|
|
|
flagTarget string
|
|
}
|
|
|
|
type storageColumn struct {
|
|
Name string
|
|
Data func(api.StoragePool) string
|
|
}
|
|
|
|
func (c *cmdStorage) command() *cobra.Command {
|
|
cmd := &cobra.Command{}
|
|
cmd.Use = cli.U("storage")
|
|
cmd.Short = i18n.G("Manage storage pools and volumes")
|
|
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Manage storage pools and volumes`))
|
|
|
|
// Create
|
|
storageCreateCmd := cmdStorageCreate{global: c.global, storage: c}
|
|
cmd.AddCommand(storageCreateCmd.command())
|
|
|
|
// Delete
|
|
storageDeleteCmd := cmdStorageDelete{global: c.global, storage: c}
|
|
cmd.AddCommand(storageDeleteCmd.command())
|
|
|
|
// Edit
|
|
storageEditCmd := cmdStorageEdit{global: c.global, storage: c}
|
|
cmd.AddCommand(storageEditCmd.command())
|
|
|
|
// Get
|
|
storageGetCmd := cmdStorageGet{global: c.global, storage: c}
|
|
cmd.AddCommand(storageGetCmd.command())
|
|
|
|
// Info
|
|
storageInfoCmd := cmdStorageInfo{global: c.global, storage: c}
|
|
cmd.AddCommand(storageInfoCmd.command())
|
|
|
|
// List
|
|
storageListCmd := cmdStorageList{global: c.global, storage: c}
|
|
cmd.AddCommand(storageListCmd.command())
|
|
|
|
// Set
|
|
storageSetCmd := cmdStorageSet{global: c.global, storage: c}
|
|
cmd.AddCommand(storageSetCmd.command())
|
|
|
|
// Show
|
|
storageShowCmd := cmdStorageShow{global: c.global, storage: c}
|
|
cmd.AddCommand(storageShowCmd.command())
|
|
|
|
// Unset
|
|
storageUnsetCmd := cmdStorageUnset{global: c.global, storage: c, storageSet: &storageSetCmd}
|
|
cmd.AddCommand(storageUnsetCmd.command())
|
|
|
|
// Bucket
|
|
storageBucketCmd := cmdStorageBucket{global: c.global}
|
|
cmd.AddCommand(storageBucketCmd.command())
|
|
|
|
// Volume
|
|
storageVolumeCmd := cmdStorageVolume{global: c.global, storage: c}
|
|
cmd.AddCommand(storageVolumeCmd.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 cmdStorageCreate struct {
|
|
global *cmdGlobal
|
|
storage *cmdStorage
|
|
|
|
flagDescription string
|
|
}
|
|
|
|
var cmdStorageCreateUsage = u.Usage{u.NewName(u.Pool).Remote(), u.Driver, u.KV.List(0)}
|
|
|
|
func (c *cmdStorageCreate) command() *cobra.Command {
|
|
cmd := &cobra.Command{}
|
|
cmd.Use = cli.U("create", cmdStorageCreateUsage...)
|
|
cmd.Aliases = []string{"add"}
|
|
cmd.Short = i18n.G("Create storage pools")
|
|
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Create storage pools`))
|
|
cmd.Example = cli.FormatSection("", i18n.G(`incus storage create s1 dir
|
|
Create a storage pool s1
|
|
|
|
incus storage create s1 dir < config.yaml
|
|
Create a storage pool s1 using the content of config.yaml
|
|
`))
|
|
|
|
cli.AddStringFlag(cmd.Flags(), &c.storage.flagTarget, "target", "", "", i18n.G("Cluster member name"))
|
|
cli.AddStringFlag(cmd.Flags(), &c.flagDescription, "description", "", "", i18n.G("Storage pool description"))
|
|
|
|
cmd.RunE = c.run
|
|
|
|
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
if len(args) == 0 {
|
|
return c.global.cmpRemotes(toComplete, false)
|
|
}
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (c *cmdStorageCreate) run(cmd *cobra.Command, args []string) error {
|
|
parsed, err := c.global.Parse(cmdStorageCreateUsage, cmd, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d := parsed[0].RemoteServer
|
|
poolName := parsed[0].RemoteObject.String
|
|
driver := parsed[1].String
|
|
keys, err := kvToMap(parsed[2])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If stdin isn't a terminal, read text from it
|
|
var stdinData api.StoragePoolPut
|
|
if !termios.IsTerminal(getStdinFd()) {
|
|
loader, err := yaml.NewLoader(os.Stdin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = loader.Load(&stdinData)
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create the new storage pool entry
|
|
pool := api.StoragePoolsPost{StoragePoolPut: stdinData}
|
|
pool.Name = poolName
|
|
pool.Driver = driver
|
|
|
|
if c.flagDescription != "" {
|
|
pool.Description = c.flagDescription
|
|
}
|
|
|
|
if pool.Config == nil {
|
|
pool.Config = map[string]string{}
|
|
}
|
|
|
|
maps.Copy(pool.Config, keys)
|
|
|
|
// If a target member was specified the API won't actually create the
|
|
// pool, but only define it as pending in the database.
|
|
if c.storage.flagTarget != "" {
|
|
d = d.UseTarget(c.storage.flagTarget)
|
|
}
|
|
|
|
// Create the pool
|
|
err = d.CreateStoragePool(pool)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !c.global.flagQuiet {
|
|
if c.storage.flagTarget != "" {
|
|
fmt.Printf(i18n.G("Storage pool %s pending on member %s")+"\n", formatRemote(c.global.conf, parsed[0]), c.storage.flagTarget)
|
|
} else {
|
|
fmt.Printf(i18n.G("Storage pool %s created")+"\n", formatRemote(c.global.conf, parsed[0]))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Delete.
|
|
type cmdStorageDelete struct {
|
|
global *cmdGlobal
|
|
storage *cmdStorage
|
|
}
|
|
|
|
var cmdStorageDeleteUsage = u.Usage{u.Pool.Remote().List(1)}
|
|
|
|
func (c *cmdStorageDelete) command() *cobra.Command {
|
|
cmd := &cobra.Command{}
|
|
cmd.Use = cli.U("delete", cmdStorageDeleteUsage...)
|
|
cmd.Aliases = []string{"rm", "remove"}
|
|
cmd.Short = i18n.G("Delete storage pools")
|
|
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(`Delete storage pools`))
|
|
|
|
cmd.RunE = c.run
|
|
|
|
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
return c.global.cmpStoragePools(toComplete)
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (c *cmdStorageDelete) run(cmd *cobra.Command, args []string) error {
|
|
parsed, err := c.global.Parse(cmdStorageDeleteUsage, cmd, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var errs []error
|
|
|
|
for _, p := range parsed[0].List {
|
|
d := p.RemoteServer
|
|
poolName := p.RemoteObject.String
|
|
|
|
// Delete the pool
|
|
err = d.DeleteStoragePool(poolName)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
|
|
if !c.global.flagQuiet {
|
|
fmt.Printf(i18n.G("Storage pool %s deleted")+"\n", formatRemote(c.global.conf, p))
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Edit.
|
|
type cmdStorageEdit struct {
|
|
global *cmdGlobal
|
|
storage *cmdStorage
|
|
}
|
|
|
|
var cmdStorageEditUsage = u.Usage{u.Pool.Remote()}
|
|
|
|
func (c *cmdStorageEdit) command() *cobra.Command {
|
|
cmd := &cobra.Command{}
|
|
cmd.Use = cli.U("edit", cmdStorageEditUsage...)
|
|
cmd.Short = i18n.G("Edit storage pool configurations as YAML")
|
|
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
|
|
`Edit storage pool configurations as YAML`,
|
|
))
|
|
cmd.Example = cli.FormatSection("", i18n.G(
|
|
`incus storage edit [<remote>:]<pool> < pool.yaml
|
|
Update a storage pool using the content of pool.yaml.`,
|
|
))
|
|
|
|
cmd.RunE = c.run
|
|
|
|
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
if len(args) == 0 {
|
|
return c.global.cmpStoragePools(toComplete)
|
|
}
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (c *cmdStorageEdit) helpTemplate() string {
|
|
return i18n.G(
|
|
`### This is a YAML representation of a storage pool.
|
|
### Any line starting with a '#' will be ignored.
|
|
###
|
|
### A storage pool consists of a set of configuration items.
|
|
###
|
|
### An example would look like:
|
|
### name: default
|
|
### driver: zfs
|
|
### used_by: []
|
|
### config:
|
|
### size: "61203283968"
|
|
### source: default
|
|
### zfs.pool_name: default`,
|
|
)
|
|
}
|
|
|
|
func (c *cmdStorageEdit) run(cmd *cobra.Command, args []string) error {
|
|
parsed, err := c.global.Parse(cmdStorageEditUsage, cmd, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d := parsed[0].RemoteServer
|
|
poolName := parsed[0].RemoteObject.String
|
|
|
|
// 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.StoragePoolPut{}
|
|
err = loader.Load(&newdata)
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
return err
|
|
}
|
|
|
|
return d.UpdateStoragePool(poolName, newdata, "")
|
|
}
|
|
|
|
// Extract the current value
|
|
pool, etag, err := d.GetStoragePool(poolName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data, err := yaml.Dump(&pool, 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.StoragePoolPut{}
|
|
err = yaml.Load(content, &newdata)
|
|
if err == nil {
|
|
err = d.UpdateStoragePool(poolName, newdata, etag)
|
|
}
|
|
|
|
// Respawn the editor
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, i18n.G("Config parsing error: %s")+"\n", 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 cmdStorageGet struct {
|
|
global *cmdGlobal
|
|
storage *cmdStorage
|
|
|
|
flagIsProperty bool
|
|
}
|
|
|
|
var cmdStorageGetUsage = u.Usage{u.Pool.Remote(), u.Key}
|
|
|
|
func (c *cmdStorageGet) command() *cobra.Command {
|
|
cmd := &cobra.Command{}
|
|
cmd.Use = cli.U("get", cmdStorageGetUsage...)
|
|
cmd.Short = i18n.G("Get values for storage pool configuration keys")
|
|
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
|
|
`Get values for storage pool configuration keys`,
|
|
))
|
|
|
|
cli.AddStringFlag(cmd.Flags(), &c.storage.flagTarget, "target", "", "", i18n.G("Cluster member name"))
|
|
cli.AddBoolFlag(cmd.Flags(), &c.flagIsProperty, "property|p", i18n.G("Get the key as a storage property"))
|
|
cmd.RunE = c.run
|
|
|
|
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
if len(args) == 0 {
|
|
return c.global.cmpStoragePools(toComplete)
|
|
}
|
|
|
|
if len(args) == 1 {
|
|
return c.global.cmpStoragePoolConfigs(args[0])
|
|
}
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (c *cmdStorageGet) run(cmd *cobra.Command, args []string) error {
|
|
parsed, err := c.global.Parse(cmdStorageGetUsage, cmd, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d := parsed[0].RemoteServer
|
|
poolName := parsed[0].RemoteObject.String
|
|
key := parsed[1].String
|
|
|
|
// If a target member was specified, we return also member-specific config values.
|
|
if c.storage.flagTarget != "" {
|
|
d = d.UseTarget(c.storage.flagTarget)
|
|
}
|
|
|
|
// Get the property
|
|
resp, _, err := d.GetStoragePool(poolName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.flagIsProperty {
|
|
w := resp.Writable()
|
|
res, err := getFieldByJSONTag(&w, key)
|
|
if err != nil {
|
|
return fmt.Errorf(i18n.G("The property %q does not exist on the storage pool %q: %v"), key, formatRemote(c.global.conf, parsed[0]), err)
|
|
}
|
|
|
|
fmt.Printf("%v\n", res)
|
|
} else {
|
|
v, ok := resp.Config[key]
|
|
if ok {
|
|
fmt.Println(v)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Info.
|
|
type cmdStorageInfo struct {
|
|
global *cmdGlobal
|
|
storage *cmdStorage
|
|
|
|
flagBytes bool
|
|
}
|
|
|
|
var cmdStorageInfoUsage = u.Usage{u.Pool.Remote()}
|
|
|
|
func (c *cmdStorageInfo) command() *cobra.Command {
|
|
cmd := &cobra.Command{}
|
|
cmd.Use = cli.U("info", cmdStorageInfoUsage...)
|
|
cmd.Short = i18n.G("Show useful information about storage pools")
|
|
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
|
|
`Show useful information about storage pools`,
|
|
))
|
|
|
|
cli.AddBoolFlag(cmd.Flags(), &c.flagBytes, "bytes", i18n.G("Show the used and free space in bytes"))
|
|
cli.AddStringFlag(cmd.Flags(), &c.storage.flagTarget, "target", "", "", i18n.G("Cluster member name"))
|
|
cmd.RunE = c.run
|
|
|
|
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
if len(args) == 0 {
|
|
return c.global.cmpStoragePools(toComplete)
|
|
}
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (c *cmdStorageInfo) run(cmd *cobra.Command, args []string) error {
|
|
parsed, err := c.global.Parse(cmdStorageInfoUsage, cmd, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d := parsed[0].RemoteServer
|
|
poolName := parsed[0].RemoteObject.String
|
|
|
|
// Targeting
|
|
if c.storage.flagTarget != "" {
|
|
if !d.IsClustered() {
|
|
return errors.New(i18n.G("To use --target, the destination remote must be a cluster"))
|
|
}
|
|
|
|
d = d.UseTarget(c.storage.flagTarget)
|
|
}
|
|
|
|
// Get the pool information
|
|
pool, _, err := d.GetStoragePool(poolName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
res, err := d.GetStoragePoolResources(poolName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Declare the poolinfo map of maps in order to build up the yaml
|
|
poolinfo := make(map[string]map[string]string)
|
|
poolusedby := make(map[string]map[string][]string)
|
|
|
|
// Translations
|
|
usedbystring := i18n.G("used by")
|
|
infostring := i18n.G("info")
|
|
namestring := i18n.G("name")
|
|
driverstring := i18n.G("driver")
|
|
descriptionstring := i18n.G("description")
|
|
totalspacestring := i18n.G("total space")
|
|
spaceusedstring := i18n.G("space used")
|
|
|
|
// Initialize the usedby map
|
|
poolusedby[usedbystring] = make(map[string][]string)
|
|
|
|
// Build up the usedby map
|
|
for _, v := range pool.UsedBy {
|
|
uri, err := url.Parse(v)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
fields := strings.Split(strings.TrimPrefix(uri.Path, "/1.0/"), "/")
|
|
fieldsLen := len(fields)
|
|
|
|
entityType := "unrecognized"
|
|
entityName := uri.Path
|
|
|
|
if fieldsLen > 1 {
|
|
entityType = fields[0]
|
|
entityName = fields[1]
|
|
|
|
if fields[fieldsLen-2] == "snapshots" {
|
|
continue // Skip snapshots as the parent entity will be included once in the list.
|
|
}
|
|
|
|
if fields[0] == "storage-pools" && fieldsLen > 3 {
|
|
entityType = fields[2]
|
|
entityName = fields[3]
|
|
|
|
if entityType == "volumes" && fieldsLen > 4 {
|
|
entityName = fields[4]
|
|
}
|
|
}
|
|
}
|
|
|
|
var sb strings.Builder
|
|
var attribs []string
|
|
sb.WriteString(entityName)
|
|
|
|
// Show info regarding the project and location if present.
|
|
values := uri.Query()
|
|
projectName := values.Get("project")
|
|
if projectName != "" {
|
|
attribs = append(attribs, fmt.Sprintf("project %q", projectName))
|
|
}
|
|
|
|
locationName := values.Get("target")
|
|
if locationName != "" {
|
|
attribs = append(attribs, fmt.Sprintf("location %q", locationName))
|
|
}
|
|
|
|
if len(attribs) > 0 {
|
|
sb.WriteString(" (")
|
|
for i, attrib := range attribs {
|
|
if i > 0 {
|
|
sb.WriteString(", ")
|
|
}
|
|
|
|
sb.WriteString(attrib)
|
|
}
|
|
|
|
sb.WriteString(")")
|
|
}
|
|
|
|
poolusedby[usedbystring][entityType] = append(poolusedby[usedbystring][entityType], sb.String())
|
|
}
|
|
|
|
// Initialize the info map
|
|
poolinfo[infostring] = map[string]string{}
|
|
|
|
// Build up the info map
|
|
poolinfo[infostring][namestring] = pool.Name
|
|
poolinfo[infostring][driverstring] = pool.Driver
|
|
poolinfo[infostring][descriptionstring] = pool.Description
|
|
if c.flagBytes {
|
|
poolinfo[infostring][totalspacestring] = strconv.FormatUint(res.Space.Total, 10)
|
|
poolinfo[infostring][spaceusedstring] = strconv.FormatUint(res.Space.Used, 10)
|
|
} else {
|
|
poolinfo[infostring][totalspacestring] = units.GetByteSizeStringIEC(int64(res.Space.Total), 2)
|
|
poolinfo[infostring][spaceusedstring] = units.GetByteSizeStringIEC(int64(res.Space.Used), 2)
|
|
}
|
|
|
|
poolinfodata, err := yaml.Dump(poolinfo, yaml.WithV2Defaults())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
poolusedbydata, err := yaml.Dump(poolusedby, yaml.WithV2Defaults())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("%s", poolinfodata)
|
|
fmt.Printf("%s", poolusedbydata)
|
|
|
|
return nil
|
|
}
|
|
|
|
// List.
|
|
type cmdStorageList struct {
|
|
global *cmdGlobal
|
|
storage *cmdStorage
|
|
|
|
flagFormat string
|
|
flagColumns string
|
|
}
|
|
|
|
var cmdStorageListUsage = u.Usage{u.RemoteColonOpt, u.Filter.List(0)}
|
|
|
|
func (c *cmdStorageList) command() *cobra.Command {
|
|
cmd := &cobra.Command{}
|
|
cmd.Use = cli.U("list", cmdStorageListUsage...)
|
|
cmd.Aliases = []string{"ls"}
|
|
cmd.Short = i18n.G("List available storage pools")
|
|
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
|
|
`List available storage pools
|
|
|
|
Default column layout: nDdus
|
|
|
|
== Columns ==
|
|
The -c option takes a comma separated list of arguments that control
|
|
which storage pools attributes to output when displaying in table or csv
|
|
format.
|
|
|
|
Column arguments are either pre-defined shorthand chars (see below),
|
|
or (extended) config keys.
|
|
|
|
Commas between consecutive shorthand chars are optional.
|
|
|
|
Pre-defined column shorthand chars:
|
|
n - Name
|
|
D - Driver
|
|
d - Description
|
|
S - Source
|
|
u - used by
|
|
s - state`,
|
|
))
|
|
cli.AddStringFlag(cmd.Flags(), &c.flagColumns, "columns|c", defaultStorageColumns, "", i18n.G("Columns"))
|
|
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`))
|
|
|
|
cmd.PreRunE = func(cmd *cobra.Command, _ []string) error {
|
|
return cli.ValidateFlagFormatForListOutput(cmd.Flag("format").Value.String())
|
|
}
|
|
|
|
cmd.RunE = c.run
|
|
|
|
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
if len(args) == 0 {
|
|
return c.global.cmpRemotes(toComplete, false)
|
|
}
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
const defaultStorageColumns = "nDdus"
|
|
|
|
func (c *cmdStorageList) parseColumns() ([]storageColumn, error) {
|
|
columnsShorthandMap := map[rune]storageColumn{
|
|
'n': {i18n.G("NAME"), c.storageNameColumnData},
|
|
'D': {i18n.G("DRIVER"), c.driverColumnData},
|
|
'd': {i18n.G("DESCRIPTION"), c.descriptionColumnData},
|
|
'S': {i18n.G("SOURCE"), c.sourceColumnData},
|
|
'u': {i18n.G("USED BY"), c.usedByColumnData},
|
|
's': {i18n.G("STATE"), c.stateColumnData},
|
|
}
|
|
|
|
columnList := strings.Split(c.flagColumns, ",")
|
|
|
|
columns := []storageColumn{}
|
|
|
|
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 *cmdStorageList) storageNameColumnData(storage api.StoragePool) string {
|
|
return storage.Name
|
|
}
|
|
|
|
func (c *cmdStorageList) driverColumnData(storage api.StoragePool) string {
|
|
return storage.Driver
|
|
}
|
|
|
|
func (c *cmdStorageList) descriptionColumnData(storage api.StoragePool) string {
|
|
return storage.Description
|
|
}
|
|
|
|
func (c *cmdStorageList) sourceColumnData(storage api.StoragePool) string {
|
|
return storage.Config["source"]
|
|
}
|
|
|
|
func (c *cmdStorageList) usedByColumnData(storage api.StoragePool) string {
|
|
return fmt.Sprintf("%d", len(storage.UsedBy))
|
|
}
|
|
|
|
func (c *cmdStorageList) stateColumnData(storage api.StoragePool) string {
|
|
return strings.ToUpper(storage.Status)
|
|
}
|
|
|
|
func (c *cmdStorageList) run(cmd *cobra.Command, args []string) error {
|
|
parsed, err := c.global.Parse(cmdStorageListUsage, cmd, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d := parsed[0].RemoteServer
|
|
filters := prepareStoragePoolsServerFilters(parsed[1].StringList, api.StoragePool{})
|
|
|
|
// Get the storage pools
|
|
pools, err := d.GetStoragePoolsWithFilter(filters)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Parse column flags.
|
|
columns, err := c.parseColumns()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data := [][]string{}
|
|
for _, pool := range pools {
|
|
line := []string{}
|
|
for _, column := range columns {
|
|
line = append(line, column.Data(pool))
|
|
}
|
|
|
|
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, pools)
|
|
}
|
|
|
|
// Set.
|
|
type cmdStorageSet struct {
|
|
global *cmdGlobal
|
|
storage *cmdStorage
|
|
|
|
flagIsProperty bool
|
|
}
|
|
|
|
var cmdStorageSetUsage = u.Usage{u.Pool.Remote(), u.LegacyKV.List(1)}
|
|
|
|
func (c *cmdStorageSet) command() *cobra.Command {
|
|
cmd := &cobra.Command{}
|
|
cmd.Use = cli.U("set", cmdStorageSetUsage...)
|
|
cmd.Short = i18n.G("Set storage pool configuration keys")
|
|
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
|
|
`Set storage pool configuration keys
|
|
|
|
For backward compatibility, a single configuration key may still be set with:
|
|
incus storage set [<remote>:]<pool> <key> <value>`,
|
|
))
|
|
|
|
cli.AddStringFlag(cmd.Flags(), &c.storage.flagTarget, "target", "", "", i18n.G("Cluster member name"))
|
|
cli.AddBoolFlag(cmd.Flags(), &c.flagIsProperty, "property|p", i18n.G("Set the key as a storage property"))
|
|
cmd.RunE = c.run
|
|
|
|
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
if len(args) == 0 {
|
|
return c.global.cmpStoragePools(toComplete)
|
|
}
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
// set runs the post-parsing command logic.
|
|
func (c *cmdStorageSet) set(cmd *cobra.Command, parsed []*u.Parsed) error {
|
|
d := parsed[0].RemoteServer
|
|
poolName := parsed[0].RemoteObject.String
|
|
keys, err := kvToMap(parsed[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.storage.flagTarget != "" {
|
|
d = d.UseTarget(c.storage.flagTarget)
|
|
}
|
|
|
|
// Get the pool entry
|
|
pool, etag, err := d.GetStoragePool(poolName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
writable := pool.Writable()
|
|
if c.flagIsProperty {
|
|
if cmd.Name() == "unset" {
|
|
for k := range keys {
|
|
err := unsetFieldByJSONTag(&writable, k)
|
|
if err != nil {
|
|
return fmt.Errorf(i18n.G("Error unsetting property: %v"), err)
|
|
}
|
|
}
|
|
} else {
|
|
err := unpackKVToWritable(&writable, keys)
|
|
if err != nil {
|
|
return fmt.Errorf(i18n.G("Error setting properties: %v"), err)
|
|
}
|
|
}
|
|
} else {
|
|
if writable.Config == nil {
|
|
writable.Config = make(map[string]string)
|
|
}
|
|
|
|
// Update the volume config keys.
|
|
maps.Copy(writable.Config, keys)
|
|
}
|
|
|
|
err = d.UpdateStoragePool(poolName, writable, etag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *cmdStorageSet) run(cmd *cobra.Command, args []string) error {
|
|
parsed, err := c.global.Parse(cmdStorageSetUsage, cmd, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.set(cmd, parsed)
|
|
}
|
|
|
|
// Show.
|
|
type cmdStorageShow struct {
|
|
global *cmdGlobal
|
|
storage *cmdStorage
|
|
|
|
flagResources bool
|
|
}
|
|
|
|
var cmdStorageShowUsage = u.Usage{u.Pool.Remote()}
|
|
|
|
func (c *cmdStorageShow) command() *cobra.Command {
|
|
cmd := &cobra.Command{}
|
|
cmd.Use = cli.U("show", cmdStorageShowUsage...)
|
|
cmd.Short = i18n.G("Show storage pool configurations and resources")
|
|
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
|
|
`Show storage pool configurations and resources`,
|
|
))
|
|
|
|
cli.AddBoolFlag(cmd.Flags(), &c.flagResources, "resources", i18n.G("Show the resources available to the storage pool"))
|
|
cli.AddStringFlag(cmd.Flags(), &c.storage.flagTarget, "target", "", "", i18n.G("Cluster member name"))
|
|
cmd.RunE = c.run
|
|
|
|
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
if len(args) == 0 {
|
|
return c.global.cmpStoragePools(toComplete)
|
|
}
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (c *cmdStorageShow) run(cmd *cobra.Command, args []string) error {
|
|
parsed, err := c.global.Parse(cmdStorageShowUsage, cmd, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d := parsed[0].RemoteServer
|
|
poolName := parsed[0].RemoteObject.String
|
|
|
|
// If a target member was specified, we return also member-specific config values.
|
|
if c.storage.flagTarget != "" {
|
|
d = d.UseTarget(c.storage.flagTarget)
|
|
}
|
|
|
|
if c.flagResources {
|
|
res, err := d.GetStoragePoolResources(poolName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data, err := yaml.Dump(&res, yaml.WithV2Defaults())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("%s", data)
|
|
|
|
return nil
|
|
}
|
|
|
|
pool, _, err := d.GetStoragePool(poolName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sort.Strings(pool.UsedBy)
|
|
|
|
data, err := yaml.Dump(&pool, yaml.WithV2Defaults())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("%s", data)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Unset.
|
|
type cmdStorageUnset struct {
|
|
global *cmdGlobal
|
|
storage *cmdStorage
|
|
storageSet *cmdStorageSet
|
|
|
|
flagIsProperty bool
|
|
}
|
|
|
|
var cmdStorageUnsetUsage = u.Usage{u.Pool.Remote(), u.Key.List(1)}
|
|
|
|
func (c *cmdStorageUnset) command() *cobra.Command {
|
|
cmd := &cobra.Command{}
|
|
cmd.Use = cli.U("unset", cmdStorageUnsetUsage...)
|
|
cmd.Short = i18n.G("Unset storage pool configuration keys")
|
|
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
|
|
`Unset storage pool configuration keys`,
|
|
))
|
|
|
|
cli.AddStringFlag(cmd.Flags(), &c.storage.flagTarget, "target", "", "", i18n.G("Cluster member name"))
|
|
cli.AddBoolFlag(cmd.Flags(), &c.flagIsProperty, "property|p", i18n.G("Unset the keys as storage properties"))
|
|
cmd.RunE = c.run
|
|
|
|
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
if len(args) == 0 {
|
|
return c.global.cmpStoragePools(toComplete)
|
|
}
|
|
|
|
if len(args) == 1 {
|
|
return c.global.cmpStoragePoolConfigs(args[0])
|
|
}
|
|
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (c *cmdStorageUnset) run(cmd *cobra.Command, args []string) error {
|
|
parsed, err := c.global.Parse(cmdStorageUnsetUsage, cmd, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.storageSet.flagIsProperty = c.flagIsProperty
|
|
return unsetKey(c.storageSet, cmd, parsed)
|
|
}
|
|
|
|
// prepareStoragePoolsServerFilters processes and formats filter criteria
|
|
// for storage pools, ensuring they are in a format that the server can interpret.
|
|
func prepareStoragePoolsServerFilters(filters []string, i any) []string {
|
|
formattedFilters := []string{}
|
|
|
|
for _, filter := range filters {
|
|
membs := strings.SplitN(filter, "=", 2)
|
|
key := membs[0]
|
|
|
|
if len(membs) == 1 {
|
|
regexpValue := key
|
|
if !strings.Contains(key, "^") && !strings.Contains(key, "$") {
|
|
regexpValue = "^" + regexpValue + "$"
|
|
}
|
|
|
|
filter = fmt.Sprintf("name=(%s|^%s.*)", regexpValue, key)
|
|
} else {
|
|
firstPart := key
|
|
if strings.Contains(key, ".") {
|
|
firstPart = strings.Split(key, ".")[0]
|
|
}
|
|
|
|
if !structHasField(reflect.TypeOf(i), firstPart) {
|
|
filter = fmt.Sprintf("config.%s", filter)
|
|
}
|
|
}
|
|
|
|
formattedFilters = append(formattedFilters, filter)
|
|
}
|
|
|
|
return formattedFilters
|
|
}
|