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  }