Repository: vmware/govmomi
Pull request #1636 (2019-09-26 23:32:46)

The tree command
This patch adds a "tree" command to govc that behaves like the Linux tree command. "govc tree PATH" prints a tree-like structure of the inventory.

This first-pass is incredibly slow and needs to be optimized. Here's some example output while listing the networks:

```shell
$ ./govc tree /SDDC-Datacenter/network
/SDDC-Datacenter/network
├── VMC Networks
│   ├── SI_LogicalSwitch_40584124-5a85-479b-a5ef-07806a2d1b73
│   ├── SI_LogicalSwitch_4e59f062-7af8-4cbc-ac1a-3a9a40511d16
│   ├── SI_LogicalSwitch_beb34a4f-50c5-4f39-b953-a9861a919747
│   ├── SI_LogicalSwitch_de819edf-141f-498c-bf9a-b706d63352b7
│   ├── VMOTION
│   ├── VSAN
│   ├── cgw-dhcp-ls
│   ├── cross-vpc-ls
│   ├── direct-connect-ls
│   ├── mgmt-app-network
│   ├── mgmt-ls
│   ├── mgw-dhcp-ls
│   ├── nsx-dlink0-vls
│   ├── o-vmk0-ls
│   ├── public-ls
│   ├── trunk-ls
│   └── vmk4-ls
├── sddc-cgw-network-1
├── sddc-cgw-network-2
├── sddc-cgw-network-3
├── sddc-cgw-network-4
├── sddc-cgw-network-5
├── sddc-cgw-network-6
├── sddc-cgw-network-lvs-1
└── sddc-cgw-network-sk8-1
```

Many thanks to @dougm!

dougm: Thanks @akutz , just trying this out, looks awesome!  I will follow up with a review and may experiment with using ContainerView instead of Finder, as I suspect the latter is to blame for when it seems slow.
dougm: @akutz , sorry it took me so long to get to this.. I've made the changes I suggested in this PR.  Switching from Finder to ContainerView reduces the number of round trips quite a bit.  I also submitted a PR to the tree package so we can customize the colors (and added a `-C` flag to enable).  If you have a chance to take a look:

https://github.com/dougm/govmomi/commit/6f6d26393f5b92e23ffedb84c86cf998c9d0760d

I'd like to squash the result into a single commit and keep you as the author.  I can do that as a separate PR or you can push to yours if you're up for it.  Open to any changes/suggestions too of course.  Thanks!
akutz: Hi @dougm,

That all makes sense to me; thank you for reaching out and checking!

Patch:
diff --git a/go.mod b/go.mod
index d1badde5..f49b3c1f 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ replace github.com/davecgh/go-xdr => github.com/rasky/go-xdr v0.0.0-201702171721
 replace github.com/kr/pretty v0.1.0 => github.com/dougm/pretty v0.0.0-20171025230240-2ee9d7453c02
 
 require (
+	github.com/a8m/tree v0.0.0-20210115125333-10a5fd5b637d
 	github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892
 	github.com/google/uuid v0.0.0-20170306145142-6a5e28554805
 	github.com/kr/pretty v0.1.0
diff --git a/go.sum b/go.sum
index 72b4a8ed..e0a5f5b0 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,7 @@
+github.com/a8m/tree v0.0.0-20181222104329-6a0b80129de4 h1:mK1/QgFPU4osbhjJ26B1w738kjQHaGJcon8uCLMS8fk=
+github.com/a8m/tree v0.0.0-20181222104329-6a0b80129de4/go.mod h1:FSdwKX97koS5efgm8WevNf7XS3PqtyFkKDDXrz778cg=
+github.com/a8m/tree v0.0.0-20210115125333-10a5fd5b637d h1:4E8RufAN3UQ/weB6AnQ4y5miZCO0Yco8ZdGId41WuQs=
+github.com/a8m/tree v0.0.0-20210115125333-10a5fd5b637d/go.mod h1:FSdwKX97koS5efgm8WevNf7XS3PqtyFkKDDXrz778cg=
 github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892 h1:qg9VbHo1TlL0KDM0vYvBG9EY0X0Yku5WYIPoFWt8f6o=
 github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892/go.mod h1:CTDl0pzVzE5DEzZhPfvhY/9sPFMQIxaJ9VAMs9AagrE=
 github.com/dougm/pretty v0.0.0-20171025230240-2ee9d7453c02 h1:tR3jsKPiO/mb6ntzk/dJlHZtm37CPfVp1C9KIo534+4=
diff --git a/govc/object/tree.go b/govc/object/tree.go
new file mode 100644
index 00000000..ec2652c4
--- /dev/null
+++ b/govc/object/tree.go
@@ -0,0 +1,302 @@
+/*
+Copyright (c) 2014-2015 VMware, Inc. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package object
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"net/url"
+	"os"
+	gopath "path"
+	"time"
+
+	gotree "github.com/a8m/tree"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/view"
+	"github.com/vmware/govmomi/vim25"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type tree struct {
+	*flags.DatacenterFlag
+
+	long  bool
+	kind  bool
+	color bool
+	level int
+}
+
+func init() {
+	cli.Register("tree", &tree{})
+}
+
+func (cmd *tree) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.DatacenterFlag, ctx = flags.NewDatacenterFlag(ctx)
+	cmd.DatacenterFlag.Register(ctx, f)
+
+	f.BoolVar(&cmd.color, "C", false, "Colorize output")
+	f.BoolVar(&cmd.long, "l", false, "Follow runtime references (e.g. HostSystem VMs)")
+	f.BoolVar(&cmd.kind, "p", false, "Print the object type")
+	f.IntVar(&cmd.level, "L", 0, "Max display depth of the inventory tree")
+}
+
+func (cmd *tree) Description() string {
+	return `List contents of the inventory in a tree-like format.
+
+Examples:
+  govc tree -C /
+  govc tree /datacenter/vm`
+}
+
+func (cmd *tree) Usage() string {
+	return "[PATH]"
+}
+
+func (cmd *tree) Run(ctx context.Context, f *flag.FlagSet) error {
+	c, err := cmd.Client()
+	if err != nil {
+		return err
+	}
+
+	path := f.Arg(0)
+	if path == "" {
+		path = "/"
+	}
+
+	vfs := &virtualFileSystem{
+		ctx:   ctx,
+		cmd:   cmd,
+		c:     c,
+		m:     view.NewManager(c),
+		names: make(map[types.ManagedObjectReference]string),
+		dvs:   make(map[types.ManagedObjectReference][]types.ManagedObjectReference),
+		path:  path,
+	}
+
+	treeOpts := &gotree.Options{
+		Fs:        vfs,
+		OutFile:   cmd.Out,
+		Colorize:  cmd.color,
+		Color:     color,
+		DeepLevel: cmd.level,
+	}
+
+	inf := gotree.New(path)
+	inf.Visit(treeOpts)
+	inf.Print(treeOpts)
+
+	return nil
+}
+
+type virtualFileSystem struct {
+	ctx   context.Context
+	cmd   *tree
+	c     *vim25.Client
+	m     *view.Manager
+	names map[types.ManagedObjectReference]string
+	dvs   map[types.ManagedObjectReference][]types.ManagedObjectReference
+	root  types.ManagedObjectReference
+	path  string
+}
+
+func style(kind string) string {
+	switch kind {
+	case "VirtualMachine":
+		return "1;32"
+	case "HostSystem":
+		return "1;33"
+	case "ResourcePool":
+		return "1;30"
+	case "Network", "OpaqueNetwork", "DistributedVirtualPortgroup":
+		return "1;35"
+	case "Datastore":
+		return "1;36"
+	case "Datacenter":
+		return "1;37"
+	default:
+		return ""
+	}
+}
+
+func color(node *gotree.Node, s string) string {
+	ref := pathReference(node.Path())
+
+	switch ref.Type {
+	case "ResourcePool":
+		return s
+	}
+
+	c := style(ref.Type)
+	if c == "" {
+		return gotree.ANSIColor(node, s)
+	}
+
+	return gotree.ANSIColorFormat(c, s)
+}
+
+func (vfs *virtualFileSystem) Stat(path string) (os.FileInfo, error) {
+	var ref types.ManagedObjectReference
+
+	if len(vfs.names) == 0 {
+		// This is the first Stat() call, where path is the initial user input
+		if path == "/" {
+			ref = vfs.c.ServiceContent.RootFolder
+		} else {
+			var err error
+			ref, err = vfs.cmd.ManagedObject(vfs.ctx, path)
+			if err != nil {
+				return nil, err
+			}
+		}
+		vfs.names[ref] = path
+		vfs.root = ref
+	} else {
+		// The Node.Path in subsequent calls to Stat() will have a MOR base
+		ref = pathReference(path)
+	}
+
+	name := vfs.names[ref]
+
+	var mode os.FileMode
+	switch ref.Type {
+	case "ComputeResource",
+		"ClusterComputeResource",
+		"Datacenter",
+		"Folder",
+		"ResourcePool",
+		"VirtualApp",
+		"StoragePod",
+		"DistributedVirtualSwitch",
+		"VmwareDistributedVirtualSwitch":
+		mode = os.ModeDir
+	case "HostSystem":
+		if vfs.cmd.long {
+			mode = os.ModeDir
+		}
+	}
+
+	if vfs.cmd.kind {
+		name = fmt.Sprintf("[%s] %s", ref.Type, name)
+	}
+
+	return fileInfo{name: name, mode: mode}, nil
+}
+
+// pathReference converts the base of the given Node.Path to a MOR
+func pathReference(s string) types.ManagedObjectReference {
+	var ref types.ManagedObjectReference
+	r, _ := url.PathUnescape(gopath.Base(s))
+	ref.FromString(r)
+	return ref
+}
+
+func (vfs *virtualFileSystem) ReadDir(path string) ([]string, error) {
+	var ref types.ManagedObjectReference
+
+	if path == vfs.path {
+		// This path is the initial user input (e.g. "/" or "/dc1")
+		ref = vfs.root
+	} else {
+		// This path will have had 1 or more MORs appended to it, as returned by this func
+		ref = pathReference(path)
+	}
+
+	var childPaths []string
+
+	switch ref.Type {
+	// In the vCenter inventory switches and portgroups are siblings, hack to display them as parent child in the tree
+	case "DistributedVirtualSwitch", "VmwareDistributedVirtualSwitch":
+		pgs := vfs.dvs[ref]
+		for _, pg := range pgs {
+			childPaths = append(childPaths, url.PathEscape(pg.String()))
+		}
+		return childPaths, nil
+	}
+
+	v, err := vfs.m.CreateContainerView(vfs.ctx, ref, nil, false)
+	if err != nil {
+		return nil, err
+	}
+	defer v.Destroy(vfs.ctx)
+
+	var kind []string
+	if !vfs.cmd.long {
+		switch ref.Type {
+		case "HostSystem":
+			return nil, nil
+		case "ResourcePool", "VirtualApp":
+			kind = []string{"ResourcePool", "VirtualApp"}
+		}
+	}
+
+	var children []types.ObjectContent
+
+	pspec := []types.PropertySpec{
+		{Type: "DistributedVirtualSwitch", PathSet: []string{"portgroup"}},
+		{Type: "VmwareDistributedVirtualSwitch", PathSet: []string{"portgroup"}},
+	}
+
+	err = v.Retrieve(vfs.ctx, kind, []string{"name"}, &children, pspec...)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, content := range children {
+		ref = content.Obj
+		for _, p := range content.PropSet {
+			switch p.Name {
+			case "name":
+				vfs.names[ref] = p.Val.(string)
+			case "portgroup":
+				vfs.dvs[ref] = p.Val.(types.ArrayOfManagedObjectReference).ManagedObjectReference
+			}
+		}
+		if ref.Type == "DistributedVirtualPortgroup" {
+			continue // Returned on ReadDir() of the DVS above
+		}
+		childPaths = append(childPaths, url.PathEscape(ref.String()))
+	}
+
+	return childPaths, nil
+}
+
+type fileInfo struct {
+	name string
+	mode os.FileMode
+}
+
+func (f fileInfo) Name() string {
+	return f.name
+}
+func (f fileInfo) Size() int64 {
+	return 0
+}
+func (f fileInfo) Mode() os.FileMode {
+	return f.mode
+}
+func (f fileInfo) ModTime() time.Time {
+	return time.Now()
+}
+func (f fileInfo) IsDir() bool {
+	return f.mode&os.ModeDir == os.ModeDir
+}
+func (f fileInfo) Sys() interface{} {
+	return nil
+}
diff --git a/vendor/github.com/a8m/tree/.gitignore b/vendor/github.com/a8m/tree/.gitignore
new file mode 100644
index 00000000..793bf2eb
--- /dev/null
+++ b/vendor/github.com/a8m/tree/.gitignore
@@ -0,0 +1,2 @@
+draft
+coverage
diff --git a/vendor/github.com/a8m/tree/.travis.yml b/vendor/github.com/a8m/tree/.travis.yml
new file mode 100644
index 00000000..dbac1c6a
--- /dev/null
+++ b/vendor/github.com/a8m/tree/.travis.yml
@@ -0,0 +1,19 @@
+arch:
+  - amd64
+  - ppc64le
+
+language: go
+sudo: false
+go:
+  - 1.13.x
+  - 1.14.x
+  - 1.15.x
+  - tip
+matrix:
+  allow_failures:
+  - go: tip
+install:
+  - go get -t -v ./...
+script:
+  - go test -v ./...
+  - ./compileall.sh
diff --git a/vendor/github.com/a8m/tree/LICENSE b/vendor/github.com/a8m/tree/LICENSE
new file mode 100644
index 00000000..fc36360a
--- /dev/null
+++ b/vendor/github.com/a8m/tree/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2015-2017 Ariel Mashraki
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/a8m/tree/README.md b/vendor/github.com/a8m/tree/README.md
new file mode 100644
index 00000000..c79cc8eb
--- /dev/null
+++ b/vendor/github.com/a8m/tree/README.md
@@ -0,0 +1,43 @@
+tree [![Build status][travis-image]][travis-url] [![License][license-image]][license-url]
+---
+> An implementation of the [`tree`](http://mama.indstate.edu/users/ice/tree/) command written in Go, that can be used  programmatically.
+
+<img src="https://raw.githubusercontent.com/a8m/tree/assets/assets/tree.png" height="300" alt="tree command">
+
+#### Installation:
+```sh
+$ go get github.com/a8m/tree/cmd/tree
+```
+
+#### How to use `tree` programmatically ?
+You can take a look on [`cmd/tree`](https://github.com/a8m/tree/blob/master/cmd/tree/tree.go), and [s3tree](http://github.com/a8m/s3tree) or see the example below.
+```go
+import (
+    "github.com/a8m/tree"
+)
+
+func main() {
+    opts := &tree.Options{
+        // Fs, and OutFile are required fields.
+        // fs should implement the tree file-system interface(see: tree.Fs),
+        // and OutFile should be type io.Writer
+        Fs: fs,
+        OutFile: os.Stdout,
+        // ...
+    }
+    inf.New("root-dir")
+    // Visit all nodes recursively
+    inf.Visit(opts)
+    // Print nodes 
+    inf.Print(opts)
+}
+```
+
+### License
+MIT
+
+
+[travis-image]: https://img.shields.io/travis/a8m/tree.svg?style=flat-square
+[travis-url]: https://travis-ci.org/a8m/tree
+[license-image]: http://img.shields.io/npm/l/deep-keys.svg?style=flat-square
+[license-url]: LICENSE
diff --git a/vendor/github.com/a8m/tree/color.go b/vendor/github.com/a8m/tree/color.go
new file mode 100644
index 00000000..2dffcceb
--- /dev/null
+++ b/vendor/github.com/a8m/tree/color.go
@@ -0,0 +1,79 @@
+package tree
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+const Escape = "\x1b"
+const (
+	Reset int = 0
+	// Not used, remove.
+	Bold  int = 1
+	Black int = iota + 28
+	Red
+	Green
+	Yellow
+	Blue
+	Magenta
+	Cyan
+	White
+)
+
+// ANSIColorFormat
+func ANSIColorFormat(style string, s string) string {
+	return fmt.Sprintf("%s[%sm%s%s[%dm", Escape, style, s, Escape, Reset)
+}
+
+// ANSIColor
+func ANSIColor(node *Node, s string) string {
+	var style string
+	var mode = node.Mode()
+	var ext = filepath.Ext(node.Name())
+	switch {
+	case contains([]string{".bat", ".btm", ".cmd", ".com", ".dll", ".exe"}, ext):
+		style = "1;32"
+	case contains([]string{".arj", ".bz2", ".deb", ".gz", ".lzh", ".rpm",
+		".tar", ".taz", ".tb2", ".tbz2", ".tbz", ".tgz", ".tz", ".tz2", ".z",
+		".zip", ".zoo"}, ext):
+		style = "1;31"
+	case contains([]string{".asf", ".avi", ".bmp", ".flac", ".gif", ".jpg",
+		"jpeg", ".m2a", ".m2v", ".mov", ".mp3", ".mpeg", ".mpg", ".ogg", ".ppm",
+		".rm", ".tga", ".tif", ".wav", ".wmv",
+		".xbm", ".xpm"}, ext):
+		style = "1;35"
+	case node.IsDir() || mode&os.ModeDir != 0:
+		style = "1;34"
+	case mode&os.ModeNamedPipe != 0:
+		style = "40;33"
+	case mode&os.ModeSocket != 0:
+		style = "40;1;35"
+	case mode&os.ModeDevice != 0 || mode&os.ModeCharDevice != 0:
+		style = "40;1;33"
+	case mode&os.ModeSymlink != 0:
+		if _, err := filepath.EvalSymlinks(node.path); err != nil {
+			style = "40;1;31"
+		} else {
+			style = "1;36"
+		}
+	case mode&modeExecute != 0:
+		style = "1;32"
+	default:
+		return s
+	}
+	return ANSIColorFormat(style, s)
+}
+
+// case-insensitive contains helper
+func contains(slice []string, str string) bool {
+	for _, val := range slice {
+		if val == strings.ToLower(str) {
+			return true
+		}
+	}
+	return false
+}
+
+// TODO: HTMLColor
diff --git a/vendor/github.com/a8m/tree/compileall.sh b/vendor/github.com/a8m/tree/compileall.sh
new file mode 100644
index 00000000..1b4a1011
--- /dev/null
+++ b/vendor/github.com/a8m/tree/compileall.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+go tool dist list >/dev/null || {
+    echo 1>&2 "go tool dist list not supported - can't check compile"
+    exit 0
+}
+
+failures=0
+while read -r line; do
+    parts=(${line//\// })
+    export GOOS=${parts[0]}
+    export GOARCH=${parts[1]}
+    if go tool compile -V >/dev/null 2>&1 ; then
+        echo Try GOOS=${GOOS} GOARCH=${GOARCH}
+        if ! go install; then
+            echo "*** Failed compiling GOOS=${GOOS} GOARCH=${GOARCH}"
+            failures=$((failures+1))
+        fi
+    else
+        echo Skipping GOOS=${GOOS} GOARCH=${GOARCH} as not supported
+    fi
+done < <(go tool dist list)
+
+if [ $failures -ne 0 ]; then
+    echo "*** $failures compile failures"
+    exit 1
+fi
diff --git a/vendor/github.com/a8m/tree/csort_bsd.go b/vendor/github.com/a8m/tree/csort_bsd.go
new file mode 100644
index 00000000..f5895a62
--- /dev/null
+++ b/vendor/github.com/a8m/tree/csort_bsd.go
@@ -0,0 +1,18 @@
+//+build darwin freebsd netbsd
+
+package tree
+
+import (
+	"os"
+	"syscall"
+)
+
+func CTimeSort(f1, f2 os.FileInfo) bool {
+	s1, ok1 := f1.Sys().(*syscall.Stat_t)
+	s2, ok2 := f2.Sys().(*syscall.Stat_t)
+	// If this type of node isn't an os node then revert to ModSort
+	if !ok1 || !ok2 {
+		return ModSort(f1, f2)
+	}
+	return s1.Ctimespec.Sec < s2.Ctimespec.Sec
+}
diff --git a/vendor/github.com/a8m/tree/csort_generic.go b/vendor/github.com/a8m/tree/csort_generic.go
new file mode 100644
index 00000000..38be9aad
--- /dev/null
+++ b/vendor/github.com/a8m/tree/csort_generic.go
@@ -0,0 +1,6 @@
+//+build !linux,!openbsd,!dragonfly,!android,!solaris,!darwin,!freebsd,!netbsd
+
+package tree
+
+// CtimeSort for unsupported OS - just compare ModTime
+var CTimeSort = ModSort
diff --git a/vendor/github.com/a8m/tree/csort_unix.go b/vendor/github.com/a8m/tree/csort_unix.go
new file mode 100644
index 00000000..6acd0922
--- /dev/null
+++ b/vendor/github.com/a8m/tree/csort_unix.go
@@ -0,0 +1,18 @@
+//+build linux openbsd dragonfly android solaris
+
+package tree
+
+import (
+	"os"
+	"syscall"
+)
+
+func CTimeSort(f1, f2 os.FileInfo) bool {
+	s1, ok1 := f1.Sys().(*syscall.Stat_t)
+	s2, ok2 := f2.Sys().(*syscall.Stat_t)
+	// If this type of node isn't an os node then revert to ModSort
+	if !ok1 || !ok2 {
+		return ModSort(f1, f2)
+	}
+	return s1.Ctim.Sec < s2.Ctim.Sec
+}
diff --git a/vendor/github.com/a8m/tree/modes_bsd.go b/vendor/github.com/a8m/tree/modes_bsd.go
new file mode 100644
index 00000000..c9720654
--- /dev/null
+++ b/vendor/github.com/a8m/tree/modes_bsd.go
@@ -0,0 +1,7 @@
+//+build dragonfly freebsd openbsd solaris windows
+
+package tree
+
+import "syscall"
+
+const modeExecute = syscall.S_IXUSR
diff --git a/vendor/github.com/a8m/tree/modes_unix.go b/vendor/github.com/a8m/tree/modes_unix.go
new file mode 100644
index 00000000..1394bf6b
--- /dev/null
+++ b/vendor/github.com/a8m/tree/modes_unix.go
@@ -0,0 +1,7 @@
+//+build android darwin linux nacl netbsd
+
+package tree
+
+import "syscall"
+
+const modeExecute = syscall.S_IXUSR | syscall.S_IXGRP | syscall.S_IXOTH
diff --git a/vendor/github.com/a8m/tree/modes_unsupported.go b/vendor/github.com/a8m/tree/modes_unsupported.go
new file mode 100644
index 00000000..2fe96cb4
--- /dev/null
+++ b/vendor/github.com/a8m/tree/modes_unsupported.go
@@ -0,0 +1,5 @@
+//+build !dragonfly,!freebsd,!openbsd,!solaris,!windows,!android,!darwin,!linux,!nacl,!netbsd
+
+package tree
+
+const modeExecute = 0
diff --git a/vendor/github.com/a8m/tree/node.go b/vendor/github.com/a8m/tree/node.go
new file mode 100644
index 00000000..4a2ef94b
--- /dev/null
+++ b/vendor/github.com/a8m/tree/node.go
@@ -0,0 +1,414 @@
+package tree
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"sort"
+	"strconv"
+	"strings"
+)
+
+// Node represent some node in the tree
+// contains FileInfo, and its childs
+type Node struct {
+	os.FileInfo
+	path   string
+	depth  int
+	err    error
+	nodes  Nodes
+	vpaths map[string]bool
+}
+
+// List of nodes
+type Nodes []*Node
+
+// To use this package programmatically, you must implement this
+// interface.
+// For example: PTAL on 'cmd/tree/tree.go'
+type Fs interface {
+	Stat(path string) (os.FileInfo, error)
+	ReadDir(path string) ([]string, error)
+}
+
+// Options store the configuration for specific tree.
+// Note, that 'Fs', and 'OutFile' are required (OutFile can be os.Stdout).
+type Options struct {
+	Fs      Fs
+	OutFile io.Writer
+	// List
+	All        bool
+	DirsOnly   bool
+	FullPath   bool
+	IgnoreCase bool
+	FollowLink bool
+	DeepLevel  int
+	Pattern    string
+	IPattern   string
+	// File
+	ByteSize bool
+	UnitSize bool
+	FileMode bool
+	ShowUid  bool
+	ShowGid  bool
+	LastMod  bool
+	Quotes   bool
+	Inodes   bool
+	Device   bool
+	// Sort
+	NoSort    bool
+	VerSort   bool
+	ModSort   bool
+	DirSort   bool
+	NameSort  bool
+	SizeSort  bool
+	CTimeSort bool
+	ReverSort bool
+	// Graphics
+	NoIndent bool
+	Colorize bool
+	// Color defaults to ANSIColor()
+	Color func(*Node, string) string
+}
+
+func (opts *Options) color(node *Node, s string) string {
+	f := opts.Color
+	if f == nil {
+		f = ANSIColor
+	}
+	return f(node, s)
+}
+
+// New get path and create new node(root).
+func New(path string) *Node {
+	return &Node{path: path, vpaths: make(map[string]bool)}
+}
+
+// Visit all files under the given node.
+func (node *Node) Visit(opts *Options) (dirs, files int) {
+	// visited paths
+	if path, err := filepath.Abs(node.path); err == nil {
+		path = filepath.Clean(path)
+		node.vpaths[path] = true
+	}
+	// stat
+	fi, err := opts.Fs.Stat(node.path)
+	if err != nil {
+		node.err = err
+		return
+	}
+	node.FileInfo = fi
+	if !fi.IsDir() {
+		return 0, 1
+	}
+	// increase dirs only if it's a dir, but not the root.
+	if node.depth != 0 {
+		dirs++
+	}
+	// DeepLevel option
+	if opts.DeepLevel > 0 && opts.DeepLevel <= node.depth {
+		return
+	}
+	names, err := opts.Fs.ReadDir(node.path)
+	if err != nil {
+		node.err = err
+		return
+	}
+	node.nodes = make(Nodes, 0)
+	for _, name := range names {
+		// "all" option
+		if !opts.All && strings.HasPrefix(name, ".") {
+			continue
+		}
+		nnode := &Node{
+			path:   filepath.Join(node.path, name),
+			depth:  node.depth + 1,
+			vpaths: node.vpaths,
+		}
+		d, f := nnode.Visit(opts)
+		if nnode.err == nil && !nnode.IsDir() {
+			// "dirs only" option
+			if opts.DirsOnly {
+				continue
+			}
+			var rePrefix string
+			if opts.IgnoreCase {
+				rePrefix = "(?i)"
+			}
+			// Pattern matching
+			if opts.Pattern != "" {
+				re, err := regexp.Compile(rePrefix + opts.Pattern)
+				if err == nil && !re.MatchString(name) {
+					continue
+				}
+			}
+			// IPattern matching
+			if opts.IPattern != "" {
+				re, err := regexp.Compile(rePrefix + opts.IPattern)
+				if err == nil && re.MatchString(name) {
+					continue
+				}
+			}
+		}
+		node.nodes = append(node.nodes, nnode)
+		dirs, files = dirs+d, files+f
+	}
+	// Sorting
+	if !opts.NoSort {
+		node.sort(opts)
+	}
+	return
+}
+
+func (node *Node) sort(opts *Options) {
+	var fn SortFunc
+	switch {
+	case opts.ModSort:
+		fn = ModSort
+	case opts.CTimeSort:
+		fn = CTimeSort
+	case opts.DirSort:
+		fn = DirSort
+	case opts.VerSort:
+		fn = VerSort
+	case opts.SizeSort:
+		fn = SizeSort
+	case opts.NameSort:
+		fn = NameSort
+	default:
+		fn = NameSort // Default should be sorted, not unsorted.
+	}
+	if fn != nil {
+		if opts.ReverSort {
+			sort.Sort(sort.Reverse(ByFunc{node.nodes, fn}))
+		} else {
+			sort.Sort(ByFunc{node.nodes, fn})
+		}
+	}
+}
+
+// Path returns the Node's absolute path
+func (node *Node) Path() string {
+	return node.path
+}
+
+// Print nodes based on the given configuration.
+func (node *Node) Print(opts *Options) { node.print("", opts) }
+
+func dirRecursiveSize(opts *Options, node *Node) (size int64, err error) {
+	if opts.DeepLevel > 0 && node.depth >= opts.DeepLevel {
+		err = errors.New("Depth too high")
+	}
+
+	for _, nnode := range node.nodes {
+		if nnode.err != nil {
+			err = nnode.err
+			continue
+		}
+
+		if !nnode.IsDir() {
+			size += nnode.Size()
+		} else {
+			nsize, e := dirRecursiveSize(opts, nnode)
+			size += nsize
+			if e != nil {
+				err = e
+			}
+		}
+	}
+	return
+}
+
+func (node *Node) print(indent string, opts *Options) {
+	if node.err != nil {
+		err := node.err.Error()
+		if msgs := strings.Split(err, ": "); len(msgs) > 1 {
+			err = msgs[1]
+		}
+		fmt.Printf("%s [%s]\n", node.path, err)
+		return
+	}
+	if !node.IsDir() {
+		var props []string
+		ok, inode, device, uid, gid := getStat(node)
+		// inodes
+		if ok && opts.Inodes {
+			props = append(props, fmt.Sprintf("%d", inode))
+		}
+		// device
+		if ok && opts.Device {
+			props = append(props, fmt.Sprintf("%3d", device))
+		}
+		// Mode
+		if opts.FileMode {
+			props = append(props, node.Mode().String())
+		}
+		// Owner/Uid
+		if ok && opts.ShowUid {
+			uidStr := strconv.Itoa(int(uid))
+			if u, err := user.LookupId(uidStr); err != nil {
+				props = append(props, fmt.Sprintf("%-8s", uidStr))
+			} else {
+				props = append(props, fmt.Sprintf("%-8s", u.Username))
+			}
+		}
+		// Gorup/Gid
+		// TODO: support groupname
+		if ok && opts.ShowGid {
+			gidStr := strconv.Itoa(int(gid))
+			props = append(props, fmt.Sprintf("%-4s", gidStr))
+		}
+		// Size
+		if opts.ByteSize || opts.UnitSize {
+			var size string
+			if opts.UnitSize {
+				size = fmt.Sprintf("%4s", formatBytes(node.Size()))
+			} else {
+				size = fmt.Sprintf("%11d", node.Size())
+			}
+			props = append(props, size)
+		}
+		// Last modification
+		if opts.LastMod {
+			props = append(props, node.ModTime().Format("Jan 02 15:04"))
+		}
+		// Print properties
+		if len(props) > 0 {
+			fmt.Fprintf(opts.OutFile, "[%s]  ", strings.Join(props, " "))
+		}
+	} else {
+		var props []string
+		// Size
+		if opts.ByteSize || opts.UnitSize {
+			var size string
+			rsize, err := dirRecursiveSize(opts, node)
+			if err != nil && rsize <= 0 {
+				if opts.UnitSize {
+					size = "????"
+				} else {
+					size = "???????????"
+				}
+			} else if opts.UnitSize {
+				size = fmt.Sprintf("%4s", formatBytes(rsize))
+			} else {
+				size = fmt.Sprintf("%11d", rsize)
+			}
+			props = append(props, size)
+		}
+		// Print properties
+		if len(props) > 0 {
+			fmt.Fprintf(opts.OutFile, "[%s]  ", strings.Join(props, " "))
+		}
+	}
+	// name/path
+	var name string
+	if node.depth == 0 || opts.FullPath {
+		name = node.path
+	} else {
+		name = node.Name()
+	}
+	// Quotes
+	if opts.Quotes {
+		name = fmt.Sprintf("\"%s\"", name)
+	}
+	// Colorize
+	if opts.Colorize {
+		name = opts.color(node, name)
+	}
+	// IsSymlink
+	if node.Mode()&os.ModeSymlink == os.ModeSymlink {
+		vtarget, err := os.Readlink(node.path)
+		if err != nil {
+			vtarget = node.path
+		}
+		targetPath, err := filepath.EvalSymlinks(node.path)
+		if err != nil {
+			targetPath = vtarget
+		}
+		fi, err := opts.Fs.Stat(targetPath)
+		if opts.Colorize && fi != nil {
+			vtarget = opts.color(&Node{FileInfo: fi, path: vtarget}, vtarget)
+		}
+		name = fmt.Sprintf("%s -> %s", name, vtarget)
+		// Follow symbolic links like directories
+		if opts.FollowLink {
+			path, err := filepath.Abs(targetPath)
+			if err == nil && fi != nil && fi.IsDir() {
+				if _, ok := node.vpaths[filepath.Clean(path)]; !ok {
+					inf := &Node{FileInfo: fi, path: targetPath}
+					inf.vpaths = node.vpaths
+					inf.Visit(opts)
+					node.nodes = inf.nodes
+				} else {
+					name += " [recursive, not followed]"
+				}
+			}
+		}
+	}
+	// Print file details
+	// the main idea of the print logic came from here: github.com/campoy/tools/tree
+	fmt.Fprintln(opts.OutFile, name)
+	add := "│   "
+	for i, nnode := range node.nodes {
+		if opts.NoIndent {
+			add = ""
+		} else {
+			if i == len(node.nodes)-1 {
+				fmt.Fprintf(opts.OutFile, indent+"└── ")
+				add = "    "
+			} else {
+				fmt.Fprintf(opts.OutFile, indent+"├── ")
+			}
+		}
+		nnode.print(indent+add, opts)
+	}
+}
+
+const (
+	_        = iota // ignore first value by assigning to blank identifier
+	KB int64 = 1 << (10 * iota)
+	MB
+	GB
+	TB
+	PB
+	EB
+)
+
+// Convert bytes to human readable string. Like a 2 MB, 64.2 KB, 52 B
+func formatBytes(i int64) (result string) {
+	var n float64
+	sFmt, eFmt := "%.01f", ""
+	switch {
+	case i > EB:
+		eFmt = "E"
+		n = float64(i) / float64(EB)
+	case i > PB:
+		eFmt = "P"
+		n = float64(i) / float64(PB)
+	case i > TB:
+		eFmt = "T"
+		n = float64(i) / float64(TB)
+	case i > GB:
+		eFmt = "G"
+		n = float64(i) / float64(GB)
+	case i > MB:
+		eFmt = "M"
+		n = float64(i) / float64(MB)
+	case i > KB:
+		eFmt = "K"
+		n = float64(i) / float64(KB)
+	default:
+		sFmt = "%.0f"
+		n = float64(i)
+	}
+	if eFmt != "" && n >= 10 {
+		sFmt = "%.0f"
+	}
+	result = fmt.Sprintf(sFmt+eFmt, n)
+	result = strings.Trim(result, " ")
+	return
+}
diff --git a/vendor/github.com/a8m/tree/sort.go b/vendor/github.com/a8m/tree/sort.go
new file mode 100644
index 00000000..55299935
--- /dev/null
+++ b/vendor/github.com/a8m/tree/sort.go
@@ -0,0 +1,100 @@
+package tree
+
+import "os"
+
+func (n Nodes) Len() int      { return len(n) }
+func (n Nodes) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
+
+type ByFunc struct {
+	Nodes
+	Fn SortFunc
+}
+
+func (b ByFunc) Less(i, j int) bool {
+	return b.Fn(b.Nodes[i].FileInfo, b.Nodes[j].FileInfo)
+}
+
+type SortFunc func(f1, f2 os.FileInfo) bool
+
+func ModSort(f1, f2 os.FileInfo) bool {
+	return f1.ModTime().Before(f2.ModTime())
+}
+
+func DirSort(f1, f2 os.FileInfo) bool {
+	return f1.IsDir() && !f2.IsDir()
+}
+
+func SizeSort(f1, f2 os.FileInfo) bool {
+	return f1.Size() < f2.Size()
+}
+
+func NameSort(f1, f2 os.FileInfo) bool {
+	return f1.Name() < f2.Name()
+}
+
+func VerSort(f1, f2 os.FileInfo) bool {
+	return NaturalLess(f1.Name(), f2.Name())
+}
+
+func isdigit(b byte) bool { return '0' <= b && b <= '9' }
+
+// NaturalLess compares two strings using natural ordering. This means that e.g.
+// "abc2" < "abc12".
+//
+// Non-digit sequences and numbers are compared separately. The former are
+// compared bytewise, while the latter are compared numerically (except that
+// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02")
+//
+// Limitation: only ASCII digits (0-9) are considered.
+// Code taken from:
+// https://github.com/fvbommel/util/blob/master/sortorder/natsort.go
+func NaturalLess(str1, str2 string) bool {
+	idx1, idx2 := 0, 0
+	for idx1 < len(str1) && idx2 < len(str2) {
+		c1, c2 := str1[idx1], str2[idx2]
+		dig1, dig2 := isdigit(c1), isdigit(c2)
+		switch {
+		case dig1 != dig2: // Digits before other characters.
+			return dig1 // True if LHS is a digit, false if the RHS is one.
+		case !dig1: // && !dig2, because dig1 == dig2
+			// UTF-8 compares bytewise-lexicographically, no need to decode
+			// codepoints.
+			if c1 != c2 {
+				return c1 < c2
+			}
+			idx1++
+			idx2++
+		default: // Digits
+			// Eat zeros.
+			for ; idx1 < len(str1) && str1[idx1] == '0'; idx1++ {
+			}
+			for ; idx2 < len(str2) && str2[idx2] == '0'; idx2++ {
+			}
+			// Eat all digits.
+			nonZero1, nonZero2 := idx1, idx2
+			for ; idx1 < len(str1) && isdigit(str1[idx1]); idx1++ {
+			}
+			for ; idx2 < len(str2) && isdigit(str2[idx2]); idx2++ {
+			}
+			// If lengths of numbers with non-zero prefix differ, the shorter
+			// one is less.
+			if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 {
+				return len1 < len2
+			}
+			// If they're not equal, string comparison is correct.
+			if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 {
+				return nr1 < nr2
+			}
+			// Otherwise, the one with less zeros is less.
+			// Because everything up to the number is equal, comparing the index
+			// after the zeros is sufficient.
+			if nonZero1 != nonZero2 {
+				return nonZero1 < nonZero2
+			}
+		}
+		// They're identical so far, so continue comparing.
+	}
+	// So far they are identical. At least one is ended. If the other continues,
+	// it sorts last.
+	return len(str1) < len(str2)
+}
diff --git a/vendor/github.com/a8m/tree/stat_unix.go b/vendor/github.com/a8m/tree/stat_unix.go
new file mode 100644
index 00000000..5ed7b0a4
--- /dev/null
+++ b/vendor/github.com/a8m/tree/stat_unix.go
@@ -0,0 +1,20 @@
+//+build !plan9,!windows
+
+package tree
+
+import (
+	"os"
+	"syscall"
+)
+
+func getStat(fi os.FileInfo) (ok bool, inode, device, uid, gid uint64) {
+	sys := fi.Sys()
+	if sys == nil {
+		return false, 0, 0, 0, 0
+	}
+	stat, ok := sys.(*syscall.Stat_t)
+	if !ok {
+		return false, 0, 0, 0, 0
+	}
+	return true, uint64(stat.Ino), uint64(stat.Dev), uint64(stat.Uid), uint64(stat.Gid)
+}
diff --git a/vendor/github.com/a8m/tree/stat_unsupported.go b/vendor/github.com/a8m/tree/stat_unsupported.go
new file mode 100644
index 00000000..b37407da
--- /dev/null
+++ b/vendor/github.com/a8m/tree/stat_unsupported.go
@@ -0,0 +1,9 @@
+//+build plan9 windows
+
+package tree
+
+import "os"
+
+func getStat(fi os.FileInfo) (ok bool, inode, device, uid, gid uint64) {
+	return false, 0, 0, 0, 0
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 8bc5ea17..eb86d181 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1,3 +1,5 @@
+# github.com/a8m/tree v0.0.0-20210115125333-10a5fd5b637d
+github.com/a8m/tree
 # github.com/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892 => github.com/rasky/go-xdr v0.0.0-20170217172119-4930550ba2e2
 github.com/davecgh/go-xdr/xdr2
 # github.com/google/uuid v0.0.0-20170306145142-6a5e28554805
@@ -7,6 +9,6 @@ github.com/kr/pretty
 # github.com/kr/text v0.1.0
 github.com/kr/text
 # github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728
+github.com/vmware/vmw-guestinfo/bdoor
 github.com/vmware/vmw-guestinfo/message
 github.com/vmware/vmw-guestinfo/vmcheck
-github.com/vmware/vmw-guestinfo/bdoor
diff --git a/view/container_view.go b/view/container_view.go
index 37fa34ce..c420acf9 100644
--- a/view/container_view.go
+++ b/view/container_view.go
@@ -36,7 +36,7 @@ func NewContainerView(c *vim25.Client, ref types.ManagedObjectReference) *Contai
 }
 
 // Retrieve populates dst as property.Collector.Retrieve does, for all entities in the view of types specified by kind.
-func (v ContainerView) Retrieve(ctx context.Context, kind []string, ps []string, dst interface{}) error {
+func (v ContainerView) Retrieve(ctx context.Context, kind []string, ps []string, dst interface{}, pspec ...types.PropertySpec) error {
 	pc := property.DefaultCollector(v.Client())
 
 	ospec := types.ObjectSpec{
@@ -50,8 +50,6 @@ func (v ContainerView) Retrieve(ctx context.Context, kind []string, ps []string,
 		},
 	}
 
-	var pspec []types.PropertySpec
-
 	if len(kind) == 0 {
 		kind = []string{"ManagedEntity"}
 	}


Test patch:
diff --git a/govc/test/object.bats b/govc/test/object.bats
index 01afa328..55dc0b32 100755
--- a/govc/test/object.bats
+++ b/govc/test/object.bats
@@ -613,3 +613,13 @@ EOF
   rm -rf "$dir"
   assert_equal 6 "$n"
 }
+
+@test "tree" {
+  vcsim_start -dc 2 -folder 1 -pod 1 -nsx 1 -pool 2
+
+  run govc tree
+  assert_success
+
+  run govc tree /DC0
+  assert_success
+}