From 800d0ecf8cf258393a4ee0cc24af7813e0dc5362 Mon Sep 17 00:00:00 2001 From: Bill Robinson Date: Thu, 4 May 2017 10:17:16 -0700 Subject: [PATCH] Changes necessary to support `netctl login` Changes: - default http client is replaced by a http.Client in the ContivClient struct - httpGet()/httpPost()/httpDelete() are moved into ContivClient and use the new internal http.Client - added SetHttpClient() which allows for overriding the internal http.Client (to disable cert checking, add timeouts, etc.) - added Login() function which performs a login POST request against auth_proxy - added SetAuthToken() which adds a custom request header (X-Auth-Token) before requests are sent out - httpGet()/httpPost()/httpDelete() are modified to set any custom request headers before performing the request - added `make systemtests` target which runs everything under the systemtests dir Signed-off-by: Bill Robinson --- Makefile | 6 +- client/contivModelClient.go | 307 ++++++++++++++++++++++++++---------- systemtests/client_test.go | 180 +++++++++++++++++++++ systemtests/mock_server.go | 167 ++++++++++++++++++++ 4 files changed, 580 insertions(+), 80 deletions(-) create mode 100644 systemtests/client_test.go create mode 100644 systemtests/mock_server.go diff --git a/Makefile b/Makefile index 8423e8660..2ced11288 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -.PHONY: all build godep modelgen +.PHONY: all build godep modelgen systemtests all: build @@ -11,3 +11,7 @@ godep: modelgen: @if [ -z "`which modelgen`" ]; then go get -v github.com/contiv/modelgen; fi + +# systemtest runs all of the systemtests +systemtests: + go test -v -timeout 5m ./systemtests -check.v diff --git a/client/contivModelClient.go b/client/contivModelClient.go index 190175c9f..8435fab54 100644 --- a/client/contivModelClient.go +++ b/client/contivModelClient.go @@ -68,6 +68,8 @@ import ( "errors" "io/ioutil" "net/http" + "regexp" + "strings" log "github.com/Sirupsen/logrus" ) @@ -78,9 +80,16 @@ type Link struct { ObjKey string `json:"key,omitempty"` } -func httpGet(url string, jdata interface{}) error { +func (c *ContivClient) httpGet(url string, jdata interface{}) error { - r, err := http.Get(url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + c.processCustomHeaders(req) + + r, err := c.httpClient.Do(req) if err != nil { return err } @@ -116,13 +125,18 @@ func httpGet(url string, jdata interface{}) error { return nil } -func httpDelete(url string) error { +func (c *ContivClient) httpDelete(url string) error { req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return err + } + + c.processCustomHeaders(req) - r, err := http.DefaultClient.Do(req) + r, err := c.httpClient.Do(req) if err != nil { - panic(err) + return err } defer r.Body.Close() @@ -150,14 +164,22 @@ func httpDelete(url string) error { return nil } -func httpPost(url string, jdata interface{}) error { +func (c *ContivClient) httpPost(url string, jdata interface{}) error { buf, err := json.Marshal(jdata) if err != nil { return err } body := bytes.NewBuffer(buf) - r, err := http.Post(url, "application/json", body) + + req, err := http.NewRequest("POST", url, body) + if err != nil { + return err + } + + c.processCustomHeaders(req) + + r, err := c.httpClient.Do(req) if err != nil { return err } @@ -192,18 +214,145 @@ func httpPost(url string, jdata interface{}) error { // ContivClient has the contiv model client instance type ContivClient struct { + // URL of netmaster (http) or auth_proxy (https) baseURL string + + // these pairs will be added as HTTP request headers before any request + // is sent by this client. (each pair = one "Name: value" header). + // names stored in this list will be lowercase but later canonicalized + // internally by Go when the request headers are added. + customRequestHeaders [][2]string + + // even if not later overriden by SetHttpClient(), having a per-client + // http.Client means each client has its own dedicated pool of TCP + // keepalive connections for the target netmaster/auth_proxy. + httpClient *http.Client } // NewContivClient creates a new client instance func NewContivClient(baseURL string) (*ContivClient, error) { + ok, err := regexp.Match(`^https?://`, []byte(baseURL)) + if !ok { + return nil, errors.New("invalid URL: must begin with http:// or https://") + } else if err != nil { + return nil, err + } + client := ContivClient{ - baseURL: baseURL, + baseURL: baseURL, + customRequestHeaders: [][2]string{}, + httpClient: &http.Client{}, } return &client, nil } +// SetHTTPClient replaces the internal *http.Client with a custom http client. +// This can be used to disable cert checking, set timeouts, and so on. +func (c *ContivClient) SetHTTPClient(newClient *http.Client) error { + if newClient == nil { + return errors.New("new http client cannot be nil") + } + + c.httpClient = newClient + + return nil +} + +const authTokenHeader = "x-auth-token" + +// SetAuthToken sets the token used to authenticate with auth_proxy +func (c *ContivClient) SetAuthToken(token string) error { + + // setting an auth token is only allowed on secure requests. + // if we didn't enforce this, the client could potentially send auth + // tokens in plain text across the network. + if !c.isHTTPS() { + return errors.New("setting auth token requires a https auth_proxy URL") + } + + // having multiple auth token headers is confusing and makes no sense and + // which one is actually used depends on the implementation of the server. + // therefore, we will raise an error if there's already an auth token set. + for _, pair := range c.customRequestHeaders { + if pair[0] == authTokenHeader { + return errors.New("an auth token has already been set") + } + } + + c.addCustomRequestHeader(authTokenHeader, token) + + return nil +} + +func (c *ContivClient) isHTTPS() bool { + return strings.HasPrefix(c.baseURL, "https://") +} + +type loginPayload struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// LoginPath is the path of auth_proxy's login endpoint +const LoginPath = "/api/v1/auth_proxy/login/" + +// Login performs a login to auth_proxy and returns the response and body +func (c *ContivClient) Login(username, password string) (*http.Response, []byte, error) { + + // login is only allowed over a secure channel + if !c.isHTTPS() { + return nil, nil, errors.New("login requires a https auth_proxy URL") + } + + url := c.baseURL + LoginPath + + // create the POST payload for login + lp := loginPayload{ + Username: username, + Password: password, + } + + payload, err := json.Marshal(lp) + if err != nil { + return nil, nil, err + } + + // send the login POST request + resp, err := c.httpClient.Post(url, "application/json", bytes.NewBuffer(payload)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, nil, err + } + + return resp, body, nil +} + +// addCustomRequestHeader records a custom request header to be added to all outgoing requests +func (c *ContivClient) addCustomRequestHeader(name, value string) { + + // lowercase the header name so we can easily check for duplicates in other places. + // there can legitimately be many headers with the same name, but in some cases + // (e.g., auth token) we want to enforce that there is only one. + // Go internally canonicalizes them when we call Header.Add() anyways. + name = strings.ToLower(name) + + c.customRequestHeaders = append(c.customRequestHeaders, [2]string{name, value}) +} + +// processCustomHeaders adds all custom request headers to the target request. +// this function is called before a GET, POST, or DELETE is sent by the client. +func (c *ContivClient) processCustomHeaders(req *http.Request) { + for _, pair := range c.customRequestHeaders { + req.Header.Add(pair[0], pair[1]) + } +} + // AciGw object type AciGw struct { // every object has a key @@ -741,7 +890,7 @@ func (c *ContivClient) AciGwPost(obj *AciGw) error { url := c.baseURL + "/api/v1/aciGws/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating aciGw %+v. Err: %v", obj, err) return err @@ -757,7 +906,7 @@ func (c *ContivClient) AciGwList() (*[]*AciGw, error) { // http get the object var objList []*AciGw - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting aciGws. Err: %v", err) return nil, err @@ -774,7 +923,7 @@ func (c *ContivClient) AciGwGet(name string) (*AciGw, error) { // http get the object var obj AciGw - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting aciGw %+v. Err: %v", keyStr, err) return nil, err @@ -790,7 +939,7 @@ func (c *ContivClient) AciGwDelete(name string) error { url := c.baseURL + "/api/v1/aciGws/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting aciGw %s. Err: %v", keyStr, err) return err @@ -807,7 +956,7 @@ func (c *ContivClient) AciGwInspect(name string) (*AciGwInspect, error) { // http get the object var obj AciGwInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting aciGw %+v. Err: %v", keyStr, err) return nil, err @@ -823,7 +972,7 @@ func (c *ContivClient) AppProfilePost(obj *AppProfile) error { url := c.baseURL + "/api/v1/appProfiles/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating appProfile %+v. Err: %v", obj, err) return err @@ -839,7 +988,7 @@ func (c *ContivClient) AppProfileList() (*[]*AppProfile, error) { // http get the object var objList []*AppProfile - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting appProfiles. Err: %v", err) return nil, err @@ -856,7 +1005,7 @@ func (c *ContivClient) AppProfileGet(tenantName string, appProfileName string) ( // http get the object var obj AppProfile - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting appProfile %+v. Err: %v", keyStr, err) return nil, err @@ -872,7 +1021,7 @@ func (c *ContivClient) AppProfileDelete(tenantName string, appProfileName string url := c.baseURL + "/api/v1/appProfiles/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting appProfile %s. Err: %v", keyStr, err) return err @@ -889,7 +1038,7 @@ func (c *ContivClient) AppProfileInspect(tenantName string, appProfileName strin // http get the object var obj AppProfileInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting appProfile %+v. Err: %v", keyStr, err) return nil, err @@ -905,7 +1054,7 @@ func (c *ContivClient) BgpPost(obj *Bgp) error { url := c.baseURL + "/api/v1/Bgps/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating Bgp %+v. Err: %v", obj, err) return err @@ -921,7 +1070,7 @@ func (c *ContivClient) BgpList() (*[]*Bgp, error) { // http get the object var objList []*Bgp - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting Bgps. Err: %v", err) return nil, err @@ -938,7 +1087,7 @@ func (c *ContivClient) BgpGet(hostname string) (*Bgp, error) { // http get the object var obj Bgp - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting Bgp %+v. Err: %v", keyStr, err) return nil, err @@ -954,7 +1103,7 @@ func (c *ContivClient) BgpDelete(hostname string) error { url := c.baseURL + "/api/v1/Bgps/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting Bgp %s. Err: %v", keyStr, err) return err @@ -971,7 +1120,7 @@ func (c *ContivClient) BgpInspect(hostname string) (*BgpInspect, error) { // http get the object var obj BgpInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting Bgp %+v. Err: %v", keyStr, err) return nil, err @@ -988,7 +1137,7 @@ func (c *ContivClient) EndpointInspect(endpointID string) (*EndpointInspect, err // http get the object var obj EndpointInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting endpoint %+v. Err: %v", keyStr, err) return nil, err @@ -1004,7 +1153,7 @@ func (c *ContivClient) EndpointGroupPost(obj *EndpointGroup) error { url := c.baseURL + "/api/v1/endpointGroups/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating endpointGroup %+v. Err: %v", obj, err) return err @@ -1020,7 +1169,7 @@ func (c *ContivClient) EndpointGroupList() (*[]*EndpointGroup, error) { // http get the object var objList []*EndpointGroup - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting endpointGroups. Err: %v", err) return nil, err @@ -1037,7 +1186,7 @@ func (c *ContivClient) EndpointGroupGet(tenantName string, groupName string) (*E // http get the object var obj EndpointGroup - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting endpointGroup %+v. Err: %v", keyStr, err) return nil, err @@ -1053,7 +1202,7 @@ func (c *ContivClient) EndpointGroupDelete(tenantName string, groupName string) url := c.baseURL + "/api/v1/endpointGroups/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting endpointGroup %s. Err: %v", keyStr, err) return err @@ -1070,7 +1219,7 @@ func (c *ContivClient) EndpointGroupInspect(tenantName string, groupName string) // http get the object var obj EndpointGroupInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting endpointGroup %+v. Err: %v", keyStr, err) return nil, err @@ -1086,7 +1235,7 @@ func (c *ContivClient) ExtContractsGroupPost(obj *ExtContractsGroup) error { url := c.baseURL + "/api/v1/extContractsGroups/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating extContractsGroup %+v. Err: %v", obj, err) return err @@ -1102,7 +1251,7 @@ func (c *ContivClient) ExtContractsGroupList() (*[]*ExtContractsGroup, error) { // http get the object var objList []*ExtContractsGroup - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting extContractsGroups. Err: %v", err) return nil, err @@ -1119,7 +1268,7 @@ func (c *ContivClient) ExtContractsGroupGet(tenantName string, contractsGroupNam // http get the object var obj ExtContractsGroup - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting extContractsGroup %+v. Err: %v", keyStr, err) return nil, err @@ -1135,7 +1284,7 @@ func (c *ContivClient) ExtContractsGroupDelete(tenantName string, contractsGroup url := c.baseURL + "/api/v1/extContractsGroups/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting extContractsGroup %s. Err: %v", keyStr, err) return err @@ -1152,7 +1301,7 @@ func (c *ContivClient) ExtContractsGroupInspect(tenantName string, contractsGrou // http get the object var obj ExtContractsGroupInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting extContractsGroup %+v. Err: %v", keyStr, err) return nil, err @@ -1168,7 +1317,7 @@ func (c *ContivClient) GlobalPost(obj *Global) error { url := c.baseURL + "/api/v1/globals/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating global %+v. Err: %v", obj, err) return err @@ -1184,7 +1333,7 @@ func (c *ContivClient) GlobalList() (*[]*Global, error) { // http get the object var objList []*Global - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting globals. Err: %v", err) return nil, err @@ -1201,7 +1350,7 @@ func (c *ContivClient) GlobalGet(name string) (*Global, error) { // http get the object var obj Global - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting global %+v. Err: %v", keyStr, err) return nil, err @@ -1217,7 +1366,7 @@ func (c *ContivClient) GlobalDelete(name string) error { url := c.baseURL + "/api/v1/globals/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting global %s. Err: %v", keyStr, err) return err @@ -1234,7 +1383,7 @@ func (c *ContivClient) GlobalInspect(name string) (*GlobalInspect, error) { // http get the object var obj GlobalInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting global %+v. Err: %v", keyStr, err) return nil, err @@ -1250,7 +1399,7 @@ func (c *ContivClient) NetprofilePost(obj *Netprofile) error { url := c.baseURL + "/api/v1/netprofiles/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating netprofile %+v. Err: %v", obj, err) return err @@ -1266,7 +1415,7 @@ func (c *ContivClient) NetprofileList() (*[]*Netprofile, error) { // http get the object var objList []*Netprofile - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting netprofiles. Err: %v", err) return nil, err @@ -1283,7 +1432,7 @@ func (c *ContivClient) NetprofileGet(tenantName string, profileName string) (*Ne // http get the object var obj Netprofile - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting netprofile %+v. Err: %v", keyStr, err) return nil, err @@ -1299,7 +1448,7 @@ func (c *ContivClient) NetprofileDelete(tenantName string, profileName string) e url := c.baseURL + "/api/v1/netprofiles/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting netprofile %s. Err: %v", keyStr, err) return err @@ -1316,7 +1465,7 @@ func (c *ContivClient) NetprofileInspect(tenantName string, profileName string) // http get the object var obj NetprofileInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting netprofile %+v. Err: %v", keyStr, err) return nil, err @@ -1332,7 +1481,7 @@ func (c *ContivClient) NetworkPost(obj *Network) error { url := c.baseURL + "/api/v1/networks/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating network %+v. Err: %v", obj, err) return err @@ -1348,7 +1497,7 @@ func (c *ContivClient) NetworkList() (*[]*Network, error) { // http get the object var objList []*Network - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting networks. Err: %v", err) return nil, err @@ -1365,7 +1514,7 @@ func (c *ContivClient) NetworkGet(tenantName string, networkName string) (*Netwo // http get the object var obj Network - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting network %+v. Err: %v", keyStr, err) return nil, err @@ -1381,7 +1530,7 @@ func (c *ContivClient) NetworkDelete(tenantName string, networkName string) erro url := c.baseURL + "/api/v1/networks/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting network %s. Err: %v", keyStr, err) return err @@ -1398,7 +1547,7 @@ func (c *ContivClient) NetworkInspect(tenantName string, networkName string) (*N // http get the object var obj NetworkInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting network %+v. Err: %v", keyStr, err) return nil, err @@ -1414,7 +1563,7 @@ func (c *ContivClient) PolicyPost(obj *Policy) error { url := c.baseURL + "/api/v1/policys/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating policy %+v. Err: %v", obj, err) return err @@ -1430,7 +1579,7 @@ func (c *ContivClient) PolicyList() (*[]*Policy, error) { // http get the object var objList []*Policy - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting policys. Err: %v", err) return nil, err @@ -1447,7 +1596,7 @@ func (c *ContivClient) PolicyGet(tenantName string, policyName string) (*Policy, // http get the object var obj Policy - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting policy %+v. Err: %v", keyStr, err) return nil, err @@ -1463,7 +1612,7 @@ func (c *ContivClient) PolicyDelete(tenantName string, policyName string) error url := c.baseURL + "/api/v1/policys/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting policy %s. Err: %v", keyStr, err) return err @@ -1480,7 +1629,7 @@ func (c *ContivClient) PolicyInspect(tenantName string, policyName string) (*Pol // http get the object var obj PolicyInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting policy %+v. Err: %v", keyStr, err) return nil, err @@ -1496,7 +1645,7 @@ func (c *ContivClient) RulePost(obj *Rule) error { url := c.baseURL + "/api/v1/rules/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating rule %+v. Err: %v", obj, err) return err @@ -1512,7 +1661,7 @@ func (c *ContivClient) RuleList() (*[]*Rule, error) { // http get the object var objList []*Rule - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting rules. Err: %v", err) return nil, err @@ -1529,7 +1678,7 @@ func (c *ContivClient) RuleGet(tenantName string, policyName string, ruleId stri // http get the object var obj Rule - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting rule %+v. Err: %v", keyStr, err) return nil, err @@ -1545,7 +1694,7 @@ func (c *ContivClient) RuleDelete(tenantName string, policyName string, ruleId s url := c.baseURL + "/api/v1/rules/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting rule %s. Err: %v", keyStr, err) return err @@ -1562,7 +1711,7 @@ func (c *ContivClient) RuleInspect(tenantName string, policyName string, ruleId // http get the object var obj RuleInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting rule %+v. Err: %v", keyStr, err) return nil, err @@ -1578,7 +1727,7 @@ func (c *ContivClient) ServiceLBPost(obj *ServiceLB) error { url := c.baseURL + "/api/v1/serviceLBs/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating serviceLB %+v. Err: %v", obj, err) return err @@ -1594,7 +1743,7 @@ func (c *ContivClient) ServiceLBList() (*[]*ServiceLB, error) { // http get the object var objList []*ServiceLB - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting serviceLBs. Err: %v", err) return nil, err @@ -1611,7 +1760,7 @@ func (c *ContivClient) ServiceLBGet(tenantName string, serviceName string) (*Ser // http get the object var obj ServiceLB - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting serviceLB %+v. Err: %v", keyStr, err) return nil, err @@ -1627,7 +1776,7 @@ func (c *ContivClient) ServiceLBDelete(tenantName string, serviceName string) er url := c.baseURL + "/api/v1/serviceLBs/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting serviceLB %s. Err: %v", keyStr, err) return err @@ -1644,7 +1793,7 @@ func (c *ContivClient) ServiceLBInspect(tenantName string, serviceName string) ( // http get the object var obj ServiceLBInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting serviceLB %+v. Err: %v", keyStr, err) return nil, err @@ -1660,7 +1809,7 @@ func (c *ContivClient) TenantPost(obj *Tenant) error { url := c.baseURL + "/api/v1/tenants/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating tenant %+v. Err: %v", obj, err) return err @@ -1676,7 +1825,7 @@ func (c *ContivClient) TenantList() (*[]*Tenant, error) { // http get the object var objList []*Tenant - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting tenants. Err: %v", err) return nil, err @@ -1693,7 +1842,7 @@ func (c *ContivClient) TenantGet(tenantName string) (*Tenant, error) { // http get the object var obj Tenant - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting tenant %+v. Err: %v", keyStr, err) return nil, err @@ -1709,7 +1858,7 @@ func (c *ContivClient) TenantDelete(tenantName string) error { url := c.baseURL + "/api/v1/tenants/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting tenant %s. Err: %v", keyStr, err) return err @@ -1726,7 +1875,7 @@ func (c *ContivClient) TenantInspect(tenantName string) (*TenantInspect, error) // http get the object var obj TenantInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting tenant %+v. Err: %v", keyStr, err) return nil, err @@ -1742,7 +1891,7 @@ func (c *ContivClient) VolumePost(obj *Volume) error { url := c.baseURL + "/api/v1/volumes/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating volume %+v. Err: %v", obj, err) return err @@ -1758,7 +1907,7 @@ func (c *ContivClient) VolumeList() (*[]*Volume, error) { // http get the object var objList []*Volume - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting volumes. Err: %v", err) return nil, err @@ -1775,7 +1924,7 @@ func (c *ContivClient) VolumeGet(tenantName string, volumeName string) (*Volume, // http get the object var obj Volume - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting volume %+v. Err: %v", keyStr, err) return nil, err @@ -1791,7 +1940,7 @@ func (c *ContivClient) VolumeDelete(tenantName string, volumeName string) error url := c.baseURL + "/api/v1/volumes/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting volume %s. Err: %v", keyStr, err) return err @@ -1808,7 +1957,7 @@ func (c *ContivClient) VolumeInspect(tenantName string, volumeName string) (*Vol // http get the object var obj VolumeInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting volume %+v. Err: %v", keyStr, err) return nil, err @@ -1824,7 +1973,7 @@ func (c *ContivClient) VolumeProfilePost(obj *VolumeProfile) error { url := c.baseURL + "/api/v1/volumeProfiles/" + keyStr + "/" // http post the object - err := httpPost(url, obj) + err := c.httpPost(url, obj) if err != nil { log.Debugf("Error creating volumeProfile %+v. Err: %v", obj, err) return err @@ -1840,7 +1989,7 @@ func (c *ContivClient) VolumeProfileList() (*[]*VolumeProfile, error) { // http get the object var objList []*VolumeProfile - err := httpGet(url, &objList) + err := c.httpGet(url, &objList) if err != nil { log.Debugf("Error getting volumeProfiles. Err: %v", err) return nil, err @@ -1857,7 +2006,7 @@ func (c *ContivClient) VolumeProfileGet(tenantName string, volumeProfileName str // http get the object var obj VolumeProfile - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting volumeProfile %+v. Err: %v", keyStr, err) return nil, err @@ -1873,7 +2022,7 @@ func (c *ContivClient) VolumeProfileDelete(tenantName string, volumeProfileName url := c.baseURL + "/api/v1/volumeProfiles/" + keyStr + "/" // http get the object - err := httpDelete(url) + err := c.httpDelete(url) if err != nil { log.Debugf("Error deleting volumeProfile %s. Err: %v", keyStr, err) return err @@ -1890,7 +2039,7 @@ func (c *ContivClient) VolumeProfileInspect(tenantName string, volumeProfileName // http get the object var obj VolumeProfileInspect - err := httpGet(url, &obj) + err := c.httpGet(url, &obj) if err != nil { log.Debugf("Error getting volumeProfile %+v. Err: %v", keyStr, err) return nil, err diff --git a/systemtests/client_test.go b/systemtests/client_test.go new file mode 100644 index 000000000..d73357884 --- /dev/null +++ b/systemtests/client_test.go @@ -0,0 +1,180 @@ +/*** +Copyright 2017 Cisco Systems Inc. All rights reserved. + +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 systemtests + +import ( + "crypto/tls" + "net/http" + "testing" + "time" + + . "github.com/contiv/check" + "github.com/contiv/contivmodel/client" +) + +func Test(t *testing.T) { TestingT(t) } + +type clientSuite struct{} + +var _ = Suite(&clientSuite{}) + +// ----- HELPER FUNCTIONS ------------------------------------------------------- + +func newClient(c *C, url string) *client.ContivClient { + cc, err := client.NewContivClient(url) + c.Assert(err, IsNil) + + return cc +} + +func newHTTPClient(c *C) *client.ContivClient { + return newClient(c, "http://localhost:9999") +} + +func newHTTPSClient(c *C) *client.ContivClient { + return newClient(c, "https://localhost:10000") +} + +func newInsecureHTTPSClient(c *C) *client.ContivClient { + cc := newHTTPSClient(c) + + tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + err := cc.SetHTTPClient(&http.Client{Transport: tr}) + c.Assert(err, IsNil) + + return cc +} + +func runHTTPSServer(f func(*MockServer)) { + ms := NewMockServer() + defer ms.Stop() + + // give the listener a bit to come up. + // tls.Listen() returns before the server is actually responding to requests. + time.Sleep(100 * time.Millisecond) + + f(ms) +} + +// ----- TESTS ------------------------------------------------------------------ + +func (cs *clientSuite) TestURLValidity(c *C) { + valid := []string{ + "http://localhost", + "http://localhost:12345", + "https://localhost", + "https://localhost:12345", + } + + invalid := []string{ + "asdf", + "localhost", + "localhost:12345", + } + + for _, url := range valid { + _, err := client.NewContivClient(url) + c.Assert(err, IsNil) + } + + for _, url := range invalid { + _, err := client.NewContivClient(url) + c.Assert(err, NotNil) + } +} + +func (cs *clientSuite) TestSettingCustomClient(c *C) { + cc := newHTTPClient(c) + + // valid client + err := cc.SetHTTPClient(&http.Client{}) + c.Assert(err, IsNil) + + // invalid client + err = cc.SetHTTPClient(nil) + c.Assert(err, NotNil) + c.Assert(err.Error(), Matches, ".* cannot be nil") +} + +func (cs *clientSuite) TestSettingAuthToken(c *C) { + + // make sure we can't set a token on a non-https client + insecureClient := newHTTPClient(c) + err := insecureClient.SetAuthToken("foo") + c.Assert(err, NotNil) + c.Assert(err.Error(), Matches, ".* requires a https .*") + + // make sure we can't set more than one token + secureClient := newHTTPSClient(c) + err = secureClient.SetAuthToken("foo") + c.Assert(err, IsNil) + + err = secureClient.SetAuthToken("bar") + c.Assert(err, NotNil) + c.Assert(err.Error(), Matches, ".* already been set") + + // make sure that the auth token is included when set and not when not set + tokenValue := "asdf" + tokenFromRequest := "" + + runHTTPSServer(func(ms *MockServer) { + + // we'll use the "list networks" endpoint for this test + ms.AddHandler("/api/v1/networks/", func(w http.ResponseWriter, req *http.Request) { + tokenFromRequest = req.Header.Get("X-Auth-Token") + w.Write([]byte("[]")) // return an empty set + }) + + cc := newInsecureHTTPSClient(c) + + // send a request without the auth token and make sure the header wasn't set + _, err := cc.NetworkList() + c.Assert(err, IsNil) + c.Assert(len(tokenFromRequest), Equals, 0) + + // set the auth token, resend the same request, and verify the header was set + cc.SetAuthToken(tokenValue) + + _, err = cc.NetworkList() + c.Assert(err, IsNil) + c.Assert(tokenFromRequest == tokenValue, Equals, true) + + }) +} + +func (cs *clientSuite) TestLogin(c *C) { + + // make sure we can't login with a non-http client + insecureClient := newHTTPClient(c) + + _, _, err := insecureClient.Login("foo", "bar") + c.Assert(err, NotNil) + c.Assert(err.Error(), Matches, ".* requires a https .*") + + // make sure a https client can send a login request + runHTTPSServer(func(ms *MockServer) { + + // add the endpoint that Login() will POST to + ms.AddHardcodedResponse(client.LoginPath, []byte("{}")) + + cc := newInsecureHTTPSClient(c) + + // send a login request + resp, _, err := cc.Login("foo", "bar") + c.Assert(err, IsNil) + c.Assert(resp.StatusCode, Equals, 200) + }) +} diff --git a/systemtests/mock_server.go b/systemtests/mock_server.go new file mode 100644 index 000000000..8cf6a82da --- /dev/null +++ b/systemtests/mock_server.go @@ -0,0 +1,167 @@ +/*** +Copyright 2017 Cisco Systems Inc. All rights reserved. + +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 systemtests + +import ( + "crypto/tls" + "net" + "net/http" + "sync" + + log "github.com/Sirupsen/logrus" +) + +// this cert and key are valid for 100 years +const certpem = ` +-----BEGIN CERTIFICATE----- +MIIDozCCAougAwIBAgIJAM+dSt5+iemKMA0GCSqGSIb3DQEBCwUAMGgxCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJDQTERMA8GA1UEBwwIU2FuIEpvc2UxDTALBgNVBAoM +BENQU0cxFjAUBgNVBAsMDUlUIERlcGFydG1lbnQxEjAQBgNVBAMMCWxvY2FsaG9z +dDAeFw0xNzA0MjAxOTI4MTJaFw0yNzA0MTgxOTI4MTJaMGgxCzAJBgNVBAYTAlVT +MQswCQYDVQQIDAJDQTERMA8GA1UEBwwIU2FuIEpvc2UxDTALBgNVBAoMBENQU0cx +FjAUBgNVBAsMDUlUIERlcGFydG1lbnQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM1MKpN5mtgdSo7gk0M70mcNJC4G +XVuPcZdC43GSfUxL1buc+80NP5kCp8dzbDYrKfshTgalwmEV4J+5bvKe4osrEMmC +aTC6927nCDH2m+G30/qWxHXMDp4QiZm8GIp/EiDLPqtOOImsoP/QUQKtRGKSqltX +Ei0D5o3wq06Y7RhXRoSnGBUkTCkp1OMGyuJJKXbpoeN+CO3xVJ6OgxMAqoKpdF9k +j8uP4qu8A1jzuiN3/L/vh/JmBajiD54vL0Pb4DoVHJRCGP1RRkLbRUuEHJkW9Smt +67SxcYmZwFnJyXN7KZF+QlyeDDFTB8t0s1t66WwyMIiyN4fr1HYxPXL/Tn0CAwEA +AaNQME4wHQYDVR0OBBYEFCGX5Uzlt8818KcOVicpoFPPEE/NMB8GA1UdIwQYMBaA +FCGX5Uzlt8818KcOVicpoFPPEE/NMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAJO8JCp4Aoi+QM0PsrDeoBL/nAiW+lwHoAwH3JGi9vnZ59RTUs0iCCBM +ecr2MUT7vpxHxWF3C2EH9dPBiXYwrr3q4b0A8Lhf+PrmGOB9nwpbxqAyEvNoj02B +Uc2dpblNsIQteceBdOBGkIKBWAkvXPXrA0ExlV31Qh0KHNsaYLb0d6uSBHZFX/d6 +zBhHQqoYuhS3WCYVaPE2PUU9eV3Q6f0Xx+e6GovaO7DgmrSQ1mbAp33XnPiKUz2b +ioF6fl0GISEpfkbrPNBbhSCrXatLrtz+4DpneJQ5vVClG054qcms+hnziiomz7P+ +TfQIVXFBQdXZedjqDxhga7ebCWb41yA= +-----END CERTIFICATE----- +` + +const certkey = ` +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAzUwqk3ma2B1KjuCTQzvSZw0kLgZdW49xl0LjcZJ9TEvVu5z7 +zQ0/mQKnx3NsNisp+yFOBqXCYRXgn7lu8p7iiysQyYJpMLr3bucIMfab4bfT+pbE +dcwOnhCJmbwYin8SIMs+q044iayg/9BRAq1EYpKqW1cSLQPmjfCrTpjtGFdGhKcY +FSRMKSnU4wbK4kkpdumh434I7fFUno6DEwCqgql0X2SPy4/iq7wDWPO6I3f8v++H +8mYFqOIPni8vQ9vgOhUclEIY/VFGQttFS4QcmRb1Ka3rtLFxiZnAWcnJc3spkX5C +XJ4MMVMHy3SzW3rpbDIwiLI3h+vUdjE9cv9OfQIDAQABAoIBAQCa0Qtyd0vsGfq1 +0Gl9VEmQ6PoVszsH5x6UIR7/8KaIuM+PUg0ZTxpcuwHniQVbvCVGepEqtinlqOfh +y6b9VBAnPuzD6ZKF6xjZC2TEuOJIz6YN3VB+PMnxLSt3Qb+IAdeb32l9Kdm9CO/I +ukG9MQjXBR9vDjRouf5Nn+avuOdjaGNaFWNCqZb3/0B4zdslsR8ynvKHgB9OH9a6 +ggmKINzkvF1Fv6UyGjgLyfVjcdxgFDZ3TY5vsxoO7/jPWzxRY3LignaWV8hEo2L5 +fFsyUFApHLmCXMW+fiEu/0QsN2zFcZp1oXCEc2+a9OF3p3e3FaXv5h9w3EdZJLql +b2zt2zzBAoGBAPC1zlZ8HkLcUxZgAA6O9d1lHCjDkSCG1HOLVQ2gKlDOhuQq8kxq +/0HsrtbC4KtjZeOTWHWnX035i7BU42G7y/cNwFtfplINr1XdH8DOgffS53UKnEbs +WyBSgBh6jsoDsPnuGrOnBVmaTB9aGLpznuHcZ/wMeZUEIrQI6wlL79nVAoGBANpW +g6d7BG62xs72e++T2tw/ZSpGNjLt+weKAWbhxHwhlvcrgEkQ5sS/FR/WIfoCplqh +MGhYV0a4zlmSOoOcg3ZQkqiwrNDT1CpIgC8ETzJzt5/eTwEE8UJtD9bIngA62Xec +iACYQgRox0v/UG9N9U1Tnr0oDLVXahZbN4BXiw4JAoGAGpWZskeG+A9pRcFYgEMd +uFPgZkgjERqTACfVPun/gmks0KpFlFcE1f0T2jgvo/4YVKgDTwsrJWt4GANoEXUy +M5jbM7w+nDVStgLz7NFh3UL3uR9w3wxfjBRQfWObvYfm1dOMM2cw2hKGcbf7nywB +0iQLf/TIwMJyKrwJaT9vv/kCgYEAvXoa4rtFS3de7LjHMVBUvJJfjuJDossX8KD5 +Onlu9HKJ+pJL0BzUx6U0Bd7kuXyXNUtxIPyZMQysNttJ4HFxPLoLrE02jDtoghFM +/IB24ke98QUR9sZ9QLI47qJHS9fGZaD3/dwkXoM3gWJeQVmcKbEJrwoUjUMBE8mx +TrWqPVECgYBamOxzDC3dUQUFEUGNx0D8bGAHmMnU8tHglwjyQeC8wTYreAdFRYMp +KNPNa4q/NpcKXUcM9etcuzQC8Di/GZDAa+uEQIvwH/xHG6FwvOTfZp8bzN8rD+gQ +yGWqZkaNREZSyW+pNDCUXnBDkCBj7qwUgb6ysgodeF7RWFAHfoXJ1g== +-----END RSA PRIVATE KEY----- +` + +// NewMockServer returns a configured, initialized, and running MockServer which +// can have routes added even though it's already running. Call Stop() to stop it. +func NewMockServer() *MockServer { + ms := &MockServer{} + ms.Init() + go ms.Serve() + + return ms +} + +// MockServer is a server which we can program to behave like netmaster for +// testing purposes. +type MockServer struct { + listener net.Listener // the actual HTTPS listener + mux *http.ServeMux // a custom ServeMux we can add routes onto later + stopChan chan bool // used to shut down the server + wg sync.WaitGroup // used to avoid a race condition when shutting down +} + +// Init just sets up the stop channel and our custom ServeMux +func (ms *MockServer) Init() { + ms.stopChan = make(chan bool, 1) + ms.mux = http.NewServeMux() +} + +// AddHardcodedResponse registers a HTTP handler func for `path' that returns `body'. +func (ms *MockServer) AddHardcodedResponse(path string, body []byte) { + ms.mux.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write(body) + }) +} + +// AddHandler allows adding a custom route handler to our custom ServeMux +func (ms *MockServer) AddHandler(path string, f func(http.ResponseWriter, *http.Request)) { + ms.mux.HandleFunc(path, f) +} + +// Serve starts the mock server using the custom ServeMux we set up. +func (ms *MockServer) Serve() { + var err error + + server := &http.Server{Handler: ms.mux} + + // because of the tight time constraints around starting/stopping the + // mock server when running tests and the fact that lingering client + // connections can cause the server not to shut down in a timely + // manner, we will just disable keepalives entirely here. + server.SetKeepAlivesEnabled(false) + + cert, err := tls.X509KeyPair([]byte(certpem), []byte(certkey)) + if err != nil { + log.Fatalln("Failed to load TLS key pair:", err) + return + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS11, + } + + ms.listener, err = tls.Listen("tcp", "0.0.0.0:10000", tlsConfig) + if err != nil { + log.Fatalln("Failed to listen:", err) + return + } + + ms.wg.Add(1) + go func() { + server.Serve(ms.listener) + ms.wg.Done() + }() + + <-ms.stopChan + + ms.listener.Close() +} + +// Stop stops the mock server. +func (ms *MockServer) Stop() { + ms.stopChan <- true + + // wait until the listener has actually been stopped + ms.wg.Wait() +}