github.com/vmware/govmomi@v0.51.0/cli/flags/debug.go (about)

     1  // © Broadcom. All Rights Reserved.
     2  // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package flags
     6  
     7  import (
     8  	"bufio"
     9  	"context"
    10  	"flag"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"reflect"
    17  	"strings"
    18  	"sync"
    19  	"text/tabwriter"
    20  	"time"
    21  
    22  	"github.com/dougm/pretty"
    23  
    24  	"github.com/vmware/govmomi/vim25/debug"
    25  	"github.com/vmware/govmomi/vim25/soap"
    26  	"github.com/vmware/govmomi/vim25/types"
    27  )
    28  
    29  type cmdFormat struct {
    30  	path string
    31  	err  error
    32  	args []string
    33  }
    34  
    35  func (c *cmdFormat) lookPath(file string, args ...string) {
    36  	c.args = args
    37  	c.path, c.err = exec.LookPath(file)
    38  }
    39  
    40  func (c *cmdFormat) cmd() (*exec.Cmd, error) {
    41  	if c.err != nil {
    42  		return nil, c.err
    43  	}
    44  	return exec.Command(c.path, c.args...), nil
    45  }
    46  
    47  type DebugFlag struct {
    48  	common
    49  
    50  	enable  bool
    51  	trace   bool
    52  	verbose bool
    53  	dump    bool
    54  	xml     cmdFormat
    55  	json    cmdFormat
    56  }
    57  
    58  var debugFlagKey = flagKey("debug")
    59  
    60  func NewDebugFlag(ctx context.Context) (*DebugFlag, context.Context) {
    61  	if v := ctx.Value(debugFlagKey); v != nil {
    62  		return v.(*DebugFlag), ctx
    63  	}
    64  
    65  	v := &DebugFlag{}
    66  	ctx = context.WithValue(ctx, debugFlagKey, v)
    67  	return v, ctx
    68  }
    69  
    70  func (flag *DebugFlag) Verbose() bool {
    71  	return flag.verbose
    72  }
    73  
    74  func (flag *DebugFlag) Register(ctx context.Context, f *flag.FlagSet) {
    75  	flag.RegisterOnce(func() {
    76  		env := "GOVC_DEBUG"
    77  		enable := false
    78  		switch env := strings.ToLower(os.Getenv(env)); env {
    79  		case "1", "true":
    80  			enable = true
    81  		}
    82  
    83  		usage := fmt.Sprintf("Store debug logs [%s]", env)
    84  		f.BoolVar(&flag.enable, "debug", enable, usage)
    85  		f.BoolVar(&flag.trace, "trace", false, "Write SOAP/REST traffic to stderr")
    86  		f.BoolVar(&flag.verbose, "verbose", false, "Write request/response data to stderr")
    87  	})
    88  }
    89  
    90  type cmdFormatCloser struct {
    91  	rc  io.Closer
    92  	in  io.Closer
    93  	cmd *exec.Cmd
    94  	wg  *sync.WaitGroup
    95  }
    96  
    97  func (c *cmdFormatCloser) Close() error {
    98  	_ = c.rc.Close()
    99  	_ = c.in.Close()
   100  	c.wg.Wait()
   101  	return c.cmd.Wait()
   102  }
   103  
   104  func (flag *DebugFlag) newFormatReader(rc io.ReadCloser, w io.Writer, ext string) (io.ReadCloser, error) {
   105  	var err error
   106  	var cmd *exec.Cmd
   107  
   108  	switch ext {
   109  	case "json":
   110  		cmd, err = flag.json.cmd()
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  	case "xml":
   115  		cmd, err = flag.xml.cmd()
   116  		if err != nil {
   117  			return nil, err
   118  		}
   119  	default:
   120  		return nil, fmt.Errorf("unsupported type %s", ext)
   121  	}
   122  
   123  	cmd.Stderr = os.Stderr
   124  
   125  	stdin, err := cmd.StdinPipe()
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	stdout, err := cmd.StdoutPipe()
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  
   135  	err = cmd.Start()
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	var wg sync.WaitGroup
   141  	wg.Add(1)
   142  	go func() {
   143  		_, _ = io.Copy(w, stdout)
   144  		wg.Done()
   145  	}()
   146  
   147  	return debug.ReadCloser{
   148  		Reader: io.TeeReader(rc, stdin),
   149  		Closer: &cmdFormatCloser{rc, stdin, cmd, &wg},
   150  	}, nil
   151  }
   152  
   153  func (flag *DebugFlag) debugTrace(rc io.ReadCloser, w io.Writer, ext string) io.ReadCloser {
   154  	fr, err := flag.newFormatReader(rc, w, ext)
   155  	if err != nil {
   156  		return debug.NewTeeReader(rc, w)
   157  	}
   158  	return fr
   159  }
   160  
   161  func (flag *DebugFlag) Process(ctx context.Context) error {
   162  	// Base path for storing debug logs.
   163  	r := os.Getenv("GOVC_DEBUG_PATH")
   164  
   165  	if flag.trace {
   166  		if flag.verbose {
   167  			flag.dump = true // output req/res as Go code
   168  			return nil
   169  		}
   170  		r = "-"
   171  		flag.enable = true
   172  		if os.Getenv("GOVC_DEBUG_FORMAT") != "false" {
   173  			debugXML := os.Getenv("GOVC_DEBUG_XML")
   174  			if debugXML == "" {
   175  				debugXML = "xmlstarlet"
   176  			}
   177  			flag.xml.lookPath(debugXML, "fo")
   178  
   179  			debugJSON := os.Getenv("GOVC_DEBUG_JSON")
   180  			if debugJSON == "" {
   181  				debugJSON = "jq"
   182  			}
   183  			flag.json.lookPath(debugJSON, ".")
   184  
   185  			soap.Trace = flag.debugTrace
   186  		}
   187  	}
   188  
   189  	if !flag.enable {
   190  		return nil
   191  	}
   192  
   193  	return flag.ProcessOnce(func() error {
   194  		switch r {
   195  		case "-":
   196  			debug.SetProvider(&debug.LogProvider{})
   197  			return nil
   198  		case "":
   199  			r = home
   200  		}
   201  		r = filepath.Join(r, "debug")
   202  
   203  		// Path for this particular run.
   204  		run := os.Getenv("GOVC_DEBUG_PATH_RUN")
   205  		if run == "" {
   206  			now := time.Now().Format("2006-01-02T15-04-05.999999999")
   207  			r = filepath.Join(r, now)
   208  		} else {
   209  			// reuse the same path
   210  			r = filepath.Join(r, run)
   211  			_ = os.RemoveAll(r)
   212  		}
   213  
   214  		err := os.MkdirAll(r, 0700)
   215  		if err != nil {
   216  			return err
   217  		}
   218  
   219  		p := debug.FileProvider{
   220  			Path: r,
   221  		}
   222  
   223  		debug.SetProvider(&p)
   224  		return nil
   225  	})
   226  }
   227  
   228  type dump struct {
   229  	roundTripper soap.RoundTripper
   230  }
   231  
   232  func (d *dump) RoundTrip(ctx context.Context, req, res soap.HasFault) error {
   233  	vreq := reflect.ValueOf(req).Elem().FieldByName("Req").Elem()
   234  
   235  	pretty.Fprintf(os.Stderr, "%# v\n", vreq.Interface())
   236  
   237  	err := d.roundTripper.RoundTrip(ctx, req, res)
   238  	if err != nil {
   239  		if fault := res.Fault(); fault != nil {
   240  			pretty.Fprintf(os.Stderr, "%# v\n", fault)
   241  		}
   242  		return err
   243  	}
   244  
   245  	vres := reflect.ValueOf(res).Elem().FieldByName("Res").Elem()
   246  	pretty.Fprintf(os.Stderr, "%# v\n", vres.Interface())
   247  
   248  	return nil
   249  }
   250  
   251  type verbose struct {
   252  	roundTripper soap.RoundTripper
   253  }
   254  
   255  func (*verbose) mor(ref types.ManagedObjectReference) string {
   256  	if strings.HasPrefix(ref.Value, "session") {
   257  		ref.Value = "session[...]"
   258  		return ref.String()
   259  	}
   260  	return ref.Value
   261  }
   262  
   263  func (*verbose) str(val reflect.Value) string {
   264  	if !val.IsValid() {
   265  		return ""
   266  	}
   267  
   268  	switch val.Kind() {
   269  	case reflect.Ptr, reflect.Interface:
   270  		if val.IsNil() {
   271  			return "nil"
   272  		}
   273  	}
   274  
   275  	p := ""
   276  
   277  	switch pval := val.Interface().(type) {
   278  	case fmt.Stringer:
   279  		p = pval.String()
   280  	case string:
   281  		if len(pval) > 45 {
   282  			pval = pval[:42] + "..."
   283  		}
   284  		p = fmt.Sprintf("%s", pval)
   285  	case []string:
   286  		p = fmt.Sprintf("%v", pval)
   287  	case []types.ManagedObjectReference:
   288  		refs := make([]string, len(pval))
   289  		for i := range pval {
   290  			refs[i] = pval[i].Value
   291  		}
   292  		p = fmt.Sprintf("%v", refs)
   293  	default:
   294  		return ""
   295  	}
   296  
   297  	return p
   298  }
   299  
   300  func (v *verbose) value(val types.AnyType) string {
   301  	rval := reflect.ValueOf(val)
   302  	if rval.Kind() == reflect.Ptr && !rval.IsNil() {
   303  		rval = rval.Elem()
   304  	}
   305  	if rval.Kind() == reflect.Struct {
   306  		if strings.HasPrefix(rval.Type().Name(), "ArrayOf") {
   307  			rval = rval.Field(0)
   308  			val = rval.Interface()
   309  		}
   310  	}
   311  	s := v.str(rval)
   312  	if s != "" {
   313  		return s
   314  	}
   315  	return v.prettyPrint(val)
   316  }
   317  
   318  func (v *verbose) propertyValue(obj types.ManagedObjectReference, name string, pval types.AnyType) string {
   319  	val := v.value(pval)
   320  	if obj.Type != "Task" && !strings.HasPrefix(obj.Value, "session") {
   321  		if len(val) > 512 {
   322  			val = fmt.Sprintf("`govc collect -dump %s %s`", obj, name)
   323  		}
   324  	}
   325  	return fmt.Sprintf("%s\t%s:\t%s", v.mor(obj), name, val)
   326  }
   327  
   328  func (v *verbose) missingSet(o types.ManagedObjectReference, m []types.MissingProperty) []string {
   329  	var s []string
   330  	for _, p := range m {
   331  		s = append(s, fmt.Sprintf("%s\t%s:\t%s", v.mor(o), p.Path, v.prettyPrint(p.Fault.Fault)))
   332  	}
   333  	return s
   334  }
   335  
   336  func (v *verbose) updateSet(u *types.UpdateSet) []string {
   337  	var s []string
   338  	if u == nil {
   339  		return s
   340  	}
   341  	for _, f := range u.FilterSet {
   342  		for _, o := range f.ObjectSet {
   343  			for _, c := range o.ChangeSet {
   344  				s = append(s, v.propertyValue(o.Obj, c.Name, c.Val))
   345  			}
   346  			s = append(s, v.missingSet(o.Obj, o.MissingSet)...)
   347  		}
   348  	}
   349  	return s
   350  }
   351  
   352  func (v *verbose) objectContent(content []types.ObjectContent) []string {
   353  	var s []string
   354  	for _, o := range content {
   355  		for _, p := range o.PropSet {
   356  			s = append(s, v.propertyValue(o.Obj, p.Name, p.Val))
   357  		}
   358  		s = append(s, v.missingSet(o.Obj, o.MissingSet)...)
   359  	}
   360  	return s
   361  }
   362  
   363  func (v *verbose) prettyPrint(val any) string {
   364  	p := pretty.Sprintf("%# v\n", val)
   365  	var res []string
   366  	scanner := bufio.NewScanner(strings.NewReader(p))
   367  	for scanner.Scan() {
   368  		line := scanner.Text()
   369  		if strings.Contains(line, "nil,") || strings.Contains(line, "(nil),") {
   370  			continue // nil pointer field
   371  		}
   372  		if strings.Contains(line, `"",`) {
   373  			continue // empty string field
   374  		}
   375  		if strings.Contains(line, `{},`) {
   376  			continue // empty embedded struct
   377  		}
   378  		if strings.Contains(line, "[context]") {
   379  			continue // noisy base64 encoded backtrace
   380  		}
   381  		res = append(res, line)
   382  	}
   383  	return strings.Join(res, "\n")
   384  }
   385  
   386  func (v *verbose) table(vals []string) {
   387  	tw := tabwriter.NewWriter(os.Stderr, 2, 0, 1, ' ', 0)
   388  	for _, val := range vals {
   389  		fmt.Fprintf(tw, "...%s\n", val)
   390  	}
   391  	tw.Flush()
   392  }
   393  
   394  func (v *verbose) RoundTrip(ctx context.Context, req, res soap.HasFault) error {
   395  	vreq := reflect.ValueOf(req).Elem().FieldByName("Req").Elem()
   396  	param := []string{""}
   397  	switch f := vreq.Field(0).Interface().(type) {
   398  	case types.ManagedObjectReference:
   399  		param[0] = v.mor(f)
   400  	default:
   401  		param[0] = fmt.Sprintf("%v", f)
   402  	}
   403  
   404  	for i := 1; i < vreq.NumField(); i++ {
   405  		val := vreq.Field(i)
   406  
   407  		if val.Kind() == reflect.Interface {
   408  			val = val.Elem()
   409  		}
   410  
   411  		p := v.str(val)
   412  		if p == "" {
   413  			switch val.Kind() {
   414  			case reflect.Ptr, reflect.Slice, reflect.Struct:
   415  				p = val.Type().String()
   416  			default:
   417  				p = fmt.Sprintf("%v", val.Interface())
   418  			}
   419  		}
   420  
   421  		param = append(param, p)
   422  	}
   423  
   424  	fmt.Fprintf(os.Stderr, "%s(%s)...\n", vreq.Type().Name(), strings.Join(param, ", "))
   425  
   426  	err := v.roundTripper.RoundTrip(ctx, req, res)
   427  	if err != nil {
   428  		if fault := res.Fault(); fault != nil {
   429  			fmt.Fprintln(os.Stderr, v.prettyPrint(fault))
   430  		} else {
   431  			fmt.Fprintf(os.Stderr, "...%s\n", err)
   432  		}
   433  		return err
   434  	}
   435  
   436  	vres := reflect.ValueOf(res).Elem().FieldByName("Res").Elem()
   437  	ret := vres.FieldByName("Returnval")
   438  	var s any = "void"
   439  
   440  	if ret.IsValid() {
   441  		switch x := ret.Interface().(type) {
   442  		case types.ManagedObjectReference:
   443  			s = v.mor(x)
   444  		case *types.UpdateSet:
   445  			s = v.updateSet(x)
   446  		case []types.ObjectContent:
   447  			s = v.objectContent(x)
   448  		case fmt.Stringer:
   449  			s = x.String()
   450  		default:
   451  			s = v.value(x)
   452  		}
   453  	}
   454  	if vals, ok := s.([]string); ok {
   455  		v.table(vals)
   456  	} else {
   457  		fmt.Fprintf(os.Stderr, "...%s\n", s)
   458  	}
   459  	fmt.Fprintln(os.Stderr)
   460  
   461  	return err
   462  }