SEBA-705 better error reporting

Change-Id: Id461c6efe2d0b7ab9c0d1ddb72482d10899b16fe
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)