Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions client/incus_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package incus
import (
"errors"
"fmt"
"net/url"

"github.com/lxc/incus/v6/shared/api"
)
Expand Down Expand Up @@ -100,6 +101,26 @@ func (r *ProtocolIncus) GetClusterMemberNames() ([]string, error) {
return urlsToResourceNames(baseURL, urls...)
}

// GetClusterMembersWithFilter returns a filtered list of cluster members as ClusterMember structs.
func (r *ProtocolIncus) GetClusterMembersWithFilter(filters []string) ([]api.ClusterMember, error) {
if !r.HasExtension("clustering") {
return nil, errors.New("The server is missing the required \"clustering\" API extension")
}

members := []api.ClusterMember{}

v := url.Values{}
v.Set("recursion", "1")
v.Set("filter", parseFilters(filters))

_, err := r.queryStruct("GET", fmt.Sprintf("/cluster/members?%s", v.Encode()), nil, "", &members)
if err != nil {
return nil, err
}

return members, nil
}

// GetClusterMembers returns the current members of the cluster.
func (r *ProtocolIncus) GetClusterMembers() ([]api.ClusterMember, error) {
if !r.HasExtension("clustering") {
Expand Down
20 changes: 20 additions & 0 deletions client/incus_storage_pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ func (r *ProtocolIncus) GetStoragePools() ([]api.StoragePool, error) {
return pools, nil
}

// GetStoragePoolsWithFilter returns a filtered list of storage pools as StoragePool structs.
func (r *ProtocolIncus) GetStoragePoolsWithFilter(filters []string) ([]api.StoragePool, error) {
if !r.HasExtension("storage") {
return nil, errors.New("The server is missing the required \"storage\" API extension")
}

pools := []api.StoragePool{}

v := url.Values{}
v.Set("recursion", "1")
v.Set("filter", parseFilters(filters))

_, err := r.queryStruct("GET", fmt.Sprintf("/storage-pools?%s", v.Encode()), nil, "", &pools)
if err != nil {
return nil, err
}

return pools, nil
}

// GetStoragePool returns a StoragePool entry for the provided pool name.
func (r *ProtocolIncus) GetStoragePool(name string) (*api.StoragePool, string, error) {
if !r.HasExtension("storage") {
Expand Down
2 changes: 2 additions & 0 deletions client/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ type InstanceServer interface {
// Storage pool functions ("storage" API extension)
GetStoragePoolNames() (names []string, err error)
GetStoragePools() (pools []api.StoragePool, err error)
GetStoragePoolsWithFilter(filters []string) ([]api.StoragePool, error)
GetStoragePool(name string) (pool *api.StoragePool, ETag string, err error)
GetStoragePoolResources(name string) (resources *api.ResourcesStoragePool, err error)
CreateStoragePool(pool api.StoragePoolsPost) (err error)
Expand Down Expand Up @@ -399,6 +400,7 @@ type InstanceServer interface {
DeletePendingClusterMember(name string, force bool) (err error)
GetClusterMemberNames() (names []string, err error)
GetClusterMembers() (members []api.ClusterMember, err error)
GetClusterMembersWithFilter(filters []string) ([]api.ClusterMember, error)
GetClusterMember(name string) (member *api.ClusterMember, ETag string, err error)
UpdateClusterMember(name string, member api.ClusterMemberPut, ETag string) (err error)
RenameClusterMember(name string, member api.ClusterMemberPost) (err error)
Expand Down
54 changes: 50 additions & 4 deletions cmd/incus/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"maps"
"os"
"reflect"
"slices"
"sort"
"strings"
Expand Down Expand Up @@ -128,7 +129,7 @@ type cmdClusterList struct {
// Command returns a cobra.Command for use with (*cobra.Command).AddCommand.
func (c *cmdClusterList) Command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("list", i18n.G("[<remote>:]"))
cmd.Use = usage("list", i18n.G("[<remote>:] [<filter>...]"))
cmd.Aliases = []string{"ls"}
cmd.Short = i18n.G("List all the cluster members")
cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
Expand Down Expand Up @@ -249,7 +250,7 @@ func (c *cmdClusterList) messageColumnData(cluster api.ClusterMember) string {
// Run runs the actual command logic.
func (c *cmdClusterList) Run(cmd *cobra.Command, args []string) error {
// Quick checks.
exit, err := c.global.checkArgs(cmd, args, 0, 1)
exit, err := c.global.checkArgs(cmd, args, 0, -1)
if exit {
return err
}
Expand All @@ -260,7 +261,7 @@ func (c *cmdClusterList) Run(cmd *cobra.Command, args []string) error {

// Parse remote
remote := ""
if len(args) == 1 {
if len(args) > 0 {
remote = args[0]
}

Expand All @@ -271,6 +272,18 @@ func (c *cmdClusterList) Run(cmd *cobra.Command, args []string) error {

resource := resources[0]

// Process the filters
filters := []string{}
if resource.name != "" {
filters = append(filters, resource.name)
}

if len(args) > 1 {
filters = append(filters, args[1:]...)
}

filters = prepareClusterMemberServerFilters(filters, api.ClusterMember{})

// Check if clustered
cluster, _, err := resource.server.GetCluster()
if err != nil {
Expand All @@ -282,7 +295,7 @@ func (c *cmdClusterList) Run(cmd *cobra.Command, args []string) error {
}

// Get the cluster members
members, err := resource.server.GetClusterMembers()
members, err := resource.server.GetClusterMembersWithFilter(filters)
if err != nil {
return err
}
Expand Down Expand Up @@ -1601,3 +1614,36 @@ func (c *cmdClusterEvacuateAction) Run(cmd *cobra.Command, args []string) error
progress.Done("")
return nil
}

// prepareClusterMemberServerFilters processes and formats filter criteria
// for cluster members, ensuring they are in a format that the server can interpret.
func prepareClusterMemberServerFilters(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("server_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
}
52 changes: 49 additions & 3 deletions cmd/incus/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"maps"
"net/url"
"os"
"reflect"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -681,7 +682,7 @@ type cmdStorageList struct {
// Command returns a cobra.Command for use with (*cobra.Command).AddCommand.
func (c *cmdStorageList) Command() *cobra.Command {
cmd := &cobra.Command{}
cmd.Use = usage("list", i18n.G("[<remote>:]"))
cmd.Use = usage("list", i18n.G("[<remote>:] [<filter>...]"))
cmd.Aliases = []string{"ls"}
cmd.Short = i18n.G("List available storage pools")
cmd.Long = cli.FormatSection(i18n.G("Description"), i18n.G(
Expand Down Expand Up @@ -788,7 +789,7 @@ func (c *cmdStorageList) stateColumnData(storage api.StoragePool) string {
// Run runs the actual command logic.
func (c *cmdStorageList) Run(cmd *cobra.Command, args []string) error {
// Quick checks.
exit, err := c.global.checkArgs(cmd, args, 0, 1)
exit, err := c.global.checkArgs(cmd, args, 0, -1)
if exit {
return err
}
Expand All @@ -806,8 +807,20 @@ func (c *cmdStorageList) Run(cmd *cobra.Command, args []string) error {

resource := resources[0]

// Process the filters
filters := []string{}
if resource.name != "" {
filters = append(filters, resource.name)
}

if len(args) > 1 {
filters = append(filters, args[1:]...)
}

filters = prepareStoragePoolsServerFilters(filters, api.StoragePool{})

// Get the storage pools
pools, err := resource.server.GetStoragePools()
pools, err := resource.server.GetStoragePoolsWithFilter(filters)
if err != nil {
return err
}
Expand Down Expand Up @@ -1086,3 +1099,36 @@ func (c *cmdStorageUnset) Run(cmd *cobra.Command, args []string) error {
args = append(args, "")
return c.storageSet.Run(cmd, args)
}

// 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
}
64 changes: 50 additions & 14 deletions cmd/incusd/api_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/gorilla/mux"

incus "github.com/lxc/incus/v6/client"
"github.com/lxc/incus/v6/internal/filter"
internalInstance "github.com/lxc/incus/v6/internal/instance"
"github.com/lxc/incus/v6/internal/server/auth"
"github.com/lxc/incus/v6/internal/server/certificate"
Expand Down Expand Up @@ -1106,6 +1107,12 @@ func clusterAcceptMember(client incus.InstanceServer, name string, address strin
// ---
// produces:
// - application/json
// parameters:
// - in: query
// name: filter
// description: Collection filter
// type: string
// example: default
// responses:
// "200":
// description: API endpoints
Expand Down Expand Up @@ -1149,6 +1156,12 @@ func clusterAcceptMember(client incus.InstanceServer, name string, address strin
// ---
// produces:
// - application/json
// parameters:
// - in: query
// name: filter
// description: Collection filter
// type: string
// example: default
// responses:
// "200":
// description: API endpoints
Expand Down Expand Up @@ -1181,6 +1194,13 @@ func clusterNodesGet(d *Daemon, r *http.Request) response.Response {
recursion := localUtil.IsRecursionRequest(r)
s := d.State()

// Parse filter value.
filterStr := r.FormValue("filter")
clauses, err := filter.Parse(filterStr, filter.QueryOperatorSet())
if err != nil {
return response.BadRequest(fmt.Errorf("Invalid filter: %w", err))
}

leaderAddress, err := s.Cluster.LeaderAddress()
if err != nil {
return response.InternalError(err)
Expand All @@ -1199,8 +1219,7 @@ func clusterNodesGet(d *Daemon, r *http.Request) response.Response {
return response.SmartError(err)
}

var members []db.NodeInfo
var membersInfo []api.ClusterMember
var members []api.ClusterMember
err = s.DB.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
failureDomains, err := tx.GetFailureDomainsNames(ctx)
if err != nil {
Expand All @@ -1212,7 +1231,7 @@ func clusterNodesGet(d *Daemon, r *http.Request) response.Response {
return fmt.Errorf("Failed loading member failure domains: %w", err)
}

members, err = tx.GetNodes(ctx)
nodes, err := tx.GetNodes(ctx)
if err != nil {
return fmt.Errorf("Failed getting cluster members: %w", err)
}
Expand All @@ -1231,16 +1250,14 @@ func clusterNodesGet(d *Daemon, r *http.Request) response.Response {
RaftNodes: raftNodes,
}

if recursion {
membersInfo = make([]api.ClusterMember, 0, len(members))
for i := range members {
member, err := members[i].ToAPI(ctx, tx, args)
if err != nil {
return err
}

membersInfo = append(membersInfo, *member)
members = make([]api.ClusterMember, 0, len(nodes))
for i := range nodes {
member, err := nodes[i].ToAPI(ctx, tx, args)
if err != nil {
return err
}

members = append(members, *member)
}

return nil
Expand All @@ -1249,13 +1266,32 @@ func clusterNodesGet(d *Daemon, r *http.Request) response.Response {
return response.SmartError(err)
}

// Apply filters.
filtered := make([]api.ClusterMember, 0)
for _, member := range members {
if clauses != nil && len(clauses.Clauses) > 0 {
match, err := filter.Match(member, *clauses)
if err != nil {
return response.SmartError(err)
}

if !match {
continue
}
}

filtered = append(filtered, member)
}

// Return full responses.
if recursion {
return response.SyncResponse(true, membersInfo)
return response.SyncResponse(true, filtered)
}

// Return URLs only.
urls := make([]string, 0, len(members))
for _, member := range members {
u := api.NewURL().Path(version.APIVersion, "cluster", "members", member.Name)
u := api.NewURL().Path(version.APIVersion, "cluster", "members", member.ServerName)
urls = append(urls, u.String())
}

Expand Down
Loading