github.com/vmware/govmomi@v0.51.0/cli/object/collect.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 object
     6  
     7  import (
     8  	"context"
     9  	"encoding/base64"
    10  	"encoding/json"
    11  	"flag"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"reflect"
    16  	"strings"
    17  	"text/tabwriter"
    18  	"time"
    19  
    20  	"github.com/vmware/govmomi/cli"
    21  	"github.com/vmware/govmomi/cli/flags"
    22  	"github.com/vmware/govmomi/property"
    23  	"github.com/vmware/govmomi/view"
    24  	"github.com/vmware/govmomi/vim25"
    25  	"github.com/vmware/govmomi/vim25/methods"
    26  	"github.com/vmware/govmomi/vim25/mo"
    27  	"github.com/vmware/govmomi/vim25/soap"
    28  	"github.com/vmware/govmomi/vim25/types"
    29  	"github.com/vmware/govmomi/vim25/xml"
    30  )
    31  
    32  type collect struct {
    33  	*flags.DatacenterFlag
    34  
    35  	object bool
    36  	single bool
    37  	simple bool
    38  	raw    string
    39  	delim  string
    40  	dump   bool
    41  	n      int
    42  	kind   kinds
    43  	wait   time.Duration
    44  
    45  	filter property.Match
    46  	obj    string
    47  }
    48  
    49  func init() {
    50  	cli.Register("collect", &collect{})
    51  	cli.Alias("collect", "object.collect")
    52  }
    53  
    54  func (cmd *collect) Register(ctx context.Context, f *flag.FlagSet) {
    55  	cmd.DatacenterFlag, ctx = flags.NewDatacenterFlag(ctx)
    56  	cmd.DatacenterFlag.Register(ctx, f)
    57  
    58  	f.BoolVar(&cmd.simple, "s", false, "Output property value only")
    59  	f.StringVar(&cmd.delim, "d", ",", "Delimiter for array values")
    60  	f.BoolVar(&cmd.object, "o", false, "Output the structure of a single Managed Object")
    61  	f.BoolVar(&cmd.dump, "O", false, "Output the CreateFilter request itself")
    62  	f.StringVar(&cmd.raw, "R", "", "Raw XML encoded CreateFilter request")
    63  	f.IntVar(&cmd.n, "n", 0, "Wait for N property updates")
    64  	f.Var(&cmd.kind, "type", "Resource type.  If specified, MOID is used for a container view root")
    65  	f.DurationVar(&cmd.wait, "wait", 0, "Max wait time for updates")
    66  }
    67  
    68  func (cmd *collect) Usage() string {
    69  	return "[MOID] [PROPERTY]..."
    70  }
    71  
    72  func (cmd *collect) Description() string {
    73  	atable := aliasHelp()
    74  
    75  	return fmt.Sprintf(`Collect managed object properties.
    76  
    77  MOID can be an inventory path or ManagedObjectReference.
    78  MOID defaults to '-', an alias for 'ServiceInstance:ServiceInstance' or the root folder if a '-type' flag is given.
    79  
    80  If a '-type' flag is given, properties are collected using a ContainerView object where MOID is the root of the view.
    81  
    82  By default only the current property value(s) are collected.  To wait for updates, use the '-n' flag or
    83  specify a property filter.  A property filter can be specified by prefixing the property name with a '-',
    84  followed by the value to match.
    85  
    86  The '-R' flag sets the Filter using the given XML encoded request, which can be captured by 'vcsim -trace' for example.
    87  It can be useful for replaying property filters created by other clients and converting filters to Go code via '-O -dump'.
    88  
    89  The '-type' flag value can be a managed entity type or one of the following aliases:
    90  
    91  %s
    92  Examples:
    93    govc collect - content
    94    govc collect -s HostSystem:ha-host hardware.systemInfo.uuid
    95    govc collect -s /ha-datacenter/vm/foo overallStatus
    96    govc collect -s /ha-datacenter/vm/foo -guest.guestOperationsReady true # property filter
    97    govc collect -type m / name runtime.powerState # collect properties for multiple objects
    98    govc collect -json -n=-1 EventManager:ha-eventmgr latestEvent | jq .
    99    govc collect -json -s $(govc collect -s - content.perfManager) description.counterType | jq .
   100    govc collect -R create-filter-request.xml # replay filter
   101    govc collect -R create-filter-request.xml -O # convert filter to Go code
   102    govc collect -s vm/my-vm summary.runtime.host | xargs govc ls -L # inventory path of VM's host
   103    govc collect -dump -o "network/VM Network" # output Managed Object structure as Go code
   104    govc collect -json -s $vm config | \ # use -json + jq to search array elements
   105      jq -r 'select(.hardware.device[].macAddress == "00:50:56:99:c4:27") | .name'`, atable)
   106  }
   107  
   108  var (
   109  	stringer = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
   110  	writer   = reflect.TypeOf((*flags.OutputWriter)(nil)).Elem()
   111  )
   112  
   113  type change struct {
   114  	cmd    *collect
   115  	Update types.ObjectUpdate
   116  }
   117  
   118  func (pc *change) MarshalJSON() ([]byte, error) {
   119  	if len(pc.cmd.kind) == 0 && !pc.cmd.simple {
   120  		return json.Marshal(pc.Update.ChangeSet)
   121  	}
   122  
   123  	return json.Marshal(pc.Dump())
   124  }
   125  
   126  func (pc *change) output(name string, rval reflect.Value, rtype reflect.Type) {
   127  	s := "..."
   128  
   129  	kind := rval.Kind()
   130  
   131  	if kind == reflect.Ptr || kind == reflect.Interface {
   132  		if rval.IsNil() {
   133  			s = ""
   134  		} else {
   135  			rval = rval.Elem()
   136  			kind = rval.Kind()
   137  		}
   138  	}
   139  
   140  	switch kind {
   141  	case reflect.Ptr, reflect.Interface:
   142  	case reflect.Slice:
   143  		if rval.Len() == 0 {
   144  			s = ""
   145  			break
   146  		}
   147  
   148  		etype := rtype.Elem()
   149  
   150  		if etype.Kind() == reflect.Uint8 {
   151  			if v, ok := rval.Interface().(types.ByteSlice); ok {
   152  				s = base64.StdEncoding.EncodeToString(v) // ArrayOfByte
   153  			} else {
   154  				s = fmt.Sprintf("%x", rval.Interface().([]byte)) // ArrayOfBase64Binary
   155  			}
   156  			break
   157  		}
   158  
   159  		if etype.Kind() != reflect.Interface && etype.Kind() != reflect.Struct || etype.Implements(stringer) {
   160  			var val []string
   161  
   162  			for i := 0; i < rval.Len(); i++ {
   163  				v := rval.Index(i).Interface()
   164  
   165  				if fstr, ok := v.(fmt.Stringer); ok {
   166  					s = fstr.String()
   167  				} else {
   168  					s = fmt.Sprintf("%v", v)
   169  				}
   170  
   171  				val = append(val, s)
   172  			}
   173  
   174  			s = strings.Join(val, pc.cmd.delim)
   175  		}
   176  	case reflect.Struct:
   177  		if rtype.Implements(stringer) {
   178  			s = rval.Interface().(fmt.Stringer).String()
   179  		}
   180  	default:
   181  		s = fmt.Sprintf("%v", rval.Interface())
   182  	}
   183  
   184  	if pc.cmd.simple {
   185  		fmt.Fprintln(pc.cmd.Out, s)
   186  		return
   187  	}
   188  
   189  	if pc.cmd.obj != "" {
   190  		fmt.Fprintf(pc.cmd.Out, "%s\t", pc.cmd.obj)
   191  	}
   192  
   193  	fmt.Fprintf(pc.cmd.Out, "%s\t%s\t%s\n", name, rtype, s)
   194  }
   195  
   196  func (pc *change) writeStruct(name string, rval reflect.Value, rtype reflect.Type) {
   197  	for i := 0; i < rval.NumField(); i++ {
   198  		fval := rval.Field(i)
   199  		field := rtype.Field(i)
   200  
   201  		if field.Anonymous {
   202  			pc.writeStruct(name, fval, fval.Type())
   203  			continue
   204  		}
   205  
   206  		fname := fmt.Sprintf("%s.%s%s", name, strings.ToLower(field.Name[:1]), field.Name[1:])
   207  		pc.output(fname, fval, field.Type)
   208  	}
   209  }
   210  
   211  func (pc *change) Write(w io.Writer) error {
   212  	tw := tabwriter.NewWriter(pc.cmd.Out, 4, 0, 2, ' ', 0)
   213  	pc.cmd.Out = tw
   214  
   215  	for _, c := range pc.Update.ChangeSet {
   216  		if c.Val == nil {
   217  			// type is unknown in this case, as xsi:type was not provided - just skip for now
   218  			continue
   219  		}
   220  
   221  		rval := reflect.ValueOf(c.Val)
   222  		rtype := rval.Type()
   223  
   224  		if strings.HasPrefix(rtype.Name(), "ArrayOf") {
   225  			rval = rval.Field(0)
   226  			rtype = rval.Type()
   227  		}
   228  
   229  		if len(pc.cmd.kind) != 0 {
   230  			pc.cmd.obj = pc.Update.Obj.String()
   231  		}
   232  
   233  		if pc.cmd.single && rtype.Kind() == reflect.Struct && !rtype.Implements(stringer) {
   234  			pc.writeStruct(c.Name, rval, rtype)
   235  			continue
   236  		}
   237  
   238  		pc.output(c.Name, rval, rtype)
   239  	}
   240  
   241  	return tw.Flush()
   242  }
   243  
   244  func (pc *change) Dump() any {
   245  	if pc.cmd.simple && len(pc.Update.ChangeSet) == 1 {
   246  		val := pc.Update.ChangeSet[0].Val
   247  		if val != nil {
   248  			rval := reflect.ValueOf(val)
   249  			rtype := rval.Type()
   250  
   251  			if strings.HasPrefix(rtype.Name(), "ArrayOf") {
   252  				return rval.Field(0).Interface()
   253  			}
   254  		}
   255  
   256  		return val
   257  	}
   258  
   259  	return pc.Update
   260  }
   261  
   262  func (cmd *collect) match(update types.ObjectUpdate) bool {
   263  	if len(cmd.filter) == 0 {
   264  		return false
   265  	}
   266  
   267  	for _, c := range update.ChangeSet {
   268  		if cmd.filter.Property(types.DynamicProperty{Name: c.Name, Val: c.Val}) {
   269  			return true
   270  		}
   271  	}
   272  
   273  	return false
   274  }
   275  
   276  func (cmd *collect) toFilter(f *flag.FlagSet, props []string) ([]string, error) {
   277  	// TODO: Only supporting 1 filter prop for now.  More than one would require some
   278  	// accounting / accumulating of multiple updates.  And need to consider objects
   279  	// then enter/leave a container view.
   280  	if len(props) != 2 || !strings.HasPrefix(props[0], "-") {
   281  		return props, nil
   282  	}
   283  
   284  	cmd.filter = property.Match{props[0][1:]: props[1]}
   285  
   286  	return cmd.filter.Keys(), nil
   287  }
   288  
   289  type dumpFilter struct {
   290  	types.CreateFilter
   291  }
   292  
   293  func (f *dumpFilter) Dump() any {
   294  	return f.CreateFilter
   295  }
   296  
   297  // Write satisfies the flags.OutputWriter interface, but is not used with dumpFilter.
   298  func (f *dumpFilter) Write(w io.Writer) error {
   299  	return nil
   300  }
   301  
   302  type dumpEntity struct {
   303  	entity any
   304  }
   305  
   306  func (e *dumpEntity) Dump() any {
   307  	return e.entity
   308  }
   309  
   310  func (e *dumpEntity) MarshalJSON() ([]byte, error) {
   311  	return json.Marshal(e.entity)
   312  }
   313  
   314  // Write satisfies the flags.OutputWriter interface, but is not used with dumpEntity.
   315  func (e *dumpEntity) Write(w io.Writer) error {
   316  	return nil
   317  }
   318  
   319  func (cmd *collect) decodeFilter(filter *property.WaitFilter) error {
   320  	var r io.Reader
   321  
   322  	if cmd.raw == "-" {
   323  		r = os.Stdin
   324  	} else {
   325  		f, err := os.Open(cmd.raw)
   326  		if err != nil {
   327  			return err
   328  		}
   329  		defer f.Close()
   330  		r = f
   331  	}
   332  
   333  	env := soap.Envelope{
   334  		Body: &methods.CreateFilterBody{Req: &filter.CreateFilter},
   335  	}
   336  
   337  	dec := xml.NewDecoder(r)
   338  	dec.TypeFunc = types.TypeFunc()
   339  	return dec.Decode(&env)
   340  }
   341  
   342  func (cmd *collect) Run(ctx context.Context, f *flag.FlagSet) error {
   343  	client, err := cmd.Client()
   344  	if err != nil {
   345  		return err
   346  	}
   347  
   348  	p := property.DefaultCollector(client)
   349  	filter := new(property.WaitFilter)
   350  
   351  	if cmd.raw == "" {
   352  		ref := vim25.ServiceInstance
   353  		arg := f.Arg(0)
   354  
   355  		if len(cmd.kind) != 0 {
   356  			ref = client.ServiceContent.RootFolder
   357  		}
   358  
   359  		switch arg {
   360  		case "", "-":
   361  		default:
   362  			ref, err = cmd.ManagedObject(ctx, arg)
   363  			if err != nil {
   364  				if !ref.FromString(arg) {
   365  					return err
   366  				}
   367  			}
   368  		}
   369  
   370  		var props []string
   371  		if f.NArg() > 1 {
   372  			props = f.Args()[1:]
   373  			cmd.single = len(props) == 1
   374  		}
   375  
   376  		props, err = cmd.toFilter(f, props)
   377  		if err != nil {
   378  			return err
   379  		}
   380  
   381  		if len(cmd.kind) == 0 {
   382  			filter.Add(ref, ref.Type, props)
   383  		} else {
   384  			m := view.NewManager(client)
   385  
   386  			v, cerr := m.CreateContainerView(ctx, ref, cmd.kind, true)
   387  			if cerr != nil {
   388  				return cerr
   389  			}
   390  
   391  			defer func() {
   392  				_ = v.Destroy(ctx)
   393  			}()
   394  
   395  			for _, kind := range cmd.kind {
   396  				filter.Add(v.Reference(), kind, props, v.TraversalSpec())
   397  			}
   398  		}
   399  	} else {
   400  		if err = cmd.decodeFilter(filter); err != nil {
   401  			return err
   402  		}
   403  	}
   404  
   405  	if cmd.dump {
   406  		if !cmd.All() {
   407  			cmd.Dump = true
   408  		}
   409  		return cmd.WriteResult(&dumpFilter{filter.CreateFilter})
   410  	}
   411  
   412  	if cmd.object {
   413  		req := types.RetrieveProperties{
   414  			SpecSet: []types.PropertyFilterSpec{filter.Spec},
   415  		}
   416  		res, err := p.RetrieveProperties(ctx, req)
   417  		if err != nil {
   418  			return err
   419  		}
   420  		content := res.Returnval
   421  		if len(content) != 1 {
   422  			return fmt.Errorf("%d objects match", len(content))
   423  		}
   424  		obj, err := mo.ObjectContentToType(content[0])
   425  		if err != nil {
   426  			return err
   427  		}
   428  		if !cmd.All() {
   429  			rval := reflect.ValueOf(obj)
   430  			if rval.Type().Implements(writer) {
   431  				return cmd.WriteResult(rval.Interface().(flags.OutputWriter))
   432  			}
   433  			cmd.Dump = true // fallback to -dump output
   434  		}
   435  		return cmd.WriteResult(&dumpEntity{obj})
   436  	}
   437  
   438  	hasFilter := len(cmd.filter) != 0
   439  
   440  	if cmd.wait != 0 {
   441  		filter.Options = &types.WaitOptions{
   442  			MaxWaitSeconds: types.NewInt32(int32(cmd.wait.Seconds())),
   443  		}
   444  	}
   445  
   446  	return cmd.WithCancel(ctx, func(wctx context.Context) error {
   447  		matches := 0
   448  		return property.WaitForUpdates(wctx, p, filter, func(updates []types.ObjectUpdate) bool {
   449  			for _, update := range updates {
   450  				c := &change{cmd, update}
   451  
   452  				if hasFilter {
   453  					if cmd.match(update) {
   454  						matches++
   455  					} else {
   456  						continue
   457  					}
   458  				}
   459  
   460  				_ = cmd.WriteResult(c)
   461  			}
   462  
   463  			if filter.Truncated {
   464  				return false // vCenter truncates updates if > 100
   465  			}
   466  
   467  			if hasFilter {
   468  				if matches > 0 {
   469  					matches = 0 // reset
   470  					return true
   471  				}
   472  				return false
   473  			}
   474  
   475  			cmd.n--
   476  
   477  			return cmd.n == -1 && cmd.wait == 0
   478  		})
   479  	})
   480  }