Skip to content

Commit addbf3d

Browse files
authored
Query parameters infra (#67)
1) Enhanced REST server to parse URL query parameters and pass them to translib APIs. Handles RESTCONF "depth" and a custom "deleteEmptyEntry" query parameters. The deleteEmptyEntry parameter is valid only for DELETE requests; takes a boolean value - "true" or "false". It will be passed to translib via SetRequest.DeleteEmptyEntry property. App modules and transofrmer changes to handle these parameters is pending. 2) Modified cli_client get() and head() python APIs to accept optional depth parameter, which should be a +ve integer. By default no depth value is passed to the server. Usage: cl.get(path, depth=2) cl.head(path, depth=1) 3) Modified cli_client.delete python API to accept an optional deleteEmptyEntry parameter. Default value is False. When specified as True, the DELETE request will include "deleteEmptyEntry=true" query parameter. Usage: cl.delete(path, deleteEmptyEntry=True)
1 parent f3b3f6f commit addbf3d

File tree

4 files changed

+276
-16
lines changed

4 files changed

+276
-16
lines changed

CLI/actioner/cli_client.py

+20-10
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class ApiClient(object):
3838
__session = requests.Session()
3939

4040

41-
def request(self, method, path, data=None, headers={}, query=None):
41+
def request(self, method, path, data=None, headers={}, query=None, response_type=None):
4242

4343
url = '{0}{1}'.format(ApiClient.__api_root, path)
4444

@@ -60,20 +60,24 @@ def request(self, method, path, data=None, headers={}, query=None):
6060
params=query,
6161
verify=False)
6262

63-
return Response(r)
63+
return Response(r, response_type)
6464

6565
except requests.RequestException as e:
6666
log_warning('cli_client request exception: ' + str(e))
6767
#TODO have more specific error message based
6868
msg = '%Error: Could not connect to Management REST Server'
6969
return ApiClient.__new_error_response(msg)
7070

71-
def post(self, path, data):
72-
return self.request("POST", path, data)
71+
def post(self, path, data={}, response_type=None):
72+
return self.request("POST", path, data, response_type=response_type)
7373

74-
def get(self, path, depth=None):
74+
def get(self, path, depth=None, ignore404=True, response_type=None):
7575
q = self.prepare_query(depth=depth)
76-
return self.request("GET", path, query=q)
76+
resp = self.request("GET", path, query=q, response_type=response_type)
77+
if ignore404 and resp.status_code == 404:
78+
resp.status_code = 200
79+
resp.content = None
80+
return resp
7781

7882
def head(self, path, depth=None):
7983
q = self.prepare_query(depth=depth)
@@ -85,18 +89,21 @@ def put(self, path, data):
8589
def patch(self, path, data):
8690
return self.request("PATCH", path, data)
8791

88-
def delete(self, path, ignore404=True):
89-
resp = self.request("DELETE", path, data=None)
92+
def delete(self, path, ignore404=True, deleteEmptyEntry=False):
93+
q = self.prepare_query(deleteEmptyEntry=deleteEmptyEntry)
94+
resp = self.request("DELETE", path, data=None, query=q)
9095
if ignore404 and resp.status_code == 404:
9196
resp.status_code = 204
9297
resp.content = None
9398
return resp
9499

95100
@staticmethod
96-
def prepare_query(depth=None):
101+
def prepare_query(depth=None, deleteEmptyEntry=None):
97102
query = {}
98103
if depth != None and depth != "unbounded":
99104
query["depth"] = depth
105+
if deleteEmptyEntry is True:
106+
query["deleteEmptyEntry"] = "true"
100107
return query
101108

102109
@staticmethod
@@ -123,14 +130,17 @@ def __str__(self):
123130

124131

125132
class Response(object):
126-
def __init__(self, response):
133+
def __init__(self, response, response_type=None):
127134
self.response = response
135+
self.response_type = response_type
128136
self.status_code = response.status_code
129137
self.content = response.content
130138

131139
try:
132140
if response.content is None or len(response.content) == 0:
133141
self.content = None
142+
elif self.response_type and self.response_type.lower() == 'string':
143+
self.content = str(response.content).decode('string_escape')
134144
elif has_json_content(response):
135145
self.content = json.loads(response.content, object_pairs_hook=OrderedDict)
136146
except ValueError:

rest/server/handler.go

+24-6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ func Process(w http.ResponseWriter, r *http.Request) {
5353
if err == nil {
5454
err = args.parseClientVersion(r, rc)
5555
}
56+
if err == nil {
57+
err = args.parseQueryParams(r)
58+
}
5659

5760
if err != nil {
5861
status, data, rtype = prepareErrorResponse(err, r)
@@ -102,6 +105,17 @@ write_resp:
102105
}
103106
}
104107

108+
// getRequestID returns the request ID for a http Request r.
109+
// ID is looked up from the RequestContext associated with this request.
110+
// Returns empty value if context is not initialized yet.
111+
func getRequestID(r *http.Request) string {
112+
cv := getContextValue(r, requestContextKey)
113+
if cv != nil {
114+
return cv.(*RequestContext).ID
115+
}
116+
return ""
117+
}
118+
105119
// getRequestBody returns the validated request body
106120
func getRequestBody(r *http.Request, rc *RequestContext) (*MediaType, []byte, error) {
107121
if r.ContentLength == 0 {
@@ -241,10 +255,12 @@ func isOperationsRequest(r *http.Request) bool {
241255

242256
// translibArgs holds arguments for invoking translib APIs.
243257
type translibArgs struct {
244-
method string // API name
245-
path string // Translib path
246-
data []byte // payload
247-
version translib.Version // client version
258+
method string // API name
259+
path string // Translib path
260+
data []byte // payload
261+
version translib.Version // client version
262+
depth uint // RESTCONF depth, for Get API only
263+
deleteEmpty bool // Delete empty entry during field delete
248264
}
249265

250266
// parseMethod maps http method name to translib method.
@@ -288,6 +304,7 @@ func invokeTranslib(args *translibArgs, rc *RequestContext) (int, []byte, error)
288304
case "GET", "HEAD":
289305
req := translib.GetRequest{
290306
Path: args.path,
307+
Depth: args.depth,
291308
ClientVersion: args.version,
292309
}
293310
resp, err1 := translib.Get(req)
@@ -330,8 +347,9 @@ func invokeTranslib(args *translibArgs, rc *RequestContext) (int, []byte, error)
330347
case "DELETE":
331348
status = 204
332349
req := translib.SetRequest{
333-
Path: args.path,
334-
ClientVersion: args.version,
350+
Path: args.path,
351+
ClientVersion: args.version,
352+
DeleteEmptyEntry: args.deleteEmpty,
335353
}
336354
_, err = translib.Delete(req)
337355

rest/server/query.go

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
////////////////////////////////////////////////////////////////////////////////
2+
// //
3+
// Copyright 2020 Broadcom. The term Broadcom refers to Broadcom Inc. and/or //
4+
// its subsidiaries. //
5+
// //
6+
// Licensed under the Apache License, Version 2.0 (the "License"); //
7+
// you may not use this file except in compliance with the License. //
8+
// You may obtain a copy of the License at //
9+
// //
10+
// http://www.apache.org/licenses/LICENSE-2.0 //
11+
// //
12+
// Unless required by applicable law or agreed to in writing, software //
13+
// distributed under the License is distributed on an "AS IS" BASIS, //
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
15+
// See the License for the specific language governing permissions and //
16+
// limitations under the License. //
17+
// //
18+
////////////////////////////////////////////////////////////////////////////////
19+
20+
package server
21+
22+
import (
23+
"net/http"
24+
"strconv"
25+
"strings"
26+
27+
"github.com/golang/glog"
28+
)
29+
30+
// parseQueryParams parses the http request's query parameters
31+
// into a translibArgs args.
32+
func (args *translibArgs) parseQueryParams(r *http.Request) error {
33+
if strings.Contains(r.URL.Path, restconfDataPathPrefix) {
34+
return args.parseRestconfQueryParams(r)
35+
}
36+
37+
return nil
38+
}
39+
40+
// parseRestconfQueryParams parses query parameters of a request 'r' to
41+
// fill translibArgs object 'args'. Returns httpError with status 400
42+
// if any parameter is unsupported or has invalid value.
43+
func (args *translibArgs) parseRestconfQueryParams(r *http.Request) error {
44+
var err error
45+
qParams := r.URL.Query()
46+
47+
for name, vals := range qParams {
48+
switch name {
49+
case "depth":
50+
args.depth, err = parseDepthParam(vals, r)
51+
case "deleteEmptyEntry":
52+
args.deleteEmpty, err = parseDeleteEmptyEntryParam(vals, r)
53+
default:
54+
err = newUnsupportedParamError(name, r)
55+
}
56+
if err != nil {
57+
return err
58+
}
59+
}
60+
61+
return nil
62+
}
63+
64+
func newUnsupportedParamError(name string, r *http.Request) error {
65+
return httpError(http.StatusBadRequest, "query parameter '%s' not supported", name)
66+
}
67+
68+
func newInvalidParamError(name string, r *http.Request) error {
69+
return httpError(http.StatusBadRequest, "invalid '%s' query parameter", name)
70+
}
71+
72+
// parseDepthParam parses query parameter value for "depth" parameter.
73+
// See https://tools.ietf.org/html/rfc8040#section-4.8.2
74+
func parseDepthParam(v []string, r *http.Request) (uint, error) {
75+
if !restconfCapabilities.depth {
76+
glog.V(1).Infof("[%s] 'depth' support disabled", getRequestID(r))
77+
return 0, newUnsupportedParamError("depth", r)
78+
}
79+
80+
if r.Method != "GET" && r.Method != "HEAD" {
81+
glog.V(1).Infof("[%s] 'depth' not supported for %s", getRequestID(r), r.Method)
82+
return 0, newUnsupportedParamError("depth", r)
83+
}
84+
85+
if len(v) != 1 {
86+
glog.V(1).Infof("[%s] Expecting only 1 depth param; found %d", getRequestID(r), len(v))
87+
return 0, newInvalidParamError("depth", r)
88+
}
89+
90+
if v[0] == "unbounded" {
91+
return 0, nil
92+
}
93+
94+
d, err := strconv.ParseUint(v[0], 10, 16)
95+
if err != nil || d == 0 {
96+
glog.V(1).Infof("[%s] Bad depth value '%s', err=%v", getRequestID(r), v[0], err)
97+
return 0, newInvalidParamError("depth", r)
98+
}
99+
100+
return uint(d), nil
101+
}
102+
103+
// parseDeleteEmptyEntryParam parses the custom "deleteEmptyEntry" query parameter.
104+
func parseDeleteEmptyEntryParam(v []string, r *http.Request) (bool, error) {
105+
if r.Method != "DELETE" {
106+
glog.V(1).Infof("[%s] deleteEmptyEntry not supported for %s", getRequestID(r), r.Method)
107+
return false, newUnsupportedParamError("deleteEmptyEntry", r)
108+
}
109+
110+
if len(v) != 1 {
111+
glog.V(1).Infof("[%s] expecting only 1 deleteEmptyEntry; found %d", getRequestID(r), len(v))
112+
return false, newInvalidParamError("deleteEmptyEntry", r)
113+
}
114+
115+
return strings.EqualFold(v[0], "true"), nil
116+
}

rest/server/query_test.go

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
////////////////////////////////////////////////////////////////////////////////
2+
// //
3+
// Copyright 2020 Broadcom. The term Broadcom refers to Broadcom Inc. and/or //
4+
// its subsidiaries. //
5+
// //
6+
// Licensed under the Apache License, Version 2.0 (the "License"); //
7+
// you may not use this file except in compliance with the License. //
8+
// You may obtain a copy of the License at //
9+
// //
10+
// http://www.apache.org/licenses/LICENSE-2.0 //
11+
// //
12+
// Unless required by applicable law or agreed to in writing, software //
13+
// distributed under the License is distributed on an "AS IS" BASIS, //
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
15+
// See the License for the specific language governing permissions and //
16+
// limitations under the License. //
17+
// //
18+
////////////////////////////////////////////////////////////////////////////////
19+
20+
package server
21+
22+
import (
23+
"net/http/httptest"
24+
"testing"
25+
)
26+
27+
func testQuery(method, queryStr string, exp *translibArgs) func(*testing.T) {
28+
return func(t *testing.T) {
29+
r := httptest.NewRequest(method, "/restconf/data/querytest?"+queryStr, nil)
30+
_, r = GetContext(r)
31+
32+
p := translibArgs{}
33+
err := p.parseQueryParams(r)
34+
35+
errCode := 0
36+
if he, ok := err.(httpErrorType); ok {
37+
errCode = he.status
38+
}
39+
40+
if exp == nil && errCode == 400 {
41+
return // success
42+
}
43+
if err != nil {
44+
t.Fatalf("Failed to process query '%s'; err=%d/%v", r.URL.RawQuery, errCode, err)
45+
}
46+
47+
// compare parsed translibArgs
48+
if p.depth != exp.depth {
49+
t.Errorf("'depth' mismatch; expecting %d, found %d", exp.depth, p.depth)
50+
}
51+
if p.deleteEmpty != exp.deleteEmpty {
52+
t.Errorf("'deleteEmptyEntry' mismatch; expting %v, found %v", exp.deleteEmpty, p.deleteEmpty)
53+
}
54+
if t.Failed() {
55+
t.Errorf("Testcase failed for query '%s'", r.URL.RawQuery)
56+
}
57+
}
58+
}
59+
60+
func TestQuery(t *testing.T) {
61+
t.Run("none", testQuery("GET", "", &translibArgs{}))
62+
t.Run("unknown", testQuery("GET", "one=1", nil))
63+
}
64+
65+
func TestQuery_depth(t *testing.T) {
66+
rcCaps := restconfCapabilities
67+
defer func() { restconfCapabilities = rcCaps }()
68+
69+
restconfCapabilities.depth = true
70+
71+
// run depth test cases for GET and HEAD
72+
testDepth(t, "=unbounded", "depth=unbounded", &translibArgs{depth: 0})
73+
testDepth(t, "=0", "depth=0", nil)
74+
testDepth(t, "=1", "depth=1", &translibArgs{depth: 1})
75+
testDepth(t, "=101", "depth=101", &translibArgs{depth: 101})
76+
testDepth(t, "=65535", "depth=65535", &translibArgs{depth: 65535})
77+
testDepth(t, "=65536", "depth=65536", nil)
78+
testDepth(t, "=junk", "depth=junk", nil)
79+
testDepth(t, "extra", "depth=1&extra=1", nil)
80+
testDepth(t, "dup", "depth=1&depth=2", nil)
81+
82+
// check for other methods
83+
t.Run("OPTIONS", testQuery("OPTIONS", "depth=1", nil))
84+
t.Run("PUT", testQuery("PUT", "depth=1", nil))
85+
t.Run("POST", testQuery("POST", "depth=1", nil))
86+
t.Run("PATCH", testQuery("PATCH", "depth=1", nil))
87+
t.Run("DELETE", testQuery("DELETE", "depth=1", nil))
88+
}
89+
90+
func TestQuery_depth_disabled(t *testing.T) {
91+
rcCaps := restconfCapabilities
92+
defer func() { restconfCapabilities = rcCaps }()
93+
94+
restconfCapabilities.depth = false
95+
96+
testDepth(t, "100", "depth=100", nil)
97+
}
98+
99+
func testDepth(t *testing.T, name, queryStr string, exp *translibArgs) {
100+
t.Run("GET/"+name, testQuery("GET", queryStr, exp))
101+
t.Run("HEAD/"+name, testQuery("HEAD", queryStr, exp))
102+
}
103+
104+
func TestQuery_deleteEmptyEntry(t *testing.T) {
105+
t.Run("=true", testQuery("DELETE", "deleteEmptyEntry=true", &translibArgs{deleteEmpty: true}))
106+
t.Run("=True", testQuery("DELETE", "deleteEmptyEntry=True", &translibArgs{deleteEmpty: true}))
107+
t.Run("=TRUE", testQuery("DELETE", "deleteEmptyEntry=TRUE", &translibArgs{deleteEmpty: true}))
108+
t.Run("=false", testQuery("DELETE", "deleteEmptyEntry=false", &translibArgs{deleteEmpty: false}))
109+
t.Run("=1", testQuery("DELETE", "deleteEmptyEntry=1", &translibArgs{deleteEmpty: false}))
110+
t.Run("GET", testQuery("GET", "deleteEmptyEntry=true", nil))
111+
t.Run("HEAD", testQuery("HEAD", "deleteEmptyEntry=true", nil))
112+
t.Run("OPTIONS", testQuery("OPTIONS", "deleteEmptyEntry=true", nil))
113+
t.Run("PUT", testQuery("PUT", "deleteEmptyEntry=true", nil))
114+
t.Run("POST", testQuery("POST", "deleteEmptyEntry=true", nil))
115+
t.Run("PATCH", testQuery("PATCH", "deleteEmptyEntry=true", nil))
116+
}

0 commit comments

Comments
 (0)