Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query parameters infra #67

Merged
merged 3 commits into from
Oct 29, 2020
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
30 changes: 20 additions & 10 deletions CLI/actioner/cli_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class ApiClient(object):
__session = requests.Session()


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

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

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

return Response(r)
return Response(r, response_type)

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

def post(self, path, data):
return self.request("POST", path, data)
def post(self, path, data={}, response_type=None):
return self.request("POST", path, data, response_type=response_type)

def get(self, path, depth=None):
def get(self, path, depth=None, ignore404=True, response_type=None):
q = self.prepare_query(depth=depth)
return self.request("GET", path, query=q)
resp = self.request("GET", path, query=q, response_type=response_type)
if ignore404 and resp.status_code == 404:
resp.status_code = 200
resp.content = None
return resp

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

def delete(self, path, ignore404=True):
resp = self.request("DELETE", path, data=None)
def delete(self, path, ignore404=True, deleteEmptyEntry=False):
q = self.prepare_query(deleteEmptyEntry=deleteEmptyEntry)
resp = self.request("DELETE", path, data=None, query=q)
if ignore404 and resp.status_code == 404:
resp.status_code = 204
resp.content = None
return resp

@staticmethod
def prepare_query(depth=None):
def prepare_query(depth=None, deleteEmptyEntry=None):
query = {}
if depth != None and depth != "unbounded":
query["depth"] = depth
if deleteEmptyEntry is True:
query["deleteEmptyEntry"] = "true"
return query

@staticmethod
Expand All @@ -123,14 +130,17 @@ def __str__(self):


class Response(object):
def __init__(self, response):
def __init__(self, response, response_type=None):
self.response = response
self.response_type = response_type
self.status_code = response.status_code
self.content = response.content

try:
if response.content is None or len(response.content) == 0:
self.content = None
elif self.response_type and self.response_type.lower() == 'string':
self.content = str(response.content).decode('string_escape')
elif has_json_content(response):
self.content = json.loads(response.content, object_pairs_hook=OrderedDict)
except ValueError:
Expand Down
30 changes: 24 additions & 6 deletions rest/server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ func Process(w http.ResponseWriter, r *http.Request) {
if err == nil {
err = args.parseClientVersion(r, rc)
}
if err == nil {
err = args.parseQueryParams(r)
}

if err != nil {
status, data, rtype = prepareErrorResponse(err, r)
Expand Down Expand Up @@ -102,6 +105,17 @@ write_resp:
}
}

// getRequestID returns the request ID for a http Request r.
// ID is looked up from the RequestContext associated with this request.
// Returns empty value if context is not initialized yet.
func getRequestID(r *http.Request) string {
cv := getContextValue(r, requestContextKey)
if cv != nil {
return cv.(*RequestContext).ID
}
return ""
}

// getRequestBody returns the validated request body
func getRequestBody(r *http.Request, rc *RequestContext) (*MediaType, []byte, error) {
if r.ContentLength == 0 {
Expand Down Expand Up @@ -241,10 +255,12 @@ func isOperationsRequest(r *http.Request) bool {

// translibArgs holds arguments for invoking translib APIs.
type translibArgs struct {
method string // API name
path string // Translib path
data []byte // payload
version translib.Version // client version
method string // API name
path string // Translib path
data []byte // payload
version translib.Version // client version
depth uint // RESTCONF depth, for Get API only
deleteEmpty bool // Delete empty entry during field delete
}

// parseMethod maps http method name to translib method.
Expand Down Expand Up @@ -288,6 +304,7 @@ func invokeTranslib(args *translibArgs, rc *RequestContext) (int, []byte, error)
case "GET", "HEAD":
req := translib.GetRequest{
Path: args.path,
Depth: args.depth,
ClientVersion: args.version,
}
resp, err1 := translib.Get(req)
Expand Down Expand Up @@ -330,8 +347,9 @@ func invokeTranslib(args *translibArgs, rc *RequestContext) (int, []byte, error)
case "DELETE":
status = 204
req := translib.SetRequest{
Path: args.path,
ClientVersion: args.version,
Path: args.path,
ClientVersion: args.version,
DeleteEmptyEntry: args.deleteEmpty,
}
_, err = translib.Delete(req)

Expand Down
116 changes: 116 additions & 0 deletions rest/server/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
////////////////////////////////////////////////////////////////////////////////
// //
// Copyright 2020 Broadcom. The term Broadcom refers to Broadcom Inc. and/or //
// its subsidiaries. //
// //
// Licensed under the Apache License, Version 2.0 (the "License"); //
// you may not use this file except in compliance with the License. //
// You may obtain a copy of the License at //
// //
// http://www.apache.org/licenses/LICENSE-2.0 //
// //
// Unless required by applicable law or agreed to in writing, software //
// distributed under the License is distributed on an "AS IS" BASIS, //
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
// See the License for the specific language governing permissions and //
// limitations under the License. //
// //
////////////////////////////////////////////////////////////////////////////////

package server

import (
"net/http"
"strconv"
"strings"

"github.com/golang/glog"
)

// parseQueryParams parses the http request's query parameters
// into a translibArgs args.
func (args *translibArgs) parseQueryParams(r *http.Request) error {
if strings.Contains(r.URL.Path, restconfDataPathPrefix) {
return args.parseRestconfQueryParams(r)
}

return nil
}

// parseRestconfQueryParams parses query parameters of a request 'r' to
// fill translibArgs object 'args'. Returns httpError with status 400
// if any parameter is unsupported or has invalid value.
func (args *translibArgs) parseRestconfQueryParams(r *http.Request) error {
var err error
qParams := r.URL.Query()

for name, vals := range qParams {
switch name {
case "depth":
args.depth, err = parseDepthParam(vals, r)
case "deleteEmptyEntry":
args.deleteEmpty, err = parseDeleteEmptyEntryParam(vals, r)
default:
err = newUnsupportedParamError(name, r)
}
if err != nil {
return err
}
}

return nil
}

func newUnsupportedParamError(name string, r *http.Request) error {
return httpError(http.StatusBadRequest, "query parameter '%s' not supported", name)
}

func newInvalidParamError(name string, r *http.Request) error {
return httpError(http.StatusBadRequest, "invalid '%s' query parameter", name)
}

// parseDepthParam parses query parameter value for "depth" parameter.
// See https://tools.ietf.org/html/rfc8040#section-4.8.2
func parseDepthParam(v []string, r *http.Request) (uint, error) {
if !restconfCapabilities.depth {
glog.V(1).Infof("[%s] 'depth' support disabled", getRequestID(r))
return 0, newUnsupportedParamError("depth", r)
}

if r.Method != "GET" && r.Method != "HEAD" {
glog.V(1).Infof("[%s] 'depth' not supported for %s", getRequestID(r), r.Method)
return 0, newUnsupportedParamError("depth", r)
}

if len(v) != 1 {
glog.V(1).Infof("[%s] Expecting only 1 depth param; found %d", getRequestID(r), len(v))
return 0, newInvalidParamError("depth", r)
}

if v[0] == "unbounded" {
return 0, nil
}

d, err := strconv.ParseUint(v[0], 10, 16)
if err != nil || d == 0 {
glog.V(1).Infof("[%s] Bad depth value '%s', err=%v", getRequestID(r), v[0], err)
return 0, newInvalidParamError("depth", r)
}

return uint(d), nil
}

// parseDeleteEmptyEntryParam parses the custom "deleteEmptyEntry" query parameter.
func parseDeleteEmptyEntryParam(v []string, r *http.Request) (bool, error) {
if r.Method != "DELETE" {
glog.V(1).Infof("[%s] deleteEmptyEntry not supported for %s", getRequestID(r), r.Method)
return false, newUnsupportedParamError("deleteEmptyEntry", r)
}

if len(v) != 1 {
glog.V(1).Infof("[%s] expecting only 1 deleteEmptyEntry; found %d", getRequestID(r), len(v))
return false, newInvalidParamError("deleteEmptyEntry", r)
}

return strings.EqualFold(v[0], "true"), nil
}
116 changes: 116 additions & 0 deletions rest/server/query_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
////////////////////////////////////////////////////////////////////////////////
// //
// Copyright 2020 Broadcom. The term Broadcom refers to Broadcom Inc. and/or //
// its subsidiaries. //
// //
// Licensed under the Apache License, Version 2.0 (the "License"); //
// you may not use this file except in compliance with the License. //
// You may obtain a copy of the License at //
// //
// http://www.apache.org/licenses/LICENSE-2.0 //
// //
// Unless required by applicable law or agreed to in writing, software //
// distributed under the License is distributed on an "AS IS" BASIS, //
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
// See the License for the specific language governing permissions and //
// limitations under the License. //
// //
////////////////////////////////////////////////////////////////////////////////

package server

import (
"net/http/httptest"
"testing"
)

func testQuery(method, queryStr string, exp *translibArgs) func(*testing.T) {
return func(t *testing.T) {
r := httptest.NewRequest(method, "/restconf/data/querytest?"+queryStr, nil)
_, r = GetContext(r)

p := translibArgs{}
err := p.parseQueryParams(r)

errCode := 0
if he, ok := err.(httpErrorType); ok {
errCode = he.status
}

if exp == nil && errCode == 400 {
return // success
}
if err != nil {
t.Fatalf("Failed to process query '%s'; err=%d/%v", r.URL.RawQuery, errCode, err)
}

// compare parsed translibArgs
if p.depth != exp.depth {
t.Errorf("'depth' mismatch; expecting %d, found %d", exp.depth, p.depth)
}
if p.deleteEmpty != exp.deleteEmpty {
t.Errorf("'deleteEmptyEntry' mismatch; expting %v, found %v", exp.deleteEmpty, p.deleteEmpty)
}
if t.Failed() {
t.Errorf("Testcase failed for query '%s'", r.URL.RawQuery)
}
}
}

func TestQuery(t *testing.T) {
t.Run("none", testQuery("GET", "", &translibArgs{}))
t.Run("unknown", testQuery("GET", "one=1", nil))
}

func TestQuery_depth(t *testing.T) {
rcCaps := restconfCapabilities
defer func() { restconfCapabilities = rcCaps }()

restconfCapabilities.depth = true

// run depth test cases for GET and HEAD
testDepth(t, "=unbounded", "depth=unbounded", &translibArgs{depth: 0})
testDepth(t, "=0", "depth=0", nil)
testDepth(t, "=1", "depth=1", &translibArgs{depth: 1})
testDepth(t, "=101", "depth=101", &translibArgs{depth: 101})
testDepth(t, "=65535", "depth=65535", &translibArgs{depth: 65535})
testDepth(t, "=65536", "depth=65536", nil)
testDepth(t, "=junk", "depth=junk", nil)
testDepth(t, "extra", "depth=1&extra=1", nil)
testDepth(t, "dup", "depth=1&depth=2", nil)

// check for other methods
t.Run("OPTIONS", testQuery("OPTIONS", "depth=1", nil))
t.Run("PUT", testQuery("PUT", "depth=1", nil))
t.Run("POST", testQuery("POST", "depth=1", nil))
t.Run("PATCH", testQuery("PATCH", "depth=1", nil))
t.Run("DELETE", testQuery("DELETE", "depth=1", nil))
}

func TestQuery_depth_disabled(t *testing.T) {
rcCaps := restconfCapabilities
defer func() { restconfCapabilities = rcCaps }()

restconfCapabilities.depth = false

testDepth(t, "100", "depth=100", nil)
}

func testDepth(t *testing.T, name, queryStr string, exp *translibArgs) {
t.Run("GET/"+name, testQuery("GET", queryStr, exp))
t.Run("HEAD/"+name, testQuery("HEAD", queryStr, exp))
}

func TestQuery_deleteEmptyEntry(t *testing.T) {
t.Run("=true", testQuery("DELETE", "deleteEmptyEntry=true", &translibArgs{deleteEmpty: true}))
t.Run("=True", testQuery("DELETE", "deleteEmptyEntry=True", &translibArgs{deleteEmpty: true}))
t.Run("=TRUE", testQuery("DELETE", "deleteEmptyEntry=TRUE", &translibArgs{deleteEmpty: true}))
t.Run("=false", testQuery("DELETE", "deleteEmptyEntry=false", &translibArgs{deleteEmpty: false}))
t.Run("=1", testQuery("DELETE", "deleteEmptyEntry=1", &translibArgs{deleteEmpty: false}))
t.Run("GET", testQuery("GET", "deleteEmptyEntry=true", nil))
t.Run("HEAD", testQuery("HEAD", "deleteEmptyEntry=true", nil))
t.Run("OPTIONS", testQuery("OPTIONS", "deleteEmptyEntry=true", nil))
t.Run("PUT", testQuery("PUT", "deleteEmptyEntry=true", nil))
t.Run("POST", testQuery("POST", "deleteEmptyEntry=true", nil))
t.Run("PATCH", testQuery("PATCH", "deleteEmptyEntry=true", nil))
}