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",