go.ligato.io/vpp-agent/v3@v3.5.0/cmd/agentctl/commands/report.go (about) 1 // Copyright (c) 2020 Pantheon.tech 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 "archive/zip" 19 "bytes" 20 "context" 21 "fmt" 22 "io" 23 "os" 24 "path/filepath" 25 "regexp" 26 "runtime" 27 "sort" 28 "strconv" 29 "strings" 30 "time" 31 32 "github.com/olekukonko/tablewriter" 33 "github.com/spf13/cobra" 34 govppapi "go.fd.io/govpp/api" 35 36 "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" 37 agentcli "go.ligato.io/vpp-agent/v3/cmd/agentctl/cli" 38 "go.ligato.io/vpp-agent/v3/pkg/version" 39 "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/api" 40 "go.ligato.io/vpp-agent/v3/proto/ligato/configurator" 41 ) 42 43 const failedReportFileName = "_failed-reports.txt" 44 45 func NewReportCommand(cli agentcli.Cli) *cobra.Command { 46 var opts ReportOptions 47 cmd := &cobra.Command{ 48 Use: "report", 49 Short: "Create error report", 50 Long: "Create report about running software stack (VPP-Agent, VPP, AgentCtl,...) " + 51 "to allow quicker resolving of problems. The report will be a zip file containing " + 52 "information grouped in multiple files", 53 Example: ` 54 # Default reporting (creates report file in current directory, whole reporting fails on subreport error) 55 {{.CommandPath}} report 56 57 # Reporting into custom directory ("/tmp") 58 {{.CommandPath}} report -o /tmp 59 60 # Reporting and ignoring errors from subreports (writing successful reports and 61 # errors from failed subreports into zip report file) 62 {{.CommandPath}} report -i 63 `, Args: cobra.NoArgs, 64 RunE: func(cmd *cobra.Command, args []string) error { 65 return runReport(cli, opts) 66 }, 67 } 68 flags := cmd.Flags() 69 flags.StringVarP(&opts.OutputDirectory, "output-directory", "o", "", 70 "Output directory (as absolute path) where report zip file will be written. "+ 71 "Default is current directory.") 72 flags.BoolVarP(&opts.IgnoreErrors, "ignore-errors", "i", false, 73 "Ignore subreport errors and create report zip file with all successfully retrieved/processed "+ 74 "information (the errors will be part of the report too)") 75 return cmd 76 } 77 78 type ReportOptions struct { 79 OutputDirectory string 80 IgnoreErrors bool 81 } 82 83 func runReport(cli agentcli.Cli, opts ReportOptions) error { 84 // create report time and dependent variables 85 reportTime := time.Now() 86 reportName := fmt.Sprintf("agentctl-report--%s", 87 strings.ReplaceAll(reportTime.UTC().Format("2006-01-02--15-04-05-.000"), ".", "")) 88 89 // create temporal directory 90 dirNamePattern := fmt.Sprintf("%v--*", reportName) 91 dirName, err := os.MkdirTemp("", dirNamePattern) 92 if err != nil { 93 return fmt.Errorf("can't create tmp directory with name pattern %s due to %v", dirNamePattern, err) 94 } 95 defer os.RemoveAll(dirName) 96 97 // create report files 98 errors := packErrors( 99 writeReportTo("_report.txt", dirName, writeMainReport, cli, reportTime), 100 writeReportTo("software-versions.txt", dirName, writeAgentctlVersionReport, cli), 101 writeReportTo("software-versions.txt", dirName, writeAgentVersionReport, cli), 102 writeReportTo("software-versions.txt", dirName, writeVPPVersionReport, cli), 103 writeReportTo("hardware.txt", dirName, writeHardwareCPUReport, cli), 104 writeReportTo("hardware.txt", dirName, writeHardwareNumaReport, cli), 105 writeReportTo("hardware.txt", dirName, writeHardwareVPPMainMemoryReport, cli), 106 writeReportTo("hardware.txt", dirName, writeHardwareVPPNumaHeapMemoryReport, cli), 107 writeReportTo("hardware.txt", dirName, writeHardwareVPPPhysMemoryReport, cli), 108 writeReportTo("hardware.txt", dirName, writeHardwareVPPAPIMemoryReport, cli), 109 writeReportTo("hardware.txt", dirName, writeHardwareVPPStatsMemoryReport, cli), 110 writeReportTo("agent-status.txt", dirName, writeAgentStatusReport, cli), 111 writeReportTo("agent-transaction-history.txt", dirName, writeAgentTxnHistoryReport, cli), 112 writeReportTo("agent-NB-config.yaml", dirName, writeAgentNBConfigReport, cli), 113 writeReportTo("agent-kvscheduler-NB-config-view.txt", dirName, writeKVschedulerNBConfigReport, cli), 114 writeReportTo("agent-kvscheduler-SB-config-view.txt", dirName, writeKVschedulerSBConfigReport, cli), 115 writeReportTo("agent-kvscheduler-cached-config-view.txt", dirName, writeKVschedulerCachedConfigReport, cli), 116 writeReportTo("vpp-startup-config.txt", dirName, writeVPPStartupConfigReport, cli), 117 writeReportTo("vpp-running-config(vpp-agent-SB-dump).yaml", dirName, writeVPPRunningConfigReport, cli), 118 writeReportTo("vpp-event-log.txt", dirName, writeVPPEventLogReport, cli), 119 writeReportTo("vpp-log.txt", dirName, writeVPPLogReport, cli), 120 writeReportTo("vpp-statistics-interfaces.txt", dirName, writeVPPInterfaceStatsReport, cli), 121 writeReportTo("vpp-statistics-errors.txt", dirName, writeVPPErrorStatsReport, cli), 122 writeReportTo("vpp-api-trace.txt", dirName, writeVPPApiTraceReport, cli), 123 writeReportTo("vpp-other-srv6.txt", dirName, writeVPPSRv6LocalsidReport, cli), 124 writeReportTo("vpp-other-srv6.txt", dirName, writeVPPSRv6PolicyReport, cli), 125 writeReportTo("vpp-other-srv6.txt", dirName, writeVPPSRv6SteeringReport, cli), 126 ) 127 // summary errors from reports (actual errors are already written in reports, 128 // user console and failedReportFileName file) 129 if len(errors) > 0 { 130 if !opts.IgnoreErrors { 131 _, err = cli.Out().Write([]byte(fmt.Sprintf("%d subreport(s) failed.\n\nIf you want to ignore errors "+ 132 "from subreports and create report from the successfully retrieved/processed information then "+ 133 "add the --ignore-errors (-i) argument to the command (i.e. 'agentctl report -i')", len(errors)))) 134 if err != nil { 135 return err 136 } 137 return errors 138 } 139 _, err = cli.Out().Write([]byte(fmt.Sprintf("%d subreport(s) couldn't be fully or partially created "+ 140 "(full list with errors will be in packed zip file as file %s)\n\n", len(errors), failedReportFileName))) 141 if err != nil { 142 return err 143 } 144 } else { // 145 if _, err = cli.Out().Write([]byte("All subreports were successfully created...\n\n")); err != nil { 146 return err 147 } 148 // remove empty "failed report" file (ignoring remove failure because it means only one more 149 // empty file in report zip file) 150 os.Remove(filepath.Join(dirName, failedReportFileName)) 151 } 152 153 // resolve zip file name 154 simpleZipFileName := reportName + ".zip" 155 zipFileName := filepath.Join(opts.OutputDirectory, simpleZipFileName) 156 if opts.OutputDirectory == "" { 157 zipFileName, err = filepath.Abs(simpleZipFileName) 158 if err != nil { 159 return fmt.Errorf("can't find out absolute path for output zip file due to: %v\n\n", err) 160 } 161 } 162 163 // combine report files into one zip file 164 if _, err := cli.Out().Write([]byte("Creating report zip file... ")); err != nil { 165 return err 166 } 167 if err := createZipFile(zipFileName, dirName); err != nil { 168 return fmt.Errorf("can't create zip file(%v) due to: %v", zipFileName, err) 169 } 170 if _, err := cli.Out().Write([]byte(fmt.Sprintf("Done.\nReport file: %v\n", zipFileName))); err != nil { 171 return err 172 } 173 174 return nil 175 } 176 177 func writeMainReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 178 // using template also for simple cases to be consistent with variable formatting (i.e. time formatting) 179 format := `################# REPORT ################# 180 report creation time: {{epoch .ReportTime}} 181 report version: {{.Version}} (=AGENTCTL version) 182 183 Subreport/file structure of this report: 184 _report.txt (this file) 185 Contains primary identification for the whole report (creation time, report version) 186 _failed-reports.txt 187 Contains all errors from all subreports. The errors are presented for user convenience at 3 places: 188 1. showed to user in console while running the reporting command of agentctl 189 2. in failed report file (_failed-reports.txt) - all errors at one place 190 3. in subreport file - error in place where the retrieved information should be 191 software-versions.txt 192 Contains identification of the software stack used (agentctl/vpp-agent/vpp) 193 hardware.txt 194 Contains some information about the hardware that the software(vpp-agent/vpp) is running on 195 agent-status.txt 196 Contains status of vpp-agent and its plugins. Contains also boot time and up time of vpp-agent. 197 agent-transaction-history.txt 198 Contains transaction history of vpp-agent. 199 agent-NB-config.yaml 200 Contains vpp-agent's northbound(desired) configuration for VPP. The output is in compatible format 201 (yaml with correct structure) with agentctl configuration import fuctionality ('agentctl config update') 202 so it can be used to setup the configuration from report target installation in local environment for 203 debugging or other purposes. 204 This version doesn't contains data for custom 3rd party configuration models, but only data for 205 vpp-agent upstreamed configuration models. For full configuration data (but not in import compatible format) 206 see agent-kvscheduler-NB-config-view.txt. 207 agent-kvscheduler-NB-config-view.txt 208 Contains vpp-agent's northbound(desired) configuration for VPP as seen by vpp-agent's KVScheduler component. 209 The KVScheduler is the source of truth in VPP-Agent so it contains all data (even the 3rd party models not 210 upstreamed into vpp-agent). The output is not compatible with agentctl configuration import functionality 211 (agentctl config update). 212 agent-kvscheduler-SB-config-view.txt 213 Contains vpp-agent's southbound configuration for VPP(actual configuration retrieved from VPP) as seen 214 by vpp-agent's KVScheduler component. This will actually retrieve new information from VPP. 215 agent-kvscheduler-cached-config-view.txt 216 Contains vpp-agent's cached northbound and southbound configuration for VPP as seen by vpp-agent's 217 KVScheduler component. This will not trigger any VPP data retrieval. It will show only cached information. 218 vpp-startup-config.txt 219 Contains startup configuration of VPP (retrieved from the VPP executable cmd line parameters) 220 vpp-running-config(vpp-agent-SB-dump).yaml 221 Contains running configuration of VPP. It is retrieved using vpp-agent. 222 vpp-event-log.txt 223 Contains event log from VPP. It contains events happening to VPP, but without additional information or errors. 224 vpp-log.txt 225 Container log of VPP. 226 vpp-api-trace.txt 227 Contains VPP API trace (formatted from VPP CLI command "api trace custom-dump"). 228 vpp-statistics-interfaces.txt 229 Contains interface statistics from VPP. 230 vpp-statistics-errors.txt 231 Contains error statistics from VPP. 232 vpp-other-*.txt 233 Contains additional information from VPP by using vppctl commands. 234 ` 235 data := map[string]interface{}{ 236 "ReportTime": otherArgs[0].(time.Time).Unix(), 237 "Version": version.Version(), 238 } 239 if err := formatAsTemplate(w, format, data); err != nil { 240 return err 241 } 242 return nil 243 } 244 245 func writeAgentctlVersionReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 246 format := `AGENTCTL: 247 Version: {{.Version}} 248 249 Go version: {{.GoVersion}} 250 OS/Arch: {{.OS}}/{{.Arch}} 251 252 Build Info: 253 Git commit: {{.GitCommit}} 254 Git branch: {{.GitBranch}} 255 User: {{.BuildUser}} 256 Host: {{.BuildHost}} 257 Built: {{epoch .BuildTime}} 258 259 ` 260 data := map[string]interface{}{ 261 "Version": version.Version(), 262 "GitCommit": version.GitCommit(), 263 "GitBranch": version.GitBranch(), 264 "BuildUser": version.BuildUser(), 265 "BuildHost": version.BuildHost(), 266 "BuildTime": version.BuildTime(), 267 "GoVersion": runtime.Version(), 268 "OS": runtime.GOOS, 269 "Arch": runtime.GOARCH, 270 } 271 if err := formatAsTemplate(w, format, data); err != nil { 272 return err 273 } 274 return nil 275 } 276 277 func writeAgentStatusReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 278 const format = `State: {{.AgentStatus.State}} 279 Started: {{epoch .AgentStatus.StartTime}} ({{ago (epoch .AgentStatus.StartTime)}} ago) 280 Last change: {{ago (epoch .AgentStatus.LastChange)}} 281 Last update: {{ago (epoch .AgentStatus.LastUpdate)}} 282 283 PLUGINS 284 {{- range $name, $plugin := .PluginStatus}} 285 {{$name}}: {{$plugin.State}} 286 {{- end}} 287 ` 288 ctx, cancel := context.WithCancel(context.Background()) 289 defer cancel() 290 291 subTaskActionName := "Retrieving agent status" 292 cliOutputDefer, cliOutputErrPassing := subTaskCliOutputs(cli, subTaskActionName) 293 defer cliOutputDefer(cli) 294 295 status, err := cli.Client().Status(ctx) 296 if err != nil { 297 return fileErrorPassing(cliOutputErrPassing(err, "getting status"), w, errorW, subTaskActionName) 298 } 299 300 if err := formatAsTemplate(w, format, status); err != nil { 301 return fileErrorPassing(cliOutputErrPassing(err, "formatting"), w, errorW, subTaskActionName) 302 } 303 return nil 304 } 305 306 func writeAgentVersionReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 307 const format = `AGENT: 308 App name: {{.App}} 309 Version: {{.Version}} 310 311 Go version: {{.GoVersion}} 312 OS/Arch: {{.OS}}/{{.Arch}} 313 314 Build Info: 315 Git commit: {{.GitCommit}} 316 Git branch: {{.GitBranch}} 317 User: {{.BuildUser}} 318 Host: {{.BuildHost}} 319 Built: {{epoch .BuildTime}} 320 ` 321 ctx, cancel := context.WithCancel(context.Background()) 322 defer cancel() 323 324 subTaskActionName := "Retrieving agent version" 325 cliOutputDefer, cliOutputErrPassing := subTaskCliOutputs(cli, subTaskActionName) 326 defer cliOutputDefer(cli) 327 328 version, err := cli.Client().AgentVersion(ctx) 329 if err != nil { 330 return fileErrorPassing(cliOutputErrPassing(err, "getting agent version"), w, errorW, subTaskActionName) 331 } 332 333 if err := formatAsTemplate(w, format, version); err != nil { 334 return fileErrorPassing(cliOutputErrPassing(err, "formatting"), w, errorW, subTaskActionName) 335 } 336 return nil 337 } 338 339 func writeVPPRunningConfigReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 340 ctx, cancel := context.WithCancel(context.Background()) 341 defer cancel() 342 343 subTaskActionName := "Retrieving vpp running configuration" 344 cliOutputDefer, cliOutputErrPassing := subTaskCliOutputs(cli, subTaskActionName) 345 defer cliOutputDefer(cli) 346 347 client, err := cli.Client().ConfiguratorClient() 348 if err != nil { 349 return fileErrorPassing(cliOutputErrPassing(err, "getting configuration client"), w, errorW, subTaskActionName) 350 } 351 resp, err := client.Dump(ctx, &configurator.DumpRequest{}) 352 if err != nil { 353 return fileErrorPassing(cliOutputErrPassing(err, "getting dump"), w, errorW, subTaskActionName) 354 } 355 356 if err := formatAsTemplate(w, "yaml", resp.Dump); err != nil { 357 return fileErrorPassing(cliOutputErrPassing(err, "formatting"), w, errorW, subTaskActionName) 358 } 359 return nil 360 } 361 362 func writeAgentNBConfigReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 363 ctx, cancel := context.WithCancel(context.Background()) 364 defer cancel() 365 366 subTaskActionName := "Retrieving agent NB configuration" 367 cliOutputDefer, cliOutputErrPassing := subTaskCliOutputs(cli, subTaskActionName) 368 defer cliOutputDefer(cli) 369 370 // TODO replace with new implementation for agentctl config get (https://github.com/ligato/vpp-agent/pull/1754) 371 client, err := cli.Client().ConfiguratorClient() 372 if err != nil { 373 return fileErrorPassing(cliOutputErrPassing(err, "getting configuration client"), 374 w, errorW, subTaskActionName) 375 } 376 resp, err := client.Get(ctx, &configurator.GetRequest{}) 377 if err != nil { 378 return fileErrorPassing(cliOutputErrPassing(err, "getting configuration"), w, errorW, subTaskActionName) 379 } 380 381 if err := formatAsTemplate(w, "yaml", resp.GetConfig()); err != nil { 382 return fileErrorPassing(cliOutputErrPassing(err, "formatting"), w, errorW, subTaskActionName) 383 } 384 return nil 385 } 386 387 func writeKVschedulerNBConfigReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 388 return writeKVschedulerReport( 389 "Retrieving agent kvscheduler NB configuration", "NB", nil, w, errorW, cli, otherArgs...) 390 } 391 392 func writeKVschedulerSBConfigReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 393 ignoreModels := []string{ // not implemented SB retrieve 394 "ligato.vpp.srv6.LocalSID", 395 "ligato.vpp.srv6.Policy", 396 "ligato.vpp.srv6.SRv6Global", 397 "ligato.vpp.srv6.Steering", 398 } 399 return writeKVschedulerReport( 400 "Retrieving agent kvscheduler SB configuration", "SB", ignoreModels, w, errorW, cli, otherArgs...) 401 } 402 403 func writeKVschedulerCachedConfigReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 404 return writeKVschedulerReport( 405 "Retrieving agent kvscheduler cached configuration", "cached", nil, w, errorW, cli, otherArgs...) 406 } 407 408 func writeKVschedulerReport(subTaskActionName string, view string, ignoreModels []string, 409 w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 410 ctx, cancel := context.WithCancel(context.Background()) 411 defer cancel() 412 413 cliOutputDefer, cliOutputErrPassing := subTaskCliOutputs(cli, subTaskActionName) 414 defer cliOutputDefer(cli) 415 416 // get key prefixes for all models 417 allModels, err := cli.Client().ModelList(ctx, types.ModelListOptions{ 418 Class: "config", 419 }) 420 if err != nil { 421 return fileErrorPassing(cliOutputErrPassing(err, "getting model list"), w, errorW, subTaskActionName) 422 } 423 ignoreModelSet := make(map[string]struct{}) 424 for _, ignoreModel := range ignoreModels { 425 ignoreModelSet[ignoreModel] = struct{}{} 426 } 427 var keyPrefixes []string 428 for _, m := range allModels { 429 if _, ignore := ignoreModelSet[m.ProtoName]; ignore { 430 continue 431 } 432 keyPrefixes = append(keyPrefixes, m.KeyPrefix) 433 } 434 435 // retrieve KVScheduler data 436 var ( 437 errs Errors 438 dumps []api.RecordedKVWithMetadata 439 ) 440 for _, keyPrefix := range keyPrefixes { 441 dump, err := cli.Client().SchedulerDump(ctx, types.SchedulerDumpOptions{ 442 KeyPrefix: keyPrefix, 443 View: view, 444 }) 445 if err != nil { 446 if strings.Contains(err.Error(), "no descriptor found matching the key prefix") { 447 if _, e := cli.Out().Write([]byte(fmt.Sprintf("Skipping key prefix %s due to: %v\n", keyPrefix, err))); err != nil { 448 return e 449 } 450 } else { 451 errs = append(errs, fmt.Errorf("Failed to get data for %s view and "+ 452 "key prefix %s due to: %v\n", view, keyPrefix, err)) 453 } 454 continue 455 } 456 dumps = append(dumps, dump...) 457 } 458 459 // sort and print retrieved KVScheduler data 460 sort.Slice(dumps, func(i, j int) bool { 461 return dumps[i].Key < dumps[j].Key 462 }) 463 printDumpTable(w, dumps) 464 465 // error handling 466 if len(errs) > 0 { 467 return fileErrorPassing(cliOutputErrPassing(errs, "dumping kvscheduler data"), w, errorW, subTaskActionName) 468 } 469 return nil 470 } 471 472 func writeAgentTxnHistoryReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 473 ctx, cancel := context.WithCancel(context.Background()) 474 defer cancel() 475 476 subTaskActionName := "Retrieving agent transaction history configuration" 477 cliOutputDefer, cliOutputErrPassing := subTaskCliOutputs(cli, subTaskActionName) 478 defer cliOutputDefer(cli) 479 480 // get txn history 481 txns, err := cli.Client().SchedulerHistory(ctx, types.SchedulerHistoryOptions{ 482 SeqNum: -1, 483 }) 484 if err != nil { 485 return fileErrorPassing(cliOutputErrPassing(err, "getting scheduler txn history"), 486 w, errorW, subTaskActionName) 487 } 488 489 // format and write it to output file 490 // Note: not using one big template to print at least history summary in case of full txn log formatting fail 491 if _, err := w.Write([]byte("Agent transaction summary:\n")); err != nil { 492 return err 493 } 494 var summaryBuf bytes.Buffer 495 printHistoryTable(&summaryBuf, txns, true) 496 _, err = w.Write([]byte(fmt.Sprintf(" %s\n", strings.ReplaceAll(stripTextColoring(summaryBuf.String()), "\n", "\n ")))) 497 if err != nil { 498 return err 499 } 500 if _, err := w.Write([]byte("Agent transaction log:\n")); err != nil { 501 return err 502 } 503 var logBuf bytes.Buffer 504 if err := formatAsTemplate(&logBuf, "{{.}}", txns); err != nil { // "log" format of history 505 return fileErrorPassing(cliOutputErrPassing(err, "formatting"), w, errorW, subTaskActionName) 506 } 507 if _, err := w.Write([]byte(fmt.Sprintf(" %s\n", strings.ReplaceAll(logBuf.String(), "\n", "\n ")))); err != nil { 508 return err 509 } 510 511 return nil 512 } 513 514 func stripTextColoring(coloredText string) string { 515 return regexp.MustCompile(`(?m)\x1B\[[0-9;]*m`).ReplaceAllString(coloredText, "") 516 } 517 518 func writeVPPInterfaceStatsReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 519 subTaskActionName := "Retrieving vpp interface statistics" 520 cliOutputDefer, cliOutputErrPassing := subTaskCliOutputs(cli, subTaskActionName) 521 defer cliOutputDefer(cli) 522 523 // get interfaces statistics 524 interfaceStats, err := cli.Client().VppGetInterfaceStats() 525 if err != nil { 526 return fileErrorPassing(cliOutputErrPassing(err, "getting interface stats"), 527 w, errorW, subTaskActionName) 528 } 529 530 // format and write it to output file 531 printInterfaceStatsTable(w, interfaceStats) 532 return nil 533 } 534 535 func writeVPPErrorStatsReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 536 subTaskActionName := "Retrieving vpp error statistics" 537 cliOutputDefer, cliOutputErrPassing := subTaskCliOutputs(cli, subTaskActionName) 538 defer cliOutputDefer(cli) 539 540 // get error statistics 541 errorStats, err := cli.Client().VppGetErrorStats() 542 if err != nil { 543 return fileErrorPassing(cliOutputErrPassing(err, "getting error stats"), w, errorW, subTaskActionName) 544 } 545 546 // format and write it to output file 547 printErrorStatsTable(w, errorStats) 548 return nil 549 } 550 551 func writeVPPVersionReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 552 // NOTE: as task/info-specialized VPP-Agent API(REST/GRPC) should be preferred 553 // (see writeVPPCLICommandReport docs) there is (plugins/govppmux/)vppcalls.VppCoreAPI containing some 554 // information(not all), but it is not exposed using REST or GRPC. 555 return writeVPPCLICommandReport("Retrieving vpp version", "show version verbose", 556 w, errorW, cli, func(vppCLICmd, cmdOutput string) string { // formatting output 557 return fmt.Sprintf("VPP:\n %s\n", 558 strings.ReplaceAll(cmdOutput, "\n", "\n ")) 559 }) 560 } 561 562 func writeVPPStartupConfigReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 563 return writeVPPCLICommandReport("Retrieving vpp startup config", 564 "show version cmdline", w, errorW, cli) 565 } 566 567 func writeHardwareCPUReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 568 return writeVPPCLICommandReport("Retrieving vpp cpu information", "show cpu", 569 w, errorW, cli, func(vppCLICmd, cmdOutput string) string { // formatting output 570 return fmt.Sprintf("CPU (vppctl# %s):\n %s\n\n", 571 vppCLICmd, strings.ReplaceAll(cmdOutput, "\n", "\n ")) 572 }) 573 } 574 575 func writeHardwareNumaReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 576 return writeVPPCLICommandReport("Retrieving vpp numa information", "show buffers", 577 w, errorW, cli, func(vppCLICmd, cmdOutput string) string { // formatting output 578 return fmt.Sprintf("NUMA (indirect by viewing vpp buffer allocated for each numa node, "+ 579 "vppctl# %s):\n %s\n\n", vppCLICmd, strings.ReplaceAll(cmdOutput, "\n", "\n ")) 580 }) 581 } 582 583 func writeHardwareVPPMainMemoryReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 584 return writeVPPCLICommandReport("Retrieving vpp main-heap memory information", 585 "show memory main-heap verbose", w, errorW, cli, func(vppCLICmd, cmdOutput string) string { // formatting output 586 return fmt.Sprintf("MEMORY (only VPP related information available):\n"+ 587 " vppctl# %s:\n %s\n\n", vppCLICmd, strings.ReplaceAll(cmdOutput, "\n", "\n ")) 588 }) 589 } 590 591 func writeHardwareVPPNumaHeapMemoryReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 592 return writeVPPCLICommandReport("Retrieving vpp numa-heap memory information", 593 "show memory numa-heaps", w, errorW, cli, func(vppCLICmd, cmdOutput string) string { // formatting output 594 return fmt.Sprintf(" vppctl# %s:\n %s\n\n", 595 vppCLICmd, strings.ReplaceAll(cmdOutput, "\n", "\n ")) 596 }) 597 } 598 599 func writeHardwareVPPPhysMemoryReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 600 return writeVPPCLICommandReport("Retrieving vpp phys memory information", 601 "show physmem", w, errorW, cli, func(vppCLICmd, cmdOutput string) string { // formatting output 602 return fmt.Sprintf(" vppctl# %s:\n %s\n\n", 603 vppCLICmd, strings.ReplaceAll(cmdOutput, "\n", "\n ")) 604 }) 605 } 606 607 func writeHardwareVPPAPIMemoryReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 608 return writeVPPCLICommandReport("Retrieving vpp api-segment memory information", 609 "show memory api-segment", w, errorW, cli, func(vppCLICmd, cmdOutput string) string { // formatting output 610 return fmt.Sprintf(" vppctl# %s:\n %s\n\n", 611 vppCLICmd, strings.ReplaceAll(cmdOutput, "\n", "\n ")) 612 }) 613 } 614 615 func writeHardwareVPPStatsMemoryReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 616 return writeVPPCLICommandReport("Retrieving vpp stats-segment memory information", 617 "show memory stats-segment", w, errorW, cli, func(vppCLICmd, cmdOutput string) string { // formatting output 618 return fmt.Sprintf(" vppctl# %s:\n %s\n\n", 619 vppCLICmd, strings.ReplaceAll(cmdOutput, "\n", "\n ")) 620 }) 621 } 622 623 func writeVPPApiTraceReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 624 var saveFileCmdOutput *string 625 var errs Errors 626 err := writeVPPCLICommandReport("Saving vpp api trace remotely to a file", 627 "api trace save agentctl-report.api", w, errorW, cli, func(vppCLICmd, cmdOutput string) string { // formatting output 628 saveFileCmdOutput = &cmdOutput 629 return fmt.Sprintf("vppctl# %s:\n%s\n\n", vppCLICmd, cmdOutput) // default formatting 630 }) 631 if err != nil { 632 errs = append(errs, err) 633 } 634 635 // retrieve file location on remote machine 636 // Example output: "API trace saved to /tmp/agentctl-report.api" 637 fileLocation := "/tmp/agentctl-report.api" 638 expectedFormattingPrefix := "API trace saved to" 639 if strings.HasPrefix(*saveFileCmdOutput, expectedFormattingPrefix) { 640 fileLocation = strings.TrimSpace(strings.TrimPrefix(*saveFileCmdOutput, expectedFormattingPrefix)) 641 } 642 643 if err := writeVPPCLICommandReport("Retrieving vpp api trace from saved remote file", 644 fmt.Sprintf("api trace custom-dump %s", fileLocation), w, errorW, cli); err != nil { 645 errs = append(errs, err) 646 } 647 return errs 648 } 649 650 func writeVPPEventLogReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 651 // retrieve (and write to report) vpp clock information 652 var clockOutput *string 653 var errs Errors 654 err := writeVPPCLICommandReport("Retrieving vpp start time information(for event-log)", 655 "show clock verbose", w, errorW, cli, func(vppCLICmd, cmdOutput string) string { // formatting output 656 clockOutput = &cmdOutput 657 return fmt.Sprintf("vppctl# %s (for precise conversion between vpp running time "+ 658 "in seconds and real time):\n%s\n\n", vppCLICmd, cmdOutput) 659 }) 660 if err != nil { 661 errs = append(errs, err) 662 } 663 664 // get VPP startup time 665 vppStartUpTime, err := vppStartupTime(*clockOutput) 666 addRealTime := err == nil 667 668 // write event-log report (+ add into each line real date/time computed from VPP start timestamp) 669 err = writeVPPCLICommandReport("Retrieving vpp event-log information", 670 "show event-logger all", w, errorW, cli, func(vppCLICmd, cmdOutput string) string { // formatting output 671 if addRealTime { 672 // Example line from output: " 6.952401056: api-msg: trace_plugin_msg_ids" 673 var reVPPTimeStamp = regexp.MustCompile(`(?m)^\s*(\d*.\d*):`) 674 var sb strings.Builder 675 for _, line := range strings.Split(cmdOutput, "\n") { 676 lineVPPTimeStrSlice := reVPPTimeStamp.FindStringSubmatch(line) 677 if len(lineVPPTimeStrSlice) == 0 { 678 sb.WriteString(line) // error => forget conversion for this line 679 continue 680 } 681 lineVPPTimeStr := lineVPPTimeStrSlice[0] 682 if len(lineVPPTimeStr) < 1 { 683 sb.WriteString(line) // error => forget conversion for this line 684 continue 685 } 686 lineVPPTime, err := strconv.ParseFloat( 687 strings.TrimSpace(lineVPPTimeStr[:len(lineVPPTimeStr)-1]), 32) 688 if err != nil { 689 sb.WriteString(line) // error => forget conversion for this line 690 continue 691 } 692 lineRealTime := (*vppStartUpTime).Add(time.Duration(int(lineVPPTime*1_000_000)) * time.Microsecond) 693 sb.WriteString(strings.Replace(line, lineVPPTimeStr, fmt.Sprintf("%s(%s +-1s)", 694 lineVPPTimeStr, lineRealTime.UTC().Format(time.RFC1123)), 1) + "\n") 695 } 696 return fmt.Sprintf("vppctl# %s:\n%s\n\n", vppCLICmd, sb.String()) 697 } 698 return fmt.Sprintf("vppctl# %s:\n%s\n\n", vppCLICmd, cmdOutput) // default formatting 699 }) 700 if err != nil { 701 errs = append(errs, err) 702 } 703 return errs 704 } 705 706 func vppStartupTime(vppClockCmdOutput string) (*time.Time, error) { 707 // vppctl# show clock verbose 708 // Example output: "Time now 705.541644, reftime 705.541644, error 0.000000, clocks/sec 2591995355.407570, Wed, 11 Nov 2020 14:32:39 GMT" 709 710 // get real date/time from command output 711 var reDateTime = regexp.MustCompile(`[^\s,]*,[^,]*\z`) 712 cmdTime, err := time.Parse(time.RFC1123, reDateTime.FindString(strings.ReplaceAll(vppClockCmdOutput, "\n", ""))) 713 if err != nil { 714 return nil, fmt.Errorf("can't parse ref time form VPP "+ 715 "show clock command due to: %v (cmd output=%s)", err, vppClockCmdOutput) 716 } 717 718 // get VPP time (seconds from VPP start) 719 var reReftime = regexp.MustCompile(`reftime\W*(\d*.\d*)\D`) 720 strSubmatch := reReftime.FindStringSubmatch(vppClockCmdOutput) 721 if len(strSubmatch) < 2 { 722 return nil, fmt.Errorf("can't find reftime in vpp clock cmd output %v", vppClockCmdOutput) 723 } 724 vppTimeAtCmdTime, err := strconv.ParseFloat(strSubmatch[1], 32) 725 if err != nil { 726 return nil, fmt.Errorf("can't parse reftime string(%v) to float due to: %v", strSubmatch[1], err) 727 } 728 729 // compute VPP startup time 730 vppStartUpTime := cmdTime.Add(-time.Duration(int(vppTimeAtCmdTime*1_000_000)) * time.Microsecond) 731 return &vppStartUpTime, nil 732 } 733 734 func writeVPPLogReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 735 return writeVPPCLICommandReport("Retrieving vpp log information", 736 "show logging", w, errorW, cli) 737 } 738 739 func writeVPPSRv6LocalsidReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 740 return writeVPPCLICommandReport("Retrieving vpp SRv6 localsid information", 741 "show sr localsid", w, errorW, cli) 742 } 743 744 func writeVPPSRv6PolicyReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 745 return writeVPPCLICommandReport("Retrieving vpp SRv6 policies information", 746 "show sr policies", w, errorW, cli) 747 } 748 749 func writeVPPSRv6SteeringReport(w io.Writer, errorW io.Writer, cli agentcli.Cli, otherArgs ...interface{}) error { 750 return writeVPPCLICommandReport("Retrieving vpp SRv6 steering information", 751 "show sr steering-policies", w, errorW, cli) 752 } 753 754 // writeVPPCLICommandReport writes to file a report based purely on one VPP CLI command output. 755 // Before using this function think about using some task/info-specialized VPP-Agent API(REST/GRPC) because 756 // it solves compatibility issues with different VPP versions. This method don't care whether the given 757 // vppCLICmd is actually a valid VPP CLI command on the version of VPP to which it will connect to. In case 758 // of incompatibility, this subreport will fail (the whole report will be created, just like other subreports 759 // if they don't fail on their own). 760 func writeVPPCLICommandReport(subTaskActionName string, vppCLICmd string, w io.Writer, errorW io.Writer, 761 cli agentcli.Cli, otherArgs ...interface{}) error { 762 ctx, cancel := context.WithCancel(context.Background()) 763 defer cancel() 764 765 cliOutputDefer, cliOutputErrPassing := subTaskCliOutputs(cli, subTaskActionName) 766 defer cliOutputDefer(cli) 767 768 cmdOutput, err := cli.Client().VppRunCli(ctx, vppCLICmd) 769 if err != nil { 770 return fileErrorPassing(cliOutputErrPassing(err), w, errorW, subTaskActionName) 771 } 772 formattedOutput := fmt.Sprintf("vppctl# %s\n%s\n", vppCLICmd, cmdOutput) 773 if len(otherArgs) > 0 { 774 formattedOutput = otherArgs[0].(func(string, string) string)(vppCLICmd, cmdOutput) 775 } 776 fmt.Fprint(w, formattedOutput) 777 return nil 778 } 779 780 func printErrorStatsTable(out io.Writer, errorStats *govppapi.ErrorStats) { 781 table := tablewriter.NewWriter(out) 782 header := []string{ 783 "Statistics name", "Error counter", 784 } 785 table.SetHeader(header) 786 table.SetRowLine(false) 787 table.SetAutoWrapText(false) 788 789 for _, errorStat := range errorStats.Errors { 790 var valSum uint64 = 0 791 // errorStat.Values are per worker counters 792 for _, val := range errorStat.Values { 793 valSum += val 794 } 795 row := []string{ 796 errorStat.CounterName, 797 fmt.Sprint(valSum), 798 } 799 table.Append(row) 800 } 801 table.Render() 802 } 803 804 func printInterfaceStatsTable(out io.Writer, interfaceStats *govppapi.InterfaceStats) { 805 table := tablewriter.NewWriter(out) 806 header := []string{ 807 "Index", "Name", "Rx", "Tx", "Rx errors", "Tx errors", "Drops", "Rx unicast/multicast/broadcast", 808 "Tx unicast/multicast/broadcast", "Other", 809 } 810 table.SetHeader(header) 811 table.SetAutoWrapText(false) 812 table.SetRowLine(true) 813 814 for _, interfaceStat := range interfaceStats.Interfaces { 815 row := []string{ 816 fmt.Sprint(interfaceStat.InterfaceIndex), 817 fmt.Sprint(interfaceStat.InterfaceName), 818 fmt.Sprintf("%d packets (%d bytes)", interfaceStat.Rx.Packets, interfaceStat.Rx.Bytes), 819 fmt.Sprintf("%d packets (%d bytes)", interfaceStat.Tx.Packets, interfaceStat.Tx.Bytes), 820 fmt.Sprint(interfaceStat.RxErrors), 821 fmt.Sprint(interfaceStat.TxErrors), 822 fmt.Sprint(interfaceStat.Drops), 823 fmt.Sprintf("%d packets (%d bytes)\n%d packets (%d bytes)\n%d packets (%d bytes)", 824 interfaceStat.RxUnicast.Packets, interfaceStat.RxUnicast.Bytes, 825 interfaceStat.RxMulticast.Packets, interfaceStat.RxMulticast.Bytes, 826 interfaceStat.RxBroadcast.Packets, interfaceStat.RxBroadcast.Bytes, 827 ), 828 fmt.Sprintf("%d packets (%d bytes)\n%d packets (%d bytes)\n%d packets (%d bytes)", 829 interfaceStat.TxUnicast.Packets, interfaceStat.TxUnicast.Bytes, 830 interfaceStat.TxMulticast.Packets, interfaceStat.TxMulticast.Bytes, 831 interfaceStat.TxBroadcast.Packets, interfaceStat.TxBroadcast.Bytes, 832 ), 833 fmt.Sprintf("Punts: %d\n"+ 834 "IPv4: %d\n"+ 835 "IPv6: %d\n"+ 836 "RxNoBuf: %d\n"+ 837 "RxMiss: %d\n"+ 838 "Mpls: %d", 839 interfaceStat.Punts, 840 interfaceStat.IP4, 841 interfaceStat.IP6, 842 interfaceStat.RxNoBuf, 843 interfaceStat.RxMiss, 844 interfaceStat.Mpls), 845 } 846 table.Append(row) 847 } 848 table.Render() 849 } 850 851 func packErrors(errors ...error) Errors { 852 var errs Errors 853 for _, err := range errors { 854 if err != nil { 855 if alreadyPackedError, isPacked := err.(Errors); isPacked { 856 errs = append(errs, alreadyPackedError...) 857 continue 858 } 859 errs = append(errs, err) 860 } 861 } 862 return errs 863 } 864 865 func subTaskCliOutputs(cli agentcli.Cli, subTaskActionName string) (func(agentcli.Cli), func(error, ...string) error) { 866 _, _ = cli.Out().Write([]byte(fmt.Sprintf("%s... ", subTaskActionName))) 867 subTaskResult := "Done." 868 pSubTaskResult := &subTaskResult 869 return func(cli agentcli.Cli) { 870 _, _ = cli.Out().Write([]byte(fmt.Sprintf("%s\n", *pSubTaskResult))) 871 }, func(err error, failedActions ...string) error { 872 if len(failedActions) > 0 { 873 err = fmt.Errorf("%s failed due to:\n%v", failedActions[0], err) 874 } 875 subTaskResult = fmt.Sprintf("Error... (this error will be part of the report zip file, "+ 876 "see %s):\n%v\nSkipping this subreport.\n", failedReportFileName, err) 877 pSubTaskResult = &subTaskResult 878 return err 879 } 880 } 881 882 func fileErrorPassing(err error, w io.Writer, errorW io.Writer, subTaskActionName string) error { 883 errorStr := fmt.Sprintf("%s... failed due to:\n%v\n", subTaskActionName, err) 884 _, _ = w.Write([]byte(fmt.Sprintf("<<\n%s>>\n", errorStr))) 885 _, _ = errorW.Write([]byte(fmt.Sprintf("%s\n%s\n", strings.Repeat("#", 70), errorStr))) 886 return err 887 } 888 889 func writeReportTo(fileName string, dirName string, 890 writeFunc func(io.Writer, io.Writer, agentcli.Cli, ...interface{}) error, 891 cli agentcli.Cli, otherWriteFuncArgs ...interface{}) (err error) { 892 // open file (and close it in the end) 893 f, err := os.OpenFile(filepath.Join(dirName, fileName), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) 894 if err != nil { 895 err = fmt.Errorf("can't open file %v due to: %v", filepath.Join(dirName, fileName), err) 896 return 897 } 898 defer func() { 899 if closeErr := f.Close(); closeErr != nil { 900 err = fmt.Errorf("can't close file %v due to: %v", filepath.Join(dirName, fileName), closeErr) 901 } 902 }() 903 904 // open error file (and close it in the end) 905 errorFile, err := os.OpenFile(filepath.Join(dirName, failedReportFileName), 906 os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) 907 if err != nil { 908 err = fmt.Errorf("can't open error file %v due to: %v", 909 filepath.Join(dirName, failedReportFileName), err) 910 return 911 } 912 defer func() { 913 if closeErr := errorFile.Close(); closeErr != nil { 914 err = fmt.Errorf("can't close error file %v due to: %v", 915 filepath.Join(dirName, failedReportFileName), closeErr) 916 } 917 }() 918 919 // append some report to file 920 err = writeFunc(f, errorFile, cli, otherWriteFuncArgs...) 921 return 922 } 923 924 // createZipFile compresses content of directory dirName into a single zip archive file named filename. 925 // Both arguments should be absolut path file/directory names. The directory content excludes subdirectories. 926 func createZipFile(zipFileName string, dirName string) (err error) { 927 // create zip writer 928 zipFile, err := os.Create(zipFileName) 929 if err != nil { 930 return fmt.Errorf("can't create empty zip file(%v) due to: %v", zipFileName, err) 931 } 932 defer func() { 933 if closeErr := zipFile.Close(); closeErr != nil { 934 err = fmt.Errorf("can't close zip file %v due to: %v", zipFileName, closeErr) 935 } 936 }() 937 zipWriter := zip.NewWriter(zipFile) 938 defer func() { 939 if closeErr := zipWriter.Close(); closeErr != nil { 940 err = fmt.Errorf("can't close zip file writer for zip file %v due to: %v", zipFileName, closeErr) 941 } 942 }() 943 944 // Add files to zip 945 dirItems, err := os.ReadDir(dirName) 946 if err != nil { 947 return fmt.Errorf("can't read report directory(%v) due to: %v", dirName, err) 948 } 949 for _, dirItem := range dirItems { 950 if !dirItem.IsDir() { 951 if err = addFileToZip(zipWriter, filepath.Join(dirName, dirItem.Name())); err != nil { 952 return fmt.Errorf("can't add file dirItem.Name() to report zip file due to: %v", err) 953 } 954 } 955 } 956 return nil 957 } 958 959 // addFileToZip adds file to zip file by using zip.Writer. The file name should be a absolute path. 960 func addFileToZip(zipWriter *zip.Writer, filename string) error { 961 // open file for addition 962 fileToZip, err := os.Open(filename) 963 if err != nil { 964 return fmt.Errorf("can't open file %v due to: %v", filename, err) 965 } 966 defer func() { 967 if closeErr := fileToZip.Close(); closeErr != nil { 968 err = fmt.Errorf("can't close zip file %v opened "+ 969 "for file appending due to: %v", filename, closeErr) 970 } 971 }() 972 973 // get information from file for addition 974 info, err := fileToZip.Stat() 975 if err != nil { 976 return fmt.Errorf("can't get information about file (%v) "+ 977 "that should be added to zip file due to: %v", filename, err) 978 } 979 980 // add file to zip file 981 header, err := zip.FileInfoHeader(info) 982 if err != nil { 983 return fmt.Errorf("can't create zip file info header for file %v due to: %v", filename, err) 984 } 985 header.Method = zip.Deflate // enables compression 986 writer, err := zipWriter.CreateHeader(header) 987 if err != nil { 988 return fmt.Errorf("can't create zip header for file %v due to: %v", filename, err) 989 } 990 _, err = io.Copy(writer, fileToZip) 991 if err != nil { 992 return fmt.Errorf("can't copy content of file %v to zip file due to: %v", filename, err) 993 } 994 return nil 995 }