blob: 33c37939fe0617ced685158c838aace956ac2221 [file] [log] [blame]
Scott Baker2c0ebda2019-05-06 16:55:47 -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 */
17package commands
18
19import (
Scott Baker2c0ebda2019-05-06 16:55:47 -070020 "fmt"
Scott Baker2c0ebda2019-05-06 16:55:47 -070021 flags "github.com/jessevdk/go-flags"
22 "github.com/jhump/protoreflect/dynamic"
Scott Baker2c0ebda2019-05-06 16:55:47 -070023 "strings"
24)
25
Scott Baker175cb402019-05-17 16:13:06 -070026const (
Scott Baker8b8a2b52019-05-21 08:56:28 -070027 DEFAULT_CREATE_FORMAT = "table{{ .Id }}\t{{ .Message }}"
Scott Baker175cb402019-05-17 16:13:06 -070028 DEFAULT_DELETE_FORMAT = "table{{ .Id }}\t{{ .Message }}"
29 DEFAULT_UPDATE_FORMAT = "table{{ .Id }}\t{{ .Message }}"
Scott Baker8b8a2b52019-05-21 08:56:28 -070030 DEFAULT_SYNC_FORMAT = "table{{ .Id }}\t{{ .Message }}"
Scott Baker175cb402019-05-17 16:13:06 -070031)
32
Scott Baker63ce82e2019-05-15 09:01:42 -070033type ModelNameString string
34
Scott Baker2c0ebda2019-05-06 16:55:47 -070035type ModelList struct {
36 OutputOptions
Scott Baker5201c0b2019-05-15 15:35:56 -070037 ShowHidden bool `long:"showhidden" description:"Show hidden fields in default output"`
38 ShowFeedback bool `long:"showfeedback" description:"Show feedback fields in default output"`
39 ShowBookkeeping bool `long:"showbookkeeping" description:"Show bookkeeping fields in default output"`
Scott Baker175cb402019-05-17 16:13:06 -070040 Filter string `short:"f" long:"filter" description:"Comma-separated list of filters"`
Scott Baker6cf525a2019-05-09 12:25:08 -070041 Args struct {
Scott Baker63ce82e2019-05-15 09:01:42 -070042 ModelName ModelNameString
Scott Baker2c0ebda2019-05-06 16:55:47 -070043 } `positional-args:"yes" required:"yes"`
44}
45
Scott Baker5281d002019-05-16 10:45:26 -070046type ModelUpdate struct {
Scott Baker6cf525a2019-05-09 12:25:08 -070047 OutputOptions
Scott Baker8b8a2b52019-05-21 08:56:28 -070048 Unbuffered bool `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
49 Filter string `short:"f" long:"filter" description:"Comma-separated list of filters"`
50 SetFields string `long:"set-field" description:"Comma-separated list of field=value to set"`
51 SetJSON string `long:"set-json" description:"JSON dictionary to use for settings fields"`
52 Sync bool `long:"sync" description:"Synchronize before returning"`
53 Args struct {
Scott Baker5281d002019-05-16 10:45:26 -070054 ModelName ModelNameString
55 } `positional-args:"yes" required:"yes"`
56 IDArgs struct {
57 ID []int32
58 } `positional-args:"yes" required:"no"`
Scott Baker6cf525a2019-05-09 12:25:08 -070059}
60
Scott Baker175cb402019-05-17 16:13:06 -070061type ModelDelete struct {
62 OutputOptions
Scott Baker8b8a2b52019-05-21 08:56:28 -070063 Unbuffered bool `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
64 Filter string `short:"f" long:"filter" description:"Comma-separated list of filters"`
65 Args struct {
66 ModelName ModelNameString
67 } `positional-args:"yes" required:"yes"`
68 IDArgs struct {
69 ID []int32
70 } `positional-args:"yes" required:"no"`
71}
72
73type ModelCreate struct {
74 OutputOptions
75 Unbuffered bool `short:"u" long:"unbuffered" description:"Do not buffer console output"`
76 SetFields string `long:"set-field" description:"Comma-separated list of field=value to set"`
77 SetJSON string `long:"set-json" description:"JSON dictionary to use for settings fields"`
78 Sync bool `long:"sync" description:"Synchronize before returning"`
79 Args struct {
80 ModelName ModelNameString
81 } `positional-args:"yes" required:"yes"`
82}
83
84type ModelSync struct {
85 OutputOptions
86 Unbuffered bool `short:"u" long:"unbuffered" description:"Do not buffer console output and suppress default output processor"`
87 Filter string `short:"f" long:"filter" description:"Comma-separated list of filters"`
88 Args struct {
Scott Baker175cb402019-05-17 16:13:06 -070089 ModelName ModelNameString
90 } `positional-args:"yes" required:"yes"`
91 IDArgs struct {
92 ID []int32
93 } `positional-args:"yes" required:"no"`
94}
95
Scott Baker2c0ebda2019-05-06 16:55:47 -070096type ModelOpts struct {
Scott Baker5281d002019-05-16 10:45:26 -070097 List ModelList `command:"list"`
98 Update ModelUpdate `command:"update"`
Scott Baker175cb402019-05-17 16:13:06 -070099 Delete ModelDelete `command:"delete"`
Scott Baker8b8a2b52019-05-21 08:56:28 -0700100 Create ModelCreate `command:"create"`
101 Sync ModelSync `command:"sync"`
Scott Baker175cb402019-05-17 16:13:06 -0700102}
103
104type ModelStatusOutputRow struct {
Scott Baker8b8a2b52019-05-21 08:56:28 -0700105 Id interface{} `json:"id"`
106 Message string `json:"message"`
Scott Baker175cb402019-05-17 16:13:06 -0700107}
108
109type ModelStatusOutput struct {
Scott Baker8b8a2b52019-05-21 08:56:28 -0700110 Rows []ModelStatusOutputRow
111 Unbuffered bool
Scott Baker2c0ebda2019-05-06 16:55:47 -0700112}
113
114var modelOpts = ModelOpts{}
115
116func RegisterModelCommands(parser *flags.Parser) {
117 parser.AddCommand("model", "model commands", "Commands to query and manipulate XOS models", &modelOpts)
118}
119
Scott Baker8b8a2b52019-05-21 08:56:28 -0700120// Initialize ModelStatusOutput structure, creating a row for each model that will be output
121func InitModelStatusOutput(unbuffered bool, count int) ModelStatusOutput {
122 return ModelStatusOutput{Rows: make([]ModelStatusOutputRow, count), Unbuffered: unbuffered}
Scott Baker175cb402019-05-17 16:13:06 -0700123}
124
Scott Baker8b8a2b52019-05-21 08:56:28 -0700125// Update model status output row for the model
126// If unbuffered is set then we will output directly to the console. Regardless of the unbuffered
127// setting, we always update the row, as callers may check that row for status.
128// Args:
129// output - ModelStatusOutput struct to update
130// i - index of row to update
131// id - id of model, <nil> if no model exists
132// status - status text to set if there is no error
133// errror - if non-nil, then apply error text instead of status text
134// final - true if successful status should be reported, false if successful status is yet to come
135
136func UpdateModelStatusOutput(output *ModelStatusOutput, i int, id interface{}, status string, err error, final bool) {
Scott Baker175cb402019-05-17 16:13:06 -0700137 if err != nil {
Scott Baker8b8a2b52019-05-21 08:56:28 -0700138 if output.Unbuffered {
139 fmt.Printf("%v: %s\n", id, HumanReadableError(err))
Scott Baker175cb402019-05-17 16:13:06 -0700140 }
Scott Baker8b8a2b52019-05-21 08:56:28 -0700141 output.Rows[i] = ModelStatusOutputRow{Id: id, Message: HumanReadableError(err)}
Scott Baker175cb402019-05-17 16:13:06 -0700142 } else {
Scott Baker8b8a2b52019-05-21 08:56:28 -0700143 if output.Unbuffered && final {
Scott Baker175cb402019-05-17 16:13:06 -0700144 fmt.Println(id)
Scott Baker175cb402019-05-17 16:13:06 -0700145 }
Scott Baker8b8a2b52019-05-21 08:56:28 -0700146 output.Rows[i] = ModelStatusOutputRow{Id: id, Message: status}
Scott Baker175cb402019-05-17 16:13:06 -0700147 }
148}
149
Scott Baker6cf525a2019-05-09 12:25:08 -0700150func (options *ModelList) Execute(args []string) error {
151 conn, descriptor, err := InitReflectionClient()
152 if err != nil {
153 return err
154 }
155
156 defer conn.Close()
157
Scott Baker63ce82e2019-05-15 09:01:42 -0700158 err = CheckModelName(descriptor, string(options.Args.ModelName))
Scott Baker6cf525a2019-05-09 12:25:08 -0700159 if err != nil {
160 return err
161 }
162
Scott Baker5281d002019-05-16 10:45:26 -0700163 queries, err := CommaSeparatedQueryToMap(options.Filter, true)
Scott Baker2c0ebda2019-05-06 16:55:47 -0700164 if err != nil {
165 return err
166 }
167
Scott Baker5281d002019-05-16 10:45:26 -0700168 models, err := ListOrFilterModels(conn, descriptor, string(options.Args.ModelName), queries)
Scott Baker2c0ebda2019-05-06 16:55:47 -0700169 if err != nil {
170 return err
171 }
172
173 field_names := make(map[string]bool)
Scott Baker5201c0b2019-05-15 15:35:56 -0700174 data := make([]map[string]interface{}, len(models))
175 for i, val := range models {
Scott Baker2c0ebda2019-05-06 16:55:47 -0700176 data[i] = make(map[string]interface{})
177 for _, field_desc := range val.GetKnownFields() {
178 field_name := field_desc.GetName()
Scott Baker2c0ebda2019-05-06 16:55:47 -0700179
180 isGuiHidden := strings.Contains(field_desc.GetFieldOptions().String(), "1005:1")
181 isFeedback := strings.Contains(field_desc.GetFieldOptions().String(), "1006:1")
182 isBookkeeping := strings.Contains(field_desc.GetFieldOptions().String(), "1007:1")
183
184 if isGuiHidden && (!options.ShowHidden) {
185 continue
186 }
187
188 if isFeedback && (!options.ShowFeedback) {
189 continue
190 }
191
Scott Baker6cf525a2019-05-09 12:25:08 -0700192 if isBookkeeping && (!options.ShowBookkeeping) {
Scott Baker2c0ebda2019-05-06 16:55:47 -0700193 continue
194 }
195
196 if field_desc.IsRepeated() {
197 continue
198 }
199
Scott Baker6cf525a2019-05-09 12:25:08 -0700200 data[i][field_name] = val.GetFieldByName(field_name)
Scott Baker2c0ebda2019-05-06 16:55:47 -0700201
202 field_names[field_name] = true
203 }
204 }
205
206 var default_format strings.Builder
207 default_format.WriteString("table")
208 first := true
209 for field_name, _ := range field_names {
210 if first {
211 fmt.Fprintf(&default_format, "{{ .%s }}", field_name)
212 first = false
213 } else {
214 fmt.Fprintf(&default_format, "\t{{ .%s }}", field_name)
215 }
216 }
217
Scott Baker5281d002019-05-16 10:45:26 -0700218 FormatAndGenerateOutput(&options.OutputOptions, default_format.String(), "{{.id}}", data)
219
220 return nil
221}
222
223func (options *ModelUpdate) Execute(args []string) error {
224 conn, descriptor, err := InitReflectionClient()
225 if err != nil {
226 return err
Scott Baker2c0ebda2019-05-06 16:55:47 -0700227 }
228
Scott Baker5281d002019-05-16 10:45:26 -0700229 defer conn.Close()
230
231 err = CheckModelName(descriptor, string(options.Args.ModelName))
232 if err != nil {
233 return err
Scott Baker2c0ebda2019-05-06 16:55:47 -0700234 }
235
Scott Baker5281d002019-05-16 10:45:26 -0700236 if (len(options.IDArgs.ID) == 0 && len(options.Filter) == 0) ||
237 (len(options.IDArgs.ID) != 0 && len(options.Filter) != 0) {
238 return fmt.Errorf("Use either an ID or a --filter to specify which models to update")
239 }
240
241 queries, err := CommaSeparatedQueryToMap(options.Filter, true)
242 if err != nil {
243 return err
244 }
245
246 updates, err := CommaSeparatedQueryToMap(options.SetFields, true)
247 if err != nil {
248 return err
249 }
250
251 modelName := string(options.Args.ModelName)
252
253 var models []*dynamic.Message
254
255 if len(options.IDArgs.ID) > 0 {
256 models = make([]*dynamic.Message, len(options.IDArgs.ID))
257 for i, id := range options.IDArgs.ID {
258 models[i], err = GetModel(conn, descriptor, modelName, id)
259 if err != nil {
260 return err
261 }
262 }
263 } else {
264 models, err = ListOrFilterModels(conn, descriptor, modelName, queries)
265 if err != nil {
266 return err
267 }
268 }
269
270 if len(models) == 0 {
271 return fmt.Errorf("Filter matches no objects")
272 } else if len(models) > 1 {
273 if !Confirmf("Filter matches %d objects. Continue [y/n] ? ", len(models)) {
274 return fmt.Errorf("Aborted by user")
275 }
276 }
277
278 fields := make(map[string]interface{})
279
280 if len(options.SetJSON) > 0 {
281 fields["_json"] = []byte(options.SetJSON)
282 }
283
284 for fieldName, value := range updates {
285 value = value[1:]
286 proto_value, err := TypeConvert(descriptor, modelName, fieldName, value)
287 if err != nil {
288 return err
289 }
290 fields[fieldName] = proto_value
291 }
292
Scott Baker8b8a2b52019-05-21 08:56:28 -0700293 modelStatusOutput := InitModelStatusOutput(options.Unbuffered, len(models))
Scott Baker175cb402019-05-17 16:13:06 -0700294 for i, model := range models {
295 id := model.GetFieldByName("id").(int32)
296 fields["id"] = id
297 err := UpdateModel(conn, descriptor, modelName, fields)
298
Scott Baker8b8a2b52019-05-21 08:56:28 -0700299 UpdateModelStatusOutput(&modelStatusOutput, i, id, "Updated", err, !options.Sync)
Scott Baker5281d002019-05-16 10:45:26 -0700300 }
301
Scott Baker8b8a2b52019-05-21 08:56:28 -0700302 if options.Sync {
303 for i, model := range models {
304 id := model.GetFieldByName("id").(int32)
305 if modelStatusOutput.Rows[i].Message == "Updated" {
306 conditional_printf(!options.Quiet, "Wait for sync: %d ", id)
307 conn, _, err = GetModelWithRetry(conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
308 conditional_printf(!options.Quiet, "\n")
309 UpdateModelStatusOutput(&modelStatusOutput, i, id, "Enacted", err, true)
310 }
311 }
312 }
313
314 if !options.Unbuffered {
315 FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_UPDATE_FORMAT, DEFAULT_UPDATE_FORMAT, modelStatusOutput.Rows)
Scott Baker175cb402019-05-17 16:13:06 -0700316 }
317
318 return nil
319}
320
321func (options *ModelDelete) Execute(args []string) error {
322 conn, descriptor, err := InitReflectionClient()
323 if err != nil {
324 return err
325 }
326
327 defer conn.Close()
328
329 err = CheckModelName(descriptor, string(options.Args.ModelName))
330 if err != nil {
331 return err
332 }
333
334 if (len(options.IDArgs.ID) == 0 && len(options.Filter) == 0) ||
335 (len(options.IDArgs.ID) != 0 && len(options.Filter) != 0) {
Scott Baker8b8a2b52019-05-21 08:56:28 -0700336 return fmt.Errorf("Use either an ID or a --filter to specify which models to delete")
Scott Baker175cb402019-05-17 16:13:06 -0700337 }
338
339 queries, err := CommaSeparatedQueryToMap(options.Filter, true)
340 if err != nil {
341 return err
342 }
343
344 modelName := string(options.Args.ModelName)
345
346 var ids []int32
347
348 if len(options.IDArgs.ID) > 0 {
349 ids = options.IDArgs.ID
350 } else {
351 models, err := ListOrFilterModels(conn, descriptor, modelName, queries)
352 if err != nil {
353 return err
354 }
355 ids = make([]int32, len(models))
356 for i, model := range models {
357 ids[i] = model.GetFieldByName("id").(int32)
358 }
359 if len(ids) == 0 {
360 return fmt.Errorf("Filter matches no objects")
361 } else if len(ids) > 1 {
362 if !Confirmf("Filter matches %d objects. Continue [y/n] ? ", len(models)) {
363 return fmt.Errorf("Aborted by user")
364 }
365 }
366 }
367
Scott Baker8b8a2b52019-05-21 08:56:28 -0700368 modelStatusOutput := InitModelStatusOutput(options.Unbuffered, len(ids))
Scott Baker175cb402019-05-17 16:13:06 -0700369 for i, id := range ids {
370 err = DeleteModel(conn, descriptor, modelName, id)
Scott Baker8b8a2b52019-05-21 08:56:28 -0700371 UpdateModelStatusOutput(&modelStatusOutput, i, id, "Deleted", err, true)
Scott Baker175cb402019-05-17 16:13:06 -0700372 }
373
Scott Baker8b8a2b52019-05-21 08:56:28 -0700374 if !options.Unbuffered {
375 FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_DELETE_FORMAT, DEFAULT_DELETE_FORMAT, modelStatusOutput.Rows)
376 }
377
378 return nil
379}
380
381func (options *ModelCreate) Execute(args []string) error {
382 conn, descriptor, err := InitReflectionClient()
383 if err != nil {
384 return err
385 }
386
387 defer conn.Close()
388
389 err = CheckModelName(descriptor, string(options.Args.ModelName))
390 if err != nil {
391 return err
392 }
393
394 updates, err := CommaSeparatedQueryToMap(options.SetFields, true)
395 if err != nil {
396 return err
397 }
398
399 modelName := string(options.Args.ModelName)
400
401 fields := make(map[string]interface{})
402
403 if len(options.SetJSON) > 0 {
404 fields["_json"] = []byte(options.SetJSON)
405 }
406
407 for fieldName, value := range updates {
408 value = value[1:]
409 proto_value, err := TypeConvert(descriptor, modelName, fieldName, value)
410 if err != nil {
411 return err
412 }
413 fields[fieldName] = proto_value
414 }
415
416 modelStatusOutput := InitModelStatusOutput(options.Unbuffered, 1)
417
418 err = CreateModel(conn, descriptor, modelName, fields)
419 UpdateModelStatusOutput(&modelStatusOutput, 0, fields["id"], "Created", err, !options.Sync)
420
421 if options.Sync {
422 if modelStatusOutput.Rows[0].Message == "Created" {
423 id := fields["id"].(int32)
424 conditional_printf(!options.Quiet, "Wait for sync: %d ", id)
425 conn, _, err = GetModelWithRetry(conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
426 conditional_printf(!options.Quiet, "\n")
427 UpdateModelStatusOutput(&modelStatusOutput, 0, id, "Enacted", err, true)
428 }
429 }
430
431 if !options.Unbuffered {
432 FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_CREATE_FORMAT, DEFAULT_CREATE_FORMAT, modelStatusOutput.Rows)
433 }
434
435 return nil
436}
437
438func (options *ModelSync) Execute(args []string) error {
439 conn, descriptor, err := InitReflectionClient()
440 if err != nil {
441 return err
442 }
443
444 defer conn.Close()
445
446 err = CheckModelName(descriptor, string(options.Args.ModelName))
447 if err != nil {
448 return err
449 }
450
451 if (len(options.IDArgs.ID) == 0 && len(options.Filter) == 0) ||
452 (len(options.IDArgs.ID) != 0 && len(options.Filter) != 0) {
453 return fmt.Errorf("Use either an ID or a --filter to specify which models to sync")
454 }
455
456 queries, err := CommaSeparatedQueryToMap(options.Filter, true)
457 if err != nil {
458 return err
459 }
460
461 modelName := string(options.Args.ModelName)
462
463 var ids []int32
464
465 if len(options.IDArgs.ID) > 0 {
466 ids = options.IDArgs.ID
467 } else {
468 models, err := ListOrFilterModels(conn, descriptor, modelName, queries)
469 if err != nil {
470 return err
471 }
472 ids = make([]int32, len(models))
473 for i, model := range models {
474 ids[i] = model.GetFieldByName("id").(int32)
475 }
476 if len(ids) == 0 {
477 return fmt.Errorf("Filter matches no objects")
478 } else if len(ids) > 1 {
479 if !Confirmf("Filter matches %d objects. Continue [y/n] ? ", len(models)) {
480 return fmt.Errorf("Aborted by user")
481 }
482 }
483 }
484
485 modelStatusOutput := InitModelStatusOutput(options.Unbuffered, len(ids))
486 for i, id := range ids {
487 conditional_printf(!options.Quiet, "Wait for sync: %d ", id)
488 conn, _, err = GetModelWithRetry(conn, descriptor, modelName, id, GM_UNTIL_ENACTED|Ternary_uint32(options.Quiet, GM_QUIET, 0))
489 conditional_printf(!options.Quiet, "\n")
490 UpdateModelStatusOutput(&modelStatusOutput, i, id, "Enacted", err, true)
491 }
492
493 if !options.Unbuffered {
494 FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_SYNC_FORMAT, DEFAULT_SYNC_FORMAT, modelStatusOutput.Rows)
Scott Baker175cb402019-05-17 16:13:06 -0700495 }
Scott Baker5281d002019-05-16 10:45:26 -0700496
Scott Baker2c0ebda2019-05-06 16:55:47 -0700497 return nil
498}
Scott Baker63ce82e2019-05-15 09:01:42 -0700499
500func (modelName *ModelNameString) Complete(match string) []flags.Completion {
501 conn, descriptor, err := InitReflectionClient()
502 if err != nil {
503 return nil
504 }
505
506 defer conn.Close()
507
508 models, err := GetModelNames(descriptor)
509 if err != nil {
510 return nil
511 }
512
513 list := make([]flags.Completion, 0)
514 for k := range models {
515 if strings.HasPrefix(k, match) {
516 list = append(list, flags.Completion{Item: k})
517 }
518 }
519
520 return list
521}