From af4a5dc34cae25a10a42746615ba9f40ec78e0ad Mon Sep 17 00:00:00 2001 From: Cheyne Womble Date: Thu, 24 Mar 2022 11:13:08 -0400 Subject: [PATCH 01/14] Update lint/fmt/vet --- Makefile | 40 +++++++++++++++++----------------------- golangci.yml | 27 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 golangci.yml diff --git a/Makefile b/Makefile index 47756bf..70eb50f 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,6 @@ # make fmtcheck -- formate check # make vet -- go vet # make lint -- go lint -# make deadcode -- deadcode checker # make test -- run tests # make clean -- clean # @@ -23,11 +22,12 @@ GOLDFLAGS := -ldflags="-s -w" DEFAULT_GOPATH := $${GOPATH%%:*} GO := go -GOFMT := goimports -GOLINT := golint -GO_DEADCODE := deadcode +GOFMT := gofmt +LINT := golangci-lint run +LINTFLAGS ?= --deadline=10m --exclude-use-default=false --print-issued-lines --print-linter-name --out-format=colored-line-number --disable-all --max-same-issues=0 --max-issues-per-linter=0 +LINTCONFIG := --config golangci.yml GOFILES := find . -name '*.go' ! -path './Godeps/*' ! -path './vendor/*' -GOFOLDERS := $(GO) list ./... | sed 's:^github.com/aristanetworks/go-cvprac:.:' | grep -vw -e './vendor' +GOFOLDERS := $(GO) list ./... | sed 's:^github.com/aristanetworks/go-cvprac/v3:.:' | grep -vw -e './vendor' VERSION_FILE = version.go GOPKGVERSION := $(shell git describe --tags --always --match "v[0-9]*" --abbrev=7 HEAD) @@ -37,35 +37,29 @@ endif # External Tools EXTERNAL_TOOLS=\ - github.com/golang/lint/golint \ - github.com/remyoudompheng/go-misc/deadcode \ golang.org/x/tools/cmd/godoc \ golang.org/x/tools/cmd/goimports -check: fmtcheck vet lint deadcode unittest - -deadcode: - @if ! which $(GO_DEADCODE) >/dev/null; then echo Please install $(GO_DEADCODE); exit 1; fi - $(GOFOLDERS) | xargs $(GO_DEADCODE) +check: fmtcheck vet lint unittest lint: - lint=`$(GOFOLDERS) | xargs -L 1 $(GOLINT)`; if test -n "$$lint"; then echo "$$lint"; exit 1; fi -# The above is ugly, but unfortunately golint doesn't exit 1 when it finds -# lint. See https://github.com/golang/lint/issues/65 + $(GOFOLDERS) | xargs $(LINT) $(LINTFLAGS) --disable-all --enable=deadcode --tests=false + $(GOFOLDERS) | xargs $(LINT) $(LINTCONFIG) $(LINTFLAGS) fmtcheck: @if ! which $(GOFMT) >/dev/null; then echo Please install $(GOFMT); exit 1; fi - goimports=`$(GOFILES) | xargs $(GOFMT) -l 2>&1`; \ - if test -n "$$goimports"; then echo Check the following files for coding style AND USE goimports; echo "$$goimports"; \ - if test "$(shell $(GO) version | awk '{ print $$3 }')" != "devel"; then exit 1; fi; \ - fi - $(GOFILES) -exec ./check_line_len.awk {} + + goimports=`$(GOFILES) | xargs $(GOFMT) -d 2>&1`; \ + if test -n "$$goimports"; then \ + echo Check the following files for coding style AND USE goimports; \ + echo "$$goimports"; \ + exit 1; \ + fi fmt: - $(GO) fmt + $(GOFOLDERS) | xargs $(GO) fmt vet: - $(GO) vet + $(GOFOLDERS) | xargs $(GO) vet test: $(GO) test $(GOTEST_FLAGS) @@ -110,5 +104,5 @@ clean: rm -rf $(COVER_TMPFILE).tmp $(COVER_TMPFILE) $(VERSION_FILE){,-t} $(GO) clean ./... -.PHONY: all fmtcheck test vet check doc lint deadcode +.PHONY: all fmtcheck test vet check doc lint .PHONY: clean coverage coverdata version diff --git a/golangci.yml b/golangci.yml new file mode 100644 index 0000000..511772f --- /dev/null +++ b/golangci.yml @@ -0,0 +1,27 @@ +linters-settings: + lll: + line-length: 100 + tab-width: 4 + +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + - path: _test\.go + linters: + - errcheck + + exclude: + # Linter: errcheck + # Almost all programs ignore errors on these functions and in most cases it's ok + - "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*printf?|os\\.(Un)?Setenv). is not checked" + +linters: + enable: + #- deadcode + - revive + - gosimple + - ineffassign + - lll + - staticcheck + - unconvert + - errcheck From 9eddc2eed1461a87c15fbce96e03cdd589d2eac4 Mon Sep 17 00:00:00 2001 From: Cheyne Womble Date: Thu, 24 Mar 2022 11:14:35 -0400 Subject: [PATCH 02/14] Fix lint errors --- api/configletbuilder.go | 3 ++- api/provisioning.go | 3 ++- api/provisioning_test.go | 9 ++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/api/configletbuilder.go b/api/configletbuilder.go index d5d596f..ebc07b0 100644 --- a/api/configletbuilder.go +++ b/api/configletbuilder.go @@ -205,7 +205,8 @@ func (c CvpRestAPI) GenerateAutoConfiglet(devKeyList []string, builderKey string } // GenerateConfigletForDevice ... -func (c CvpRestAPI) GenerateConfigletForDevice(dev *NetElement, builder *ConfigletBuilder) (*Configlet, error) { +func (c CvpRestAPI) GenerateConfigletForDevice(dev *NetElement, + builder *ConfigletBuilder) (*Configlet, error) { if dev == nil { return nil, errors.Errorf("GenerateConfigletForDevice: dev nil") } diff --git a/api/provisioning.go b/api/provisioning.go index 61655e9..a87bf4f 100644 --- a/api/provisioning.go +++ b/api/provisioning.go @@ -660,7 +660,8 @@ func (c CvpRestAPI) ValidateAndApplyConfigletsToDevice(appName string, dev *NetE } // Run Validation of new configlets to be applied - validateResp, err := c.ValidateConfigletsForDevice(dev.SystemMacAddress, configletAndBuilders.keys) + validateResp, err := c.ValidateConfigletsForDevice(dev.SystemMacAddress, + configletAndBuilders.keys) if err != nil { return nil, errors.Errorf("ApplyConfigletsToDevice: %s", err) } diff --git a/api/provisioning_test.go b/api/provisioning_test.go index e361346..621da54 100644 --- a/api/provisioning_test.go +++ b/api/provisioning_test.go @@ -144,7 +144,8 @@ func Test_checkConfigMapping(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, configletAndBuilders, err := checkConfigMapping(tt.args.applied, tt.args.newconfiglets) + got, configletAndBuilders, err := checkConfigMapping(tt.args.applied, + tt.args.newconfiglets) if (err != nil) != tt.wantErr { t.Errorf("checkConfigMapping() error = %v, wantErr %v", err, tt.wantErr) return @@ -246,11 +247,13 @@ func Test_checkRemoveConfigMapping(t *testing.T) { } if !reflect.DeepEqual(configletAndBuilders, tt.want1) { - t.Errorf("checkRemoveConfigMapping() configletAndBuilders = %v, want %v", configletAndBuilders, tt.want1) + t.Errorf("checkRemoveConfigMapping() configletAndBuilders = %v, want %v", + configletAndBuilders, tt.want1) } if !reflect.DeepEqual(rmConfigletAndBuilders, tt.want2) { - t.Errorf("checkRemoveConfigMapping() rmConfigletAndBuilders = %v, want %v", rmConfigletAndBuilders, tt.want2) + t.Errorf("checkRemoveConfigMapping() rmConfigletAndBuilders = %v, want %v", + rmConfigletAndBuilders, tt.want2) } }) } From 51900158a562f87b5b2fbd53acffc0486f181b7d Mon Sep 17 00:00:00 2001 From: Cheyne Womble Date: Thu, 24 Mar 2022 11:15:36 -0400 Subject: [PATCH 03/14] Fix lint/vet warnings --- api/cvprac_system_test.go | 3 ++- api/export_test.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/api/cvprac_system_test.go b/api/cvprac_system_test.go index eefe13f..31211e4 100644 --- a/api/cvprac_system_test.go +++ b/api/cvprac_system_test.go @@ -1,4 +1,5 @@ -// +build systest +//go:build systest + // // Copyright (c) 2016-2017, Arista Networks, Inc. All rights reserved. // diff --git a/api/export_test.go b/api/export_test.go index 2b92ef8..6f0a9f1 100644 --- a/api/export_test.go +++ b/api/export_test.go @@ -1,4 +1,5 @@ -// +build systest +//go:build systest + // // Copyright (c) 2016-2017, Arista Networks, Inc. All rights reserved. // From de4a6602b2cc83a164a44be45580e9ab841aa271 Mon Sep 17 00:00:00 2001 From: Cheyne Womble Date: Thu, 24 Mar 2022 11:18:23 -0400 Subject: [PATCH 04/14] Support resource APIs * Inspect uri and modify based on /api | /cvpservice or if CVaaS enabled. --- api/testutils_test.go | 3 ++ client/client.go | 68 +++++++++++++++++++++++++------------------ 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/api/testutils_test.go b/api/testutils_test.go index e6e6d0d..d844950 100644 --- a/api/testutils_test.go +++ b/api/testutils_test.go @@ -114,6 +114,7 @@ func LoadConfigFile(file string) (Config, error) { // assert fails the test if the condition is false. func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { + tb.Helper() if !condition { _, file, line, _ := runtime.Caller(1) tb.Fatalf("\033[31m%s:%d: "+msg+"\033[39m\n\n", @@ -123,6 +124,7 @@ func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { // ok fails the test if an err is not nil. func ok(tb testing.TB, err error) { + tb.Helper() if err != nil { _, file, line, _ := runtime.Caller(1) tb.Fatalf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", @@ -132,6 +134,7 @@ func ok(tb testing.TB, err error) { // equals fails the test if exp is not equal to act. func equals(tb testing.TB, exp, act interface{}) { + tb.Helper() if !reflect.DeepEqual(exp, act) { _, file, line, _ := runtime.Caller(1) tb.Fatalf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", diff --git a/client/client.go b/client/client.go index ef24a72..b0184f4 100644 --- a/client/client.go +++ b/client/client.go @@ -77,21 +77,21 @@ type authInfo struct { // CvpClient represents a CVP client api connection type CvpClient struct { cvpapi.ClientInterface - Hosts []string - HostPool *HostIterator - Port int - Protocol string - authInfo *authInfo - Timeout time.Duration - Transport http.RoundTripper - Client *resty.Client - SessID string - url string - API *cvpapi.CvpRestAPI - Debug bool - IsCvaas bool - Tenant string - Token string + Hosts []string + HostPool *HostIterator + Port int + Protocol string + authInfo *authInfo + Timeout time.Duration + Transport http.RoundTripper + Client *resty.Client + SessID string + urlPrefixShort string + API *cvpapi.CvpRestAPI + Debug bool + IsCvaas bool + Tenant string + Token string } // Option is a Client Option...function that sets a value and returns @@ -279,7 +279,7 @@ func (c *CvpClient) Connect(username string, password string) error { return c.createSession(true) } -// Connect to CVP with a token. Takes the cvpToken parameter as an input for the string. +// Connect to CVP with a token. Takes the cvpToken parameter as an input for the string. func (c *CvpClient) ConnectWithToken(cvpToken string) error { c.Token = cvpToken return c.createSession(true) @@ -312,10 +312,7 @@ func (c *CvpClient) initSession(host string) error { return errors.Errorf("initSession: No host provided") } - c.url = fmt.Sprintf("%s://%s:%d", c.Protocol, host, c.GetPort()) - if !c.IsCvaas { - c.url = c.url + "/web" - } + c.urlPrefixShort = fmt.Sprintf("%s://%s:%d", c.Protocol, host, c.GetPort()) c.Client = resty.New() @@ -331,7 +328,6 @@ func (c *CvpClient) initSession(host string) error { c.Client.SetAuthToken(c.Token) } - c.Client.SetHostURL(c.url) c.Client.SetHeaders(headers) c.Client.SetTimeout(c.Timeout) c.Client.SetDebug(c.Debug) @@ -351,7 +347,9 @@ func (c *CvpClient) resetSession() error { } func (c *CvpClient) login() error { - if c.Token != "" { // If a token exists do not use one of the logincvaas or loginonprem and do not create a cookie the auth header is used with the token. + // If a token exists do not use one of the logincvaas or loginonprem + // and do not create a cookie the auth header is used with the token. + if c.Token != "" { return nil } if c.IsCvaas { @@ -368,7 +366,9 @@ func (c *CvpClient) loginCvaas() error { request := c.Client.R() auth := `{"org":"` + c.Tenant + `", "name":"` + c.authInfo.Username + `", "password":"` + c.authInfo.Password + `"}` - resp, err := request.SetBody(auth).Post("/api/v1/oauth?provider=local&next=false") + + resp, err := request.SetBody(auth).Post(c.urlPrefixShort + + "/api/v1/oauth?provider=local&next=false") if err != nil { return errors.Wrap(err, "login") } @@ -390,7 +390,7 @@ func (c *CvpClient) loginOnPrem() error { auth := "{\"userId\":\"" + c.authInfo.Username + "\", \"password\":\"" + c.authInfo.Password + "\"}" - resp, err := request.SetBody(auth).Post("/login/authenticate.do") + resp, err := request.SetBody(auth).Post(c.urlPrefixShort + "/web/login/authenticate.do") if err != nil { return errors.Wrap(err, "login") } @@ -416,7 +416,18 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, var formattedParams map[string]string if c.Client == nil { - return nil, errors.Errorf("makeRequest: No valid session to CVP [%s]", c.url) + return nil, errors.Errorf("makeRequest: No valid session to CVP [%s]", + c.urlPrefixShort) + } + + var fullURL string + if strings.Contains(url, "/api/") || strings.Contains(url, "/cvpservice/") { + fullURL = c.urlPrefixShort + url + } else if c.IsCvaas { + // For CVaaS use cvpservice instead of web or api + fullURL = c.urlPrefixShort + "/cvpservice" + url + } else { + fullURL = c.urlPrefixShort + "/web" + url } retryCnt := NumRetryRequests @@ -456,11 +467,11 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, // Check reqType switch reqType { case "GET": - resp, err = request.Get(url) + resp, err = request.Get(fullURL) case "POST": - resp, err = request.SetBody(data).Post(url) + resp, err = request.SetBody(data).Post(fullURL) case "DELETE": - resp, err = request.SetBody(data).Delete(url) + resp, err = request.SetBody(data).Delete(fullURL) default: return nil, errors.Errorf("Invalid. Request type [%s] not implemented", reqType) } @@ -558,7 +569,6 @@ func checkResponse(resp *resty.Response) error { if err := checkResponseStatus(resp); err != nil { return errors.Wrap(err, "checkResponse") } - var info cvpapi.ErrorResponse if err := json.Unmarshal(resp.Body(), &info); err != nil { return errors.Wrap(err, "checkResponse") From 763e163d2c105beaa3db0fc403c5b79fe3521968 Mon Sep 17 00:00:00 2001 From: Cheyne Womble Date: Thu, 24 Mar 2022 11:20:20 -0400 Subject: [PATCH 05/14] Minor EOF newline update --- examples/example1/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/example1/main.go b/examples/example1/main.go index bf04967..49ddd73 100644 --- a/examples/example1/main.go +++ b/examples/example1/main.go @@ -62,4 +62,4 @@ func main() { } fmt.Printf("Configlets: %v\n", configletList) -} \ No newline at end of file +} From de506e4243a787ba3af14f30a80d6bb932044091 Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Thu, 26 Jan 2023 17:15:15 +0000 Subject: [PATCH 06/14] Initial work on Change Control Resource getters, scheduling Adds support for getting all ChangeControls, single lookup by id, and addition of a schedule by id. This currently also includes a rough hack for enabling resource APIs that may be duplicating work on origin/resource-api. --- api/resources_changecontrol_v1.go | 226 ++++++++++++++++++++++++++++++ client/client.go | 13 ++ 2 files changed, 239 insertions(+) create mode 100644 api/resources_changecontrol_v1.go diff --git a/api/resources_changecontrol_v1.go b/api/resources_changecontrol_v1.go new file mode 100644 index 0000000..5269bc2 --- /dev/null +++ b/api/resources_changecontrol_v1.go @@ -0,0 +1,226 @@ +// +// Copyright (c) 2016-2017, Arista Networks, Inc. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of Arista Networks nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +// IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +package cvpapi + +import ( + "bytes" + "encoding/json" + "fmt" + "net/url" + "time" + + "github.com/pkg/errors" +) + +type ResultRsc struct { + Result ValueRsc +} + +type ValueRsc struct { + Value json.RawMessage + Time time.Time + Type string +} + +type ChangeControlKeyRsc struct { + Id string +} + +type StringMap struct { + Values map[string]string +} + +type StringList struct { + Values []string +} + +type ActionRsc struct { + Name string + Timeout uint32 + Args StringMap +} + +type StageRsc struct { + Name string + Action *ActionRsc + Rows struct { + Values []StringList + } + Status *string + Error *string +} + +type ChangeRsc struct { + Name string + RootStageId string + Stages struct { + Values map[string]StageRsc + } + Notes string + User string + Time time.Time +} + +type FlagRsc struct { + Value bool + Notes string + Time time.Time + User string +} + +type FlagConfigRsc struct { + Value bool + Notes string +} + +type TimestampFlagRsc struct { + Value time.Time + Notes string + Time time.Time + User string +} + +type TimestampFlagConfigRsc struct { + Value bool + Notes string +} + +type ChangeControlRsc struct { + Key ChangeControlKeyRsc + Change ChangeRsc + Flag *FlagRsc + Start *FlagRsc + Status *string + Schedule *TimestampFlagRsc + + Error *string +} + +func resultList(data []byte) ([]ResultRsc, error) { + out := []ResultRsc{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + + for decoder.More() { + el := ResultRsc{} + + if err := decoder.Decode(&el); err != nil { + return nil, err + } + + out = append(out, el) + } + + return out, nil +} + +// GetChangeControlsRsc returns a list of `ChangeControlRsc`s via the +// ChangeControl resource API availablre on CVP 2021.2.0 or newer. +func (c CvpRestAPI) GetChangeControlsRsc() ([]ChangeControlRsc, error) { + ccs := []ChangeControlRsc{} + resp, err := c.client.Get("/api/resources/changecontrol/v1/ChangeControl/all", nil) + + if err != nil { + return nil, errors.Errorf("GetChangeControlsRsc: %s", + err) + } + + results, err := resultList(resp) + + if err != nil { + return nil, errors.Errorf("GetChangeControlsRsc: %s", + err) + } + + for _, result := range results { + var cc ChangeControlRsc + if err = json.Unmarshal(result.Result.Value, &cc); err != nil { + return nil, errors.Errorf("GetChangeControlsRsc: %s Payload:\n%s", + err, resp) + } + ccs = append(ccs, cc) + } + + return ccs, nil +} + +func (c CvpRestAPI) GetChangeControlRsc(key string) (ChangeControlRsc, error) { + result := ValueRsc{} + cc := ChangeControlRsc{} + + query := &url.Values{ + "key.id": {key}, + } + + resp, err := c.client.Get("/api/resources/changecontrol/v1/ChangeControl", query) + + if err != nil { + return cc, errors.Errorf("GetChangeControlRsc: %s", + err) + } + + if err = json.Unmarshal(resp, &result); err != nil { + return cc, errors.Errorf("GetChangeControlRsc [Result]: %s Payload:\n%s", + err, resp) + } + + if err = json.Unmarshal(result.Value, &cc); err != nil { + return cc, errors.Errorf("GetChangeControlRsc [Inner]: %s Payload:\n%s", + err, resp) + } + + return cc, nil +} + +func (c CvpRestAPI) ScheduleChangeControlRsc(key string, sched_time time.Time, notes string) error { + data := map[string]interface{}{ + "key": map[string]interface{}{"id": key}, + "schedule": map[string]interface{}{"value": sched_time, "notes": notes}, + // "time": time.Now(), + } + + js, err := json.Marshal(data) + + // if err != nil { + // err = errors.Errorf("ScheduleChangeControlRsc: %s", err) + // } + + resp, err := c.client.Post("/api/resources/changecontrol/v1/ChangeControlConfig", nil, data) + + if err != nil { + err = errors.Errorf("ScheduleChangeControlRsc: %s", err) + } + + fmt.Printf("Sched: got back %v from %v\n", string(resp), string(js)) + + return err +} diff --git a/client/client.go b/client/client.go index b0184f4..8fd9465 100644 --- a/client/client.go +++ b/client/client.go @@ -430,6 +430,10 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, fullURL = c.urlPrefixShort + "/web" + url } + // Resource queries on CVP >v3 (non-CVaaS) need to route through "/api/...", + // but existing library endpoints do not. This is simpler in the short term than adding + // /web to the front of all older endpoints. + stripWeb := strings.HasPrefix(url, "/api") && !c.IsCvaas retryCnt := NumRetryRequests if params != nil { @@ -464,6 +468,12 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, // Clear our errors err = nil + fmt.Printf("Req to URL: %v %v\n", c.url, url) + + // Temporarily strip and readd web/ prefix as needed + if stripWeb { + c.Client.SetHostURL(strings.TrimSuffix(c.url, "/web")) + } // Check reqType switch reqType { case "GET": @@ -475,6 +485,9 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, default: return nil, errors.Errorf("Invalid. Request type [%s] not implemented", reqType) } + if stripWeb { + c.Client.SetHostURL(c.url) + } if err != nil { return nil, err From a95d1c6f8f572a3151a84df8487e63e01b6ebbc5 Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Fri, 27 Jan 2023 15:28:02 +0000 Subject: [PATCH 07/14] ChangeControl: Add Descheduling Includes other assorted cleanup to make marshaling behave correctly. Error conditions seem to behave differently from what the API docs indicate, at least from what my manual prodding has shown. Additionally, it doesn't seem possible to manually enforce a user and date as the source of these changes -- looking at an API-written schedule on CVP 2022.1.0, the content is correct but has no user and a zero timestamp. Inserting these details ourselves gives a 400 Bad Request. --- api/resources_changecontrol_v1.go | 145 ++++++++++++++++++------------ client/client.go | 4 +- 2 files changed, 90 insertions(+), 59 deletions(-) diff --git a/api/resources_changecontrol_v1.go b/api/resources_changecontrol_v1.go index 5269bc2..17cc3a6 100644 --- a/api/resources_changecontrol_v1.go +++ b/api/resources_changecontrol_v1.go @@ -34,7 +34,6 @@ package cvpapi import ( "bytes" "encoding/json" - "fmt" "net/url" "time" @@ -42,89 +41,91 @@ import ( ) type ResultRsc struct { - Result ValueRsc + Result ValueRsc `json:"result"` } type ValueRsc struct { - Value json.RawMessage - Time time.Time - Type string + Value json.RawMessage `json:"value"` + Time time.Time `json:"time"` + Type string `json:"type"` } type ChangeControlKeyRsc struct { - Id string + Id string `json:"id"` } type StringMap struct { - Values map[string]string + Values map[string]string `json:"values"` } type StringList struct { - Values []string + Values []string `json:"values"` } type ActionRsc struct { - Name string - Timeout uint32 - Args StringMap + Name string `json:"name"` + Timeout uint32 `json:"timeout"` + Args StringMap `json:"args"` } type StageRsc struct { - Name string - Action *ActionRsc + Name string `json:"name"` + Action *ActionRsc `json:"action"` Rows struct { - Values []StringList - } - Status *string - Error *string + Values []StringList `json:"values"` + } `json:"rows"` + Status *string `json:"status"` + Error *string `json:"error"` } type ChangeRsc struct { - Name string - RootStageId string + Name string `json:"name"` + RootStageId string `json:"rootStageId"` Stages struct { - Values map[string]StageRsc - } - Notes string - User string - Time time.Time + Values map[string]StageRsc `json:"values"` + } `json:"stages"` + Notes string `json:"notes"` + User string `json:"user"` + Time time.Time `json:"time"` } type FlagRsc struct { - Value bool - Notes string - Time time.Time - User string + Value bool `json:"value"` + Notes string `json:"notes"` + Time time.Time `json:"time"` + User string `json:"user"` } type FlagConfigRsc struct { - Value bool - Notes string + Value bool `json:"value"` + Notes string `json:"notes"` } type TimestampFlagRsc struct { - Value time.Time - Notes string - Time time.Time - User string + Value *time.Time `json:"value,omitempty"` + Notes string `json:"notes"` + Time *time.Time `json:"time,omitempty"` + User *string `json:"user,omitempty"` } type TimestampFlagConfigRsc struct { - Value bool - Notes string + Value bool `json:"value"` + Notes string `json:"notes"` } type ChangeControlRsc struct { - Key ChangeControlKeyRsc - Change ChangeRsc - Flag *FlagRsc - Start *FlagRsc - Status *string - Schedule *TimestampFlagRsc - - Error *string + Key ChangeControlKeyRsc `json:"key"` + Change *ChangeRsc `json:"change,omitempty"` + Flag *FlagRsc `json:"flag,omitempty"` + Start *FlagRsc `json:"start,omitempty"` + Status *string `json:"status,omitempty"` + Schedule *TimestampFlagRsc `json:"schedule,omitempty"` + + Error *string `json:"error,omitempty"` } +// Resource APIs hand us back ndJson on /all endpoints. +// This converts a call into a list of usable Json objects. func resultList(data []byte) ([]ResultRsc, error) { out := []ResultRsc{} @@ -144,7 +145,9 @@ func resultList(data []byte) ([]ResultRsc, error) { } // GetChangeControlsRsc returns a list of `ChangeControlRsc`s via the -// ChangeControl resource API availablre on CVP 2021.2.0 or newer. +// ChangeControl resource API available. +// +// This endpoint is available on CVP 2021.2.0 or newer. func (c CvpRestAPI) GetChangeControlsRsc() ([]ChangeControlRsc, error) { ccs := []ChangeControlRsc{} resp, err := c.client.Get("/api/resources/changecontrol/v1/ChangeControl/all", nil) @@ -173,6 +176,10 @@ func (c CvpRestAPI) GetChangeControlsRsc() ([]ChangeControlRsc, error) { return ccs, nil } +// GetChangeControsRsc returns a single of `ChangeControlRsc` matching the given +// `key` via the ChangeControl resource API available. +// +// This endpoint is available on CVP 2021.2.0 or newer. func (c CvpRestAPI) GetChangeControlRsc(key string) (ChangeControlRsc, error) { result := ValueRsc{} cc := ChangeControlRsc{} @@ -201,26 +208,52 @@ func (c CvpRestAPI) GetChangeControlRsc(key string) (ChangeControlRsc, error) { return cc, nil } +// ScheduleChangeControlRsc schedules the Change Control given by `key` to occur +// at `sched_time`, with optional `notes`. +// +// This endpoint is available on CVP 2022.1.0 or newer. func (c CvpRestAPI) ScheduleChangeControlRsc(key string, sched_time time.Time, notes string) error { - data := map[string]interface{}{ - "key": map[string]interface{}{"id": key}, - "schedule": map[string]interface{}{"value": sched_time, "notes": notes}, - // "time": time.Now(), - } + cfg := ChangeControlRsc{} + cfg.Key.Id = key - js, err := json.Marshal(data) + // Note: The API seems to disallow setting the user or time on cvprac v2022.1.0 -- not tested others. + sched := TimestampFlagRsc{} + sched.Value = &sched_time + sched.Notes = notes - // if err != nil { - // err = errors.Errorf("ScheduleChangeControlRsc: %s", err) - // } + cfg.Schedule = &sched - resp, err := c.client.Post("/api/resources/changecontrol/v1/ChangeControlConfig", nil, data) + _, err := c.client.Post("/api/resources/changecontrol/v1/ChangeControlConfig", nil, cfg) if err != nil { err = errors.Errorf("ScheduleChangeControlRsc: %s", err) } - fmt.Printf("Sched: got back %v from %v\n", string(resp), string(js)) + // FIXME: check error handling of the top-level struct received. + // Scheduling unapproved jobs seems to have no issue, scheduling unapproved + // causes the change control to become unapproved again, counter to what docs say... + + return err +} + +// DescheduleChangeControlRsc removes any schedule data from a Change Control given by `key`. +// +// This endpoint is available on CVP 2022.1.0 or newer. +func (c CvpRestAPI) DescheduleChangeControlRsc(key string, notes string) error { + cfg := ChangeControlRsc{} + cfg.Key.Id = key + + // Note: The API seems to disallow setting the user or time on cvprac v2022.1.0 -- not tested others. + sched := TimestampFlagRsc{} + sched.Notes = notes + + cfg.Schedule = &sched + + _, err := c.client.Post("/api/resources/changecontrol/v1/ChangeControlConfig", nil, cfg) + + if err != nil { + err = errors.Errorf("DescheduleChangeControlRsc: %s", err) + } return err } diff --git a/client/client.go b/client/client.go index 8fd9465..f5cb901 100644 --- a/client/client.go +++ b/client/client.go @@ -468,9 +468,7 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, // Clear our errors err = nil - fmt.Printf("Req to URL: %v %v\n", c.url, url) - - // Temporarily strip and readd web/ prefix as needed + // Temporarily strip and readd "web/" prefix as needed if stripWeb { c.Client.SetHostURL(strings.TrimSuffix(c.url, "/web")) } From 97fa6d5eca89235ea82d166eb239c16500a8a986 Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Tue, 31 Jan 2023 10:34:55 +0000 Subject: [PATCH 08/14] Rebase onto origin/resource-api Reverts some early changes for URL processing in favour of the other in-progress branch. --- client/client.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/client/client.go b/client/client.go index f5cb901..48aa08f 100644 --- a/client/client.go +++ b/client/client.go @@ -420,6 +420,8 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, c.urlPrefixShort) } + // Resource queries on CVP >v3 (on-prem) need to route through "/api/...", + // but existing library endpoints do not. var fullURL string if strings.Contains(url, "/api/") || strings.Contains(url, "/cvpservice/") { fullURL = c.urlPrefixShort + url @@ -430,10 +432,6 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, fullURL = c.urlPrefixShort + "/web" + url } - // Resource queries on CVP >v3 (non-CVaaS) need to route through "/api/...", - // but existing library endpoints do not. This is simpler in the short term than adding - // /web to the front of all older endpoints. - stripWeb := strings.HasPrefix(url, "/api") && !c.IsCvaas retryCnt := NumRetryRequests if params != nil { @@ -468,10 +466,6 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, // Clear our errors err = nil - // Temporarily strip and readd "web/" prefix as needed - if stripWeb { - c.Client.SetHostURL(strings.TrimSuffix(c.url, "/web")) - } // Check reqType switch reqType { case "GET": @@ -483,9 +477,6 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, default: return nil, errors.Errorf("Invalid. Request type [%s] not implemented", reqType) } - if stripWeb { - c.Client.SetHostURL(c.url) - } if err != nil { return nil, err From 81d0a1c708a17a522d60846dee6d135ee355437a Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Tue, 31 Jan 2023 16:26:01 +0000 Subject: [PATCH 09/14] ChangeControl: Add Create method Extends/modifies the ChangeControl resource types to allow for correct serialisation of create requests, and adds a method mirroring (python) cvprac to create a change control. --- api/resources_changecontrol_v1.go | 91 +++++++++++++++++++++++++------ client/client.go | 3 + 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/api/resources_changecontrol_v1.go b/api/resources_changecontrol_v1.go index 17cc3a6..d6cc29f 100644 --- a/api/resources_changecontrol_v1.go +++ b/api/resources_changecontrol_v1.go @@ -34,14 +34,16 @@ package cvpapi import ( "bytes" "encoding/json" + "fmt" "net/url" "time" "github.com/pkg/errors" ) -type ResultRsc struct { - Result ValueRsc `json:"result"` +type resultRsc struct { + Result ValueRsc `json:"result"` + Time *time.Time `json:"time,omitempty"` } type ValueRsc struct { @@ -62,6 +64,10 @@ type StringList struct { Values []string `json:"values"` } +type StringMatrix struct { + Values []StringList `json:"values"` +} + type ActionRsc struct { Name string `json:"name"` Timeout uint32 `json:"timeout"` @@ -69,13 +75,11 @@ type ActionRsc struct { } type StageRsc struct { - Name string `json:"name"` - Action *ActionRsc `json:"action"` - Rows struct { - Values []StringList `json:"values"` - } `json:"rows"` - Status *string `json:"status"` - Error *string `json:"error"` + Name string `json:"name"` + Action *ActionRsc `json:"action,omitempty"` + Rows *StringMatrix `json:"rows,omitempty"` + Status *string `json:"status,omitempty"` + Error *string `json:"error,omitempty"` } type ChangeRsc struct { @@ -84,9 +88,9 @@ type ChangeRsc struct { Stages struct { Values map[string]StageRsc `json:"values"` } `json:"stages"` - Notes string `json:"notes"` - User string `json:"user"` - Time time.Time `json:"time"` + Notes *string `json:"notes,omitempty"` + User *string `json:"user,omitempty"` + Time *time.Time `json:"time,omitempty"` } type FlagRsc struct { @@ -126,13 +130,13 @@ type ChangeControlRsc struct { // Resource APIs hand us back ndJson on /all endpoints. // This converts a call into a list of usable Json objects. -func resultList(data []byte) ([]ResultRsc, error) { - out := []ResultRsc{} +func resultList(data []byte) ([]resultRsc, error) { + out := []resultRsc{} decoder := json.NewDecoder(bytes.NewReader(data)) for decoder.More() { - el := ResultRsc{} + el := resultRsc{} if err := decoder.Decode(&el); err != nil { return nil, err @@ -176,7 +180,7 @@ func (c CvpRestAPI) GetChangeControlsRsc() ([]ChangeControlRsc, error) { return ccs, nil } -// GetChangeControsRsc returns a single of `ChangeControlRsc` matching the given +// GetChangeControlRsc returns a single `ChangeControlRsc` matching the given // `key` via the ChangeControl resource API available. // // This endpoint is available on CVP 2021.2.0 or newer. @@ -208,6 +212,61 @@ func (c CvpRestAPI) GetChangeControlRsc(key string) (ChangeControlRsc, error) { return cc, nil } +// CreateChangeControlRsc creates a new `ChangeControlRsc` with the given uuid `key`, +// `name`, and task list (in serial or parallel). +// +// This endpoint is available on CVP 2021.2.0 or newer. +func (c CvpRestAPI) CreateChangeControlRsc(key, name string, tasks []string, sequential bool) error { + change := ChangeRsc{ + Name: name, + RootStageId: "root", + } + change.Stages.Values = make(map[string]StageRsc) + + cfg := ChangeControlRsc{Change: &change} + cfg.Key.Id = key + + rootStage := StageRsc{ + Name: "root", + } + + var rows []StringList + + for i, task := range tasks { + localName := fmt.Sprintf("stage%d", i) + localAction := ActionRsc{Name: "task"} + localAction.Args.Values = make(map[string]string) + localAction.Args.Values["TaskID"] = task + localStage := StageRsc{ + Name: localName, + Action: &localAction, + } + + change.Stages.Values[localName] = localStage + + if sequential || i == 0 { + el := StringList{Values: []string{localName}} + rows = append(rows, el) + // change.Stages.Values["root"].Rows.Values + } else { + rows[0].Values = append(rows[0].Values, localName) + } + } + + matrix := StringMatrix{Values: rows} + rootStage.Rows = &matrix + + change.Stages.Values["root"] = rootStage + + _, err := c.client.Post("/api/resources/changecontrol/v1/ChangeControlConfig", nil, cfg) + + if err != nil { + err = errors.Errorf("CreateChangeControlRsc: %s", err) + } + + return err +} + // ScheduleChangeControlRsc schedules the Change Control given by `key` to occur // at `sched_time`, with optional `notes`. // diff --git a/client/client.go b/client/client.go index 48aa08f..0876bb6 100644 --- a/client/client.go +++ b/client/client.go @@ -517,6 +517,9 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, // client error if status != http.StatusOK { // retry another session + + // FIXME: include body here? + // This is important for debugging resource APIs. err = errors.Errorf("Status [%d]", status) continue } From dc5800b616f64ee6a8862296aee1f104327da878 Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Wed, 1 Feb 2023 15:13:18 +0000 Subject: [PATCH 10/14] ChangeControl: Add deletion, approval get/set methods --- api/resources_changecontrol_v1.go | 291 +++++++++++++++++++++++++----- client/client.go | 6 +- 2 files changed, 251 insertions(+), 46 deletions(-) diff --git a/api/resources_changecontrol_v1.go b/api/resources_changecontrol_v1.go index d6cc29f..e21c820 100644 --- a/api/resources_changecontrol_v1.go +++ b/api/resources_changecontrol_v1.go @@ -41,6 +41,9 @@ import ( "github.com/pkg/errors" ) +// FIXME: document structs and factor out common properties. +// FIXME: break out shared utilities (resultRsc) to "resources.go"? + type resultRsc struct { Result ValueRsc `json:"result"` Time *time.Time `json:"time,omitempty"` @@ -60,6 +63,10 @@ type StringMap struct { Values map[string]string `json:"values"` } +type StageMap struct { + Values map[string]StageRsc `json:"values"` +} + type StringList struct { Values []string `json:"values"` } @@ -83,21 +90,19 @@ type StageRsc struct { } type ChangeRsc struct { - Name string `json:"name"` - RootStageId string `json:"rootStageId"` - Stages struct { - Values map[string]StageRsc `json:"values"` - } `json:"stages"` - Notes *string `json:"notes,omitempty"` - User *string `json:"user,omitempty"` - Time *time.Time `json:"time,omitempty"` + Name *string `json:"name,omitempty"` + RootStageId *string `json:"rootStageId,omitempty"` + Stages *StageMap `json:"stages,omitempty"` + Notes *string `json:"notes,omitempty"` + User *string `json:"user,omitempty"` + Time *time.Time `json:"time,omitempty"` } type FlagRsc struct { - Value bool `json:"value"` - Notes string `json:"notes"` - Time time.Time `json:"time"` - User string `json:"user"` + Value bool `json:"value"` + Notes string `json:"notes"` + Time *time.Time `json:"time,omitempty"` + User string `json:"user,omitempty"` } type FlagConfigRsc struct { @@ -128,6 +133,12 @@ type ChangeControlRsc struct { Error *string `json:"error,omitempty"` } +type ApproveConfigRsc struct { + Key ChangeControlKeyRsc `json:"key"` + Approve FlagConfigRsc `json:"approve"` + Version *time.Time `json:"version,omitempty"` +} + // Resource APIs hand us back ndJson on /all endpoints. // This converts a call into a list of usable Json objects. func resultList(data []byte) ([]resultRsc, error) { @@ -148,8 +159,42 @@ func resultList(data []byte) ([]resultRsc, error) { return out, nil } +func deserChangeControl(data []byte) (*ChangeControlRsc, error) { + result := ValueRsc{} + cc := ChangeControlRsc{} + + if err := json.Unmarshal(data, &result); err != nil { + return nil, errors.Errorf("ChangeControlRsc [Result]: %s Payload:\n%s", + err, data) + } + + if err := json.Unmarshal(result.Value, &cc); err != nil { + return nil, errors.Errorf("ChangeControlRsc [Inner]: %s Payload:\n%s", + err, data) + } + + return &cc, nil +} + +func deserApproval(data []byte) (*ApproveConfigRsc, error) { + result := ValueRsc{} + cc := ApproveConfigRsc{} + + if err := json.Unmarshal(data, &result); err != nil { + return nil, errors.Errorf("ApproveConfigRsc [Result]: %s Payload:\n%s", + err, data) + } + + if err := json.Unmarshal(result.Value, &cc); err != nil { + return nil, errors.Errorf("ApproveConfigRsc [Inner]: %s Payload:\n%s", + err, data) + } + + return &cc, nil +} + // GetChangeControlsRsc returns a list of `ChangeControlRsc`s via the -// ChangeControl resource API available. +// ChangeControl resource API. // // This endpoint is available on CVP 2021.2.0 or newer. func (c CvpRestAPI) GetChangeControlsRsc() ([]ChangeControlRsc, error) { @@ -181,47 +226,57 @@ func (c CvpRestAPI) GetChangeControlsRsc() ([]ChangeControlRsc, error) { } // GetChangeControlRsc returns a single `ChangeControlRsc` matching the given -// `key` via the ChangeControl resource API available. +// `ccId` via the ChangeControl resource API. // // This endpoint is available on CVP 2021.2.0 or newer. -func (c CvpRestAPI) GetChangeControlRsc(key string) (ChangeControlRsc, error) { - result := ValueRsc{} - cc := ChangeControlRsc{} - +func (c CvpRestAPI) GetChangeControlRsc(ccId string) (*ChangeControlRsc, error) { query := &url.Values{ - "key.id": {key}, + "key.id": {ccId}, } resp, err := c.client.Get("/api/resources/changecontrol/v1/ChangeControl", query) if err != nil { - return cc, errors.Errorf("GetChangeControlRsc: %s", + return nil, errors.Errorf("GetChangeControlRsc: %s", err) } - if err = json.Unmarshal(resp, &result); err != nil { - return cc, errors.Errorf("GetChangeControlRsc [Result]: %s Payload:\n%s", - err, resp) + return deserChangeControl(resp) +} + +// DeleteChangeControlRsc removes a single `ChangeControlRsc` matching the given +// `ccId` via the ChangeControl resource API. +// +// This method will fail if a ChangeControl has finished running. +// +// This endpoint is available on CVP 2021.2.0 or newer. +func (c CvpRestAPI) DeleteChangeControlRsc(ccId string) error { + query := &url.Values{ + "key.id": {ccId}, } - if err = json.Unmarshal(result.Value, &cc); err != nil { - return cc, errors.Errorf("GetChangeControlRsc [Inner]: %s Payload:\n%s", - err, resp) + _, err := c.client.Delete("/api/resources/changecontrol/v1/ChangeControlConfig", query, nil) + + if err != nil { + return errors.Errorf("DeleteChangeControlRsc: %s", + err) } - return cc, nil + return nil } -// CreateChangeControlRsc creates a new `ChangeControlRsc` with the given uuid `key`, +// CreateChangeControlRsc creates a new `ChangeControlRsc` with the given uuid `ccId`, // `name`, and task list (in serial or parallel). // -// This endpoint is available on CVP 2021.2.0 or newer. -func (c CvpRestAPI) CreateChangeControlRsc(key, name string, tasks []string, sequential bool) error { +// This endpoint is available on CVP 2021.1.0 or newer. +func (c CvpRestAPI) CreateChangeControlRsc(key, name string, tasks []string, sequential bool) (*ChangeControlRsc, error) { + root := "root" + stages := StageMap{Values: make(map[string]StageRsc)} change := ChangeRsc{ - Name: name, - RootStageId: "root", + Name: &name, + RootStageId: &root, + Stages: &stages, } - change.Stages.Values = make(map[string]StageRsc) cfg := ChangeControlRsc{Change: &change} cfg.Key.Id = key @@ -258,26 +313,96 @@ func (c CvpRestAPI) CreateChangeControlRsc(key, name string, tasks []string, seq change.Stages.Values["root"] = rootStage - _, err := c.client.Post("/api/resources/changecontrol/v1/ChangeControlConfig", nil, cfg) + resp, err := c.client.Post("/api/resources/changecontrol/v1/ChangeControlConfig", nil, cfg) if err != nil { err = errors.Errorf("CreateChangeControlRsc: %s", err) + return nil, err + } + + return deserChangeControl(resp) +} + +// AddNoteToChangeControlRsc adds a note to the `ChangeControlRsc` matching +// `ccId` via the ChangeControl resource API. +// +// This endpoint is available on CVP 2021.2.0 or newer. +func (c CvpRestAPI) AddNoteToChangeControlRsc(ccId, notes string) error { + change := ChangeRsc{ + Notes: ¬es, + } + + cfg := ChangeControlRsc{Change: &change} + cfg.Key.Id = ccId + + rsp, err := c.client.Post("/api/resources/changecontrol/v1/ChangeControlConfig", nil, cfg) + + if err != nil { + err = errors.Errorf("AddNoteToChangeControlRsc: %s", err) + } + + fmt.Printf("R: %+v\n", string(rsp)) + + return err +} + +// StartChangeControlRsc starts the the `ChangeControlRsc` matching the uuid +// `ccId` via the ChangeControl resource API, with an expository `note`. +// +// Will return an error if the matching change control has not yet been approved. +// +// This endpoint is available on CVP 2021.2.0 or newer. +func (c CvpRestAPI) StartChangeControlRsc(ccId, notes string) error { + start := FlagRsc{ + Notes: notes, + Value: true, + } + + cfg := ChangeControlRsc{Start: &start} + cfg.Key.Id = ccId + + _, err := c.client.Post("/api/resources/changecontrol/v1/ChangeControlConfig", nil, cfg) + + if err != nil { + err = errors.Errorf("StartChangeControlRsc: %s", err) } return err } -// ScheduleChangeControlRsc schedules the Change Control given by `key` to occur -// at `sched_time`, with optional `notes`. +// StopChangeControlRsc stops the the `ChangeControlRsc` matching the uuid +// `ccId` via the ChangeControl resource API, with an expository `note`. +// +// This endpoint is available on CVP 2021.2.0 or newer. +func (c CvpRestAPI) StopChangeControlRsc(ccId, notes string) error { + start := FlagRsc{ + Notes: notes, + Value: false, + } + + cfg := ChangeControlRsc{Start: &start} + cfg.Key.Id = ccId + + _, err := c.client.Post("/api/resources/changecontrol/v1/ChangeControlConfig", nil, cfg) + + if err != nil { + err = errors.Errorf("StopChangeControlRsc: %s", err) + } + + return err +} + +// ScheduleChangeControlRsc schedules the Change Control given by `ccId` to occur +// at `schedTime`, with optional `notes`. // // This endpoint is available on CVP 2022.1.0 or newer. -func (c CvpRestAPI) ScheduleChangeControlRsc(key string, sched_time time.Time, notes string) error { +func (c CvpRestAPI) ScheduleChangeControlRsc(ccId string, schedTime time.Time, notes string) error { cfg := ChangeControlRsc{} - cfg.Key.Id = key + cfg.Key.Id = ccId // Note: The API seems to disallow setting the user or time on cvprac v2022.1.0 -- not tested others. sched := TimestampFlagRsc{} - sched.Value = &sched_time + sched.Value = &schedTime sched.Notes = notes cfg.Schedule = &sched @@ -291,16 +416,18 @@ func (c CvpRestAPI) ScheduleChangeControlRsc(key string, sched_time time.Time, n // FIXME: check error handling of the top-level struct received. // Scheduling unapproved jobs seems to have no issue, scheduling unapproved // causes the change control to become unapproved again, counter to what docs say... + // + // Error string can appear at several levels in the hierarchy return err } -// DescheduleChangeControlRsc removes any schedule data from a Change Control given by `key`. +// DescheduleChangeControlRsc removes any schedule data from a Change Control given by `ccId`. // // This endpoint is available on CVP 2022.1.0 or newer. -func (c CvpRestAPI) DescheduleChangeControlRsc(key string, notes string) error { +func (c CvpRestAPI) DescheduleChangeControlRsc(ccId, notes string) error { cfg := ChangeControlRsc{} - cfg.Key.Id = key + cfg.Key.Id = ccId // Note: The API seems to disallow setting the user or time on cvprac v2022.1.0 -- not tested others. sched := TimestampFlagRsc{} @@ -316,3 +443,83 @@ func (c CvpRestAPI) DescheduleChangeControlRsc(key string, notes string) error { return err } + +// GetChangeControlApprovalsRsc returns a list of `ApproveConfigRsc`s via the +// ChangeControl resource API. +// +// This endpoint is available on CVP 2021.2.0 or newer. +func (c CvpRestAPI) GetChangeControlApprovalsRsc() ([]ApproveConfigRsc, error) { + ccs := []ApproveConfigRsc{} + resp, err := c.client.Get("/api/resources/changecontrol/v1/ApproveConfig/all", nil) + + if err != nil { + return nil, errors.Errorf("GetChangeControlApprovalsRsc: %s", + err) + } + + results, err := resultList(resp) + + if err != nil { + return nil, errors.Errorf("GetChangeControlApprovalsRsc: %s", + err) + } + + for _, result := range results { + var cc ApproveConfigRsc + if err = json.Unmarshal(result.Result.Value, &cc); err != nil { + return nil, errors.Errorf("GetChangeControlApprovalsRsc: %s Payload:\n%s", + err, resp) + } + ccs = append(ccs, cc) + } + + return ccs, nil +} + +// GetChangeControlRsc returns a single `ChangeControlRsc` matching the given +// `ccId` via the ChangeControl resource API. +// +// This endpoint is available on CVP 2021.2.0 or newer. +func (c CvpRestAPI) GetChangeControlApprovalRsc(ccId string) (*ApproveConfigRsc, error) { + query := &url.Values{ + "key.id": {ccId}, + } + + resp, err := c.client.Get("/api/resources/changecontrol/v1/ApproveConfig", query) + + if err != nil { + return nil, errors.Errorf("GetChangeControlApprovalRsc: %s", + err) + } + + return deserApproval(resp) +} + +// ApproveChangeControlRsc sets the approval state for a single `ChangeControlRsc` matching the given +// `ccId` via the ChangeControl resource API. +// +// This requires a `version` matching the last modified timestamp of the intended change control +// (i.e., `.Change.Time`). This call will error if another modification has +// taken place, causing a `version` mismatch. +// +// This endpoint is available on CVP 2021.2.0 or newer. +// explain version. +func (c CvpRestAPI) ApproveChangeControlRsc(ccId string, approve bool, version time.Time, notes string) error { + flag := FlagConfigRsc{ + Value: approve, + Notes: notes, + } + approval := ApproveConfigRsc{ + Approve: flag, + Version: &version, + } + approval.Key.Id = ccId + + _, err := c.client.Post("/api/resources/changecontrol/v1/ApproveConfig", nil, approval) + + if err != nil { + err = errors.Errorf("ApproveChangeControlRsc: %s", err) + } + + return err +} diff --git a/client/client.go b/client/client.go index 0876bb6..1e810ed 100644 --- a/client/client.go +++ b/client/client.go @@ -517,10 +517,8 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, // client error if status != http.StatusOK { // retry another session - - // FIXME: include body here? - // This is important for debugging resource APIs. - err = errors.Errorf("Status [%d]", status) + // Body is included as it is important for debugging resource APIs. + err = errors.Errorf("Status [%d], Body %s", status, string(resp.Body())) continue } break From 41bf8a6fabfecc0f4a46e563c727991892cbd36e Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Thu, 2 Feb 2023 14:26:40 +0000 Subject: [PATCH 11/14] ChangeControl: Allow creation with custom stages --- api/resources_changecontrol_v1.go | 65 ++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/api/resources_changecontrol_v1.go b/api/resources_changecontrol_v1.go index e21c820..0c0d81f 100644 --- a/api/resources_changecontrol_v1.go +++ b/api/resources_changecontrol_v1.go @@ -268,8 +268,43 @@ func (c CvpRestAPI) DeleteChangeControlRsc(ccId string) error { // CreateChangeControlRsc creates a new `ChangeControlRsc` with the given uuid `ccId`, // `name`, and task list (in serial or parallel). // +// The user must ensure that any referenced tasks exist beforehand. +// +// This endpoint is available on CVP 2021.1.0 or newer. +func (c CvpRestAPI) CreateChangeControlRsc(ccId, name string, tasks []string, sequential bool) (*ChangeControlRsc, error) { + var rootStages [][]StageRsc + + for i, task := range tasks { + stageName := fmt.Sprintf("stage%d", i) + action := ActionRsc{Name: "task"} + action.Args.Values = make(map[string]string) + action.Args.Values["TaskID"] = task + stage := StageRsc{ + Name: stageName, + Action: &action, + } + + if sequential || i == 0 { + el := []StageRsc{stage} + rootStages = append(rootStages, el) + // change.Stages.Values["root"].Rows.Values + } else { + rootStages[0] = append(rootStages[0], stage) + } + } + + return c.CreateChangeControlWithActionsRsc(ccId, name, rootStages) +} + +// CreateChangeControlWithActionsRsc creates a new `ChangeControlRsc` with the given uuid `ccId`, +// `name`, executing the given `rootStages` attached to a new root node. `extraStages` can be optionally +// included as needed. +// +// `rootStages` consists of rows of 'StageRsc's which are visited serially. Elements in each row are +// run in parallel. +// // This endpoint is available on CVP 2021.1.0 or newer. -func (c CvpRestAPI) CreateChangeControlRsc(key, name string, tasks []string, sequential bool) (*ChangeControlRsc, error) { +func (c CvpRestAPI) CreateChangeControlWithActionsRsc(ccId, name string, rootStages [][]StageRsc, extraStages ...StageRsc) (*ChangeControlRsc, error) { root := "root" stages := StageMap{Values: make(map[string]StageRsc)} change := ChangeRsc{ @@ -279,7 +314,7 @@ func (c CvpRestAPI) CreateChangeControlRsc(key, name string, tasks []string, seq } cfg := ChangeControlRsc{Change: &change} - cfg.Key.Id = key + cfg.Key.Id = ccId rootStage := StageRsc{ Name: "root", @@ -287,25 +322,17 @@ func (c CvpRestAPI) CreateChangeControlRsc(key, name string, tasks []string, seq var rows []StringList - for i, task := range tasks { - localName := fmt.Sprintf("stage%d", i) - localAction := ActionRsc{Name: "task"} - localAction.Args.Values = make(map[string]string) - localAction.Args.Values["TaskID"] = task - localStage := StageRsc{ - Name: localName, - Action: &localAction, + for _, parallelStages := range rootStages { + var rowToAdd []string + for _, stage := range parallelStages { + rowToAdd = append(rowToAdd, stage.Name) + change.Stages.Values[stage.Name] = stage } + rows = append(rows, StringList{Values: rowToAdd}) + } - change.Stages.Values[localName] = localStage - - if sequential || i == 0 { - el := StringList{Values: []string{localName}} - rows = append(rows, el) - // change.Stages.Values["root"].Rows.Values - } else { - rows[0].Values = append(rows[0].Values, localName) - } + for _, stage := range extraStages { + change.Stages.Values[stage.Name] = stage } matrix := StringMatrix{Values: rows} From 6c875ec407ff701eb91947032f3980123b55c74f Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Mon, 6 Feb 2023 14:35:44 +0000 Subject: [PATCH 12/14] ChangeControl: Break out common datatypes, document structs --- api/resources.go | 159 +++++++++++++++++++++++ api/resources_changecontrol_v1.go | 203 +++++++++++++----------------- 2 files changed, 245 insertions(+), 117 deletions(-) create mode 100644 api/resources.go diff --git a/api/resources.go b/api/resources.go new file mode 100644 index 0000000..d79a546 --- /dev/null +++ b/api/resources.go @@ -0,0 +1,159 @@ +// +// Copyright (c) 2016-2023, Arista Networks, Inc. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of Arista Networks nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +// IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +package cvpapi + +import ( + "bytes" + "encoding/json" + "time" + + "github.com/pkg/errors" +) + +// Result type for a `GetRsc` or `GetsRsc` request. +type resultRsc struct { + // Response body. + Result *valueRsc `json:"result"` + // The time that this resource was last modified. + Time *time.Time `json:"time,omitempty"` + + resourceGetError +} + +// The response body for a `GetRsc` or `GetsRsc` request. +type valueRsc struct { + // Generic resource type. + Value json.RawMessage `json:"value"` + // Optional time at which this resource was last modified. + Time time.Time `json:"time"` + // Optional type data included with some resources (e.g., + // change control approvals). + Type string `json:"type"` + + resourceGetError +} + +// Failure state associated with resource API requests. +type resourceGetError struct { + // Error code supplied if a request fails. + Code *int `json:"code,omitempty"` + // Optional explanation for a request failure. + Message *string `json:"message,omitempty"` +} + +// Errors associated with the state of an item from the resource API. +type ResourceError struct { + // An error attached to this item. + // + // The presence of this string does not necessarily mean that *this* + // request failed, e.g., a scheduled execution might expire before its change + // is approved. + Error *string `json:"error,omitempty"` +} + +// Wrapper type for a list of strings in Resource API requests +// and responses. +type StringList struct { + Values []string `json:"values"` +} + +// Wrapper type for a 2D string array in Resource API requests +// and responses. +type StringMatrix struct { + Values []StringList `json:"values"` +} + +// Wrapper type for a string map in Resource API requests +// and responses. +type StringMap struct { + Values map[string]string `json:"values"` +} + +// A boolean flag. +type FlagRsc struct { + Value bool `json:"value"` + + ModifiedBy +} + +// A timestamp flag. +type TimestampFlagRsc struct { + Value *time.Time `json:"value,omitempty"` + + ModifiedBy +} + +// Modification data for fields of a Resource API value. +type ModifiedBy struct { + // The time this field was updated at. + Time *time.Time `json:"time,omitempty"` + // The user who last updated this field. + User *string `json:"user,omitempty"` + // A comment explaining the last change made to this field. + Notes *string `json:"notes,omitempty"` +} + +func (v *resourceGetError) GetError() error { + if v.Code != nil { + err := errors.Errorf("resource API lookup failed [code %d]", v.Code) + if v.Message != nil { + err = errors.Errorf("%s: %s", v.Message) + } + return err + } + + return nil +} + +// Resource APIs hand us back newline-delimited Json on /all endpoints. +// This converts a call into a list of usable Json objects. +func resultList(data []byte) ([]resultRsc, error) { + out := []resultRsc{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + + for decoder.More() { + el := resultRsc{} + + if err := decoder.Decode(&el); err != nil { + return nil, err + } + + if err := el.GetError(); err != nil { + return nil, err + } + + out = append(out, el) + } + + return out, nil +} diff --git a/api/resources_changecontrol_v1.go b/api/resources_changecontrol_v1.go index 0c0d81f..2e28c26 100644 --- a/api/resources_changecontrol_v1.go +++ b/api/resources_changecontrol_v1.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2016-2017, Arista Networks, Inc. All rights reserved. +// Copyright (c) 2016-2023, Arista Networks, Inc. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are @@ -32,7 +32,6 @@ package cvpapi import ( - "bytes" "encoding/json" "fmt" "net/url" @@ -41,126 +40,95 @@ import ( "github.com/pkg/errors" ) -// FIXME: document structs and factor out common properties. -// FIXME: break out shared utilities (resultRsc) to "resources.go"? - -type resultRsc struct { - Result ValueRsc `json:"result"` - Time *time.Time `json:"time,omitempty"` -} - -type ValueRsc struct { - Value json.RawMessage `json:"value"` - Time time.Time `json:"time"` - Type string `json:"type"` -} - +// Key type for identifying individual change controls. type ChangeControlKeyRsc struct { Id string `json:"id"` } -type StringMap struct { - Values map[string]string `json:"values"` -} - +// Wrapper type for a collection of named stages in the change +// control Resource API. type StageMap struct { Values map[string]StageRsc `json:"values"` } -type StringList struct { - Values []string `json:"values"` -} - -type StringMatrix struct { - Values []StringList `json:"values"` -} - +// An individual action within a change control. type ActionRsc struct { - Name string `json:"name"` - Timeout uint32 `json:"timeout"` - Args StringMap `json:"args"` + // The name of the action to execute. + // + // Refer to CVP documentation for valid choices and their + // corresponding arguments. + Name string `json:"name"` + // Timeout of this action in seconds. + Timeout uint32 `json:"timeout"` + // Key-value pairs of strings used as arguments for + // this action. + Args StringMap `json:"args"` } +// A stage within a change control. type StageRsc struct { - Name string `json:"name"` - Action *ActionRsc `json:"action,omitempty"` - Rows *StringMatrix `json:"rows,omitempty"` - Status *string `json:"status,omitempty"` - Error *string `json:"error,omitempty"` -} - -type ChangeRsc struct { - Name *string `json:"name,omitempty"` - RootStageId *string `json:"rootStageId,omitempty"` - Stages *StageMap `json:"stages,omitempty"` - Notes *string `json:"notes,omitempty"` - User *string `json:"user,omitempty"` - Time *time.Time `json:"time,omitempty"` -} - -type FlagRsc struct { - Value bool `json:"value"` - Notes string `json:"notes"` - Time *time.Time `json:"time,omitempty"` - User string `json:"user,omitempty"` -} + // The name of this stage. + // + // This is used when this stage is referred to by either the + // root stage ID, or as a row within another stage. + Name string `json:"name"` + // The action attached to this stage, + Action *ActionRsc `json:"action,omitempty"` + // A matrix containing the names of other sub-stages to execute. + // + // Entries in each row are executed in parallel, while + // rows are run serially. + Rows *StringMatrix `json:"rows,omitempty"` + // Execution state of this stage, filled in once a stage has + // begun (e.g., `STAGE_STATUS_{UNSPECIIFED,RUNNING,COMPLETED}`). + Status *string `json:"status,omitempty"` -type FlagConfigRsc struct { - Value bool `json:"value"` - Notes string `json:"notes"` + ResourceError } -type TimestampFlagRsc struct { - Value *time.Time `json:"value,omitempty"` - Notes string `json:"notes"` - Time *time.Time `json:"time,omitempty"` - User *string `json:"user,omitempty"` -} +// The main body of a change control. +type ChangeRsc struct { + // The user-friendly name of this change. + Name *string `json:"name,omitempty"` + // The name of the root stage to be executed. This should match + // an entry in `Stages`. + RootStageId *string `json:"rootStageId,omitempty"` + // All stages associated with this change. + Stages *StageMap `json:"stages,omitempty"` -type TimestampFlagConfigRsc struct { - Value bool `json:"value"` - Notes string `json:"notes"` + ModifiedBy } +// A change control paired with any scheduling information. type ChangeControlRsc struct { - Key ChangeControlKeyRsc `json:"key"` - Change *ChangeRsc `json:"change,omitempty"` - Flag *FlagRsc `json:"flag,omitempty"` - Start *FlagRsc `json:"start,omitempty"` - Status *string `json:"status,omitempty"` - Schedule *TimestampFlagRsc `json:"schedule,omitempty"` - - Error *string `json:"error,omitempty"` -} + // A unique key or UUID identifying this change control. + Key ChangeControlKeyRsc `json:"key"` + // The main body and actions of this change. + Change *ChangeRsc `json:"change,omitempty"` + // Flag which starts or stops the execution of this change control. + Start *FlagRsc `json:"start,omitempty"` + // An optional time at which that this change control will automatically + // execute if it has been approved. + Schedule *TimestampFlagRsc `json:"schedule,omitempty"` -type ApproveConfigRsc struct { - Key ChangeControlKeyRsc `json:"key"` - Approve FlagConfigRsc `json:"approve"` - Version *time.Time `json:"version,omitempty"` + ResourceError } -// Resource APIs hand us back ndJson on /all endpoints. -// This converts a call into a list of usable Json objects. -func resultList(data []byte) ([]resultRsc, error) { - out := []resultRsc{} - - decoder := json.NewDecoder(bytes.NewReader(data)) - - for decoder.More() { - el := resultRsc{} - - if err := decoder.Decode(&el); err != nil { - return nil, err - } - - out = append(out, el) - } - - return out, nil +// Approval state for a change control. +type ApproveConfigRsc struct { + // The unique key or UUID identifying the target change control. + Key ChangeControlKeyRsc `json:"key"` + // Approval state and metadata. + Approve FlagRsc `json:"approve"` + // The timestamp matching the last update time of the target change control. + // + // If `Version` does not match the change control, then this approval will + // fail or be cancelled. + Version *time.Time `json:"version,omitempty"` } func deserChangeControl(data []byte) (*ChangeControlRsc, error) { - result := ValueRsc{} + result := valueRsc{} cc := ChangeControlRsc{} if err := json.Unmarshal(data, &result); err != nil { @@ -168,6 +136,10 @@ func deserChangeControl(data []byte) (*ChangeControlRsc, error) { err, data) } + if err := result.GetError(); err != nil { + return nil, errors.Errorf("ChangeControlRsc [API]: %s", err) + } + if err := json.Unmarshal(result.Value, &cc); err != nil { return nil, errors.Errorf("ChangeControlRsc [Inner]: %s Payload:\n%s", err, data) @@ -177,7 +149,7 @@ func deserChangeControl(data []byte) (*ChangeControlRsc, error) { } func deserApproval(data []byte) (*ApproveConfigRsc, error) { - result := ValueRsc{} + result := valueRsc{} cc := ApproveConfigRsc{} if err := json.Unmarshal(data, &result); err != nil { @@ -185,6 +157,10 @@ func deserApproval(data []byte) (*ApproveConfigRsc, error) { err, data) } + if err := result.GetError(); err != nil { + return nil, errors.Errorf("ApproveConfigRsc [API]: %s", err) + } + if err := json.Unmarshal(result.Value, &cc); err != nil { return nil, errors.Errorf("ApproveConfigRsc [Inner]: %s Payload:\n%s", err, data) @@ -355,9 +331,8 @@ func (c CvpRestAPI) CreateChangeControlWithActionsRsc(ccId, name string, rootSta // // This endpoint is available on CVP 2021.2.0 or newer. func (c CvpRestAPI) AddNoteToChangeControlRsc(ccId, notes string) error { - change := ChangeRsc{ - Notes: ¬es, - } + change := ChangeRsc{} + change.Notes = ¬es cfg := ChangeControlRsc{Change: &change} cfg.Key.Id = ccId @@ -380,10 +355,8 @@ func (c CvpRestAPI) AddNoteToChangeControlRsc(ccId, notes string) error { // // This endpoint is available on CVP 2021.2.0 or newer. func (c CvpRestAPI) StartChangeControlRsc(ccId, notes string) error { - start := FlagRsc{ - Notes: notes, - Value: true, - } + start := FlagRsc{Value: true} + start.Notes = ¬es cfg := ChangeControlRsc{Start: &start} cfg.Key.Id = ccId @@ -402,10 +375,8 @@ func (c CvpRestAPI) StartChangeControlRsc(ccId, notes string) error { // // This endpoint is available on CVP 2021.2.0 or newer. func (c CvpRestAPI) StopChangeControlRsc(ccId, notes string) error { - start := FlagRsc{ - Notes: notes, - Value: false, - } + start := FlagRsc{Value: false} + start.Notes = ¬es cfg := ChangeControlRsc{Start: &start} cfg.Key.Id = ccId @@ -428,9 +399,8 @@ func (c CvpRestAPI) ScheduleChangeControlRsc(ccId string, schedTime time.Time, n cfg.Key.Id = ccId // Note: The API seems to disallow setting the user or time on cvprac v2022.1.0 -- not tested others. - sched := TimestampFlagRsc{} - sched.Value = &schedTime - sched.Notes = notes + sched := TimestampFlagRsc{Value: &schedTime} + sched.Notes = ¬es cfg.Schedule = &sched @@ -458,7 +428,7 @@ func (c CvpRestAPI) DescheduleChangeControlRsc(ccId, notes string) error { // Note: The API seems to disallow setting the user or time on cvprac v2022.1.0 -- not tested others. sched := TimestampFlagRsc{} - sched.Notes = notes + sched.Notes = ¬es cfg.Schedule = &sched @@ -532,10 +502,9 @@ func (c CvpRestAPI) GetChangeControlApprovalRsc(ccId string) (*ApproveConfigRsc, // This endpoint is available on CVP 2021.2.0 or newer. // explain version. func (c CvpRestAPI) ApproveChangeControlRsc(ccId string, approve bool, version time.Time, notes string) error { - flag := FlagConfigRsc{ - Value: approve, - Notes: notes, - } + flag := FlagRsc{Value: approve} + flag.Notes = ¬es + approval := ApproveConfigRsc{ Approve: flag, Version: &version, From 5225ddf72005eb534089c4eacebc5f4d0d00d0e1 Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Mon, 6 Feb 2023 14:42:33 +0000 Subject: [PATCH 13/14] ChangeControl: Remove TODO comment in scheduling code Testing confirmed that the API will return a HTTP code 400 if a provided time is illegal -- checking error fields is only useful some time *after* a schedule has occurred. --- api/resources_changecontrol_v1.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/api/resources_changecontrol_v1.go b/api/resources_changecontrol_v1.go index 2e28c26..df2016b 100644 --- a/api/resources_changecontrol_v1.go +++ b/api/resources_changecontrol_v1.go @@ -410,12 +410,6 @@ func (c CvpRestAPI) ScheduleChangeControlRsc(ccId string, schedTime time.Time, n err = errors.Errorf("ScheduleChangeControlRsc: %s", err) } - // FIXME: check error handling of the top-level struct received. - // Scheduling unapproved jobs seems to have no issue, scheduling unapproved - // causes the change control to become unapproved again, counter to what docs say... - // - // Error string can appear at several levels in the hierarchy - return err } From 2c0c82b4966af6b18d23085644c94591201593cb Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Mon, 6 Feb 2023 17:18:04 +0000 Subject: [PATCH 14/14] ChangeControl: Unit tests on deserialisation --- api/resources.go | 6 +- api/resources_changecontrol_v1_test.go | 96 ++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 api/resources_changecontrol_v1_test.go diff --git a/api/resources.go b/api/resources.go index d79a546..bdc2775 100644 --- a/api/resources.go +++ b/api/resources.go @@ -126,7 +126,7 @@ func (v *resourceGetError) GetError() error { if v.Code != nil { err := errors.Errorf("resource API lookup failed [code %d]", v.Code) if v.Message != nil { - err = errors.Errorf("%s: %s", v.Message) + err = errors.Errorf("%s: %s", err, *v.Message) } return err } @@ -152,6 +152,10 @@ func resultList(data []byte) ([]resultRsc, error) { return nil, err } + if el.Result == nil { + return nil, errors.Errorf("missing 'Value' field from ndJson response") + } + out = append(out, el) } diff --git a/api/resources_changecontrol_v1_test.go b/api/resources_changecontrol_v1_test.go new file mode 100644 index 0000000..8447f58 --- /dev/null +++ b/api/resources_changecontrol_v1_test.go @@ -0,0 +1,96 @@ +// +// Copyright (c) 2016-2023, Arista Networks, Inc. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of Arista Networks nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +// IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +package cvpapi + +import ( + "testing" +) + +func Test_GetRsc_Error_UnitTest(t *testing.T) { + respStr := `{"code":5, "message":"resource not found"}` + + client := NewMockClient(respStr, nil) + api := NewCvpRestAPI(client) + + _, err := api.GetChangeControlRsc("fakeId") + assert(t, err != nil, "Failed to treat API error code as failure") +} + +func Test_GetChangeControlsRsc_UnitTest(t *testing.T) { + respStr := `{"result":{"value":{"key":{"id":"QM0BiWeRl"},"change":{"name":"Change 20230131_121158","rootStageId":"G0TTuEfr8q","stages":{"values":{"BttT5IDrq":{"name":"Exit BGP Maint","action":{"name":"exitbgpmaintmode","args":{"values":{"DeviceID":"SN-veos1"}}}},"G0TTuEfr8q":{"name":"Change 20230131_121158 Root","rows":{"values":[{"values":["BttT5IDrq"]}]}}}},"notes":"","time":"2023-01-31T12:12:16.054988005Z","user":"cvpadmin"}},"time":"2023-01-31T12:12:16.054988005Z","type":"INITIAL"}} +{"result":{"value":{"key":{"id":"QIKoyUtZjaFd-Y0LH3clY"},"change":{"name":"Change 20230125_151303","rootStageId":"kTUrxEn8coeXMKMVx0J4H","stages":{"values":{"6HvsEQ4tM5AOPPuE9Ftbz":{"name":"Exit BGP Maint","action":{"name":"exitbgpmaintmode","args":{"values":{"DeviceID":"SN-DC1-LEAF1A"}}},"rows":{},"status":"STAGE_STATUS_COMPLETED","error":"Error executing stage 6HvsEQ4tM5AOPPuE9Ftbz : On-boot maintenance check failed: Can not find on boot maintenance status from CLI response, tried path: [maintenanceUnits System onBootMaintenance]"},"kTUrxEn8coeXMKMVx0J4H":{"name":"Change 20230125_151303 Root","rows":{"values":[{"values":["6HvsEQ4tM5AOPPuE9Ftbz"]}]},"status":"STAGE_STATUS_COMPLETED","error":"Error executing stage 6HvsEQ4tM5AOPPuE9Ftbz : On-boot maintenance check failed: Can not find on boot maintenance status from CLI response, tried path: [maintenanceUnits System onBootMaintenance]"}}},"notes":"","time":"2023-01-26T09:29:50.263493510Z","user":"cvpadmin"},"approve":{"value":true,"notes":"Test approval.","time":"2023-02-02T14:38:40.446862005Z","user":"cvpadmin"},"start":{"value":true,"notes":"Started by Scheduled Change Control","time":"2023-02-02T14:40:00.053313982Z","user":"cvpadmin"},"status":"CHANGE_CONTROL_STATUS_COMPLETED","error":"Error executing stage 6HvsEQ4tM5AOPPuE9Ftbz : On-boot maintenance check failed: Can not find on boot maintenance status from CLI response, tried path: [maintenanceUnits System onBootMaintenance]","schedule":{"value":"2023-02-02T14:40:00.043Z","time":"2023-02-02T14:38:20.187702678Z","user":"cvpadmin"}},"time":"2023-02-02T14:40:00.748121056Z","type":"INITIAL"}}` + + client := NewMockClient(respStr, nil) + api := NewCvpRestAPI(client) + + vals, err := api.GetChangeControlsRsc() + ok(t, err) + assert(t, len(vals) == 2, "Expected 2 change controls") +} + +func Test_GetChangeControlRsc_UnitTest(t *testing.T) { + ccId := "vYdaCZu_D798Bpubak4E_" + respStr := `{"value":{"key":{"id":"vYdaCZu_D798Bpubak4E_"}, "change":{"name":"Change 20230126_113349", "rootStageId":"f9aGfU6iqmKW90-Z9soJV", "stages":{"values":{"f9aGfU6iqmKW90-Z9soJV":{"name":"Change 20230126_113349 Root", "rows":{}}}}, "notes":"", "time":"2023-01-26T12:16:39.819278546Z", "user":"cvpadmin"}, "error":"Reschedule required: Schedule time in the past for ChangeControl ID vYdaCZu_D798Bpubak4E_", "schedule":{"value":"2023-02-02T13:19:45.031602Z", "notes":"Testing sched timer.", "time":"2023-02-01T13:19:45.773491427Z", "user":"cvpadmin"}}, "time":"2023-02-06T12:04:47.521740196Z"}` + + client := NewMockClient(respStr, nil) + api := NewCvpRestAPI(client) + + val, err := api.GetChangeControlRsc(ccId) + ok(t, err) + assert(t, val.Change != nil && *val.Change.Name == "Change 20230126_113349", "failed to deserialize change/name") + assert(t, val.Schedule != nil && val.Schedule.Time != nil, "Failed to handle Rsc API Schedule") +} + +func Test_GetChangeControlApprovalsRsc_UnitTest(t *testing.T) { + respStr := `{"result":{"value":{"key":{"id":"test"},"approve":{"value":true,"notes":"More Notes"},"version":"2023-02-01T15:11:26.624538804Z"},"time":"2023-02-02T11:59:50.507671741Z","type":"INITIAL"}} +{"result":{"value":{"key":{"id":"QIKoyUtZjaFd-Y0LH3clY"},"approve":{"value":true,"notes":"Test approval."},"version":"2023-01-26T09:29:50.263493510Z"},"time":"2023-02-02T14:38:40.446862005Z","type":"INITIAL"}} +{"result":{"value":{"key":{"id":"test2"},"approve":{"value":true,"notes":"More Notes"},"version":"2023-01-31T16:01:16.107005406Z"},"time":"2023-02-01T13:21:19.422609230Z","type":"INITIAL"}}` + + client := NewMockClient(respStr, nil) + api := NewCvpRestAPI(client) + + vals, err := api.GetChangeControlApprovalsRsc() + ok(t, err) + assert(t, len(vals) == 3, "Expected 3 approvals") +} + +func Test_GetChangeControlApprovalRsc_UnitTest(t *testing.T) { + ccId := "vYdaCZu_D798Bpubak4E_" + respStr := `{"value":{"key":{"id":"vYdaCZu_D798Bpubak4E_"}, "approve":{"value":true, "notes":"More Notes"}, "version":"2023-01-31T16:01:16.107005406Z"}, "time":"2023-02-01T13:21:19.422609230Z"}` + + client := NewMockClient(respStr, nil) + api := NewCvpRestAPI(client) + + val, err := api.GetChangeControlApprovalRsc(ccId) + ok(t, err) + assert(t, val.Approve.Value && *val.Approve.Notes == "More Notes", "failed to deserialize approval flag state") +}