SEBA-705 better error reporting
Change-Id: Id461c6efe2d0b7ab9c0d1ddb72482d10899b16fe
diff --git a/cmd/cordctl.go b/cmd/cordctl.go
index 19a27c7..55991d5 100644
--- a/cmd/cordctl.go
+++ b/cmd/cordctl.go
@@ -19,6 +19,7 @@
import (
flags "github.com/jessevdk/go-flags"
"github.com/opencord/cordctl/commands"
+ corderrors "github.com/opencord/cordctl/error"
"os"
"path"
)
@@ -48,9 +49,18 @@
if real.Type == flags.ErrHelp {
return
}
- } else {
- panic(err)
}
+
+ corderror, ok := err.(corderrors.CordCtlError)
+ if ok {
+ if corderror.ShouldDumpStack() || commands.GlobalOptions.Debug {
+ os.Stderr.WriteString("\n" + corderror.Stack())
+ }
+ }
+
+ // parser.ParseArgs already printed the error message
+ // Any stack trace emitted by panic() here would be of main() and not useful
+ // So just exit and be done with it.
os.Exit(1)
}
}
diff --git a/commands/backup.go b/commands/backup.go
index de435fb..ceda54a 100644
--- a/commands/backup.go
+++ b/commands/backup.go
@@ -18,9 +18,8 @@
import (
"context"
- "errors"
- "fmt"
flags "github.com/jessevdk/go-flags"
+ corderrors "github.com/opencord/cordctl/error"
"time"
)
@@ -107,13 +106,13 @@
// we've failed. leave.
if status != "created" {
- return errors.New("BackupOp status is " + status)
+ return corderrors.NewInternalError("BackupOp status is %s", status)
}
// STEP 3: Retrieve URI
backupfile_id := completed_backupop.GetFieldByName("file_id").(int32)
if backupfile_id == 0 {
- return errors.New("BackupOp.file_id is not set")
+ return corderrors.NewInternalError("BackupOp.file_id is not set")
}
completed_backupfile, err := GetModel(ctx, conn, descriptor, "BackupFile", backupfile_id)
@@ -136,9 +135,9 @@
// STEP 5: Verify checksum
if completed_backupfile.GetFieldByName("checksum").(string) != h.GetChecksum() {
- return fmt.Errorf("Checksum mismatch, received=%s, expected=%s",
- h.GetChecksum(),
- completed_backupfile.GetFieldByName("checksum").(string))
+ return corderrors.WithStackTrace(&corderrors.ChecksumMismatchError{
+ Actual: h.GetChecksum(),
+ Expected: completed_backupfile.GetFieldByName("checksum").(string)})
}
// STEP 6: Show results
@@ -188,15 +187,15 @@
upload_status := GetEnumValue(upload_result, "status")
if upload_status != "SUCCESS" {
- return errors.New("Upload status was " + upload_status)
+ return corderrors.NewInternalError("Upload status was %s", upload_status)
}
// STEP 2: Verify checksum
if upload_result.GetFieldByName("checksum").(string) != h.GetChecksum() {
- return fmt.Errorf("Checksum mismatch, expected=%s, received=%s",
- h.GetChecksum(),
- upload_result.GetFieldByName("checksum").(string))
+ return corderrors.WithStackTrace(&corderrors.ChecksumMismatchError{
+ Expected: h.GetChecksum(),
+ Actual: upload_result.GetFieldByName("checksum").(string)})
}
// STEP 2: Create a BackupFile object
diff --git a/commands/common.go b/commands/common.go
index bb26273..12b869f 100644
--- a/commands/common.go
+++ b/commands/common.go
@@ -23,6 +23,7 @@
versionUtils "github.com/hashicorp/go-version"
"github.com/jhump/protoreflect/dynamic"
"github.com/jhump/protoreflect/grpcreflect"
+ corderrors "github.com/opencord/cordctl/error"
"golang.org/x/net/context"
"google.golang.org/grpc"
reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
@@ -113,8 +114,10 @@
}
if !constraint.Check(serverVersion) {
- return nil, nil, fmt.Errorf("Core version %s does not match constraint '%s'",
- serverVersion, CORE_VERSION_CONSTRAINT)
+ return nil, nil, corderrors.WithStackTrace(&corderrors.VersionConstraintError{
+ Name: "xos-core",
+ Version: serverVersion.String(),
+ Constraint: CORE_VERSION_CONSTRAINT})
}
}
diff --git a/commands/models.go b/commands/models.go
index 49e85d8..e539bd9 100644
--- a/commands/models.go
+++ b/commands/models.go
@@ -22,6 +22,7 @@
"github.com/fullstorydev/grpcurl"
flags "github.com/jessevdk/go-flags"
"github.com/jhump/protoreflect/dynamic"
+ corderrors "github.com/opencord/cordctl/error"
"google.golang.org/grpc"
"sort"
"strings"
@@ -190,7 +191,7 @@
val, ok := kindMap[strings.ToLower(kindArg)]
if !ok {
- return "", fmt.Errorf("Failed to understand model state %s", kindArg)
+ return "", corderrors.WithStackTrace(&corderrors.UnknownModelStateError{Name: kindArg})
}
return val, nil
@@ -216,7 +217,7 @@
}
if (exclusiveCount == 0) || (exclusiveCount > 1) {
- return nil, fmt.Errorf("Use either an ID, --filter, or --all to specify which models to operate on")
+ return nil, corderrors.WithStackTrace(&corderrors.FilterRequiredError{})
}
queries, err := CommaSeparatedQueryToMap(filter, true)
@@ -236,10 +237,10 @@
ids[i] = model.GetFieldByName("id").(int32)
}
if len(ids) == 0 {
- return nil, fmt.Errorf("Filter matches no objects")
+ return nil, corderrors.WithStackTrace(&corderrors.NoMatchError{})
} else if len(ids) > 1 {
if !Confirmf("Filter matches %d objects. Continue [y/n] ? ", len(models)) {
- return nil, fmt.Errorf("Aborted by user")
+ return nil, corderrors.WithStackTrace(&corderrors.AbortedError{})
}
}
}
@@ -353,7 +354,7 @@
if (len(options.IDArgs.ID) == 0 && len(options.Filter) == 0) ||
(len(options.IDArgs.ID) != 0 && len(options.Filter) != 0) {
- return fmt.Errorf("Use either an ID or a --filter to specify which models to update")
+ return corderrors.WithStackTrace(&corderrors.FilterRequiredError{})
}
queries, err := CommaSeparatedQueryToMap(options.Filter, true)
@@ -386,10 +387,10 @@
}
if len(models) == 0 {
- return fmt.Errorf("Filter matches no objects")
+ return corderrors.WithStackTrace(&corderrors.NoMatchError{})
} else if len(models) > 1 {
if !Confirmf("Filter matches %d objects. Continue [y/n] ? ", len(models)) {
- return fmt.Errorf("Aborted by user")
+ return corderrors.WithStackTrace(&corderrors.AbortedError{})
}
}
diff --git a/commands/models_test.go b/commands/models_test.go
index c4f6c47..3a6031b 100644
--- a/commands/models_test.go
+++ b/commands/models_test.go
@@ -2,7 +2,7 @@
* Portions copyright 2019-present Open Networking Foundation
* Original copyright 2019-present Ciena Corporation
*
- * Licensed under the Apache License, Version 2.0 (the "License");
+ * Licensed under the Apache License, Version 2.0 (the"github.com/stretchr/testify/assert" "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
@@ -18,7 +18,9 @@
import (
"bytes"
+ corderrors "github.com/opencord/cordctl/error"
"github.com/opencord/cordctl/testutils"
+ "github.com/stretchr/testify/assert"
"testing"
"time"
)
@@ -267,7 +269,9 @@
options.Update.IDArgs.ID = []int32{77}
options.Update.SetFields = "name=mockslice1_newname"
err := options.Update.Execute([]string{})
- testutils.AssertErrorEqual(t, err, "rpc error: code = Unknown desc = Slice matching query does not exist.")
+
+ _, matched := err.(*corderrors.ModelNotFoundError)
+ assert.True(t, matched)
}
func TestModelUpdateUsingFilterNoExist(t *testing.T) {
@@ -281,7 +285,8 @@
options.Update.SetFields = "name=mockslice1_newname"
err := options.Update.Execute([]string{})
- testutils.AssertErrorEqual(t, err, "Filter matches no objects")
+ _, matched := err.(*corderrors.NoMatchError)
+ assert.True(t, matched)
}
func TestModelCreate(t *testing.T) {
@@ -373,7 +378,9 @@
options.Delete.OutputAs = "json"
options.Delete.Filter = "id=77"
err := options.Delete.Execute([]string{})
- testutils.AssertErrorEqual(t, err, "Filter matches no objects")
+
+ _, matched := err.(*corderrors.NoMatchError)
+ assert.True(t, matched)
}
func TestModelSync(t *testing.T) {
diff --git a/commands/orm.go b/commands/orm.go
index 5938698..f57f21d 100644
--- a/commands/orm.go
+++ b/commands/orm.go
@@ -18,13 +18,13 @@
import (
"context"
- "errors"
"fmt"
"github.com/fullstorydev/grpcurl"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/protoc-gen-go/descriptor"
"github.com/jhump/protoreflect/desc"
"github.com/jhump/protoreflect/dynamic"
+ corderrors "github.com/opencord/cordctl/error"
"google.golang.org/grpc"
"io"
"strconv"
@@ -65,7 +65,7 @@
if strings.HasPrefix(query, "!=") {
return strings.TrimSpace(query[2:]), "EQUAL", true, nil
} else if strings.HasPrefix(query, "==") {
- return "", "", false, errors.New("Operator == is now allowed. Suggest using = instead.")
+ return "", "", false, corderrors.NewInvalidInputError("Operator == is now allowed. Suggest using = instead.")
} else if strings.HasPrefix(query, "=") {
return strings.TrimSpace(query[1:]), "EQUAL", false, nil
} else if strings.HasPrefix(query, ">=") {
@@ -109,7 +109,7 @@
field_descriptor := h.Model.FindFieldByName(field_name)
if field_descriptor == nil {
- return fmt.Errorf("Field %s does not exist", field_name)
+ return corderrors.WithStackTrace(&corderrors.FieldDoesNotExistError{ModelName: h.Model.GetName(), FieldName: field_name})
}
field_type := field_descriptor.GetType()
@@ -123,9 +123,9 @@
i, err = strconv.ParseInt(value, 10, 32)
nm.SetFieldByName("iValue", uint32(i))
case descriptor.FieldDescriptorProto_TYPE_FLOAT:
- err = errors.New("Floating point filters are unsupported")
+ err = corderrors.NewInvalidInputError("Floating point filters are unsupported")
case descriptor.FieldDescriptorProto_TYPE_DOUBLE:
- err = errors.New("Floating point filters are unsupported")
+ err = corderrors.NewInvalidInputError("Floating point filters are unsupported")
default:
nm.SetFieldByName("sValue", value)
err = nil
@@ -168,7 +168,7 @@
}
}
if operator_pos == -1 {
- return nil, fmt.Errorf("Illegal operator/value string %s", query_str)
+ return nil, corderrors.WithStackTrace(&corderrors.IllegalQueryError{Query: query_str})
}
queries[strings.TrimSpace(query_str[:operator_pos])] = query_str[operator_pos:]
}
@@ -193,11 +193,11 @@
}
model_md, ok := model_descriptor.(*desc.MessageDescriptor)
if !ok {
- return nil, fmt.Errorf("Failed to convert model %s to a messagedescriptor", modelName)
+ return nil, corderrors.WithStackTrace(&corderrors.TypeConversionError{Source: modelName, Destination: "messageDescriptor"})
}
field_descriptor := model_md.FindFieldByName(field_name)
if field_descriptor == nil {
- return nil, fmt.Errorf("Field %s does not exist in model %s", field_name, modelName)
+ return nil, corderrors.WithStackTrace(&corderrors.FieldDoesNotExistError{ModelName: modelName, FieldName: field_name})
}
field_type := field_descriptor.GetType()
@@ -254,7 +254,7 @@
}
_, present := models[name]
if !present {
- return errors.New("Model " + name + " does not exist. Use `cordctl models available` to get a list of available models")
+ return corderrors.WithStackTrace(&corderrors.UnknownModelTypeError{Name: name})
}
return nil
}
@@ -330,11 +330,11 @@
}
err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Get"+modelName, headers, h, h.GetParams)
if err != nil {
- return nil, err
+ return nil, corderrors.RpcErrorWithIdToCordError(err, modelName, id)
}
if h.Status != nil && h.Status.Err() != nil {
- return nil, h.Status.Err()
+ return nil, corderrors.RpcErrorWithIdToCordError(h.Status.Err(), modelName, id) //h.Status.Err()
}
d, err := dynamic.AsDynamicMessage(h.Response)
@@ -379,7 +379,8 @@
continue
}
- if until_found && strings.Contains(err.Error(), "rpc error: code = NotFound") {
+ _, is_not_found_error := err.(*corderrors.ModelNotFoundError)
+ if until_found && is_not_found_error {
if !quiet {
fmt.Print("x")
}
@@ -475,7 +476,7 @@
}
model_md, ok := model_descriptor.(*desc.MessageDescriptor)
if !ok {
- return nil, errors.New("Failed to convert model to a messagedescriptor")
+ return nil, corderrors.WithStackTrace(&corderrors.TypeConversionError{Source: modelName, Destination: "messageDescriptor"})
}
h := &QueryEventHandler{
@@ -525,7 +526,7 @@
}
if len(models) == 0 {
- return nil, errors.New("rpc error: code = NotFound")
+ return nil, corderrors.WithStackTrace(&corderrors.ModelNotFoundError{ModelName: modelName, Queries: queries})
}
return models[0], nil
@@ -565,7 +566,8 @@
continue
}
- if until_found && strings.Contains(err.Error(), "rpc error: code = NotFound") {
+ _, is_not_found_error := err.(*corderrors.ModelNotFoundError)
+ if until_found && is_not_found_error {
if !quiet {
fmt.Print("x")
}
diff --git a/commands/orm_test.go b/commands/orm_test.go
index 1050de1..9a0dad1 100644
--- a/commands/orm_test.go
+++ b/commands/orm_test.go
@@ -18,6 +18,7 @@
import (
"context"
+ corderrors "github.com/opencord/cordctl/error"
"github.com/stretchr/testify/assert"
"testing"
)
@@ -60,7 +61,7 @@
assert.Equal(t, err, nil)
}
-func TestCommaSeparatedQueryStringsToMap(t *testing.T) {
+func TestCommaSeparatedQueryToMap(t *testing.T) {
m, err := CommaSeparatedQueryToMap("foo=7,bar!=stuff, x = 5, y= 27", true)
assert.Equal(t, err, nil)
assert.Equal(t, m["foo"], "=7")
@@ -69,6 +70,28 @@
assert.Equal(t, m["y"], "= 27")
}
+func TestCommaSeparatedQueryToMapIllegal(t *testing.T) {
+ // Query string missing operator
+ _, err := CommaSeparatedQueryToMap("foo", true)
+
+ _, matched := err.(*corderrors.IllegalQueryError)
+ assert.True(t, matched)
+
+ // Query string is contains an empty element
+ _, err = CommaSeparatedQueryToMap(",foo=bar", true)
+
+ _, matched = err.(*corderrors.IllegalQueryError)
+ assert.True(t, matched)
+}
+
+func TestCommaSeparatedQueryToMapEmpty(t *testing.T) {
+ // Query string missing operator
+ m, err := CommaSeparatedQueryToMap("", true)
+
+ assert.Equal(t, err, nil)
+ assert.Equal(t, len(m), 0)
+}
+
func TestTypeConvert(t *testing.T) {
conn, descriptor, err := InitClient(INIT_DEFAULT)
assert.Equal(t, err, nil)
@@ -96,7 +119,8 @@
assert.Equal(t, err, nil)
err = CheckModelName(descriptor, "DoesNotExist")
- assert.Equal(t, err.Error(), "Model DoesNotExist does not exist. Use `cordctl models available` to get a list of available models")
+ _, matched := err.(*corderrors.UnknownModelTypeError)
+ assert.True(t, matched)
}
func TestCreateModel(t *testing.T) {
@@ -139,6 +163,18 @@
assert.Equal(t, m.GetFieldByName("name").(string), "mockslice1")
}
+func TestGetModelNoExist(t *testing.T) {
+ conn, descriptor, err := InitClient(INIT_DEFAULT)
+ assert.Equal(t, err, nil)
+ defer conn.Close()
+
+ _, err = GetModel(context.Background(), conn, descriptor, "Slice", int32(77))
+ assert.NotEqual(t, err, nil)
+
+ _, matched := err.(*corderrors.ModelNotFoundError)
+ assert.True(t, matched)
+}
+
func TestListModels(t *testing.T) {
conn, descriptor, err := InitClient(INIT_DEFAULT)
assert.Equal(t, err, nil)
@@ -169,6 +205,34 @@
assert.Equal(t, m[0].GetFieldByName("name").(string), "mockslice1")
}
+func TestFindModel(t *testing.T) {
+ conn, descriptor, err := InitClient(INIT_DEFAULT)
+ assert.Equal(t, err, nil)
+ defer conn.Close()
+
+ qm := map[string]string{"id": "=1"}
+
+ m, err := FindModel(context.Background(), conn, descriptor, "Slice", qm)
+ assert.Equal(t, err, nil)
+
+ assert.Equal(t, m.GetFieldByName("id").(int32), int32(1))
+ assert.Equal(t, m.GetFieldByName("name").(string), "mockslice1")
+}
+
+func TestFindModelNoExist(t *testing.T) {
+ conn, descriptor, err := InitClient(INIT_DEFAULT)
+ assert.Equal(t, err, nil)
+ defer conn.Close()
+
+ qm := map[string]string{"id": "=77"}
+
+ _, err = FindModel(context.Background(), conn, descriptor, "Slice", qm)
+ assert.NotEqual(t, err, nil)
+
+ _, matched := err.(*corderrors.ModelNotFoundError)
+ assert.True(t, matched)
+}
+
func TestDeleteModel(t *testing.T) {
conn, descriptor, err := InitClient(INIT_DEFAULT)
assert.Equal(t, err, nil)
diff --git a/commands/transfer.go b/commands/transfer.go
index 229f0fd..e746058 100644
--- a/commands/transfer.go
+++ b/commands/transfer.go
@@ -17,9 +17,8 @@
package commands
import (
- "errors"
- "fmt"
flags "github.com/jessevdk/go-flags"
+ corderrors "github.com/opencord/cordctl/error"
"strings"
)
@@ -76,11 +75,11 @@
uri := options.Args.URI
if IsFileUri(local_name) {
- return errors.New("local_name argument should not be a uri")
+ return corderrors.NewInvalidInputError("local_name argument should not be a uri")
}
if !IsFileUri(uri) {
- return errors.New("uri argument should be a file:// uri")
+ return corderrors.NewInvalidInputError("uri argument should be a file:// uri")
}
h, upload_result, err := UploadFile(conn, descriptor, local_name, uri, options.ChunkSize)
@@ -89,9 +88,9 @@
}
if upload_result.GetFieldByName("checksum").(string) != h.GetChecksum() {
- return fmt.Errorf("Checksum mismatch, expected=%s, received=%s",
- h.GetChecksum(),
- upload_result.GetFieldByName("checksum").(string))
+ return corderrors.WithStackTrace(&corderrors.ChecksumMismatchError{
+ Expected: h.GetChecksum(),
+ Actual: upload_result.GetFieldByName("checksum").(string)})
}
data := make([]TransferOutput, 1)
@@ -120,11 +119,11 @@
uri := options.Args.URI
if IsFileUri(local_name) {
- return errors.New("local_name argument should not be a uri")
+ return corderrors.NewInvalidInputError("local_name argument should not be a uri")
}
if !IsFileUri(uri) {
- return errors.New("uri argument should be a file:// uri")
+ return corderrors.NewInvalidInputError("uri argument should be a file:// uri")
}
h, err := DownloadFile(conn, descriptor, uri, local_name)
diff --git a/error/error.go b/error/error.go
new file mode 100644
index 0000000..c30096b
--- /dev/null
+++ b/error/error.go
@@ -0,0 +1,344 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original copyright 2019-present Ciena Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package error
+
+/* Cordctl error classes
+
+ The basic idea is to throw specific error classes, so it's easier to test for them rather than doing string
+ comparisons or other ad hoc mechanisms for determining the type of error. This decouples the human
+ readable text of an error from programmatic testing of error type.
+
+ We differentiate between errors that we want to generate brief output, such as for example a
+ user mistyping a model name, versus errors that we want to generate additional context. This prevents
+ overwhelming a user with voluminous output for a simple mistake. A command-line option may be provided
+ to force full error output should it be desired.
+
+ Additionally, an added benefit is ease of maintenance and localisation, by locating all error text
+ in one place.
+
+ To return an error, for example:
+
+ return WithStackTrace(&ChecksumMismatchError{Actual: "123", Expected: "456"})
+
+ To check to see if a specific error was returned, either of the following are acceptable:
+
+ _, ok := err.(*ChecksumMismatchError)
+ ...
+
+ switch err.(type) {
+ case *ChecksumMismatchError:
+ ...
+*/
+
+import (
+ "bytes"
+ "fmt"
+ go_errors "github.com/go-errors/errors"
+ "os"
+ "runtime"
+ "strings"
+)
+
+const (
+ MaxStackDepth = 50
+)
+
+// Prefix applied to all error messages. Initialized in module init() function from os.Args[0]
+var prefix string
+
+/* CordCtlError is the interface for errors created by cordctl.
+ * ShouldDumpStack()
+ * Returns false for well-understood problems such as invalid user input where a brief error message is sufficient
+ * Returns true for poorly-understood / unexpected problems where a full dump context may be useful
+ * Stack()
+ * Returns a string containing the stack trace where the error occurred
+ * Only useful if WithStackTrace() was called on the error
+ */
+
+type CordCtlError interface {
+ error
+ ShouldDumpStack() bool
+ Stack() string
+ AddStackTrace(skip int)
+}
+
+/* BaseError supports attaching stack traces to errors
+ * Borrowed the technique from github.com/go-errors. Decided against using go-errors directly since it requires
+ * wrapping our error classes. Instead, incorporated the stack trace directly into our error class.
+ */
+
+type BaseError struct {
+ stack []uintptr
+ frames []go_errors.StackFrame
+}
+
+func (f *BaseError) AddStackTrace(skip int) {
+ stack := make([]uintptr, MaxStackDepth)
+ length := runtime.Callers(2+skip, stack[:])
+ f.stack = stack[:length]
+}
+
+func (f *BaseError) Stack() string {
+ buf := bytes.Buffer{}
+
+ for _, frame := range f.StackFrames() {
+ buf.WriteString(frame.String())
+ }
+
+ return string(buf.Bytes())
+}
+
+func (f *BaseError) StackFrames() []go_errors.StackFrame {
+ if f.frames == nil {
+ f.frames = make([]go_errors.StackFrame, len(f.stack))
+
+ for i, pc := range f.stack {
+ f.frames[i] = go_errors.NewStackFrame(pc)
+ }
+ }
+
+ return f.frames
+}
+
+// ***************************************************************************
+// UserError is composed into Errors that are due to user input
+
+type UserError struct {
+ BaseError
+}
+
+func (f UserError) ShouldDumpStack() bool {
+ return false
+}
+
+// **************************************************************************
+// TransferError is composed into Errors that are due to failures in transfers
+
+type TransferError struct {
+ BaseError
+}
+
+func (f TransferError) ShouldDumpStack() bool {
+ return false
+}
+
+// ***************************************************************************
+// UnexpectedError is things that we don't expect to happen. They should
+// generate maximum error context, to provide useful information for developer
+// diagnosis.
+
+type UnexpectedError struct {
+ BaseError
+}
+
+func (f UnexpectedError) ShouldDumpStack() bool {
+ return true
+}
+
+// ***************************************************************************
+// Specific error classes follow
+
+// Checksum mismatch when downloading or uploading a file
+type ChecksumMismatchError struct {
+ TransferError
+ Name string // (optional) Name of file
+ Expected string
+ Actual string
+}
+
+func (f ChecksumMismatchError) Error() string {
+ if f.Name != "" {
+ return fmt.Sprintf("%s %s: checksum mismatch (actual=%s, expected=%s)", prefix, f.Name, f.Expected, f.Actual)
+ } else {
+ return fmt.Sprintf("%s: checksum mismatch (actual=%s, expected=%s)", prefix, f.Expected, f.Actual)
+ }
+}
+
+// User specified a model type that is not valid
+type UnknownModelTypeError struct {
+ UserError
+ Name string // Name of model
+}
+
+func (f UnknownModelTypeError) Error() string {
+ return fmt.Sprintf("%s: Model %s does not exist. Use `cordctl modeltype list` to get a list of available models", prefix, f.Name)
+}
+
+// User specified a model state that is not valid
+type UnknownModelStateError struct {
+ UserError
+ Name string // Name of state
+}
+
+func (f UnknownModelStateError) Error() string {
+ return fmt.Sprintf("%s: Model state %s does not exist", prefix, f.Name)
+}
+
+// Command requires a filter be specified
+type FilterRequiredError struct {
+ UserError
+}
+
+func (f FilterRequiredError) Error() string {
+ return "Filter required. Use either an ID, --filter, or --all to specify which models to operate on"
+}
+
+// Command was aborted by the user
+type AbortedError struct {
+ UserError
+}
+
+func (f AbortedError) Error() string {
+ return "Aborted"
+}
+
+// Command was aborted by the user
+type NoMatchError struct {
+ UserError
+}
+
+func (f NoMatchError) Error() string {
+ return "No Match"
+}
+
+// User specified a field name that is not valid
+type FieldDoesNotExistError struct {
+ UserError
+ ModelName string
+ FieldName string
+}
+
+func (f FieldDoesNotExistError) Error() string {
+ return fmt.Sprintf("%s: Model %s does not have field %s", prefix, f.ModelName, f.FieldName)
+}
+
+// User specified a query string that is not properly formatted
+type IllegalQueryError struct {
+ UserError
+ Query string
+}
+
+func (f IllegalQueryError) Error() string {
+ return fmt.Sprintf("%s: Illegal query string %s", prefix, f.Query)
+}
+
+// We failed to type convert something that we thought should have converted
+type TypeConversionError struct {
+ UnexpectedError
+ Source string
+ Destination string
+}
+
+func (f TypeConversionError) Error() string {
+ return fmt.Sprintf("%s: Failed to type convert from %s to %s", prefix, f.Source, f.Destination)
+}
+
+// Version did not match a constraint
+type VersionConstraintError struct {
+ UserError
+ Name string
+ Version string
+ Constraint string
+}
+
+func (f VersionConstraintError) Error() string {
+ return fmt.Sprintf("%s: %s version %s did not match constraint '%s'", prefix, f.Name, f.Version, f.Constraint)
+}
+
+// A model was not found
+type ModelNotFoundError struct {
+ UserError
+ ModelName string
+ Id int32
+ Queries map[string]string
+}
+
+func (f ModelNotFoundError) Error() string {
+ if f.Queries != nil {
+ return fmt.Sprintf("%s: %s query %v not Found", prefix, f.ModelName, f.Queries)
+ } else {
+ return fmt.Sprintf("%s: Model %s id %d not Found", prefix, f.ModelName, f.Id)
+ }
+}
+
+// InvalidInputError is a catch-all for user mistakes that aren't covered elsewhere
+type InvalidInputError struct {
+ UserError
+ Message string
+}
+
+func (f InvalidInputError) Error() string {
+ return fmt.Sprintf("%s: %s", prefix, f.Message)
+}
+
+func NewInvalidInputError(format string, params ...interface{}) *InvalidInputError {
+ msg := fmt.Sprintf(format, params...)
+ err := &InvalidInputError{Message: msg}
+ err.AddStackTrace(2)
+ return err
+}
+
+// InternalError is a catch-all for errors that don't fit somewhere else
+type InternalError struct {
+ UnexpectedError
+ Message string
+}
+
+func (f InternalError) Error() string {
+ return fmt.Sprintf("%s: %s", prefix, f.Message)
+}
+
+func NewInternalError(format string, params ...interface{}) *InternalError {
+ msg := fmt.Sprintf(format, params...)
+ err := &InternalError{Message: msg}
+ err.AddStackTrace(2)
+ return err
+}
+
+// ***************************************************************************
+// Global exported function declarations
+
+// Attach a stack trace to an error. The error passed in must be a pointer to an error structure for the
+// CordCtlError interface to match.
+func WithStackTrace(err CordCtlError) error {
+ err.AddStackTrace(2)
+ return err
+}
+
+// Set the prefix rather than using os.Args[0]. This is useful for testing.
+func SetPrefix(s string) {
+ prefix = s
+}
+
+// Convert an RPC error into a Cord Error
+func RpcErrorWithIdToCordError(err error, modelName string, id int32) error {
+ if err == nil {
+ return err
+ }
+ if strings.Contains(err.Error(), "rpc error: code = NotFound") {
+ err := &ModelNotFoundError{ModelName: modelName, Id: id}
+ err.AddStackTrace(2)
+ return err
+ }
+ return err
+}
+
+// Module initialization. Automatically defaults prefix to program name
+func init() {
+ prefix = os.Args[0]
+}
diff --git a/error/error_test.go b/error/error_test.go
new file mode 100644
index 0000000..072b74e
--- /dev/null
+++ b/error/error_test.go
@@ -0,0 +1,81 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original copyright 2019-present Ciena Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package error
+
+import (
+ "fmt"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func init() {
+ SetPrefix("cordctl")
+}
+
+func TestGenericError(t *testing.T) {
+ var err error
+
+ err = fmt.Errorf("Some error")
+
+ // Type conversion from `error` to ChecksumMismatchError should fail
+ _, ok := err.(ChecksumMismatchError)
+ assert.False(t, ok)
+
+ // Type conversion from `error` to CordCtlError should fail
+ _, ok = err.(CordCtlError)
+ assert.False(t, ok)
+}
+
+func TestChecksumMismatchError(t *testing.T) {
+ var err error
+
+ err = WithStackTrace(&ChecksumMismatchError{Actual: "123", Expected: "456"})
+
+ //assert.Equal(t, err.(*ChecksumMismatchError).Stack(), "foo")
+
+ // Check that the Error() function returns the right text
+ assert.Equal(t, err.Error(), "cordctl: checksum mismatch (actual=456, expected=123)")
+
+ // Type conversion from `error` to ChecksumMismatchError should succeed
+ _, ok := err.(*ChecksumMismatchError)
+ assert.True(t, ok)
+
+ // Type switch is another way of doing the same
+ switch err.(type) {
+ case *ChecksumMismatchError:
+ // do nothing
+ case CordCtlError:
+ assert.Fail(t, "Should have used the ChecksumMismatchError case instead")
+ default:
+ assert.Fail(t, "Wrong part of switch statement was called")
+ }
+
+ // Type conversion from `error` to CordCtlError should succeed
+ cce, ok := err.(CordCtlError)
+ assert.True(t, ok)
+
+ // ShouldDumpStack() returned from a ChecksumMismatchError should be false
+ assert.False(t, cce.ShouldDumpStack())
+}
+
+func TestUnknownModelTypeError(t *testing.T) {
+ var err error
+
+ err = WithStackTrace(&UnknownModelTypeError{Name: "foo"})
+
+ _ = err
+}
diff --git a/mock/data.json b/mock/data.json
index fb168f6..368c2e8 100644
--- a/mock/data.json
+++ b/mock/data.json
@@ -48,7 +48,7 @@
{
"method": "GetSlice",
"input": {"id": 77},
- "error": { "code": 2, "message": "Slice matching query does not exist."}
+ "error": { "code": 5, "message": "Slice matching query does not exist."}
},
{
"method": "ListSlice",
@@ -134,7 +134,7 @@
{
"method": "UpdateSlice",
"input": { "id": 77, "name": "mockslice1_newname"},
- "error": { "code": 2, "message": "Slice matching query does not exist."}
+ "error": { "code": 5, "message": "Slice matching query does not exist."}
},
{
"method": "CreateSlice",
@@ -153,7 +153,7 @@
{
"method": "DeleteSlice",
"input": {"id": 77},
- "error": { "code": 2, "message": "Slice matching query does not exist."}
+ "error": { "code": 5, "message": "Slice matching query does not exist."}
},
{
"method": "GetLoadStatus",