go.ligato.io/vpp-agent/v3@v3.5.0/cmd/agentctl/commands/config.go (about) 1 // Copyright (c) 2019 Cisco and/or its affiliates. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at: 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package commands 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "os" 22 "sort" 23 "strconv" 24 "strings" 25 "time" 26 27 yaml2 "github.com/ghodss/yaml" 28 "github.com/olekukonko/tablewriter" 29 "github.com/sirupsen/logrus" 30 "github.com/spf13/cobra" 31 "google.golang.org/grpc" 32 "google.golang.org/grpc/metadata" 33 "google.golang.org/protobuf/encoding/protojson" 34 "google.golang.org/protobuf/proto" 35 36 "go.ligato.io/vpp-agent/v3/client" 37 "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" 38 agentcli "go.ligato.io/vpp-agent/v3/cmd/agentctl/cli" 39 kvs "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/api" 40 "go.ligato.io/vpp-agent/v3/proto/ligato/configurator" 41 "go.ligato.io/vpp-agent/v3/proto/ligato/kvscheduler" 42 ) 43 44 func NewConfigCommand(cli agentcli.Cli) *cobra.Command { 45 cmd := &cobra.Command{ 46 Use: "config", 47 Short: "Manage agent configuration", 48 } 49 cmd.AddCommand( 50 newConfigGetCommand(cli), 51 newConfigUpdateCommand(cli), 52 newConfigDeleteCommand(cli), 53 newConfigRetrieveCommand(cli), 54 newConfigWatchCommand(cli), 55 newConfigResyncCommand(cli), 56 newConfigHistoryCommand(cli), 57 ) 58 return cmd 59 } 60 61 func newConfigGetCommand(cli agentcli.Cli) *cobra.Command { 62 var ( 63 opts ConfigGetOptions 64 ) 65 cmd := &cobra.Command{ 66 Use: "get", 67 Short: "Get config from agent", 68 Args: cobra.NoArgs, 69 RunE: func(cmd *cobra.Command, args []string) error { 70 return runConfigGet(cli, opts) 71 }, 72 } 73 flags := cmd.Flags() 74 flags.StringVarP(&opts.Format, "format", "f", "", "Format output") 75 flags.StringSliceVar(&opts.Labels, "labels", []string{}, "Output only config items that have given labels. "+ 76 "Format of labels is: \"<string>=<string>\" key-value pairs separated by comma. "+ 77 "If the key is prefixed with \"!\" the config items that contain that label are excluded from the result. "+ 78 "Empty keys and duplicated keys are not allowed. "+ 79 "If value of label is empty, equals sign can be omitted. "+ 80 "For example: --labels=\"foo=bar\",\"baz=\",\"qux\", \"!quux=corge\", \"!grault\"") 81 return cmd 82 } 83 84 type ConfigGetOptions struct { 85 Format string 86 Labels []string 87 } 88 89 func runConfigGet(cli agentcli.Cli, opts ConfigGetOptions) error { 90 // get generic client 91 c, err := cli.Client().GenericClient() 92 if err != nil { 93 return err 94 } 95 96 // create dynamically config that can hold all remote known models 97 // (not using local model registry that gives only locally available models) 98 knownModels, err := c.KnownModels("config") 99 if err != nil { 100 return fmt.Errorf("getting registered models: %w", err) 101 } 102 config, err := client.NewDynamicConfig(knownModels) 103 if err != nil { 104 return fmt.Errorf("can't create all-config proto message dynamically due to: %w", err) 105 } 106 107 // fill labels map 108 labels, err := parseLabels(opts.Labels) 109 if err != nil { 110 return fmt.Errorf("parsing labels failed: %w", err) 111 } 112 113 // retrieve data into config 114 err = c.GetFilteredConfig(client.Filter{Labels: labels}, config) 115 if err != nil { 116 return fmt.Errorf("can't retrieve configuration due to: %w", err) 117 } 118 119 // handle data output 120 format := opts.Format 121 if len(format) == 0 { 122 format = `yaml` 123 } 124 if err := formatAsTemplate(cli.Out(), format, config); err != nil { 125 return err 126 } 127 return nil 128 } 129 130 func newConfigUpdateCommand(cli agentcli.Cli) *cobra.Command { 131 var ( 132 opts ConfigUpdateOptions 133 ) 134 cmd := &cobra.Command{ 135 Use: "update", 136 Short: "Update config in agent", 137 Long: "Update configuration in agent from file", 138 Args: cobra.MaximumNArgs(1), 139 RunE: func(cmd *cobra.Command, args []string) error { 140 return runConfigUpdate(cli, opts, args) 141 }, 142 } 143 flags := cmd.Flags() 144 flags.StringVarP(&opts.Format, "format", "f", "", "Format output") 145 flags.BoolVar(&opts.Replace, "replace", false, "Replaces all existing config") 146 // TODO implement waitdone also for generic client 147 // flags.BoolVar(&opts.WaitDone, "waitdone", false, "Waits until config update is done") 148 // TODO implement transaction output when verbose is used 149 // flags.BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output") 150 flags.DurationVarP(&opts.Timeout, "timeout", "t", 151 5*time.Minute, "Timeout for sending updated data") 152 flags.StringSliceVar(&opts.Labels, "labels", []string{}, "Labels associated with updated config items. "+ 153 "Format of labels is: \"<string>=<string>\" key-value pairs separated by comma. "+ 154 "Empty keys and duplicated keys are not allowed. "+ 155 "If value of label is empty, equals sign can be omitted. "+ 156 "For example: --labels=\"foo=bar\",\"baz=\",\"qux\"") 157 return cmd 158 } 159 160 type ConfigUpdateOptions struct { 161 Format string 162 Replace bool 163 // WaitDone bool 164 // Verbose bool 165 Timeout time.Duration 166 Labels []string 167 } 168 169 func runConfigUpdate(cli agentcli.Cli, opts ConfigUpdateOptions, args []string) error { 170 ctx, cancel := context.WithTimeout(context.Background(), opts.Timeout) 171 defer cancel() 172 173 // get input file 174 if len(args) == 0 { 175 return fmt.Errorf("missing file argument") 176 } 177 file := args[0] 178 b, err := os.ReadFile(file) 179 if err != nil { 180 return fmt.Errorf("reading file %s: %w", file, err) 181 } 182 183 // get generic client 184 c, err := cli.Client().GenericClient() 185 if err != nil { 186 return err 187 } 188 189 // create dynamically config that can hold all remote known models 190 // (not using local model registry that gives only locally available models) 191 knownModels, err := c.KnownModels("config") 192 if err != nil { 193 return fmt.Errorf("getting registered models: %w", err) 194 } 195 config, err := client.NewDynamicConfig(knownModels) 196 if err != nil { 197 return fmt.Errorf("can't create all-config proto message dynamically due to: %w", err) 198 } 199 200 // filling dynamically created config with data from input file 201 bj, err := yaml2.YAMLToJSON(b) 202 if err != nil { 203 return fmt.Errorf("converting to JSON: %w", err) 204 } 205 err = protojson.Unmarshal(bj, config) 206 if err != nil { 207 return fmt.Errorf("can't unmarshall input file data "+ 208 "into dynamically created config due to: %v", err) 209 } 210 logrus.Infof("loaded config :\n%s", config) 211 212 // extracting proto messages from dynamically created config structure 213 // (generic client wants single proto messages and not one big hierarchical config) 214 configMessages, err := client.DynamicConfigExport(config) 215 if err != nil { 216 return fmt.Errorf("can't extract single configuration proto messages "+ 217 "from one big configuration proto message due to: %v", err) 218 } 219 220 // fill labels map 221 labels, err := parseLabels(opts.Labels) 222 if err != nil { 223 return fmt.Errorf("parsing labels failed: %w", err) 224 } 225 if len(opts.Labels) == 0 { 226 labels["io.ligato.from-client"] = "agentctl" 227 } 228 229 // update/resync configuration 230 _, err = c.UpdateItems(ctx, createUpdateItems(configMessages, labels), opts.Replace) 231 if err != nil { 232 return fmt.Errorf("update failed: %w", err) 233 } 234 235 // handle configuration update result and command output 236 format := opts.Format 237 if len(format) == 0 { 238 format = `{{.}}` 239 } 240 if err := formatAsTemplate(cli.Out(), format, "OK"); err != nil { 241 return err 242 } 243 244 return nil 245 } 246 247 func newConfigDeleteCommand(cli agentcli.Cli) *cobra.Command { 248 var ( 249 opts ConfigDeleteOptions 250 ) 251 cmd := &cobra.Command{ 252 Use: "delete", 253 Short: "Delete config in agent", 254 Long: "Delete configuration in agent", 255 Args: cobra.MaximumNArgs(1), 256 RunE: func(cmd *cobra.Command, args []string) error { 257 return runConfigDelete(cli, opts, args) 258 }, 259 } 260 flags := cmd.Flags() 261 flags.StringVarP(&opts.Format, "format", "f", "", "Format output") 262 flags.BoolVar(&opts.WaitDone, "waitdone", false, "Waits until config update is done") 263 flags.BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output") 264 return cmd 265 } 266 267 type ConfigDeleteOptions struct { 268 Format string 269 WaitDone bool 270 Verbose bool 271 } 272 273 func runConfigDelete(cli agentcli.Cli, opts ConfigDeleteOptions, args []string) error { 274 ctx, cancel := context.WithCancel(context.Background()) 275 defer cancel() 276 277 c, err := cli.Client().ConfiguratorClient() 278 if err != nil { 279 return err 280 } 281 282 if len(args) == 0 { 283 return fmt.Errorf("missing file argument") 284 } 285 file := args[0] 286 b, err := os.ReadFile(file) 287 if err != nil { 288 return fmt.Errorf("reading file %s: %w", file, err) 289 } 290 291 var update = &configurator.Config{} 292 bj, err := yaml2.YAMLToJSON(b) 293 if err != nil { 294 return fmt.Errorf("converting to JSON: %w", err) 295 } 296 err = protojson.Unmarshal(bj, update) 297 if err != nil { 298 return err 299 } 300 logrus.Infof("loaded config delete:\n%s", update) 301 302 var data interface{} 303 304 var header metadata.MD 305 resp, err := c.Delete(ctx, &configurator.DeleteRequest{ 306 Delete: update, 307 WaitDone: opts.WaitDone, 308 }, grpc.Header(&header)) 309 if err != nil { 310 logrus.Warnf("delete failed: %v", err) 311 data = err 312 } else { 313 data = resp 314 } 315 316 if opts.Verbose { 317 logrus.Debugf("grpc header: %+v", header) 318 if seqNum, ok := header["seqnum"]; ok { 319 ref, _ := strconv.Atoi(seqNum[0]) 320 txns, err := cli.Client().SchedulerHistory(ctx, types.SchedulerHistoryOptions{ 321 SeqNum: ref, 322 }) 323 if err != nil { 324 logrus.Warnf("getting history for seqNum %d failed: %v", ref, err) 325 } else { 326 data = txns 327 } 328 } 329 } 330 331 format := opts.Format 332 if len(format) == 0 { 333 format = `{{.}}` 334 } 335 if err := formatAsTemplate(cli.Out(), format, data); err != nil { 336 return err 337 } 338 339 return nil 340 } 341 342 func newConfigRetrieveCommand(cli agentcli.Cli) *cobra.Command { 343 var ( 344 opts ConfigRetrieveOptions 345 ) 346 cmd := &cobra.Command{ 347 Use: "retrieve", 348 Aliases: []string{"ret", "read", "dump"}, 349 Short: "Retrieve currently running config", 350 Args: cobra.NoArgs, 351 RunE: func(cmd *cobra.Command, args []string) error { 352 return runConfigRetrieve(cli, opts) 353 }, 354 } 355 flags := cmd.Flags() 356 flags.StringVarP(&opts.Format, "format", "f", "", "Format output") 357 return cmd 358 } 359 360 type ConfigRetrieveOptions struct { 361 Format string 362 } 363 364 func runConfigRetrieve(cli agentcli.Cli, opts ConfigRetrieveOptions) error { 365 ctx, cancel := context.WithCancel(context.Background()) 366 defer cancel() 367 368 c, err := cli.Client().ConfiguratorClient() 369 if err != nil { 370 return err 371 } 372 resp, err := c.Dump(ctx, &configurator.DumpRequest{}) 373 if err != nil { 374 return err 375 } 376 377 format := opts.Format 378 if len(format) == 0 { 379 format = `yaml` 380 } 381 if err := formatAsTemplate(cli.Out(), format, resp.Dump); err != nil { 382 return err 383 } 384 385 return nil 386 } 387 388 func newConfigWatchCommand(cli agentcli.Cli) *cobra.Command { 389 var ( 390 opts ConfigWatchOptions 391 ) 392 cmd := &cobra.Command{ 393 Use: "watch", 394 Aliases: []string{"notify", "subscribe"}, 395 Short: "Watch events", 396 Example: "Filter events by VPP interface name 'loop1'" + 397 `{{.CommandPath}} config watch --filter='{"vpp_notification":{"interface":{"state":{"name":"loop1"}}}}'` + 398 "" + 399 "Filter events by VPP interface UPDOWN type" + 400 `{{.CommandPath}} config watch --filter='{"vpp_notification":{"interface":{"type":"UPDOWN"}}}'`, 401 Args: cobra.NoArgs, 402 RunE: func(cmd *cobra.Command, args []string) error { 403 return runConfigWatch(cli, opts) 404 }, 405 } 406 flags := cmd.Flags() 407 flags.StringArrayVar(&opts.Filters, "filter", nil, "Filter(s) for notifications (multiple filters are used with AND operator). Value should be JSON data of configurator.Notification.") 408 flags.StringVarP(&opts.Format, "format", "f", "", "Format output") 409 return cmd 410 } 411 412 type ConfigWatchOptions struct { 413 Format string 414 Filters []string 415 } 416 417 func runConfigWatch(cli agentcli.Cli, opts ConfigWatchOptions) error { 418 ctx, cancel := context.WithCancel(context.Background()) 419 defer cancel() 420 421 c, err := cli.Client().ConfiguratorClient() 422 if err != nil { 423 return err 424 } 425 426 filters, err := prepareNotifyFilters(opts.Filters) 427 if err != nil { 428 return fmt.Errorf("filters error: %w", err) 429 } 430 431 var nextIdx uint32 432 stream, err := c.Notify(ctx, &configurator.NotifyRequest{ 433 Idx: nextIdx, 434 Filters: filters, 435 }) 436 if err != nil { 437 return err 438 } 439 440 format := opts.Format 441 if len(format) == 0 { 442 format = `------------------ 443 NOTIFICATION #{{.NextIdx}} 444 ------------------ 445 {{if .Notification.GetVppNotification}}Source: VPP 446 Value: {{protomulti .Notification.GetVppNotification}} 447 {{else if .Notification.GetLinuxNotification}}Source: LINUX 448 Value: {{protomulti .Notification.GetLinuxNotification}} 449 {{else}}Source: {{printf "%T" .Notification.GetNotification}} 450 Value: {{protomulti .Notification.GetNotification}} 451 {{end}}` 452 } 453 454 for { 455 notif, err := stream.Recv() 456 if err == io.EOF { 457 break 458 } else if err != nil { 459 return err 460 } 461 462 logrus.Debugf("Notification[%d]: %v", 463 notif.NextIdx-1, notif.Notification) 464 465 if err := formatAsTemplate(cli.Out(), format, notif); err != nil { 466 return err 467 } 468 } 469 470 return nil 471 } 472 473 func prepareNotifyFilters(filters []string) ([]*configurator.Notification, error) { 474 var list []*configurator.Notification 475 for _, filter := range filters { 476 notif := &configurator.Notification{} 477 err := protojson.Unmarshal([]byte(filter), notif) 478 if err != nil { 479 return nil, err 480 } 481 list = append(list, notif) 482 } 483 return list, nil 484 } 485 486 func newConfigResyncCommand(cli agentcli.Cli) *cobra.Command { 487 var ( 488 opts ConfigResyncOptions 489 ) 490 cmd := &cobra.Command{ 491 Use: "resync", 492 Short: "Run config resync", 493 Args: cobra.NoArgs, 494 RunE: func(cmd *cobra.Command, args []string) error { 495 return runConfigResync(cli, opts) 496 }, 497 } 498 flags := cmd.Flags() 499 flags.StringVarP(&opts.Format, "format", "f", "", "Format output") 500 flags.BoolVar(&opts.Verbose, "verbose", false, "Run resync in verbose mode") 501 flags.BoolVar(&opts.Retry, "retry", false, "Run resync with retries") 502 return cmd 503 } 504 505 type ConfigResyncOptions struct { 506 Format string 507 Verbose bool 508 Retry bool 509 } 510 511 // TODO: define default format with go template 512 const defaultFormatConfigResync = `json` 513 514 func runConfigResync(cli agentcli.Cli, opts ConfigResyncOptions) error { 515 ctx, cancel := context.WithCancel(context.Background()) 516 defer cancel() 517 518 rectxn, err := cli.Client().SchedulerResync(ctx, types.SchedulerResyncOptions{ 519 Retry: opts.Retry, 520 Verbose: opts.Verbose, 521 }) 522 if err != nil { 523 return err 524 } 525 526 format := opts.Format 527 if len(format) == 0 { 528 format = defaultFormatConfigResync 529 } 530 if err := formatAsTemplate(cli.Out(), format, rectxn); err != nil { 531 return err 532 } 533 534 return nil 535 } 536 537 func newConfigHistoryCommand(cli agentcli.Cli) *cobra.Command { 538 var ( 539 opts ConfigHistoryOptions 540 ) 541 cmd := &cobra.Command{ 542 Use: "history [REF]", 543 Short: "Show config history", 544 Long: `Show history of config changes and status updates 545 546 Prints a table of most important information about the history of changes to 547 config and status updates that have occurred. You can filter the output by 548 specifying a reference to sequence number (txn ID). 549 550 Type can be one of: 551 - config change (NB - full resync) 552 - status update (SB) 553 - config sync (NB - upstream resync) 554 - status sync (NB - downstream resync) 555 - retry #X for Y (retry of TX) 556 `, 557 Example: ` 558 # Show entire history 559 {{.CommandPath}} config history 560 561 # Show entire history with details 562 {{.CommandPath}} config history --details 563 564 # Show entire history in transaction log format 565 {{.CommandPath}} config history -f log 566 567 # Show entire history in classic log format 568 {{.CommandPath}} config history -f log 569 570 # Show history point with sequence number 3 571 {{.CommandPath}} config history 3 572 573 # Show history point with seq. number 3 in log format 574 {{.CommandPath}} config history -f log 3 575 `, 576 Args: cobra.MaximumNArgs(1), 577 RunE: func(cmd *cobra.Command, args []string) error { 578 if len(args) > 0 { 579 opts.TxnRef = args[0] 580 } 581 return runConfigHistory(cli, opts) 582 }, 583 } 584 flags := cmd.Flags() 585 flags.StringVarP(&opts.Format, "format", "f", "", "Format output") 586 flags.BoolVar(&opts.Details, "details", false, "Include details") 587 return cmd 588 } 589 590 type ConfigHistoryOptions struct { 591 Format string 592 Details bool 593 TxnRef string 594 } 595 596 func runConfigHistory(cli agentcli.Cli, opts ConfigHistoryOptions) (err error) { 597 ctx, cancel := context.WithCancel(context.Background()) 598 defer cancel() 599 600 ref := -1 601 if opts.TxnRef != "" { 602 ref, err = strconv.Atoi(opts.TxnRef) 603 if err != nil { 604 return fmt.Errorf("invalid reference: %q, use number > 0", opts.TxnRef) 605 } 606 } 607 608 // register remote models into the default registry 609 _, err = cli.Client().ModelList(ctx, types.ModelListOptions{ 610 Class: "config", 611 }) 612 if err != nil { 613 return err 614 } 615 616 txns, err := cli.Client().SchedulerHistory(ctx, types.SchedulerHistoryOptions{ 617 SeqNum: ref, 618 }) 619 if err != nil { 620 return err 621 } 622 623 format := opts.Format 624 if len(format) == 0 { 625 printHistoryTable(cli.Out(), txns, opts.Details) 626 } else if format == "log" { 627 format = "{{.}}" 628 } 629 if err := formatAsTemplate(cli.Out(), format, txns); err != nil { 630 return err 631 } 632 633 return nil 634 } 635 636 func printHistoryTable(out io.Writer, txns kvs.RecordedTxns, withDetails bool) { 637 table := tablewriter.NewWriter(out) 638 header := []string{ 639 "Seq", "Type", "Start", "Input", "Operations", "Result", "Summary", 640 } 641 if withDetails { 642 header = append(header, "Details") 643 } 644 table.SetHeader(header) 645 table.SetAutoWrapText(false) 646 table.SetAutoFormatHeaders(true) 647 table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 648 table.SetAlignment(tablewriter.ALIGN_LEFT) 649 table.SetCenterSeparator("") 650 table.SetColumnSeparator("") 651 table.SetRowSeparator("") 652 table.SetHeaderLine(false) 653 table.SetBorder(false) 654 table.SetTablePadding("\t") 655 for _, txn := range txns { 656 typ := getTxnType(txn) 657 clr := getTxnColor(txn) 658 age := shortHumanDuration(time.Since(txn.Start)) 659 var result string 660 var resClr int 661 var detail string 662 var summary string 663 var input string 664 if len(txn.Values) > 0 { 665 input = fmt.Sprintf("%-2d values", len(txn.Values)) 666 } else { 667 input = "<none>" 668 } 669 var operation string 670 if len(txn.Executed) > 0 { 671 operation = txnOperations(txn) 672 summary = txnValueStates(txn) 673 } else { 674 operation = "<none>" 675 summary = "<none>" 676 } 677 errs := txnErrors(txn) 678 if errs != nil { 679 result = "error" 680 resClr = tablewriter.FgHiRedColor 681 if len(errs) > 1 { 682 result = fmt.Sprintf("%d errors", len(errs)) 683 } 684 } else if len(txn.Executed) > 0 { 685 result = "ok" 686 resClr = tablewriter.FgGreenColor 687 } 688 if withDetails { 689 for _, e := range errs { 690 if detail != "" { 691 detail += "\n" 692 } 693 detail += fmt.Sprintf("%v", e.Error()) 694 } 695 if reasons := txnPendingReasons(txn); reasons != "" { 696 if detail != "" { 697 detail += "\n" 698 } 699 detail += reasons 700 } 701 } 702 row := []string{ 703 fmt.Sprint(txn.SeqNum), 704 typ, 705 age, 706 input, 707 operation, 708 result, 709 summary, 710 } 711 if withDetails { 712 row = append(row, detail) 713 } 714 clrs := []tablewriter.Colors{ 715 {}, 716 {tablewriter.Normal, clr}, 717 {}, 718 {}, 719 {}, 720 {resClr}, 721 {}, 722 {}, 723 } 724 table.Rich(row, clrs) 725 } 726 table.Render() 727 } 728 729 func getTxnColor(txn *kvs.RecordedTxn) int { 730 var clr int 731 switch txn.TxnType { 732 case kvs.NBTransaction: 733 if txn.ResyncType == kvs.NotResync { 734 clr = tablewriter.FgYellowColor 735 } else if txn.ResyncType == kvs.FullResync { 736 clr = tablewriter.FgHiYellowColor 737 } else { 738 clr = tablewriter.FgYellowColor 739 } 740 case kvs.SBNotification: 741 clr = tablewriter.FgCyanColor 742 case kvs.RetryFailedOps: 743 clr = tablewriter.FgMagentaColor 744 } 745 return clr 746 } 747 748 func getTxnType(txn *kvs.RecordedTxn) string { 749 switch txn.TxnType { 750 case kvs.SBNotification: 751 return "status update" 752 case kvs.NBTransaction: 753 if txn.ResyncType == kvs.FullResync { 754 return "config replace" 755 } else if txn.ResyncType == kvs.UpstreamResync { 756 return "config sync" 757 } else if txn.ResyncType == kvs.DownstreamResync { 758 return "status sync" 759 } 760 return "config change" 761 case kvs.RetryFailedOps: 762 return fmt.Sprintf("retry #%d for %d", txn.RetryAttempt, txn.RetryForTxn) 763 } 764 return "?" 765 } 766 767 func txnValueStates(txn *kvs.RecordedTxn) string { 768 opermap := map[string]int{} 769 for _, r := range txn.Executed { 770 opermap[r.NewState.String()]++ 771 } 772 var opers []string 773 for k, v := range opermap { 774 opers = append(opers, fmt.Sprintf("%s:%v", k, v)) 775 } 776 sort.Strings(opers) 777 return strings.Join(opers, ", ") 778 } 779 780 func txnOperations(txn *kvs.RecordedTxn) string { 781 opermap := map[string]int{} 782 for _, r := range txn.Executed { 783 opermap[r.Operation.String()]++ 784 } 785 var opers []string 786 for k, v := range opermap { 787 opers = append(opers, fmt.Sprintf("%s:%v", k, v)) 788 } 789 sort.Strings(opers) 790 return strings.Join(opers, ", ") 791 } 792 793 func txnPendingReasons(txn *kvs.RecordedTxn) string { 794 var details []string 795 for _, r := range txn.Executed { 796 if r.NewState == kvscheduler.ValueState_PENDING { 797 // TODO: include pending resons in details 798 detail := fmt.Sprintf("[%s] %s -> %s", r.Operation, r.Key, r.NewState) 799 details = append(details, detail) 800 } 801 } 802 return strings.Join(details, "\n") 803 } 804 805 func txnErrors(txn *kvs.RecordedTxn) Errors { 806 var errs Errors 807 for _, r := range txn.Executed { 808 if r.NewErrMsg != "" { 809 r.NewErr = fmt.Errorf("[%s] %s -> %s: %v", r.Operation, r.Key, r.NewState, r.NewErrMsg) 810 errs = append(errs, r.NewErr) 811 } 812 } 813 return errs 814 } 815 816 // parseLabels parses labels obtained from command line flags 817 // This function does not allow duplicate or empty ("") keys 818 func parseLabels(rawLabels []string) (map[string]string, error) { 819 labels := make(map[string]string) 820 if len(rawLabels) == 0 { 821 return labels, nil 822 } 823 var lkey, lval string 824 for _, rawLabel := range rawLabels { 825 i := strings.IndexByte(rawLabel, '=') 826 if i == -1 { 827 lkey, lval = rawLabel, "" 828 } else { 829 lkey, lval = rawLabel[:i], rawLabel[i+1:] 830 } 831 if lkey == "" || lkey == "!" { 832 return nil, fmt.Errorf("key of label %s is empty", rawLabel) 833 } 834 if _, ok := labels[lkey]; ok { 835 return nil, fmt.Errorf("label key %s is duplicated", lkey) 836 } 837 labels[lkey] = lval 838 } 839 return labels, nil 840 } 841 842 func createUpdateItems(msgs []proto.Message, labels map[string]string) []client.UpdateItem { 843 var result []client.UpdateItem 844 for _, msg := range msgs { 845 result = append(result, client.UpdateItem{Message: msg, Labels: labels}) 846 } 847 return result 848 }