incus-mirror/cmd/incus/list.go
Benoit TAPIA 24b477b8ca
incus/list: Add --all-remotes
Signed-off-by: Benoit TAPIA <benoit.tapia@sidhes.com>
2026-07-29 22:17:01 -04:00

1045 lines
27 KiB
Go

package main
import (
"errors"
"fmt"
"maps"
"net"
"os"
"reflect"
"slices"
"sort"
"strconv"
"strings"
"sync"
"github.com/spf13/cobra"
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/internal/instance"
"github.com/lxc/incus/v7/shared/api"
cli "github.com/lxc/incus/v7/shared/cmd"
"github.com/lxc/incus/v7/shared/units"
"github.com/lxc/incus/v7/shared/util"
)
type column struct {
Name string
Data columnData
NeedsState bool
NeedsSnapshots bool
}
type columnData func(api.InstanceFull) string
// instanceWithRemote associates an instance with its remote for JSON/YAML output.
type instanceWithRemote struct {
api.InstanceFull `yaml:",inline"`
Remote string `json:"remote" yaml:"remote"`
}
type cmdList struct {
global *cmdGlobal
flagColumns string
flagFast bool
flagFormat string
flagAllProjects bool
flagAllRemotes bool
rawFormat bool
currentRemote string
shorthandFilters map[string]func(*api.Instance, *api.InstanceState, string) bool
}
var cmdListUsage = u.Usage{u.RemoteColonOpt, u.Filter.List(0)}
func (c *cmdList) command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = cli.U("list", cmdListUsage...)
cmd.Aliases = []string{"ls"}
cmd.Short = i18n.G("List instances")
cmd.Long = cli.FormatSection(color.DescriptionPrefix, i18n.G(
`List instances
Default column layout: ns46tS
Fast column layout: nsacPt
A single keyword like "web" which will list any instance with a name starting by "web".
A regular expression on the instance name. (e.g. .*web.*01$).
A key/value pair referring to a configuration item. For those, the
namespace can be abbreviated to the smallest unambiguous identifier.
A key/value pair where the key is a shorthand. Multiple values must be delimited by ','. Available shorthands:
- type={instance type}
- status={instance current lifecycle status}
- architecture={instance architecture}
- location={location name}
- ipv4={ip or CIDR}
- ipv6={ip or CIDR}
Examples:
- "user.blah=abc" will list all instances with the "blah" user property set to "abc".
- "u.blah=abc" will do the same
- "security.privileged=true" will list all privileged instances
- "s.privileged=true" will do the same
- "type=container" will list all container instances
- "type=container status=running" will list all running container instances
A regular expression matching a configuration item or its value. (e.g. volatile.eth0.hwaddr=10:66:6a:.*).
When multiple filters are passed, they are added one on top of the other,
selecting instances which satisfy them all.
== Columns ==
The -c option takes a comma separated list of arguments that control
which instance 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:
4 - IPv4 address
6 - IPv6 address
a - Architecture
b - Storage pool
c - Creation date
d - Description
D - disk usage
e - Project name
l - Last used date
m - Memory usage
M - Memory usage (%)
n - Name
N - Number of Processes
p - PID of the instance's init process
P - Profiles
R - Remote name
s - State
S - Number of snapshots
t - Type (persistent or ephemeral)
u - CPU usage (in seconds)
U - Started date
L - Location of the instance (e.g. its cluster member)
f - Base Image Fingerprint (short)
F - Base Image Fingerprint (long)
Custom columns are defined with "[config:|devices:]key[:name][:maxWidth]":
KEY: The (extended) config or devices key to display. If [config:|devices:] is omitted then it defaults to config key.
NAME: Name to display in the column header.
Defaults to the key if not specified or empty.
MAXWIDTH: Max width of the column (longer results are truncated).
Defaults to -1 (unlimited). Use 0 to limit to the column header size.`,
))
cmd.Example = cli.FormatSection("", i18n.G(
`incus list -c nFs46,volatile.eth0.hwaddr:MAC,config:image.os,devices:eth0.parent:ETHP
Show instances using the "NAME", "BASE IMAGE", "STATE", "IPV4", "IPV6" and "MAC" columns.
"BASE IMAGE", "MAC" and "IMAGE OS" are custom columns generated from instance configuration keys.
"ETHP" is a custom column generated from a device key.
incus list -c ns,user.comment:comment
List instances with their running state and user comment.`,
))
cmd.RunE = c.run
cli.AddStringFlag(cmd.Flags(), &c.flagColumns, "columns|c", defaultColumns, "", 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`))
cli.AddBoolFlag(cmd.Flags(), &c.flagFast, "fast", i18n.G("Fast mode (same as --columns=nsacPt)"))
cli.AddBoolFlag(cmd.Flags(), &c.flagAllProjects, "all-projects", i18n.G("Display instances from all projects"))
cli.AddBoolFlag(cmd.Flags(), &c.flagAllRemotes, "all-remotes", i18n.G("Display instances from all remotes"))
cmd.PreRunE = func(cmd *cobra.Command, _ []string) error {
return cli.ValidateFlagFormatForListOutput(cmd.Flag("format").Value.String())
}
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 (
defaultColumns = "ns46tSL"
defaultColumnsAllProjects = "ens46tSL"
configColumnType = "config"
deviceColumnType = "devices"
)
func (c *cmdList) shouldShow(filters []string, inst *api.Instance, state *api.InstanceState) bool {
c.mapShorthandFilters()
for _, filter := range filters {
membs := strings.SplitN(filter, "=", 2)
key := membs[0]
var value string
if len(membs) < 2 {
value = ""
} else {
value = membs[1]
}
if c.evaluateShorthandFilter(key, value, inst, state) {
continue
}
return false
}
return true
}
func (c *cmdList) evaluateShorthandFilter(key string, value string, inst *api.Instance, state *api.InstanceState) bool {
const shorthandValueDelimiter = ","
shorthandFilterFunction, isShorthandFilter := c.shorthandFilters[strings.ToLower(key)]
if !isShorthandFilter {
return false
}
if !strings.Contains(value, shorthandValueDelimiter) {
return shorthandFilterFunction(inst, state, value)
}
matched := false
for _, curValue := range strings.Split(value, shorthandValueDelimiter) {
if shorthandFilterFunction(inst, state, curValue) {
matched = true
}
}
return matched
}
// fetchInstancesData retrieves any state and snapshot data needed by the columns.
func (c *cmdList) fetchInstancesData(d incus.InstanceServer, instances []api.Instance, columns []column) []api.InstanceFull {
threads := min(len(instances), 10)
// Shortcut when needing state and snapshot info.
hasSnapshots := false
hasState := false
for _, column := range columns {
if column.NeedsSnapshots {
hasSnapshots = true
}
if column.NeedsState {
hasState = true
}
}
if hasSnapshots && hasState {
cInfo := []api.InstanceFull{}
cInfoLock := sync.Mutex{}
cInfoQueue := make(chan string, threads)
cInfoWg := sync.WaitGroup{}
for range threads {
cInfoWg.Go(func() {
for {
cName, more := <-cInfoQueue
if !more {
break
}
state, _, err := d.GetInstanceFull(cName)
if err != nil {
continue
}
cInfoLock.Lock()
cInfo = append(cInfo, *state)
cInfoLock.Unlock()
}
})
}
for _, info := range instances {
cInfoQueue <- info.Name
}
close(cInfoQueue)
cInfoWg.Wait()
return cInfo
}
cStates := map[string]*api.InstanceState{}
cStatesLock := sync.Mutex{}
cStatesQueue := make(chan string, threads)
cStatesWg := sync.WaitGroup{}
cSnapshots := map[string][]api.InstanceSnapshot{}
cSnapshotsLock := sync.Mutex{}
cSnapshotsQueue := make(chan string, threads)
cSnapshotsWg := sync.WaitGroup{}
for range threads {
cStatesWg.Go(func() {
for {
cName, more := <-cStatesQueue
if !more {
break
}
state, _, err := d.GetInstanceState(cName)
if err != nil {
continue
}
cStatesLock.Lock()
cStates[cName] = state
cStatesLock.Unlock()
}
})
cSnapshotsWg.Go(func() {
for {
cName, more := <-cSnapshotsQueue
if !more {
break
}
snaps, err := d.GetInstanceSnapshots(cName)
if err != nil {
continue
}
cSnapshotsLock.Lock()
cSnapshots[cName] = snaps
cSnapshotsLock.Unlock()
}
})
}
for _, inst := range instances {
for _, column := range columns {
if column.NeedsState && inst.IsActive() {
cStatesLock.Lock()
_, ok := cStates[inst.Name]
cStatesLock.Unlock()
if ok {
continue
}
cStatesLock.Lock()
cStates[inst.Name] = nil
cStatesLock.Unlock()
cStatesQueue <- inst.Name
}
if column.NeedsSnapshots {
cSnapshotsLock.Lock()
_, ok := cSnapshots[inst.Name]
cSnapshotsLock.Unlock()
if ok {
continue
}
cSnapshotsLock.Lock()
cSnapshots[inst.Name] = nil
cSnapshotsLock.Unlock()
cSnapshotsQueue <- inst.Name
}
}
}
close(cStatesQueue)
close(cSnapshotsQueue)
cStatesWg.Wait()
cSnapshotsWg.Wait()
// Convert to Instance
data := make([]api.InstanceFull, len(instances))
for i := range instances {
data[i].Instance = instances[i]
data[i].State = cStates[instances[i].Name]
data[i].Snapshots = cSnapshots[instances[i].Name]
}
return data
}
// getInstances retrieves the instances from the given server along with any data needed by the columns.
func (c *cmdList) getInstances(d incus.InstanceServer, filters []string, needsData bool, columns []column) ([]api.InstanceFull, []string, error) {
serverFilters, clientFilters := getServerSupportedFilters(filters, []string{"ipv4", "ipv6"}, true)
if needsData && d.HasExtension("container_full") {
// Using the GetInstancesFull shortcut
var instances []api.InstanceFull
var err error
serverFilters = prepareInstanceServerFilters(serverFilters, api.InstanceFull{})
if c.flagAllProjects {
instances, err = d.GetInstancesFullAllProjectsWithFilter(api.InstanceTypeAny, serverFilters)
} else {
instances, err = d.GetInstancesFullWithFilter(api.InstanceTypeAny, serverFilters)
}
if err != nil {
return nil, nil, err
}
return instances, clientFilters, nil
}
// Get the list of instances
var instances []api.Instance
var err error
serverFilters = prepareInstanceServerFilters(serverFilters, api.Instance{})
if c.flagAllProjects {
instances, err = d.GetInstancesAllProjectsWithFilter(api.InstanceTypeAny, serverFilters)
} else {
instances, err = d.GetInstancesWithFilter(api.InstanceTypeAny, serverFilters)
}
if err != nil {
return nil, nil, err
}
// Fetch any remaining data
return c.fetchInstancesData(d, instances, columns), clientFilters, nil
}
func (c *cmdList) run(cmd *cobra.Command, args []string) error {
parsed, err := c.global.Parse(cmdListUsage, cmd, args)
if err != nil {
return err
}
d := parsed[0].RemoteServer
filters := parsed[1].StringList
if c.global.flagProject != "" && c.flagAllProjects {
return errors.New(i18n.G("Can't specify --project with --all-projects"))
}
if c.flagAllRemotes && !parsed[0].Skipped {
return errors.New(i18n.G("Can't specify a remote with --all-remotes"))
}
// Check for raw unit formatting.
formatFields := strings.SplitN(c.flagFormat, ",", 2)
if len(formatFields) == 2 {
formatOptions := strings.Split(formatFields[1], ",")
if slices.Contains(formatOptions, "raw") {
c.rawFormat = true
}
}
// Get the list of columns
columns, needsData, err := c.parseColumns(c.flagAllRemotes || d.IsClustered())
if err != nil {
return err
}
// Get the list of servers to query.
servers := map[string]incus.InstanceServer{parsed[0].RemoteName: d}
if c.flagAllRemotes {
servers = map[string]incus.InstanceServer{}
for name, remote := range c.global.conf.Remotes {
if remote.Public || remote.Protocol != "incus" {
continue
}
if name == parsed[0].RemoteName {
servers[name] = d
continue
}
server, err := c.global.conf.GetInstanceServer(name)
if err != nil {
fmt.Fprintf(os.Stderr, i18n.G("Skipping remote %q: %v")+"\n", name, err)
continue
}
servers[name] = server
}
}
// Generate the table data
data := [][]string{}
instancesFiltered := []api.InstanceFull{}
remoteInstancesFiltered := []instanceWithRemote{}
for _, name := range slices.Sorted(maps.Keys(servers)) {
instances, clientFilters, err := c.getInstances(servers[name], filters, needsData, columns)
if err != nil {
if !c.flagAllRemotes {
return err
}
fmt.Fprintf(os.Stderr, i18n.G("Skipping remote %q: %v")+"\n", name, err)
continue
}
c.currentRemote = name
for _, inst := range instances {
if !c.shouldShow(clientFilters, &inst.Instance, inst.State) {
continue
}
if c.flagAllRemotes {
remoteInstancesFiltered = append(remoteInstancesFiltered, instanceWithRemote{InstanceFull: inst, Remote: name})
} else {
instancesFiltered = append(instancesFiltered, inst)
}
col := []string{}
for _, column := range columns {
col = append(col, column.Data(inst))
}
data = append(data, col)
}
}
sort.Sort(cli.SortColumnsNaturally(data))
headers := []string{}
for _, column := range columns {
headers = append(headers, column.Name)
}
// Render the table
if c.flagAllRemotes {
return cli.RenderTable(os.Stdout, c.flagFormat, headers, data, remoteInstancesFiltered)
}
return cli.RenderTable(os.Stdout, c.flagFormat, headers, data, instancesFiltered)
}
func (c *cmdList) parseColumns(clustered bool) ([]column, bool, error) {
columnsShorthandMap := map[rune]column{
'4': {i18n.G("IPV4"), c.ip4ColumnData, true, false},
'6': {i18n.G("IPV6"), c.ip6ColumnData, true, false},
'a': {i18n.G("ARCHITECTURE"), c.architectureColumnData, false, false},
'b': {i18n.G("STORAGE POOL"), c.storagePoolColumnData, false, false},
'c': {i18n.G("CREATED AT"), c.createdColumnData, false, false},
'd': {i18n.G("DESCRIPTION"), c.descriptionColumnData, false, false},
'D': {i18n.G("DISK USAGE"), c.diskUsageColumnData, true, false},
'e': {i18n.G("PROJECT"), c.projectColumnData, false, false},
'f': {i18n.G("BASE IMAGE"), c.baseImageColumnData, false, false},
'F': {i18n.G("BASE IMAGE"), c.baseImageFullColumnData, false, false},
'l': {i18n.G("LAST USED AT"), c.lastUsedColumnData, false, false},
'm': {i18n.G("MEMORY USAGE"), c.memoryUsageColumnData, true, false},
'M': {i18n.G("MEMORY USAGE%"), c.memoryUsagePercentColumnData, true, false},
'n': {i18n.G("NAME"), c.nameColumnData, false, false},
'N': {i18n.G("PROCESSES"), c.numberOfProcessesColumnData, true, false},
'p': {i18n.G("PID"), c.pidColumnData, true, false},
'P': {i18n.G("PROFILES"), c.profilesColumnData, false, false},
'R': {i18n.G("REMOTE"), c.remoteColumnData, false, false},
'S': {i18n.G("SNAPSHOTS"), c.numberSnapshotsColumnData, false, true},
's': {i18n.G("STATE"), c.statusColumnData, false, false},
't': {i18n.G("TYPE"), c.typeColumnData, false, false},
'u': {i18n.G("CPU USAGE"), c.cpuUsageSecondsColumnData, true, false},
'U': {i18n.G("STARTED AT"), c.startedColumnData, true, false},
}
// Add project column if --all-projects flag specified and
// no one of --fast or --c was passed
if c.flagAllProjects {
if c.flagColumns == defaultColumns {
c.flagColumns = defaultColumnsAllProjects
}
}
if c.flagFast {
if c.flagColumns != defaultColumns && c.flagColumns != defaultColumnsAllProjects {
// --columns was specified too
return nil, false, errors.New(i18n.G("Can't specify --fast with --columns"))
}
if c.flagColumns == defaultColumnsAllProjects {
c.flagColumns = "ensacPt"
} else {
c.flagColumns = "nsacPt"
}
}
// Add remote column if --all-remotes is used with a default column layout.
if c.flagAllRemotes && slices.Contains([]string{defaultColumns, defaultColumnsAllProjects, "nsacPt", "ensacPt"}, c.flagColumns) {
c.flagColumns = "R" + c.flagColumns
}
if clustered {
columnsShorthandMap['L'] = column{
i18n.G("LOCATION"), c.locationColumnData, false, false,
}
}
columnList := strings.Split(c.flagColumns, ",")
columns := []column{}
needsData := false
for _, columnEntry := range columnList {
if columnEntry == "" {
return nil, false, fmt.Errorf(i18n.G("Empty column entry (redundant, leading or trailing command) in '%s'"), c.flagColumns)
}
// Config keys always contain a period, parse anything without a
// period as a series of shorthand runes.
if !strings.Contains(columnEntry, ".") {
if !clustered {
if columnEntry != defaultColumns && columnEntry != defaultColumnsAllProjects {
if strings.ContainsAny(columnEntry, "L") {
return nil, false, errors.New(i18n.G("Can't specify column L when not clustered"))
}
}
columnEntry = strings.ReplaceAll(columnEntry, "L", "")
}
for _, columnRune := range columnEntry {
column, ok := columnsShorthandMap[columnRune]
if !ok {
return nil, false, fmt.Errorf(i18n.G("Unknown column shorthand char '%c' in '%s'"), columnRune, columnEntry)
}
columns = append(columns, column)
if column.NeedsState || column.NeedsSnapshots {
needsData = true
}
}
} else {
cc := strings.Split(columnEntry, ":")
colType := configColumnType
if (cc[0] == configColumnType || cc[0] == deviceColumnType) && len(cc) > 1 {
colType = cc[0]
cc = slices.Delete(cc, 0, 1)
}
if len(cc) > 3 {
return nil, false, fmt.Errorf(i18n.G("Invalid config key column format (too many fields): '%s'"), columnEntry)
}
k := cc[0]
if colType == configColumnType {
_, err := instance.ConfigKeyChecker(k, api.InstanceTypeAny)
if err != nil {
return nil, false, fmt.Errorf(i18n.G("Invalid config key '%s' in '%s'"), k, columnEntry)
}
}
column := column{Name: k}
if len(cc) > 1 {
if len(cc[1]) == 0 && len(cc) != 3 {
return nil, false, fmt.Errorf(i18n.G("Invalid name in '%s', empty string is only allowed when defining maxWidth"), columnEntry)
}
column.Name = cc[1]
}
maxWidth := -1
if len(cc) > 2 {
temp, err := strconv.ParseInt(cc[2], 10, 64)
if err != nil {
return nil, false, fmt.Errorf(i18n.G("Invalid max width (must be an integer) '%s' in '%s'"), cc[2], columnEntry)
}
if temp < -1 {
return nil, false, fmt.Errorf(i18n.G("Invalid max width (must -1, 0 or a positive integer) '%s' in '%s'"), cc[2], columnEntry)
}
if temp == 0 {
maxWidth = len(column.Name)
} else {
maxWidth = int(temp)
}
}
if colType == configColumnType {
column.Data = func(cInfo api.InstanceFull) string {
v, ok := cInfo.Config[k]
if !ok {
v = cInfo.ExpandedConfig[k]
}
// Truncate the data according to the max width. A negative max width
// indicates there is no effective limit.
if maxWidth > 0 && len(v) > maxWidth {
return v[:maxWidth]
}
return v
}
}
if colType == deviceColumnType {
column.Data = func(cInfo api.InstanceFull) string {
d := strings.SplitN(k, ".", 2)
if len(d) == 1 || len(d) > 2 {
return ""
}
v, ok := cInfo.Devices[d[0]][d[1]]
if !ok {
v = cInfo.ExpandedDevices[d[0]][d[1]]
}
// Truncate the data according to the max width. A negative max width
// indicates there is no effective limit.
if maxWidth > 0 && len(v) > maxWidth {
return v[:maxWidth]
}
return v
}
}
columns = append(columns, column)
if column.NeedsState || column.NeedsSnapshots {
needsData = true
}
}
}
return columns, needsData, nil
}
func (c *cmdList) getBaseImage(cInfo api.InstanceFull, long bool) string {
v, ok := cInfo.Config["volatile.base_image"]
if !ok {
return ""
}
if !long && len(v) >= 12 {
v = v[:12]
}
return v
}
func (c *cmdList) baseImageColumnData(cInfo api.InstanceFull) string {
return c.getBaseImage(cInfo, false)
}
func (c *cmdList) baseImageFullColumnData(cInfo api.InstanceFull) string {
return c.getBaseImage(cInfo, true)
}
func (c *cmdList) nameColumnData(cInfo api.InstanceFull) string {
return cInfo.Name
}
func (c *cmdList) descriptionColumnData(cInfo api.InstanceFull) string {
return cInfo.Description
}
func (c *cmdList) statusColumnData(cInfo api.InstanceFull) string {
return strings.ToUpper(cInfo.Status)
}
func (c *cmdList) ip4ColumnData(cInfo api.InstanceFull) string {
if cInfo.IsActive() && cInfo.State != nil && cInfo.State.Network != nil {
ipv4s := []string{}
for netName, network := range cInfo.State.Network {
if network.Type == "loopback" {
continue
}
for _, addr := range network.Addresses {
if slices.Contains([]string{"link", "local"}, addr.Scope) {
continue
}
if addr.Family == "inet" {
ipv4s = append(ipv4s, fmt.Sprintf("%s (%s)", addr.Address, netName))
}
}
}
sort.Sort(sort.Reverse(sort.StringSlice(ipv4s)))
return strings.Join(ipv4s, "\n")
}
return ""
}
func (c *cmdList) ip6ColumnData(cInfo api.InstanceFull) string {
if cInfo.IsActive() && cInfo.State != nil && cInfo.State.Network != nil {
ipv6s := []string{}
for netName, network := range cInfo.State.Network {
if network.Type == "loopback" {
continue
}
for _, addr := range network.Addresses {
if slices.Contains([]string{"link", "local"}, addr.Scope) {
continue
}
if addr.Family == "inet6" {
ipv6s = append(ipv6s, fmt.Sprintf("%s (%s)", addr.Address, netName))
}
}
}
sort.Sort(sort.Reverse(sort.StringSlice(ipv6s)))
return strings.Join(ipv6s, "\n")
}
return ""
}
func (c *cmdList) projectColumnData(cInfo api.InstanceFull) string {
return cInfo.Project
}
func (c *cmdList) remoteColumnData(_ api.InstanceFull) string {
return c.currentRemote
}
func (c *cmdList) memoryUsageColumnData(cInfo api.InstanceFull) string {
if cInfo.IsActive() && cInfo.State != nil && cInfo.State.Memory.Usage > 0 {
if !c.rawFormat {
return units.GetByteSizeStringIEC(cInfo.State.Memory.Usage, 2)
}
return strconv.FormatInt(cInfo.State.Memory.Usage, 10)
}
return ""
}
func (c *cmdList) memoryUsagePercentColumnData(cInfo api.InstanceFull) string {
if cInfo.IsActive() && cInfo.State != nil && cInfo.State.Memory.Usage > 0 {
if cInfo.ExpandedConfig["limits.memory"] != "" {
memorylimit := cInfo.ExpandedConfig["limits.memory"]
if strings.Contains(memorylimit, "%") {
return ""
}
val, err := units.ParseByteSizeString(cInfo.ExpandedConfig["limits.memory"])
if err == nil && val > 0 {
return fmt.Sprintf("%.1f%%", (float64(cInfo.State.Memory.Usage)/float64(val))*float64(100))
}
}
}
return ""
}
func (c *cmdList) cpuUsageSecondsColumnData(cInfo api.InstanceFull) string {
if cInfo.IsActive() && cInfo.State != nil && cInfo.State.CPU.Usage > 0 {
return fmt.Sprintf("%ds", cInfo.State.CPU.Usage/1000000000)
}
return ""
}
func (c *cmdList) diskUsageColumnData(cInfo api.InstanceFull) string {
rootDisk, _, _ := instance.GetRootDiskDevice(cInfo.ExpandedDevices)
if cInfo.State != nil && cInfo.State.Disk != nil && cInfo.State.Disk[rootDisk].Usage > 0 {
if !c.rawFormat {
return units.GetByteSizeStringIEC(cInfo.State.Disk[rootDisk].Usage, 2)
}
return strconv.FormatInt(cInfo.State.Disk[rootDisk].Usage, 10)
}
return ""
}
func (c *cmdList) typeColumnData(cInfo api.InstanceFull) string {
ret := strings.ToUpper(cInfo.Type)
if ret == "" {
ret = "CONTAINER"
}
if util.IsTrue(cInfo.ExpandedConfig["volatile.container.oci"]) {
ret = fmt.Sprintf("%s (%s)", ret, i18n.G("APP"))
}
if cInfo.Ephemeral {
ret = fmt.Sprintf("%s (%s)", ret, i18n.G("EPHEMERAL"))
}
return ret
}
func (c *cmdList) numberSnapshotsColumnData(cInfo api.InstanceFull) string {
if cInfo.Snapshots != nil {
return fmt.Sprintf("%d", len(cInfo.Snapshots))
}
return "0"
}
func (c *cmdList) pidColumnData(cInfo api.InstanceFull) string {
if cInfo.IsActive() && cInfo.State != nil {
return fmt.Sprintf("%d", cInfo.State.Pid)
}
return ""
}
func (c *cmdList) architectureColumnData(cInfo api.InstanceFull) string {
return cInfo.Architecture
}
func (c *cmdList) storagePoolColumnData(cInfo api.InstanceFull) string {
for _, v := range cInfo.ExpandedDevices {
if v["type"] == "disk" && v["path"] == "/" {
return v["pool"]
}
}
return ""
}
func (c *cmdList) profilesColumnData(cInfo api.InstanceFull) string {
return strings.Join(cInfo.Profiles, "\n")
}
func (c *cmdList) createdColumnData(cInfo api.InstanceFull) string {
if !cInfo.CreatedAt.IsZero() {
return cInfo.CreatedAt.Local().Format(dateLayout)
}
return ""
}
func (c *cmdList) startedColumnData(cInfo api.InstanceFull) string {
if cInfo.State != nil && !cInfo.State.StartedAt.IsZero() {
return cInfo.State.StartedAt.Local().Format(dateLayout)
}
return ""
}
func (c *cmdList) lastUsedColumnData(cInfo api.InstanceFull) string {
if !cInfo.LastUsedAt.IsZero() {
return cInfo.LastUsedAt.Local().Format(dateLayout)
}
return ""
}
func (c *cmdList) numberOfProcessesColumnData(cInfo api.InstanceFull) string {
if cInfo.IsActive() && cInfo.State != nil {
return fmt.Sprintf("%d", cInfo.State.Processes)
}
return ""
}
func (c *cmdList) locationColumnData(cInfo api.InstanceFull) string {
return cInfo.Location
}
func (c *cmdList) matchByNet(cState *api.InstanceState, query string, family string) bool {
// Skip if no state.
if cState == nil {
return false
}
// Skip if no network data.
if cState.Network == nil {
return false
}
// Consider the filter as a CIDR.
_, subnet, _ := net.ParseCIDR(query)
// Go through interfaces.
for _, network := range cState.Network {
for _, addr := range network.Addresses {
if family == "ipv6" && addr.Family != "inet6" {
continue
}
if family == "ipv4" && addr.Family != "inet" {
continue
}
if addr.Address == query {
return true
}
if subnet != nil {
ipAddr := net.ParseIP(addr.Address)
if ipAddr != nil && subnet.Contains(ipAddr) {
return true
}
}
}
}
return false
}
func (c *cmdList) matchByIPV6(_ *api.Instance, cState *api.InstanceState, query string) bool {
return c.matchByNet(cState, query, "ipv6")
}
func (c *cmdList) matchByIPV4(_ *api.Instance, cState *api.InstanceState, query string) bool {
return c.matchByNet(cState, query, "ipv4")
}
func (c *cmdList) mapShorthandFilters() {
c.shorthandFilters = map[string]func(*api.Instance, *api.InstanceState, string) bool{
"ipv4": c.matchByIPV4,
"ipv6": c.matchByIPV6,
}
}
// prepareInstanceServerFilters processes and formats filter criteria
// for instances, ensuring they are in a format that the server can interpret.
func prepareInstanceServerFilters(filters []string, i any) []string {
formatedFilters := []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("expanded_config.%s", filter)
}
if key == "state" {
filter = fmt.Sprintf("status=%s", membs[1])
}
}
formatedFilters = append(formatedFilters, filter)
}
return formatedFilters
}