blob: 9377deeaca64212bc50fe27c09165f319bcb1ce3 [file] [log] [blame]
Scott Baker20481aa2019-06-20 11:00:54 -07001/*
2 * Portions copyright 2019-present Open Networking Foundation
3 * Original copyright 2019-present Ciena Corporation
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package error
19
20/* Cordctl error classes
21
22 The basic idea is to throw specific error classes, so it's easier to test for them rather than doing string
23 comparisons or other ad hoc mechanisms for determining the type of error. This decouples the human
24 readable text of an error from programmatic testing of error type.
25
26 We differentiate between errors that we want to generate brief output, such as for example a
27 user mistyping a model name, versus errors that we want to generate additional context. This prevents
28 overwhelming a user with voluminous output for a simple mistake. A command-line option may be provided
29 to force full error output should it be desired.
30
31 Additionally, an added benefit is ease of maintenance and localisation, by locating all error text
32 in one place.
33
34 To return an error, for example:
35
36 return WithStackTrace(&ChecksumMismatchError{Actual: "123", Expected: "456"})
37
38 To check to see if a specific error was returned, either of the following are acceptable:
39
40 _, ok := err.(*ChecksumMismatchError)
41 ...
42
43 switch err.(type) {
44 case *ChecksumMismatchError:
45 ...
46*/
47
48import (
49 "bytes"
50 "fmt"
51 go_errors "github.com/go-errors/errors"
Scott Bakerce11c492019-07-30 15:53:34 -070052 "github.com/opencord/cordctl/internal/pkg/config"
Scott Bakera55e6452019-06-25 11:10:30 -070053 "google.golang.org/grpc/status"
Scott Baker20481aa2019-06-20 11:00:54 -070054 "runtime"
55 "strings"
56)
57
58const (
59 MaxStackDepth = 50
60)
61
Scott Baker20481aa2019-06-20 11:00:54 -070062/* CordCtlError is the interface for errors created by cordctl.
63 * ShouldDumpStack()
64 * Returns false for well-understood problems such as invalid user input where a brief error message is sufficient
65 * Returns true for poorly-understood / unexpected problems where a full dump context may be useful
66 * Stack()
67 * Returns a string containing the stack trace where the error occurred
68 * Only useful if WithStackTrace() was called on the error
69 */
70
71type CordCtlError interface {
72 error
73 ShouldDumpStack() bool
74 Stack() string
75 AddStackTrace(skip int)
76}
77
Scott Bakera55e6452019-06-25 11:10:30 -070078/* ObjectReference contains information about the object that the error applies to.
79 This may be empty (ModelName="") or it may contain a ModelName together with
80 option Id or Queries.
81*/
82
83type ObjectReference struct {
84 ModelName string
85 Id int32
86 Queries map[string]string
87}
88
89// Returns true if the reference is populated
90func (f *ObjectReference) IsValid() bool {
91 return (f.ModelName != "")
92}
93
94func (f *ObjectReference) String() string {
95 if !f.IsValid() {
96 // The reference is empty
97 return ""
98 }
99
100 if f.Queries != nil {
101 kv := make([]string, 0, len(f.Queries))
102 for k, v := range f.Queries {
103 kv = append(kv, fmt.Sprintf("%s%s", k, v))
104 }
105 return fmt.Sprintf("%s <%v>", f.ModelName, strings.Join(kv, ", "))
106 }
107
108 if f.Id > 0 {
109 return fmt.Sprintf("%s <id=%d>", f.ModelName, f.Id)
110 }
111
112 return fmt.Sprintf("%s", f.ModelName)
113}
114
115// Returns " on model ModelName [id]" if the reference is populated, or "" otherwise.
116func (f *ObjectReference) Clause() string {
117 if !f.IsValid() {
118 // The reference is empty
119 return ""
120 }
121
122 return fmt.Sprintf(" [on model %s]", f.String())
123}
124
125/* BaseError
126 *
127 * Supports attaching stack traces to errors
Scott Baker20481aa2019-06-20 11:00:54 -0700128 * Borrowed the technique from github.com/go-errors. Decided against using go-errors directly since it requires
129 * wrapping our error classes. Instead, incorporated the stack trace directly into our error class.
Scott Bakera55e6452019-06-25 11:10:30 -0700130 *
131 * Also supports encapsulating error messages, so that a CordError can encapsulate the error message from a
132 * function that was called.
Scott Baker20481aa2019-06-20 11:00:54 -0700133 */
134
135type BaseError struct {
Scott Bakera55e6452019-06-25 11:10:30 -0700136 Obj ObjectReference
137 Encapsulated error // in case this error encapsulates an error from a lower level
138 stack []uintptr // for stack trace
139 frames []go_errors.StackFrame // for stack trace
Scott Baker20481aa2019-06-20 11:00:54 -0700140}
141
142func (f *BaseError) AddStackTrace(skip int) {
143 stack := make([]uintptr, MaxStackDepth)
144 length := runtime.Callers(2+skip, stack[:])
145 f.stack = stack[:length]
146}
147
148func (f *BaseError) Stack() string {
149 buf := bytes.Buffer{}
150
151 for _, frame := range f.StackFrames() {
152 buf.WriteString(frame.String())
153 }
154
155 return string(buf.Bytes())
156}
157
158func (f *BaseError) StackFrames() []go_errors.StackFrame {
159 if f.frames == nil {
160 f.frames = make([]go_errors.StackFrame, len(f.stack))
161
162 for i, pc := range f.stack {
163 f.frames[i] = go_errors.NewStackFrame(pc)
164 }
165 }
166
167 return f.frames
168}
169
170// ***************************************************************************
171// UserError is composed into Errors that are due to user input
172
173type UserError struct {
174 BaseError
175}
176
177func (f UserError) ShouldDumpStack() bool {
178 return false
179}
180
181// **************************************************************************
182// TransferError is composed into Errors that are due to failures in transfers
183
184type TransferError struct {
185 BaseError
186}
187
188func (f TransferError) ShouldDumpStack() bool {
189 return false
190}
191
192// ***************************************************************************
193// UnexpectedError is things that we don't expect to happen. They should
194// generate maximum error context, to provide useful information for developer
195// diagnosis.
196
197type UnexpectedError struct {
198 BaseError
199}
200
201func (f UnexpectedError) ShouldDumpStack() bool {
202 return true
203}
204
205// ***************************************************************************
206// Specific error classes follow
207
208// Checksum mismatch when downloading or uploading a file
209type ChecksumMismatchError struct {
210 TransferError
211 Name string // (optional) Name of file
212 Expected string
213 Actual string
214}
215
216func (f ChecksumMismatchError) Error() string {
217 if f.Name != "" {
Scott Bakera55e6452019-06-25 11:10:30 -0700218 return fmt.Sprintf("%s: checksum mismatch (actual=%s, expected=%s)", f.Name, f.Expected, f.Actual)
Scott Baker20481aa2019-06-20 11:00:54 -0700219 } else {
Scott Bakera55e6452019-06-25 11:10:30 -0700220 return fmt.Sprintf("checksum mismatch (actual=%s, expected=%s)", f.Expected, f.Actual)
Scott Baker20481aa2019-06-20 11:00:54 -0700221 }
222}
223
224// User specified a model type that is not valid
225type UnknownModelTypeError struct {
226 UserError
227 Name string // Name of model
228}
229
230func (f UnknownModelTypeError) Error() string {
Scott Bakera55e6452019-06-25 11:10:30 -0700231 return fmt.Sprintf("Model %s does not exist. Use `cordctl modeltype list` to get a list of available models", f.Name)
Scott Baker20481aa2019-06-20 11:00:54 -0700232}
233
234// User specified a model state that is not valid
235type UnknownModelStateError struct {
236 UserError
237 Name string // Name of state
238}
239
240func (f UnknownModelStateError) Error() string {
Scott Bakera55e6452019-06-25 11:10:30 -0700241 return fmt.Sprintf("Model state %s does not exist", f.Name)
Scott Baker20481aa2019-06-20 11:00:54 -0700242}
243
244// Command requires a filter be specified
245type FilterRequiredError struct {
246 UserError
247}
248
249func (f FilterRequiredError) Error() string {
250 return "Filter required. Use either an ID, --filter, or --all to specify which models to operate on"
251}
252
253// Command was aborted by the user
254type AbortedError struct {
255 UserError
256}
257
258func (f AbortedError) Error() string {
259 return "Aborted"
260}
261
262// Command was aborted by the user
263type NoMatchError struct {
264 UserError
265}
266
267func (f NoMatchError) Error() string {
268 return "No Match"
269}
270
271// User specified a field name that is not valid
272type FieldDoesNotExistError struct {
273 UserError
274 ModelName string
275 FieldName string
276}
277
278func (f FieldDoesNotExistError) Error() string {
Scott Bakera55e6452019-06-25 11:10:30 -0700279 return fmt.Sprintf("Model %s does not have field %s", f.ModelName, f.FieldName)
Scott Baker20481aa2019-06-20 11:00:54 -0700280}
281
282// User specified a query string that is not properly formatted
283type IllegalQueryError struct {
284 UserError
285 Query string
286}
287
288func (f IllegalQueryError) Error() string {
Scott Bakera55e6452019-06-25 11:10:30 -0700289 return fmt.Sprintf("Illegal query string %s", f.Query)
Scott Baker20481aa2019-06-20 11:00:54 -0700290}
291
292// We failed to type convert something that we thought should have converted
293type TypeConversionError struct {
294 UnexpectedError
295 Source string
296 Destination string
297}
298
299func (f TypeConversionError) Error() string {
Scott Bakera55e6452019-06-25 11:10:30 -0700300 return fmt.Sprintf("Failed to type convert from %s to %s", f.Source, f.Destination)
Scott Baker20481aa2019-06-20 11:00:54 -0700301}
302
303// Version did not match a constraint
304type VersionConstraintError struct {
305 UserError
306 Name string
307 Version string
308 Constraint string
309}
310
311func (f VersionConstraintError) Error() string {
Scott Bakera55e6452019-06-25 11:10:30 -0700312 return fmt.Sprintf("%s version %s did not match constraint '%s'", f.Name, f.Version, f.Constraint)
Scott Baker20481aa2019-06-20 11:00:54 -0700313}
314
315// A model was not found
316type ModelNotFoundError struct {
317 UserError
Scott Baker20481aa2019-06-20 11:00:54 -0700318}
319
320func (f ModelNotFoundError) Error() string {
Scott Bakera55e6452019-06-25 11:10:30 -0700321 return fmt.Sprintf("Not Found%s", f.Obj.Clause())
322}
323
324// Permission Denied
325type PermissionDeniedError struct {
326 UserError
327}
328
329func (f PermissionDeniedError) Error() string {
330 return fmt.Sprintf("Permission Denied%s. Please verify username and password are correct", f.Obj.Clause())
Scott Baker20481aa2019-06-20 11:00:54 -0700331}
332
Scott Bakerce11c492019-07-30 15:53:34 -0700333// Unavailable
334type UnavailableError struct {
335 UserError
336}
337
338func (f UnavailableError) Error() string {
339 return fmt.Sprintf("Server Unavailable%s. Please verify server settings (%s). If correct, this may be a transient failure -- please try again", f.Obj.Clause(), config.GlobalConfig.Server)
340}
341
Scott Baker20481aa2019-06-20 11:00:54 -0700342// InvalidInputError is a catch-all for user mistakes that aren't covered elsewhere
343type InvalidInputError struct {
344 UserError
345 Message string
346}
347
348func (f InvalidInputError) Error() string {
Scott Bakera55e6452019-06-25 11:10:30 -0700349 return fmt.Sprintf("%s", f.Message)
Scott Baker20481aa2019-06-20 11:00:54 -0700350}
351
352func NewInvalidInputError(format string, params ...interface{}) *InvalidInputError {
353 msg := fmt.Sprintf(format, params...)
354 err := &InvalidInputError{Message: msg}
355 err.AddStackTrace(2)
356 return err
357}
358
359// InternalError is a catch-all for errors that don't fit somewhere else
360type InternalError struct {
361 UnexpectedError
362 Message string
363}
364
365func (f InternalError) Error() string {
Scott Bakera55e6452019-06-25 11:10:30 -0700366 return fmt.Sprintf("Internal Error%s: %s", f.Obj.Clause(), f.Message)
Scott Baker20481aa2019-06-20 11:00:54 -0700367}
368
369func NewInternalError(format string, params ...interface{}) *InternalError {
370 msg := fmt.Sprintf(format, params...)
371 err := &InternalError{Message: msg}
372 err.AddStackTrace(2)
373 return err
374}
375
376// ***************************************************************************
377// Global exported function declarations
378
379// Attach a stack trace to an error. The error passed in must be a pointer to an error structure for the
380// CordCtlError interface to match.
381func WithStackTrace(err CordCtlError) error {
382 err.AddStackTrace(2)
383 return err
384}
385
Scott Bakera55e6452019-06-25 11:10:30 -0700386/* RpcErrorWithObjToCordError
387 *
388 * Convert an RPC error into a Cord Error. The ObjectReference allows methods to attach
389 * object-related information to the error, and this varies by method. For example the Delete()
390 * method comes with an ModelName and an Id. The List() method has only a ModelName.
391 *
392 * Stubs (RpcErrorWithModelNameToCordError) are provided below to make common usage more convenient.
393 */
Scott Baker20481aa2019-06-20 11:00:54 -0700394
Scott Bakera55e6452019-06-25 11:10:30 -0700395func RpcErrorWithObjToCordError(err error, obj ObjectReference) error {
Scott Baker20481aa2019-06-20 11:00:54 -0700396 if err == nil {
397 return err
398 }
Scott Bakera55e6452019-06-25 11:10:30 -0700399
400 st, ok := status.FromError(err)
401 if ok {
402 switch st.Code().String() {
403 case "PermissionDenied":
404 cordErr := &PermissionDeniedError{}
405 cordErr.Obj = obj
406 cordErr.Encapsulated = err
407 cordErr.AddStackTrace(2)
408 return cordErr
409 case "NotFound":
410 cordErr := &ModelNotFoundError{}
411 cordErr.Obj = obj
412 cordErr.Encapsulated = err
413 cordErr.AddStackTrace(2)
414 return cordErr
Scott Bakerce11c492019-07-30 15:53:34 -0700415 case "Unavailable":
416 cordErr := &UnavailableError{}
417 cordErr.Obj = obj
418 cordErr.Encapsulated = err
419 cordErr.AddStackTrace(2)
420 return cordErr
Scott Bakera55e6452019-06-25 11:10:30 -0700421 case "Unknown":
422 msg := st.Message()
423 if strings.HasPrefix(msg, "Exception calling application: ") {
424 msg = msg[31:]
425 }
426 cordErr := &InternalError{Message: msg}
427 cordErr.Obj = obj
428 cordErr.Encapsulated = err
429 cordErr.AddStackTrace(2)
430 return cordErr
431 }
Scott Bakerce11c492019-07-30 15:53:34 -0700432 // Errors encapsulated by grpCurl
433 } else if strings.Contains(err.Error(), "failed to query for service descriptor") &&
434 strings.Contains(err.Error(), "Unavailable") {
435 cordErr := &UnavailableError{}
436 cordErr.Obj = obj
437 cordErr.Encapsulated = err
438 cordErr.AddStackTrace(2)
439 return cordErr
Scott Baker20481aa2019-06-20 11:00:54 -0700440 }
Scott Bakera55e6452019-06-25 11:10:30 -0700441
Scott Baker20481aa2019-06-20 11:00:54 -0700442 return err
443}
444
Scott Bakera55e6452019-06-25 11:10:30 -0700445func RpcErrorToCordError(err error) error {
446 return RpcErrorWithObjToCordError(err, ObjectReference{})
447}
448
449func RpcErrorWithModelNameToCordError(err error, modelName string) error {
450 return RpcErrorWithObjToCordError(err, ObjectReference{ModelName: modelName})
451}
452
453func RpcErrorWithIdToCordError(err error, modelName string, id int32) error {
454 return RpcErrorWithObjToCordError(err, ObjectReference{ModelName: modelName, Id: id})
455}
456
457func RpcErrorWithQueriesToCordError(err error, modelName string, queries map[string]string) error {
458 return RpcErrorWithObjToCordError(err, ObjectReference{ModelName: modelName, Queries: queries})
Scott Baker20481aa2019-06-20 11:00:54 -0700459}