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/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/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. // 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) } }) } diff --git a/api/resources.go b/api/resources.go new file mode 100644 index 0000000..bdc2775 --- /dev/null +++ b/api/resources.go @@ -0,0 +1,163 @@ +// +// 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", err, *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 + } + + if el.Result == nil { + return nil, errors.Errorf("missing 'Value' field from ndJson response") + } + + out = append(out, el) + } + + return out, nil +} diff --git a/api/resources_changecontrol_v1.go b/api/resources_changecontrol_v1.go new file mode 100644 index 0000000..df2016b --- /dev/null +++ b/api/resources_changecontrol_v1.go @@ -0,0 +1,515 @@ +// +// 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 ( + "encoding/json" + "fmt" + "net/url" + "time" + + "github.com/pkg/errors" +) + +// Key type for identifying individual change controls. +type ChangeControlKeyRsc struct { + Id string `json:"id"` +} + +// Wrapper type for a collection of named stages in the change +// control Resource API. +type StageMap struct { + Values map[string]StageRsc `json:"values"` +} + +// An individual action within a change control. +type ActionRsc struct { + // 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 { + // 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"` + + ResourceError +} + +// 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"` + + ModifiedBy +} + +// A change control paired with any scheduling information. +type ChangeControlRsc struct { + // 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"` + + ResourceError +} + +// 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{} + cc := ChangeControlRsc{} + + if err := json.Unmarshal(data, &result); err != nil { + return nil, errors.Errorf("ChangeControlRsc [Result]: %s Payload:\n%s", + 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) + } + + 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 := 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) + } + + return &cc, nil +} + +// GetChangeControlsRsc returns a list of `ChangeControlRsc`s via the +// ChangeControl resource API. +// +// 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) + + 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 +} + +// 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) GetChangeControlRsc(ccId string) (*ChangeControlRsc, error) { + query := &url.Values{ + "key.id": {ccId}, + } + + resp, err := c.client.Get("/api/resources/changecontrol/v1/ChangeControl", query) + + if err != nil { + return nil, errors.Errorf("GetChangeControlRsc: %s", + err) + } + + 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}, + } + + _, err := c.client.Delete("/api/resources/changecontrol/v1/ChangeControlConfig", query, nil) + + if err != nil { + return errors.Errorf("DeleteChangeControlRsc: %s", + err) + } + + return nil +} + +// 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) CreateChangeControlWithActionsRsc(ccId, name string, rootStages [][]StageRsc, extraStages ...StageRsc) (*ChangeControlRsc, error) { + root := "root" + stages := StageMap{Values: make(map[string]StageRsc)} + change := ChangeRsc{ + Name: &name, + RootStageId: &root, + Stages: &stages, + } + + cfg := ChangeControlRsc{Change: &change} + cfg.Key.Id = ccId + + rootStage := StageRsc{ + Name: "root", + } + + var rows []StringList + + 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}) + } + + for _, stage := range extraStages { + change.Stages.Values[stage.Name] = stage + } + + matrix := StringMatrix{Values: rows} + rootStage.Rows = &matrix + + change.Stages.Values["root"] = rootStage + + 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{} + change.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{Value: true} + start.Notes = ¬es + + 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 +} + +// 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{Value: false} + start.Notes = ¬es + + 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(ccId string, schedTime time.Time, notes string) error { + cfg := ChangeControlRsc{} + 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{Value: &schedTime} + sched.Notes = ¬es + + cfg.Schedule = &sched + + _, err := c.client.Post("/api/resources/changecontrol/v1/ChangeControlConfig", nil, cfg) + + if err != nil { + err = errors.Errorf("ScheduleChangeControlRsc: %s", err) + } + + return err +} + +// 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(ccId, notes string) error { + cfg := ChangeControlRsc{} + 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.Notes = ¬es + + 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 +} + +// 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 := FlagRsc{Value: approve} + flag.Notes = ¬es + + 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/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") +} 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..1e810ed 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,20 @@ 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) + } + + // 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 + } 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 +469,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) } @@ -504,7 +517,8 @@ func (c *CvpClient) makeRequest(reqType string, url string, params *url.Values, // client error if status != http.StatusOK { // retry another session - 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 @@ -558,7 +572,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") 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 +} 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