github.com/fafucoder/cilium@v1.6.11/cilium/cmd/debuginfo.go (about)

     1  // Copyright 2017-2019 Authors of Cilium
     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 cmd
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"regexp"
    27  	"strconv"
    28  	"strings"
    29  	"text/tabwriter"
    30  	"time"
    31  
    32  	"github.com/cilium/cilium/api/v1/models"
    33  	pkg "github.com/cilium/cilium/pkg/client"
    34  	"github.com/cilium/cilium/pkg/command"
    35  
    36  	"github.com/russross/blackfriday"
    37  	"github.com/spf13/cobra"
    38  )
    39  
    40  // outputTypes enum definition
    41  type outputType int
    42  
    43  // outputTypes enum values
    44  const (
    45  	STDOUT outputType = 0 + iota
    46  	MARKDOWN
    47  	HTML
    48  	JSONOUTPUT
    49  	JSONPATH
    50  )
    51  
    52  var (
    53  	// Can't call it jsonOutput because another var in this package uses that.
    54  	jsonOutputDebuginfo = "json"
    55  	markdownOutput      = "markdown"
    56  	htmlOutput          = "html"
    57  	jsonpathOutput      = "jsonpath"
    58  	jsonPathRegExp      = regexp.MustCompile(`^jsonpath\=(.*)`)
    59  )
    60  
    61  // outputTypes enum strings
    62  var outputTypes = [...]string{
    63  	"STDOUT",
    64  	markdownOutput,
    65  	htmlOutput,
    66  	jsonOutputDebuginfo,
    67  	jsonpathOutput,
    68  }
    69  
    70  var debuginfoCmd = &cobra.Command{
    71  	Use:   "debuginfo",
    72  	Short: "Request available debugging information from agent",
    73  	Run:   runDebugInfo,
    74  }
    75  
    76  var (
    77  	outputToFile   bool
    78  	html           string
    79  	filePerCommand bool
    80  	outputOpts     []string
    81  	outputDir      string
    82  )
    83  
    84  type addSection func(*tabwriter.Writer, *models.DebugInfo)
    85  
    86  var sections = map[string]addSection{
    87  	"cilium-version":          addCiliumVersion,
    88  	"kernel-version":          addKernelVersion,
    89  	"cilium-status":           addCiliumStatus,
    90  	"cilium-environment-keys": addCiliumEnvironmentKeys,
    91  	"cilium-endpoint-list":    addCiliumEndpointList,
    92  	"cilium-service-list":     addCiliumServiceList,
    93  	"cilium-policy":           addCiliumPolicy,
    94  	"cilium-memory-map":       addCiliumMemoryMap,
    95  	"cilium-subsystems":       addSubsystems,
    96  }
    97  
    98  func init() {
    99  	rootCmd.AddCommand(debuginfoCmd)
   100  	debuginfoCmd.Flags().BoolVarP(&outputToFile, "file", "f", false, "Redirect output to file(s)")
   101  	debuginfoCmd.Flags().BoolVarP(&filePerCommand, "file-per-command", "", false, "Generate a single file per command")
   102  	debuginfoCmd.Flags().StringSliceVar(&outputOpts, "output", []string{}, "markdown| html| json| jsonpath='{}'")
   103  	debuginfoCmd.Flags().StringVar(&outputDir, "output-directory", "", "directory for files (if specified will use directory in which this command was ran)")
   104  }
   105  
   106  func validateInput() []outputType {
   107  	if outputDir != "" && !outputToFile {
   108  		fmt.Fprintf(os.Stderr, "invalid option combination; specified output-directory %q, but did not specify for output to be redirected to file; exiting\n", outputDir)
   109  		os.Exit(1)
   110  	}
   111  	return validateOutputOpts()
   112  }
   113  
   114  func validateOutputOpts() []outputType {
   115  
   116  	var outputTypes []outputType
   117  	for _, outputOpt := range outputOpts {
   118  		switch strings.ToLower(outputOpt) {
   119  		case markdownOutput:
   120  			outputTypes = append(outputTypes, MARKDOWN)
   121  		case htmlOutput:
   122  			if !outputToFile {
   123  				fmt.Fprintf(os.Stderr, "if HTML is specified as the output format, it is required that you provide the `--file` argument as well\n")
   124  				os.Exit(1)
   125  			}
   126  			outputTypes = append(outputTypes, HTML)
   127  		case jsonOutputDebuginfo:
   128  			if filePerCommand {
   129  				fmt.Fprintf(os.Stderr, "%s does not support dumping a file per command; exiting\n", outputOpt)
   130  				os.Exit(1)
   131  			}
   132  			outputTypes = append(outputTypes, JSONOUTPUT)
   133  		// Empty JSONPath filter case.
   134  		case jsonpathOutput:
   135  			if filePerCommand {
   136  				fmt.Fprintf(os.Stderr, "%s does not support dumping a file per command; exiting\n", outputOpt)
   137  				os.Exit(1)
   138  			}
   139  			outputTypes = append(outputTypes, JSONPATH)
   140  		default:
   141  			// Check to see if arg contains jsonpath filtering as well.
   142  			if jsonPathRegExp.MatchString(outputOpt) {
   143  				outputTypes = append(outputTypes, JSONPATH)
   144  				continue
   145  			}
   146  			fmt.Fprintf(os.Stderr, "%s is not a valid output format; exiting\n", outputOpt)
   147  			os.Exit(1)
   148  		}
   149  	}
   150  	return outputTypes
   151  }
   152  
   153  func formatFileName(outputDir string, cmdTime time.Time, outtype outputType) string {
   154  	var fileName string
   155  	var sep string
   156  	if outputDir != "" {
   157  		sep = outputDir + "/"
   158  	}
   159  	timeStr := cmdTime.Format("20060102-150405.999-0700-MST")
   160  	switch outtype {
   161  	case MARKDOWN:
   162  		fileName = fmt.Sprintf("%scilium-debuginfo-%s.md", sep, timeStr)
   163  	case HTML:
   164  		fileName = fmt.Sprintf("%scilium-debuginfo-%s.html", sep, timeStr)
   165  	case JSONOUTPUT:
   166  		fileName = fmt.Sprintf("%scilium-debuginfo-%s.json", sep, timeStr)
   167  	case JSONPATH:
   168  		fileName = fmt.Sprintf("%scilium-debuginfo-%s.jsonpath", sep, timeStr)
   169  	default:
   170  		fileName = fmt.Sprintf("%scilium-debuginfo-%s.md", sep, timeStr)
   171  	}
   172  	return fileName
   173  }
   174  
   175  func rootWarningMessage() {
   176  	fmt.Fprint(os.Stderr, "Warning, some of the BPF commands might fail when not run as root\n")
   177  }
   178  
   179  func runDebugInfo(cmd *cobra.Command, args []string) {
   180  
   181  	outputTypes := validateInput()
   182  
   183  	resp, err := client.Daemon.GetDebuginfo(nil)
   184  	if err != nil {
   185  		fmt.Fprintf(os.Stderr, "%s\n", pkg.Hint(err))
   186  		os.Exit(1)
   187  	}
   188  
   189  	// create tab-writer to fill buffer
   190  	var buf bytes.Buffer
   191  	w := tabwriter.NewWriter(&buf, 5, 0, 3, ' ', 0)
   192  	p := resp.Payload
   193  
   194  	cmdTime := time.Now()
   195  
   196  	if outputToFile && len(outputTypes) == 0 {
   197  		outputTypes = append(outputTypes, MARKDOWN)
   198  	}
   199  
   200  	// Dump payload for each output format.
   201  	for i, output := range outputTypes {
   202  		var fileName string
   203  
   204  		// Only warn when not dumping output as JSON so that when the output of the
   205  		// command is specified to be JSON, the only outputted content is the JSON
   206  		// model of debuginfo.
   207  		if os.Getuid() != 0 && output != JSONOUTPUT && output != JSONPATH {
   208  			rootWarningMessage()
   209  		}
   210  
   211  		if outputToFile {
   212  			fileName = formatFileName(outputDir, cmdTime, output)
   213  		}
   214  
   215  		// Generate multiple files for each subsection of the command if
   216  		// specified, except in the JSON cases, because in the JSON cases,
   217  		// we want to dump the entire DebugInfo JSON object, not sections of it.
   218  		if filePerCommand && (output != JSONOUTPUT && output != JSONPATH) {
   219  			for cmdName, section := range sections {
   220  				addHeader(w)
   221  				section(w, p)
   222  				writeToOutput(buf, output, fileName, cmdName)
   223  				buf.Reset()
   224  			}
   225  			continue
   226  		}
   227  
   228  		// Generate a single file, except not for JSON; no formatting is
   229  		// needed.
   230  		if output == JSONOUTPUT || output == JSONPATH {
   231  			marshaledDebugInfo, _ := p.MarshalBinary()
   232  			buf.Write(marshaledDebugInfo)
   233  			if output == JSONOUTPUT {
   234  				writeToOutput(buf, output, fileName, "")
   235  			} else {
   236  				writeJSONPathToOutput(buf, fileName, "", outputOpts[i])
   237  			}
   238  			buf.Reset()
   239  		} else {
   240  			addHeader(w)
   241  			for _, section := range sections {
   242  				section(w, p)
   243  			}
   244  			writeToOutput(buf, output, fileName, "")
   245  			buf.Reset()
   246  		}
   247  	}
   248  
   249  	if len(outputTypes) > 0 {
   250  		return
   251  	}
   252  
   253  	if os.Getuid() != 0 {
   254  		rootWarningMessage()
   255  	}
   256  
   257  	// Just write to stdout in markdown formats if no output option specified.
   258  	addHeader(w)
   259  	for _, section := range sections {
   260  		section(w, p)
   261  	}
   262  	writeToOutput(buf, STDOUT, "", "")
   263  
   264  }
   265  
   266  func addHeader(w *tabwriter.Writer) {
   267  	fmt.Fprintf(w, "# Cilium debug information\n")
   268  }
   269  
   270  func addCiliumVersion(w *tabwriter.Writer, p *models.DebugInfo) {
   271  	printMD(w, "Cilium version", p.CiliumVersion)
   272  }
   273  
   274  func addKernelVersion(w *tabwriter.Writer, p *models.DebugInfo) {
   275  	printMD(w, "Kernel version", p.KernelVersion)
   276  }
   277  
   278  func addCiliumStatus(w *tabwriter.Writer, p *models.DebugInfo) {
   279  	printMD(w, "Cilium status", "")
   280  	printTicks(w)
   281  	pkg.FormatStatusResponse(w, p.CiliumStatus, true, true, true, true)
   282  	printTicks(w)
   283  }
   284  
   285  func addCiliumEnvironmentKeys(w *tabwriter.Writer, p *models.DebugInfo) {
   286  	printMD(w, "Cilium environment keys", strings.Join(p.EnvironmentVariables, "\n"))
   287  }
   288  
   289  func addCiliumEndpointList(w *tabwriter.Writer, p *models.DebugInfo) {
   290  	printMD(w, "Endpoint list", "")
   291  	printTicks(w)
   292  	printEndpointList(w, p.EndpointList)
   293  	printTicks(w)
   294  
   295  	for _, ep := range p.EndpointList {
   296  		epID := strconv.FormatInt(ep.ID, 10)
   297  		printList(w, "BPF Policy Get "+epID, "bpf", "policy", "get", epID, "-n")
   298  		printList(w, "BPF CT List "+epID, "bpf", "ct", "list", epID)
   299  		printList(w, "Endpoint Get "+epID, "endpoint", "get", epID)
   300  		printList(w, "Endpoint Health "+epID, "endpoint", "health", epID)
   301  		printList(w, "Endpoint Log "+epID, "endpoint", "log", epID)
   302  
   303  		if ep.Status != nil && ep.Status.Identity != nil {
   304  			id := strconv.FormatInt(ep.Status.Identity.ID, 10)
   305  			printList(w, "Identity get "+id, "identity", "get", id)
   306  		}
   307  	}
   308  }
   309  
   310  func addCiliumServiceList(w *tabwriter.Writer, p *models.DebugInfo) {
   311  	printMD(w, "Service list", "")
   312  	printTicks(w)
   313  	printServiceList(w, p.ServiceList)
   314  	printTicks(w)
   315  }
   316  
   317  func addCiliumPolicy(w *tabwriter.Writer, p *models.DebugInfo) {
   318  	printMD(w, "Policy get", fmt.Sprintf(":\n %s\nRevision: %d\n", p.Policy.Policy, p.Policy.Revision))
   319  }
   320  
   321  func addSubsystems(w *tabwriter.Writer, p *models.DebugInfo) {
   322  	for name, status := range p.Subsystem {
   323  		printMD(w, name, status)
   324  	}
   325  }
   326  
   327  func addCiliumMemoryMap(w *tabwriter.Writer, p *models.DebugInfo) {
   328  	printMD(w, "Cilium memory map\n", p.CiliumMemoryMap)
   329  	if nm := p.CiliumNodemonitorMemoryMap; len(nm) > 0 {
   330  		printMD(w, "Cilium nodemonitor memory map", p.CiliumNodemonitorMemoryMap)
   331  	}
   332  }
   333  
   334  func writeJSONPathToOutput(buf bytes.Buffer, path string, suffix string, jsonPath string) {
   335  	data := buf.Bytes()
   336  	db := &models.DebugInfo{}
   337  	err := db.UnmarshalBinary(data)
   338  	if err != nil {
   339  		fmt.Fprintf(os.Stderr, "error unmarshaling binary: %s\n", err)
   340  	}
   341  	jsonBytes, err := command.DumpJSONToSlice(db, jsonPath)
   342  	if err != nil {
   343  		fmt.Fprintf(os.Stderr, "error printing JSON: %s\n", err)
   344  	}
   345  
   346  	if path == "" {
   347  		fmt.Println(string(jsonBytes[:]))
   348  		return
   349  	}
   350  
   351  	fileName := fileName(path, suffix)
   352  	writeFile(jsonBytes, fileName)
   353  
   354  	fmt.Printf("%s output at %s\n", jsonpathOutput, fileName)
   355  	return
   356  
   357  }
   358  
   359  func writeToOutput(buf bytes.Buffer, output outputType, path string, suffix string) {
   360  	data := buf.Bytes()
   361  
   362  	if path == "" {
   363  		switch output {
   364  		case JSONOUTPUT:
   365  			db := &models.DebugInfo{}
   366  			err := db.UnmarshalBinary(data)
   367  			if err != nil {
   368  				fmt.Fprintf(os.Stderr, "error unmarshaling binary: %s\n", err)
   369  			}
   370  
   371  			err = command.PrintOutputWithType(db, "json")
   372  			if err != nil {
   373  				fmt.Fprintf(os.Stderr, "error printing JSON: %s\n", err)
   374  			}
   375  		default:
   376  			fmt.Println(string(data))
   377  		}
   378  		return
   379  	}
   380  
   381  	if output == STDOUT {
   382  		// Write to standard output
   383  		fmt.Println(string(data))
   384  		return
   385  	}
   386  
   387  	fileName := fileName(path, suffix)
   388  
   389  	switch output {
   390  	case MARKDOWN:
   391  		// Markdown file
   392  		writeMarkdown(data, fileName)
   393  	case HTML:
   394  		// HTML file
   395  		writeHTML(data, fileName)
   396  	case JSONOUTPUT:
   397  		writeJSON(data, fileName)
   398  	case JSONPATH:
   399  		writeJSON(data, fileName)
   400  	}
   401  
   402  	fmt.Printf("%s output at %s\n", outputTypes[output], fileName)
   403  }
   404  
   405  func fileName(path, suffix string) string {
   406  	if len(suffix) == 0 {
   407  		// no suffix, return path
   408  		return path
   409  	}
   410  
   411  	ext := filepath.Ext(path)
   412  	if ext != "" {
   413  		// insert suffix and move extension to back
   414  		return fmt.Sprintf("%s-%s%s", strings.TrimSuffix(path, ext), suffix, ext)
   415  	}
   416  	// no extension, just append suffix
   417  	return fmt.Sprintf("%s-%s", path, suffix)
   418  }
   419  
   420  func printList(w io.Writer, header string, args ...string) {
   421  	output, _ := exec.Command("cilium", args...).CombinedOutput()
   422  	printMD(w, header, string(output))
   423  }
   424  
   425  func printMD(w io.Writer, header string, body string) {
   426  	if len(body) > 0 {
   427  		fmt.Fprintf(w, "\n#### %s\n\n```\n%s\n```\n\n", header, body)
   428  	} else {
   429  		fmt.Fprintf(w, "\n#### %s\n\n", header)
   430  	}
   431  }
   432  
   433  func printTicks(w io.Writer) {
   434  	fmt.Fprint(w, "```\n")
   435  }
   436  
   437  func writeHTML(data []byte, path string) {
   438  	output := blackfriday.MarkdownCommon(data)
   439  	if err := ioutil.WriteFile(path, output, 0644); err != nil {
   440  		fmt.Fprintf(os.Stderr, "Error while writing HTML file %s", err)
   441  		return
   442  	}
   443  }
   444  
   445  func writeMarkdown(data []byte, path string) {
   446  	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
   447  	if err != nil {
   448  		fmt.Fprintf(os.Stderr, "Could not create file %s", path)
   449  	}
   450  	w := tabwriter.NewWriter(f, 5, 0, 3, ' ', 0)
   451  	w.Write(data)
   452  }
   453  
   454  func writeFile(data []byte, path string) {
   455  	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
   456  	if err != nil {
   457  		fmt.Fprintf(os.Stderr, "Could not create file %s", path)
   458  		os.Exit(1)
   459  	}
   460  	f.Write(data)
   461  }
   462  
   463  func writeJSON(data []byte, path string) {
   464  	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
   465  	if err != nil {
   466  		fmt.Fprintf(os.Stderr, "Could not create file %s", path)
   467  		os.Exit(1)
   468  	}
   469  
   470  	db := &models.DebugInfo{}
   471  
   472  	// Unmarshal the binary so we can indent the JSON appropriately when we
   473  	// display it to end-users.
   474  	err = db.UnmarshalBinary(data)
   475  	if err != nil {
   476  		fmt.Fprintf(os.Stderr, "error unmarshaling binary: %s\n", err)
   477  		os.Exit(1)
   478  	}
   479  	result, err := json.MarshalIndent(db, "", "  ")
   480  	if err != nil {
   481  		fmt.Fprintf(os.Stderr, "error marshal-indenting data: %s\n", err)
   482  		os.Exit(1)
   483  	}
   484  	f.Write(result)
   485  
   486  }