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 }