blob: e35a14aaee2c42026c73116e392a3b23807e1a72 [file] [log] [blame]
/*
* 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"
"google.golang.org/grpc/status"
"runtime"
"strings"
)
const (
MaxStackDepth = 50
)
/* 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)
}
/* ObjectReference contains information about the object that the error applies to.
This may be empty (ModelName="") or it may contain a ModelName together with
option Id or Queries.
*/
type ObjectReference struct {
ModelName string
Id int32
Queries map[string]string
}
// Returns true if the reference is populated
func (f *ObjectReference) IsValid() bool {
return (f.ModelName != "")
}
func (f *ObjectReference) String() string {
if !f.IsValid() {
// The reference is empty
return ""
}
if f.Queries != nil {
kv := make([]string, 0, len(f.Queries))
for k, v := range f.Queries {
kv = append(kv, fmt.Sprintf("%s%s", k, v))
}
return fmt.Sprintf("%s <%v>", f.ModelName, strings.Join(kv, ", "))
}
if f.Id > 0 {
return fmt.Sprintf("%s <id=%d>", f.ModelName, f.Id)
}
return fmt.Sprintf("%s", f.ModelName)
}
// Returns " on model ModelName [id]" if the reference is populated, or "" otherwise.
func (f *ObjectReference) Clause() string {
if !f.IsValid() {
// The reference is empty
return ""
}
return fmt.Sprintf(" [on model %s]", f.String())
}
/* 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.
*
* Also supports encapsulating error messages, so that a CordError can encapsulate the error message from a
* function that was called.
*/
type BaseError struct {
Obj ObjectReference
Encapsulated error // in case this error encapsulates an error from a lower level
stack []uintptr // for stack trace
frames []go_errors.StackFrame // for stack trace
}
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: checksum mismatch (actual=%s, expected=%s)", f.Name, f.Expected, f.Actual)
} else {
return fmt.Sprintf("checksum mismatch (actual=%s, expected=%s)", 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("Model %s does not exist. Use `cordctl modeltype list` to get a list of available models", 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("Model state %s does not exist", 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("Model %s does not have field %s", 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("Illegal query string %s", 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("Failed to type convert from %s to %s", 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 version %s did not match constraint '%s'", f.Name, f.Version, f.Constraint)
}
// A model was not found
type ModelNotFoundError struct {
UserError
}
func (f ModelNotFoundError) Error() string {
return fmt.Sprintf("Not Found%s", f.Obj.Clause())
}
// Permission Denied
type PermissionDeniedError struct {
UserError
}
func (f PermissionDeniedError) Error() string {
return fmt.Sprintf("Permission Denied%s. Please verify username and password are correct", f.Obj.Clause())
}
// 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", 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("Internal Error%s: %s", f.Obj.Clause(), 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
}
/* RpcErrorWithObjToCordError
*
* Convert an RPC error into a Cord Error. The ObjectReference allows methods to attach
* object-related information to the error, and this varies by method. For example the Delete()
* method comes with an ModelName and an Id. The List() method has only a ModelName.
*
* Stubs (RpcErrorWithModelNameToCordError) are provided below to make common usage more convenient.
*/
func RpcErrorWithObjToCordError(err error, obj ObjectReference) error {
if err == nil {
return err
}
st, ok := status.FromError(err)
if ok {
switch st.Code().String() {
case "PermissionDenied":
cordErr := &PermissionDeniedError{}
cordErr.Obj = obj
cordErr.Encapsulated = err
cordErr.AddStackTrace(2)
return cordErr
case "NotFound":
cordErr := &ModelNotFoundError{}
cordErr.Obj = obj
cordErr.Encapsulated = err
cordErr.AddStackTrace(2)
return cordErr
case "Unknown":
msg := st.Message()
if strings.HasPrefix(msg, "Exception calling application: ") {
msg = msg[31:]
}
cordErr := &InternalError{Message: msg}
cordErr.Obj = obj
cordErr.Encapsulated = err
cordErr.AddStackTrace(2)
return cordErr
}
}
return err
}
func RpcErrorToCordError(err error) error {
return RpcErrorWithObjToCordError(err, ObjectReference{})
}
func RpcErrorWithModelNameToCordError(err error, modelName string) error {
return RpcErrorWithObjToCordError(err, ObjectReference{ModelName: modelName})
}
func RpcErrorWithIdToCordError(err error, modelName string, id int32) error {
return RpcErrorWithObjToCordError(err, ObjectReference{ModelName: modelName, Id: id})
}
func RpcErrorWithQueriesToCordError(err error, modelName string, queries map[string]string) error {
return RpcErrorWithObjToCordError(err, ObjectReference{ModelName: modelName, Queries: queries})
}