blob: c30096bf668eb8b4f1c58e6bdcb29cb00a8465e8 [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"
52 "os"
53 "runtime"
54 "strings"
55)
56
57const (
58 MaxStackDepth = 50
59)
60
61// Prefix applied to all error messages. Initialized in module init() function from os.Args[0]
62var prefix string
63
64/* CordCtlError is the interface for errors created by cordctl.
65 * ShouldDumpStack()
66 * Returns false for well-understood problems such as invalid user input where a brief error message is sufficient
67 * Returns true for poorly-understood / unexpected problems where a full dump context may be useful
68 * Stack()
69 * Returns a string containing the stack trace where the error occurred
70 * Only useful if WithStackTrace() was called on the error
71 */
72
73type CordCtlError interface {
74 error
75 ShouldDumpStack() bool
76 Stack() string
77 AddStackTrace(skip int)
78}
79
80/* BaseError supports attaching stack traces to errors
81 * Borrowed the technique from github.com/go-errors. Decided against using go-errors directly since it requires
82 * wrapping our error classes. Instead, incorporated the stack trace directly into our error class.
83 */
84
85type BaseError struct {
86 stack []uintptr
87 frames []go_errors.StackFrame
88}
89
90func (f *BaseError) AddStackTrace(skip int) {
91 stack := make([]uintptr, MaxStackDepth)
92 length := runtime.Callers(2+skip, stack[:])
93 f.stack = stack[:length]
94}
95
96func (f *BaseError) Stack() string {
97 buf := bytes.Buffer{}
98
99 for _, frame := range f.StackFrames() {
100 buf.WriteString(frame.String())
101 }
102
103 return string(buf.Bytes())
104}
105
106func (f *BaseError) StackFrames() []go_errors.StackFrame {
107 if f.frames == nil {
108 f.frames = make([]go_errors.StackFrame, len(f.stack))
109
110 for i, pc := range f.stack {
111 f.frames[i] = go_errors.NewStackFrame(pc)
112 }
113 }
114
115 return f.frames
116}
117
118// ***************************************************************************
119// UserError is composed into Errors that are due to user input
120
121type UserError struct {
122 BaseError
123}
124
125func (f UserError) ShouldDumpStack() bool {
126 return false
127}
128
129// **************************************************************************
130// TransferError is composed into Errors that are due to failures in transfers
131
132type TransferError struct {
133 BaseError
134}
135
136func (f TransferError) ShouldDumpStack() bool {
137 return false
138}
139
140// ***************************************************************************
141// UnexpectedError is things that we don't expect to happen. They should
142// generate maximum error context, to provide useful information for developer
143// diagnosis.
144
145type UnexpectedError struct {
146 BaseError
147}
148
149func (f UnexpectedError) ShouldDumpStack() bool {
150 return true
151}
152
153// ***************************************************************************
154// Specific error classes follow
155
156// Checksum mismatch when downloading or uploading a file
157type ChecksumMismatchError struct {
158 TransferError
159 Name string // (optional) Name of file
160 Expected string
161 Actual string
162}
163
164func (f ChecksumMismatchError) Error() string {
165 if f.Name != "" {
166 return fmt.Sprintf("%s %s: checksum mismatch (actual=%s, expected=%s)", prefix, f.Name, f.Expected, f.Actual)
167 } else {
168 return fmt.Sprintf("%s: checksum mismatch (actual=%s, expected=%s)", prefix, f.Expected, f.Actual)
169 }
170}
171
172// User specified a model type that is not valid
173type UnknownModelTypeError struct {
174 UserError
175 Name string // Name of model
176}
177
178func (f UnknownModelTypeError) Error() string {
179 return fmt.Sprintf("%s: Model %s does not exist. Use `cordctl modeltype list` to get a list of available models", prefix, f.Name)
180}
181
182// User specified a model state that is not valid
183type UnknownModelStateError struct {
184 UserError
185 Name string // Name of state
186}
187
188func (f UnknownModelStateError) Error() string {
189 return fmt.Sprintf("%s: Model state %s does not exist", prefix, f.Name)
190}
191
192// Command requires a filter be specified
193type FilterRequiredError struct {
194 UserError
195}
196
197func (f FilterRequiredError) Error() string {
198 return "Filter required. Use either an ID, --filter, or --all to specify which models to operate on"
199}
200
201// Command was aborted by the user
202type AbortedError struct {
203 UserError
204}
205
206func (f AbortedError) Error() string {
207 return "Aborted"
208}
209
210// Command was aborted by the user
211type NoMatchError struct {
212 UserError
213}
214
215func (f NoMatchError) Error() string {
216 return "No Match"
217}
218
219// User specified a field name that is not valid
220type FieldDoesNotExistError struct {
221 UserError
222 ModelName string
223 FieldName string
224}
225
226func (f FieldDoesNotExistError) Error() string {
227 return fmt.Sprintf("%s: Model %s does not have field %s", prefix, f.ModelName, f.FieldName)
228}
229
230// User specified a query string that is not properly formatted
231type IllegalQueryError struct {
232 UserError
233 Query string
234}
235
236func (f IllegalQueryError) Error() string {
237 return fmt.Sprintf("%s: Illegal query string %s", prefix, f.Query)
238}
239
240// We failed to type convert something that we thought should have converted
241type TypeConversionError struct {
242 UnexpectedError
243 Source string
244 Destination string
245}
246
247func (f TypeConversionError) Error() string {
248 return fmt.Sprintf("%s: Failed to type convert from %s to %s", prefix, f.Source, f.Destination)
249}
250
251// Version did not match a constraint
252type VersionConstraintError struct {
253 UserError
254 Name string
255 Version string
256 Constraint string
257}
258
259func (f VersionConstraintError) Error() string {
260 return fmt.Sprintf("%s: %s version %s did not match constraint '%s'", prefix, f.Name, f.Version, f.Constraint)
261}
262
263// A model was not found
264type ModelNotFoundError struct {
265 UserError
266 ModelName string
267 Id int32
268 Queries map[string]string
269}
270
271func (f ModelNotFoundError) Error() string {
272 if f.Queries != nil {
273 return fmt.Sprintf("%s: %s query %v not Found", prefix, f.ModelName, f.Queries)
274 } else {
275 return fmt.Sprintf("%s: Model %s id %d not Found", prefix, f.ModelName, f.Id)
276 }
277}
278
279// InvalidInputError is a catch-all for user mistakes that aren't covered elsewhere
280type InvalidInputError struct {
281 UserError
282 Message string
283}
284
285func (f InvalidInputError) Error() string {
286 return fmt.Sprintf("%s: %s", prefix, f.Message)
287}
288
289func NewInvalidInputError(format string, params ...interface{}) *InvalidInputError {
290 msg := fmt.Sprintf(format, params...)
291 err := &InvalidInputError{Message: msg}
292 err.AddStackTrace(2)
293 return err
294}
295
296// InternalError is a catch-all for errors that don't fit somewhere else
297type InternalError struct {
298 UnexpectedError
299 Message string
300}
301
302func (f InternalError) Error() string {
303 return fmt.Sprintf("%s: %s", prefix, f.Message)
304}
305
306func NewInternalError(format string, params ...interface{}) *InternalError {
307 msg := fmt.Sprintf(format, params...)
308 err := &InternalError{Message: msg}
309 err.AddStackTrace(2)
310 return err
311}
312
313// ***************************************************************************
314// Global exported function declarations
315
316// Attach a stack trace to an error. The error passed in must be a pointer to an error structure for the
317// CordCtlError interface to match.
318func WithStackTrace(err CordCtlError) error {
319 err.AddStackTrace(2)
320 return err
321}
322
323// Set the prefix rather than using os.Args[0]. This is useful for testing.
324func SetPrefix(s string) {
325 prefix = s
326}
327
328// Convert an RPC error into a Cord Error
329func RpcErrorWithIdToCordError(err error, modelName string, id int32) error {
330 if err == nil {
331 return err
332 }
333 if strings.Contains(err.Error(), "rpc error: code = NotFound") {
334 err := &ModelNotFoundError{ModelName: modelName, Id: id}
335 err.AddStackTrace(2)
336 return err
337 }
338 return err
339}
340
341// Module initialization. Automatically defaults prefix to program name
342func init() {
343 prefix = os.Args[0]
344}