[lxc-devel] [lxd/master] Supporting filtering GET requests for instances and images

freeekanayaka on Github lxc-bot at linuxcontainers.org
Wed Jan 29 13:37:31 UTC 2020


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 301 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20200129/e9a4a442/attachment.bin>
-------------- next part --------------
From 27d49a01758945e477f4b379d9469ef3a381e7fc Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Wed, 29 Jan 2020 13:13:45 +0000
Subject: [PATCH 1/5] api: Add api_filtering extension

Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
 doc/api-extensions.md | 3 +++
 shared/version/api.go | 1 +
 2 files changed, 4 insertions(+)

diff --git a/doc/api-extensions.md b/doc/api-extensions.md
index a5dad365ea..08916acc3b 100644
--- a/doc/api-extensions.md
+++ b/doc/api-extensions.md
@@ -902,3 +902,6 @@ This adds the ability to use LVM stripes on normal volumes and thin pool volumes
 
 ## vm\_boot\_priority
 Adds a `boot.priority` property on nic and disk devices to control the boot order.
+
+## api\_filtering
+Adds support for filtering the result of a GET request for instances and images.
diff --git a/shared/version/api.go b/shared/version/api.go
index b06864911b..2dc3ee9dab 100644
--- a/shared/version/api.go
+++ b/shared/version/api.go
@@ -184,6 +184,7 @@ var APIExtensions = []string{
 	"resources_disk_id",
 	"storage_lvm_stripes",
 	"vm_boot_priority",
+	"api_filtering",
 }
 
 // APIExtensionsCount returns the number of available API extensions.

From 8033d75c664277c8881ffd635e9c6d215b971abe Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Wed, 29 Jan 2020 12:25:15 +0000
Subject: [PATCH 2/5] lxd/filter: Add API filtering package

Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
 lxd/filter/clause.go      | 89 +++++++++++++++++++++++++++++++++++++++
 lxd/filter/clause_test.go | 47 +++++++++++++++++++++
 lxd/filter/doc.go         |  3 ++
 lxd/filter/match.go       | 31 ++++++++++++++
 lxd/filter/match_test.go  | 53 +++++++++++++++++++++++
 lxd/filter/value.go       | 59 ++++++++++++++++++++++++++
 lxd/filter/value_test.go  | 52 +++++++++++++++++++++++
 7 files changed, 334 insertions(+)
 create mode 100644 lxd/filter/clause.go
 create mode 100644 lxd/filter/clause_test.go
 create mode 100644 lxd/filter/doc.go
 create mode 100644 lxd/filter/match.go
 create mode 100644 lxd/filter/match_test.go
 create mode 100644 lxd/filter/value.go
 create mode 100644 lxd/filter/value_test.go

diff --git a/lxd/filter/clause.go b/lxd/filter/clause.go
new file mode 100644
index 0000000000..4b6d0a836b
--- /dev/null
+++ b/lxd/filter/clause.go
@@ -0,0 +1,89 @@
+package filter
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/lxc/lxd/shared"
+)
+
+// Clause is a single filter clause in a filter string.
+type Clause struct {
+	PrevLogical string
+	Not         bool
+	Field       string
+	Operator    string
+	Value       string
+}
+
+// Parse a user-provided filter string.
+func Parse(s string) ([]Clause, error) {
+	clauses := []Clause{}
+
+	parts := strings.Fields(s)
+
+	index := 0
+	prevLogical := "and"
+
+	for index < len(parts) {
+		clause := Clause{}
+
+		if strings.EqualFold(parts[index], "not") {
+			clause.Not = true
+			index++
+			if index == len(parts) {
+				return nil, fmt.Errorf("incomplete not clause")
+			}
+		} else {
+			clause.Not = false
+		}
+
+		clause.Field = parts[index]
+
+		index++
+		if index == len(parts) {
+			return nil, fmt.Errorf("clause has no operator")
+		}
+		clause.Operator = parts[index]
+
+		index++
+		if index == len(parts) {
+			return nil, fmt.Errorf("clause has no value")
+		}
+		value := parts[index]
+
+		// support strings with spaces that are quoted
+		if strings.HasPrefix(value, "\"") {
+			value = value[1:]
+			for {
+				index++
+				if index == len(parts) {
+					return nil, fmt.Errorf("unterminated quote")
+				}
+				if strings.HasSuffix(parts[index], "\"") {
+					break
+				}
+				value += " " + parts[index]
+			}
+			end := parts[index]
+			value += " " + end[0:len(end)-1]
+		}
+		clause.Value = value
+		index++
+
+		clause.PrevLogical = prevLogical
+		if index < len(parts) {
+			prevLogical = parts[index]
+			if !shared.StringInSlice(prevLogical, []string{"and", "or"}) {
+				return nil, fmt.Errorf("invalid clause composition")
+			}
+			index++
+			if index == len(parts) {
+				return nil, fmt.Errorf("unterminated compound clause")
+			}
+		}
+		clauses = append(clauses, clause)
+	}
+
+	return clauses, nil
+}
diff --git a/lxd/filter/clause_test.go b/lxd/filter/clause_test.go
new file mode 100644
index 0000000000..0cfa73f5f9
--- /dev/null
+++ b/lxd/filter/clause_test.go
@@ -0,0 +1,47 @@
+package filter_test
+
+import (
+	"testing"
+
+	"github.com/lxc/lxd/lxd/filter"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParse_Error(t *testing.T) {
+	cases := map[string]string{
+		"not":                    "incomplete not clause",
+		"foo":                    "clause has no operator",
+		"not foo":                "clause has no operator",
+		"foo eq":                 "clause has no value",
+		"foo eq \"bar":           "unterminated quote",
+		"foo eq bar and":         "unterminated compound clause",
+		"foo eq \"bar egg\" and": "unterminated compound clause",
+		"foo eq bar xxx":         "invalid clause composition",
+	}
+	for s, message := range cases {
+		t.Run(s, func(t *testing.T) {
+			clauses, err := filter.Parse(s)
+			assert.Nil(t, clauses)
+			assert.EqualError(t, err, message)
+		})
+	}
+}
+
+func TestParse(t *testing.T) {
+	clauses, err := filter.Parse("foo eq \"bar egg\" or not baz eq yuk")
+	require.NoError(t, err)
+	assert.Len(t, clauses, 2)
+	clause1 := clauses[0]
+	clause2 := clauses[1]
+	assert.False(t, clause1.Not)
+	assert.Equal(t, "and", clause1.PrevLogical)
+	assert.Equal(t, "foo", clause1.Field)
+	assert.Equal(t, "eq", clause1.Operator)
+	assert.Equal(t, "bar egg", clause1.Value)
+	assert.True(t, clause2.Not)
+	assert.Equal(t, "baz", clause2.Field)
+	assert.Equal(t, "or", clause2.PrevLogical)
+	assert.Equal(t, "eq", clause2.Operator)
+	assert.Equal(t, "yuk", clause2.Value)
+}
diff --git a/lxd/filter/doc.go b/lxd/filter/doc.go
new file mode 100644
index 0000000000..9f13a18aed
--- /dev/null
+++ b/lxd/filter/doc.go
@@ -0,0 +1,3 @@
+// API filtering.
+
+package filter
diff --git a/lxd/filter/match.go b/lxd/filter/match.go
new file mode 100644
index 0000000000..99525f284a
--- /dev/null
+++ b/lxd/filter/match.go
@@ -0,0 +1,31 @@
+package filter
+
+// Match returns true if the given object matches the given filter.
+func Match(obj interface{}, clauses []Clause) bool {
+	match := true
+
+	for _, clause := range clauses {
+		value := ValueOf(obj, clause.Field)
+		clauseMatch := value == clause.Value
+
+		if clause.Operator == "ne" {
+			clauseMatch = !clauseMatch
+		}
+
+		// Finish out logic
+		if clause.Not {
+			clauseMatch = !clauseMatch
+		}
+
+		switch clause.PrevLogical {
+		case "and":
+			match = match && clauseMatch
+		case "or":
+			match = match || clauseMatch
+		default:
+			panic("unexpected clause operator")
+		}
+	}
+
+	return match
+}
diff --git a/lxd/filter/match_test.go b/lxd/filter/match_test.go
new file mode 100644
index 0000000000..7c5c89f848
--- /dev/null
+++ b/lxd/filter/match_test.go
@@ -0,0 +1,53 @@
+package filter_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/lxc/lxd/lxd/filter"
+	"github.com/lxc/lxd/shared/api"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestMatch_Instance(t *testing.T) {
+	instance := api.Instance{
+		InstancePut: api.InstancePut{
+			Architecture: "x86_64",
+			Config: map[string]string{
+				"image.os": "Busybox",
+			},
+			Stateful: false,
+		},
+		CreatedAt: time.Date(2020, 1, 29, 11, 10, 32, 0, time.UTC),
+		Name:      "c1",
+		ExpandedConfig: map[string]string{
+			"image.os": "Busybox",
+		},
+		ExpandedDevices: map[string]map[string]string{
+			"root": {
+				"path": "/",
+				"pool": "default",
+				"type": "disk",
+			},
+		},
+		Status: "Running",
+	}
+	cases := map[string]interface{}{
+		"architecture eq x86_64":                                         true,
+		"architecture eq i686":                                           false,
+		"name eq c1 and status eq Running":                               true,
+		"config.image.os eq Busybox and expanded_devices.root.path eq /": true,
+		"name eq c2 or status eq Running":                                true,
+		"name eq c2 or name eq c3":                                       false,
+	}
+	for s := range cases {
+		t.Run(s, func(t *testing.T) {
+			f, err := filter.Parse(s)
+			require.NoError(t, err)
+			match := filter.Match(instance, f)
+			assert.Equal(t, cases[s], match)
+		})
+	}
+
+}
diff --git a/lxd/filter/value.go b/lxd/filter/value.go
new file mode 100644
index 0000000000..70b8ecaebb
--- /dev/null
+++ b/lxd/filter/value.go
@@ -0,0 +1,59 @@
+package filter
+
+import (
+	"reflect"
+	"strings"
+)
+
+// ValueOf returns the value of the given field.
+func ValueOf(obj interface{}, field string) interface{} {
+	value := reflect.ValueOf(obj)
+	typ := value.Type()
+	parts := strings.Split(field, ".")
+
+	key := parts[0]
+	rest := strings.Join(parts[1:], ".")
+
+	var parent interface{}
+
+	if value.Kind() == reflect.Map {
+		switch reflect.TypeOf(obj).Elem().Kind() {
+		case reflect.String:
+			m := value.Interface().(map[string]string)
+			return m[field]
+		case reflect.Map:
+			for _, entry := range value.MapKeys() {
+				if entry.Interface() != key {
+					continue
+				}
+				m := value.MapIndex(entry)
+				return ValueOf(m.Interface(), rest)
+			}
+		}
+		return nil
+	}
+
+	for i := 0; i < value.NumField(); i++ {
+		fieldValue := value.Field(i)
+		fieldType := typ.Field(i)
+		yaml := fieldType.Tag.Get("yaml")
+
+		if yaml == ",inline" {
+			parent = fieldValue.Interface()
+		}
+
+		if yaml == key {
+			v := fieldValue.Interface()
+			if len(parts) == 1 {
+				return v
+			}
+			return ValueOf(v, rest)
+		}
+	}
+
+	if parent != nil {
+		return ValueOf(parent, field)
+	}
+
+	return nil
+}
diff --git a/lxd/filter/value_test.go b/lxd/filter/value_test.go
new file mode 100644
index 0000000000..5682f65195
--- /dev/null
+++ b/lxd/filter/value_test.go
@@ -0,0 +1,52 @@
+package filter_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/lxc/lxd/lxd/filter"
+	"github.com/lxc/lxd/shared/api"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValueOf_Instance(t *testing.T) {
+	instance := api.Instance{
+		InstancePut: api.InstancePut{
+			Architecture: "x86_64",
+			Config: map[string]string{
+				"image.os": "Busybox",
+			},
+			Stateful: false,
+		},
+		CreatedAt: time.Date(2020, 1, 29, 11, 10, 32, 0, time.UTC),
+		Name:      "c1",
+		ExpandedConfig: map[string]string{
+			"image.os": "Busybox",
+		},
+		ExpandedDevices: map[string]map[string]string{
+			"root": {
+				"path": "/",
+				"pool": "default",
+				"type": "disk",
+			},
+		},
+		Status: "Running",
+	}
+	cases := map[string]interface{}{
+		"architecture":               "x86_64",
+		"created_at":                 time.Date(2020, 1, 29, 11, 10, 32, 0, time.UTC),
+		"config.image.os":            "Busybox",
+		"name":                       "c1",
+		"expanded_config.image.os":   "Busybox",
+		"expanded_devices.root.pool": "default",
+		"status":                     "Running",
+		"stateful":                   false,
+	}
+	for field := range cases {
+		t.Run(field, func(t *testing.T) {
+			value := filter.ValueOf(instance, field)
+			assert.Equal(t, cases[field], value)
+		})
+	}
+
+}

From 1724463ec294ea350b2186f23b79e6c4e7bc73bf Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Wed, 29 Jan 2020 13:11:43 +0000
Subject: [PATCH 3/5] lxd/instance: Add instance list filtering functions

Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
 lxd/instance/filter.go | 30 ++++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)
 create mode 100644 lxd/instance/filter.go

diff --git a/lxd/instance/filter.go b/lxd/instance/filter.go
new file mode 100644
index 0000000000..495f6717b5
--- /dev/null
+++ b/lxd/instance/filter.go
@@ -0,0 +1,30 @@
+package instance
+
+import (
+	"github.com/lxc/lxd/lxd/filter"
+	"github.com/lxc/lxd/shared/api"
+)
+
+// Filter returns a filtered list of instances that match the given clauses.
+func Filter(instances []*api.Instance, clauses []filter.Clause) []*api.Instance {
+	filtered := []*api.Instance{}
+	for _, instance := range instances {
+		if !filter.Match(*instance, clauses) {
+			continue
+		}
+		filtered = append(filtered, instance)
+	}
+	return filtered
+}
+
+// FilterFull returns a filtered list of full instances that match the given clauses.
+func FilterFull(instances []*api.InstanceFull, clauses []filter.Clause) []*api.InstanceFull {
+	filtered := []*api.InstanceFull{}
+	for _, instance := range instances {
+		if !filter.Match(*instance, clauses) {
+			continue
+		}
+		filtered = append(filtered, instance)
+	}
+	return filtered
+}

From e3ef0353c87cde52bfa4112e1ddc31941772fe0c Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Wed, 29 Jan 2020 13:12:58 +0000
Subject: [PATCH 4/5] lxd: Make use of filtering for instances and images

Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
 lxd/containers_get.go | 44 +++++++++++++++++++++++++++++++++++--------
 lxd/images.go         | 21 +++++++++++++++------
 2 files changed, 51 insertions(+), 14 deletions(-)

diff --git a/lxd/containers_get.go b/lxd/containers_get.go
index 5fe0015cf3..2c7635d093 100644
--- a/lxd/containers_get.go
+++ b/lxd/containers_get.go
@@ -15,6 +15,7 @@ import (
 	"github.com/lxc/lxd/lxd/cluster"
 	"github.com/lxc/lxd/lxd/db"
 	"github.com/lxc/lxd/lxd/db/query"
+	"github.com/lxc/lxd/lxd/filter"
 	"github.com/lxc/lxd/lxd/instance"
 	"github.com/lxc/lxd/lxd/instance/instancetype"
 	"github.com/lxc/lxd/lxd/response"
@@ -82,6 +83,16 @@ func doContainersGet(d *Daemon, r *http.Request) (interface{}, error) {
 		recursion = 0
 	}
 
+	// Parse filter value
+	filterStr := r.FormValue("filter")
+	var clauses []filter.Clause
+	if filterStr != "" {
+		clauses, err = filter.Parse(filterStr)
+		if err != nil {
+			return nil, errors.Wrap(err, "Invalid filter")
+		}
+	}
+
 	// Parse the project field
 	project := projectParam(r)
 
@@ -109,7 +120,8 @@ func doContainersGet(d *Daemon, r *http.Request) (interface{}, error) {
 
 	// Get the local instances
 	nodeCts := map[string]instance.Instance{}
-	if recursion > 0 {
+	mustLoadObjects := recursion > 0 || (recursion == 0 && clauses != nil)
+	if mustLoadObjects {
 		cts, err := instanceLoadNodeProjectAll(d.State(), project, instanceType)
 		if err != nil {
 			return nil, err
@@ -160,9 +172,9 @@ func doContainersGet(d *Daemon, r *http.Request) (interface{}, error) {
 		}
 
 		// Mark containers on unavailable nodes as down
-		if recursion > 0 && address == "0.0.0.0" {
+		if mustLoadObjects && address == "0.0.0.0" {
 			for _, container := range containers {
-				if recursion == 1 {
+				if recursion < 2 {
 					resultListAppend(container, api.Instance{}, fmt.Errorf("unavailable"))
 				} else {
 					resultFullListAppend(container, api.InstanceFull{}, fmt.Errorf("unavailable"))
@@ -174,7 +186,7 @@ func doContainersGet(d *Daemon, r *http.Request) (interface{}, error) {
 
 		// For recursion requests we need to fetch the state of remote
 		// containers from their respective nodes.
-		if recursion > 0 && address != "" && !isClusterNotification(r) {
+		if mustLoadObjects && address != "" && !isClusterNotification(r) {
 			wg.Add(1)
 			go func(address string, containers []string) {
 				defer wg.Done()
@@ -213,8 +225,7 @@ func doContainersGet(d *Daemon, r *http.Request) (interface{}, error) {
 
 			continue
 		}
-
-		if recursion == 0 {
+		if !mustLoadObjects {
 			for _, container := range containers {
 				instancePath := "instances"
 				if strings.HasPrefix(mux.CurrentRoute(r).GetName(), "container") {
@@ -243,7 +254,7 @@ func doContainersGet(d *Daemon, r *http.Request) (interface{}, error) {
 							break
 						}
 
-						if recursion == 1 {
+						if recursion < 2 {
 							c, _, err := nodeCts[container].Render()
 							if err != nil {
 								resultListAppend(container, api.Instance{}, err)
@@ -276,6 +287,18 @@ func doContainersGet(d *Daemon, r *http.Request) (interface{}, error) {
 	wg.Wait()
 
 	if recursion == 0 {
+		if clauses != nil {
+			for _, container := range instance.Filter(resultList, clauses) {
+				instancePath := "instances"
+				if strings.HasPrefix(mux.CurrentRoute(r).GetName(), "container") {
+					instancePath = "containers"
+				} else if strings.HasPrefix(mux.CurrentRoute(r).GetName(), "vm") {
+					instancePath = "virtual-machines"
+				}
+				url := fmt.Sprintf("/%s/%s/%s", version.APIVersion, instancePath, container.Name)
+				resultString = append(resultString, url)
+			}
+		}
 		return resultString, nil
 	}
 
@@ -284,7 +307,9 @@ func doContainersGet(d *Daemon, r *http.Request) (interface{}, error) {
 		sort.Slice(resultList, func(i, j int) bool {
 			return resultList[i].Name < resultList[j].Name
 		})
-
+		if clauses != nil {
+			resultList = instance.Filter(resultList, clauses)
+		}
 		return resultList, nil
 	}
 
@@ -293,6 +318,9 @@ func doContainersGet(d *Daemon, r *http.Request) (interface{}, error) {
 		return resultFullList[i].Name < resultFullList[j].Name
 	})
 
+	if clauses != nil {
+		resultFullList = instance.FilterFull(resultFullList, clauses)
+	}
 	return resultFullList, nil
 }
 
diff --git a/lxd/images.go b/lxd/images.go
index 277e3e00f6..78f1ea4ca0 100644
--- a/lxd/images.go
+++ b/lxd/images.go
@@ -916,12 +916,16 @@ func getImageMetadata(fname string) (*api.ImageMetadata, string, error) {
 	return &result, imageType, nil
 }
 
-func doImagesGet(d *Daemon, recursion bool, project string, public bool) (interface{}, error) {
+func doImagesGet(d *Daemon, recursion bool, project string, public bool, filterStr string) (interface{}, error) {
 	results, err := d.cluster.ImagesGet(project, public)
 	if err != nil {
 		return []string{}, err
 	}
 
+	if filterStr != "" {
+		recursion = true
+	}
+
 	resultString := make([]string, len(results))
 	resultMap := make([]*api.Image, len(results))
 	i := 0
@@ -930,7 +934,7 @@ func doImagesGet(d *Daemon, recursion bool, project string, public bool) (interf
 			url := fmt.Sprintf("/%s/images/%s", version.APIVersion, name)
 			resultString[i] = url
 		} else {
-			image, response := doImageGet(d.cluster, project, name, public)
+			image, response := doImageGet(d.cluster, project, name, public, filterStr)
 			if response != nil {
 				continue
 			}
@@ -943,15 +947,18 @@ func doImagesGet(d *Daemon, recursion bool, project string, public bool) (interf
 	if !recursion {
 		return resultString, nil
 	}
-
+	if filterStr != "" {
+		panic("TODO")
+	}
 	return resultMap, nil
 }
 
 func imagesGet(d *Daemon, r *http.Request) response.Response {
 	project := projectParam(r)
+	filter := r.FormValue("filter")
 	public := d.checkTrustedClient(r) != nil || AllowProjectPermission("images", "view")(d, r) != response.EmptySyncResponse
 
-	result, err := doImagesGet(d, util.IsRecursionRequest(r), project, public)
+	result, err := doImagesGet(d, util.IsRecursionRequest(r), project, public, filter)
 	if err != nil {
 		return response.SmartError(err)
 	}
@@ -1516,8 +1523,9 @@ func imageDeleteFromDisk(fingerprint string) {
 	}
 }
 
-func doImageGet(db *db.Cluster, project, fingerprint string, public bool) (*api.Image, response.Response) {
+func doImageGet(db *db.Cluster, project, fingerprint string, public bool, filter string) (*api.Image, response.Response) {
 	_, imgInfo, err := db.ImageGet(project, fingerprint, public, false)
+
 	if err != nil {
 		return nil, response.SmartError(err)
 	}
@@ -1560,8 +1568,9 @@ func imageGet(d *Daemon, r *http.Request) response.Response {
 	fingerprint := mux.Vars(r)["fingerprint"]
 	public := d.checkTrustedClient(r) != nil || AllowProjectPermission("images", "view")(d, r) != response.EmptySyncResponse
 	secret := r.FormValue("secret")
+	filter := r.FormValue("filter")
 
-	info, resp := doImageGet(d.cluster, project, fingerprint, false)
+	info, resp := doImageGet(d.cluster, project, fingerprint, false, filter)
 	if resp != nil {
 		return resp
 	}

From 81d8b65afa11f81b676eb55a5d1c7d312e4ffa85 Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Wed, 29 Jan 2020 13:13:54 +0000
Subject: [PATCH 5/5] doc/rest-api: Document filtering

Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
 doc/rest-api.md | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/doc/rest-api.md b/doc/rest-api.md
index 81012c0b3b..716274c630 100644
--- a/doc/rest-api.md
+++ b/doc/rest-api.md
@@ -157,6 +157,37 @@ they point to (typically a dict).
 Recursion is implemented by simply replacing any pointer to an job (URL)
 by the object itself.
 
+## Filtering
+To filter your results on certain values, filter is implemented for collections.
+A `filter` argument can be passed to a GET query against a collection.
+
+Filtering is available for the instance and image endpoints.
+
+There is no default value for filter which means that all results found will
+be returned. The following is the language used for the filter argument:
+
+?filter=field_name eq desired_field_assignment
+
+The language follows the OData conventions for structuring REST API filtering
+logic. Logical operators are also supported for filtering: not(not), equals(eq),
+not equals(ne), and(and), or(or). Filters are evaluated with left associativity.
+Values with spaces can be surrounded with quotes. Nesting filtering is also supported. 
+For instance, to filter on a field in a config you would pass:
+
+?filter=config.field_name eq desired_field_assignment
+
+For filtering on device attributes you would pass:
+
+?filter=devices.device_name.field_name eq desired_field_assignment
+
+Here are a few GET query examples of the different filtering methods mentioned above:
+
+containers?filter=name eq "my container" and status eq Running
+
+containers?filter=config.image.os eq ubuntu or devices.eth0.nictype eq bridged
+
+images?filter=Properties.os eq Centos and not UpdateSource.Protocol eq simplestreams
+
 ## Async operations
 Any operation which may take more than a second to be done must be done
 in the background, returning a background operation ID to the client.


More information about the lxc-devel mailing list