go.ligato.io/vpp-agent/v3@v3.5.0/cmd/agentctl/commands/dump.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  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/olekukonko/tablewriter"
    26  	"github.com/spf13/cobra"
    27  	"go.ligato.io/cn-infra/v2/logging"
    28  
    29  	"go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types"
    30  	agentcli "go.ligato.io/vpp-agent/v3/cmd/agentctl/cli"
    31  	"go.ligato.io/vpp-agent/v3/pkg/models"
    32  	"go.ligato.io/vpp-agent/v3/plugins/kvscheduler/api"
    33  )
    34  
    35  func NewDumpCommand(cli agentcli.Cli) *cobra.Command {
    36  	var (
    37  		opts DumpOptions
    38  	)
    39  	cmd := &cobra.Command{
    40  		Use:   "dump MODEL [MODEL...]",
    41  		Short: "Dump running state",
    42  		Long:  "Dumps actual running state",
    43  		Example: `
    44  # Dump everything
    45  {{.CommandPath}} all
    46  
    47  # Dump VPP interfaces & routes
    48  {{.CommandPath}} vpp.interfaces vpp.l3.routes
    49  
    50  # Dump all VPP data in JSON format
    51  {{.CommandPath}} -f json vpp.*
    52  
    53  # Dump only VPP memif interfaces
    54  {{.CommandPath}} -f '{{` + "`{{range .}}{{if eq .Value.Type.String \"MEMIF\" }}{{json .}}{{end}}{{end}}`" + `}}' vpp.interfaces
    55  
    56  # Dump everything currently defined at northbound
    57  {{.CommandPath}} --view=NB all
    58  
    59  # Dump all VPP & Linux data directly from southband
    60  {{.CommandPath}} --view=SB vpp.* linux.*
    61  
    62  # Dump all VPP & Linux data directly from southband
    63  {{.CommandPath}} --view=SB all
    64  `,
    65  		Args: func(cmd *cobra.Command, args []string) error {
    66  			if len(args) == 0 {
    67  				fmt.Fprintf(cli.Err(), "You must specify models to dump. Use \"%s models\" for a complete list of known models.\n", cmd.Root().Name())
    68  				return fmt.Errorf("no models specified")
    69  			}
    70  			opts.Models = args
    71  			return opts.Validate()
    72  		},
    73  		RunE: func(cmd *cobra.Command, args []string) error {
    74  			return runDump(cli, opts)
    75  		},
    76  	}
    77  	flags := cmd.Flags()
    78  	flags.StringVar(&opts.View, "view", "cached", "Dump view type: cached, NB, SB")
    79  	flags.StringVar(&opts.Origin, "origin", "", "Show only data with specific origin: NB, SB, unknown")
    80  	flags.StringVarP(&opts.Format, "format", "f", "", "Format output (json|yaml|go-template|proto)")
    81  	return cmd
    82  }
    83  
    84  type DumpOptions struct {
    85  	Models []string
    86  	View   string
    87  	Origin string
    88  	Format string
    89  }
    90  
    91  func (opts *DumpOptions) Validate() error {
    92  	// models
    93  	if opts.Models[0] == "all" {
    94  		opts.Models = []string{"*"}
    95  	}
    96  	// view
    97  	switch strings.ToLower(opts.View) {
    98  	case "cached", "cache", "":
    99  		opts.View = "cached"
   100  	case "nb", "north", "northbound":
   101  		opts.View = "NB"
   102  	case "sb", "south", "southbound":
   103  		opts.View = "SB"
   104  	default:
   105  		return fmt.Errorf("invalid view type: %q", opts.View)
   106  	}
   107  	// origin
   108  	switch strings.ToLower(opts.Origin) {
   109  	case "":
   110  	case "unknown":
   111  		opts.Origin = api.UnknownOrigin.String()
   112  	case "from-nb", "nb", "north", "northbound":
   113  		opts.Origin = api.FromNB.String()
   114  	case "from-sb", "sb", "south", "southbound":
   115  		opts.Origin = api.FromSB.String()
   116  	default:
   117  		return fmt.Errorf("invalid origin: %q", opts.Origin)
   118  	}
   119  	return nil
   120  }
   121  
   122  func runDump(cli agentcli.Cli, opts DumpOptions) error {
   123  	ctx, cancel := context.WithCancel(context.Background())
   124  	defer cancel()
   125  
   126  	allModels, err := cli.Client().ModelList(ctx, types.ModelListOptions{
   127  		Class: "config",
   128  	})
   129  	if err != nil {
   130  		return err
   131  	}
   132  	var keyPrefixes []string
   133  	for _, m := range filterModelsByRefs(allModels, opts.Models) {
   134  		keyPrefixes = append(keyPrefixes, m.KeyPrefix)
   135  	}
   136  	if len(keyPrefixes) == 0 {
   137  		return fmt.Errorf("no matching models found for %q", opts.Models)
   138  	}
   139  	var (
   140  		errs  Errors
   141  		dumps []api.RecordedKVWithMetadata
   142  	)
   143  	for _, keyPrefix := range keyPrefixes {
   144  		dump, err := cli.Client().SchedulerDump(ctx, types.SchedulerDumpOptions{
   145  			KeyPrefix: keyPrefix,
   146  			View:      opts.View,
   147  		})
   148  		if err != nil {
   149  			errs = append(errs, fmt.Errorf("dump for %s failed: %v", keyPrefix, err))
   150  			continue
   151  		}
   152  		dumps = append(dumps, dump...)
   153  	}
   154  	if errs != nil {
   155  		logging.Debugf("dump finished with %d errors\n%v", len(errs), errs)
   156  	}
   157  	if len(errs) == len(keyPrefixes) {
   158  		return fmt.Errorf("dump failed:\n%v", errs)
   159  	}
   160  
   161  	dumps = filterDumpByOrigin(dumps, opts.Origin)
   162  	sort.Slice(dumps, func(i, j int) bool {
   163  		return dumps[i].Key < dumps[j].Key
   164  	})
   165  
   166  	format := opts.Format
   167  	if len(format) == 0 {
   168  		printDumpTable(cli.Out(), dumps)
   169  	} else {
   170  		fdumps, err := convertDumps(dumps)
   171  		if err != nil {
   172  			return err
   173  		}
   174  		if err := formatAsTemplate(cli.Out(), format, fdumps); err != nil {
   175  			return err
   176  		}
   177  	}
   178  	return nil
   179  }
   180  
   181  func filterDumpByOrigin(dumps []api.RecordedKVWithMetadata, origin string) []api.RecordedKVWithMetadata {
   182  	if origin == "" {
   183  		return dumps
   184  	}
   185  	var filtered []api.RecordedKVWithMetadata
   186  	for _, d := range dumps {
   187  		if !strings.EqualFold(d.Origin.String(), origin) {
   188  			continue
   189  		}
   190  		filtered = append(filtered, d)
   191  	}
   192  	return filtered
   193  }
   194  
   195  func printDumpTable(out io.Writer, dump []api.RecordedKVWithMetadata) {
   196  	table := tablewriter.NewWriter(out)
   197  	table.SetHeader([]string{
   198  		"Model", "Origin", "Value", "Metadata", "Key",
   199  	})
   200  	table.SetAutoMergeCells(true)
   201  	table.SetAutoWrapText(false)
   202  	table.SetRowLine(true)
   203  
   204  	for _, d := range dump {
   205  		val := yamlTmpl(d.Value)
   206  		var meta string
   207  		if d.Metadata != nil {
   208  			meta = yamlTmpl(d.Metadata)
   209  		}
   210  		var (
   211  			name  = "-"
   212  			model string
   213  			orig  = d.Origin
   214  		)
   215  		if m, err := models.GetModelForKey(d.Key); err == nil {
   216  			name, _ = m.ParseKey(d.Key)
   217  			model = m.Name()
   218  			if name == "" {
   219  				name = d.Key
   220  			}
   221  		}
   222  		val = fmt.Sprintf("# %s\n%s", d.Value.ProtoReflect().Descriptor().FullName(), val)
   223  		row := []string{
   224  			model,
   225  			orig.String(),
   226  			val,
   227  			meta,
   228  			name,
   229  		}
   230  		table.Append(row)
   231  	}
   232  	table.Render()
   233  }
   234  
   235  // formatDump is a helper type that can be used with user defined custom dump formats
   236  type formatDump struct {
   237  	Key      string
   238  	Value    map[string]interface{}
   239  	Metadata api.Metadata
   240  	Origin   api.ValueOrigin
   241  }
   242  
   243  func convertDumps(in []api.RecordedKVWithMetadata) (out []formatDump, err error) {
   244  	for _, d := range in {
   245  		b, err := d.Value.MarshalJSON()
   246  		if err != nil {
   247  			return nil, err
   248  		}
   249  		var values map[string]interface{}
   250  		if err = json.Unmarshal(b, &values); err != nil {
   251  			return nil, err
   252  		}
   253  		// TODO: this "ProtoMsgData" string key has to be the same as the field name of
   254  		// the ProtoWithName struct that contains the message data. ProtoWithName struct
   255  		// is a part of kvschedulers internal utils package. Perhaps we could make this
   256  		// field name a part of the public kvscheduler API so we do not have to rely
   257  		// on string key here.
   258  		if val, ok := values["ProtoMsgData"]; ok {
   259  			out = append(out, formatDump{
   260  				Key:      d.Key,
   261  				Value:    val.(map[string]interface{}),
   262  				Metadata: d.Metadata,
   263  				Origin:   d.Origin,
   264  			})
   265  		}
   266  	}
   267  	return out, nil
   268  }