Repository: vmware/govmomi
Pull request #1994 (2020-05-26 16:39:26)

Add appliance log forwarding config handler and govc verb
Appliance logging config can be retrieved via the VAPI REST
endpoint.  The govc output is as follows.

```
$ govc appliance.logging.forwaring
Hostname:  10.186.54.118
Port:      5480
Protocol:  TLS
Hostname:  2.2.2.2
Port:      9999
Protocol:  TLS
```


Patch:
diff --git a/govc/flags/search.go b/govc/flags/search.go
index dd18b16a..e004000d 100644
--- a/govc/flags/search.go
+++ b/govc/flags/search.go
@@ -171,8 +171,7 @@ func (flag *SearchFlag) searchByDNSName(c *vim25.Client, dc *object.Datacenter)
 	}
 }
 
-func (flag *SearchFlag) searchByInventoryPath(c *vim25.Client, dc *object.Datacenter) (object.Reference, error) {
-	// TODO(PN): The datacenter flag should not be set because it is ignored.
+func (flag *SearchFlag) searchByInventoryPath(c *vim25.Client) (object.Reference, error) {
 	ctx := context.TODO()
 	return flag.searchIndex(c).FindByInventoryPath(ctx, flag.byInventoryPath)
 }
@@ -226,24 +225,29 @@ func (flag *SearchFlag) search() (object.Reference, error) {
 	ctx := context.TODO()
 	var ref object.Reference
 	var err error
+	var dc *object.Datacenter
 
 	c, err := flag.Client()
 	if err != nil {
 		return nil, err
 	}
 
-	dc, err := flag.Datacenter()
-	if err != nil {
-		return nil, err
+	isPath := flag.byInventoryPath != ""
+	if !isPath {
+		// All other SearchIndex methods require a Datacenter param
+		dc, err = flag.Datacenter()
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	switch {
+	case isPath:
+		ref, err = flag.searchByInventoryPath(c)
 	case flag.byDatastorePath != "":
 		ref, err = flag.searchByDatastorePath(c, dc)
 	case flag.byDNSName != "":
 		ref, err = flag.searchByDNSName(c, dc)
-	case flag.byInventoryPath != "":
-		ref, err = flag.searchByInventoryPath(c, dc)
 	case flag.byIP != "":
 		ref, err = flag.searchByIP(c, dc)
 	case flag.byUUID != "":
diff --git a/govc/importx/ovf.go b/govc/importx/ovf.go
index ed7570e9..9672389f 100644
--- a/govc/importx/ovf.go
+++ b/govc/importx/ovf.go
@@ -23,6 +23,7 @@ import (
 	"fmt"
 	"path"
 
+	"github.com/vmware/govmomi/find"
 	"github.com/vmware/govmomi/govc/cli"
 	"github.com/vmware/govmomi/govc/flags"
 	"github.com/vmware/govmomi/nfc"
@@ -163,34 +164,42 @@ func (cmd *ovfx) Map(op []Property) (p []types.KeyValue) {
 	return
 }
 
-func (cmd *ovfx) NetworkMap(e *ovf.Envelope) (p []types.OvfNetworkMapping) {
+func (cmd *ovfx) NetworkMap(e *ovf.Envelope) ([]types.OvfNetworkMapping, error) {
 	ctx := context.TODO()
 	finder, err := cmd.DatastoreFlag.Finder()
 	if err != nil {
-		return
+		return nil, err
 	}
 
-	networks := map[string]string{}
-
-	if e.Network != nil {
-		for _, net := range e.Network.Networks {
-			networks[net.Name] = net.Name
+	var nmap []types.OvfNetworkMapping
+	for _, m := range cmd.Options.NetworkMapping {
+		if m.Network == "" {
+			continue // Not set, let vSphere choose the default network
 		}
-	}
 
-	for _, net := range cmd.Options.NetworkMapping {
-		networks[net.Name] = net.Network
-	}
+		var ref types.ManagedObjectReference
 
-	for src, dst := range networks {
-		if net, err := finder.Network(ctx, dst); err == nil {
-			p = append(p, types.OvfNetworkMapping{
-				Name:    src,
-				Network: net.Reference(),
-			})
+		net, err := finder.Network(ctx, m.Network)
+		if err != nil {
+			switch err.(type) {
+			case *find.NotFoundError:
+				if !ref.FromString(m.Network) {
+					return nil, err
+				} // else this is a raw MO ref
+			default:
+				return nil, err
+			}
+		} else {
+			ref = net.Reference()
 		}
+
+		nmap = append(nmap, types.OvfNetworkMapping{
+			Name:    m.Name,
+			Network: ref,
+		})
 	}
-	return
+
+	return nmap, err
 }
 
 func (cmd *ovfx) Import(fpath string) (*types.ManagedObjectReference, error) {
@@ -224,6 +233,11 @@ func (cmd *ovfx) Import(fpath string) (*types.ManagedObjectReference, error) {
 		name = cmd.Name
 	}
 
+	nmap, err := cmd.NetworkMap(e)
+	if err != nil {
+		return nil, err
+	}
+
 	cisp := types.OvfCreateImportSpecParams{
 		DiskProvisioning:   cmd.Options.DiskProvisioning,
 		EntityName:         name,
@@ -233,7 +247,7 @@ func (cmd *ovfx) Import(fpath string) (*types.ManagedObjectReference, error) {
 			DeploymentOption: cmd.Options.Deployment,
 			Locale:           "US"},
 		PropertyMapping: cmd.Map(cmd.Options.PropertyMapping),
-		NetworkMapping:  cmd.NetworkMap(e),
+		NetworkMapping:  nmap,
 	}
 
 	host, err := cmd.HostSystemIfSpecified()
diff --git a/govc/main.go b/govc/main.go
index eb3edc8d..1be1658a 100644
--- a/govc/main.go
+++ b/govc/main.go
@@ -84,6 +84,7 @@ import (
 	_ "github.com/vmware/govmomi/govc/tags/category"
 	_ "github.com/vmware/govmomi/govc/task"
 	_ "github.com/vmware/govmomi/govc/vapp"
+	_ "github.com/vmware/govmomi/govc/vcsa/log"
 	_ "github.com/vmware/govmomi/govc/version"
 	_ "github.com/vmware/govmomi/govc/vm"
 	_ "github.com/vmware/govmomi/govc/vm/disk"
diff --git a/govc/vcsa/log/forwarding.go b/govc/vcsa/log/forwarding.go
new file mode 100644
index 00000000..ca8cbbc0
--- /dev/null
+++ b/govc/vcsa/log/forwarding.go
@@ -0,0 +1,93 @@
+/*
+Copyright (c) 2020 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 logging
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"io"
+	"text/tabwriter"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	vlogging "github.com/vmware/govmomi/vapi/appliance/logging"
+)
+
+type info struct {
+	*flags.ClientFlag
+	*flags.OutputFlag
+}
+
+func init() {
+	cli.Register("vcsa.log.forwarding.info", &info{})
+}
+
+func (cmd *info) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+
+	cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx)
+	cmd.OutputFlag.Register(ctx, f)
+}
+
+func (cmd *info) Process(ctx context.Context) error {
+	if err := cmd.ClientFlag.Process(ctx); err != nil {
+		return err
+	}
+	if err := cmd.OutputFlag.Process(ctx); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (cmd *info) Description() string {
+	return `Retrieve the VC Appliance log forwarding configuration
+
+Examples:
+  govc vcsa.log.forwarding.info`
+}
+
+func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error {
+	c, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+
+	fwd := vlogging.NewManager(c)
+
+	res, err := fwd.Forwarding(ctx)
+	if err != nil {
+		return nil
+	}
+
+	return cmd.WriteResult(forwardingConfigResult(res))
+}
+
+type forwardingConfigResult []vlogging.Forwarding
+
+func (res forwardingConfigResult) Write(w io.Writer) error {
+	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
+
+	for _, c := range res {
+		fmt.Fprintf(tw, "Hostname:\t%s\n", c.Hostname)
+		fmt.Fprintf(tw, "Port:\t%d\n", c.Port)
+		fmt.Fprintf(tw, "Protocol:\t%s\n", c.Protocol)
+	}
+
+	return tw.Flush()
+}
diff --git a/govc/vm/network/add.go b/govc/vm/network/add.go
index 66f0aa1f..9f4c5d8f 100644
--- a/govc/vm/network/add.go
+++ b/govc/vm/network/add.go
@@ -47,6 +47,7 @@ func (cmd *add) Description() string {
 
 Examples:
   govc vm.network.add -vm $vm -net "VM Network" -net.adapter e1000e
+  govc vm.network.add -vm $vm -net SwitchName/PortgroupName
   govc device.info -vm $vm ethernet-*`
 }
 
diff --git a/list/lister.go b/list/lister.go
index 2ee32e6b..9a4caed6 100644
--- a/list/lister.go
+++ b/list/lister.go
@@ -165,6 +165,8 @@ func (l Lister) List(ctx context.Context) ([]Element, error) {
 		return l.ListHostSystem(ctx)
 	case "VirtualApp":
 		return l.ListVirtualApp(ctx)
+	case "VmwareDistributedVirtualSwitch", "DistributedVirtualSwitch":
+		return l.ListDistributedVirtualSwitch(ctx)
 	default:
 		return nil, fmt.Errorf("cannot traverse type " + l.Reference.Type)
 	}
@@ -497,6 +499,69 @@ func (l Lister) ListHostSystem(ctx context.Context) ([]Element, error) {
 	return es, nil
 }
 
+func (l Lister) ListDistributedVirtualSwitch(ctx context.Context) ([]Element, error) {
+	ospec := types.ObjectSpec{
+		Obj:  l.Reference,
+		Skip: types.NewBool(true),
+	}
+
+	fields := []string{
+		"portgroup",
+	}
+
+	for _, f := range fields {
+		tspec := types.TraversalSpec{
+			Path: f,
+			Skip: types.NewBool(false),
+			Type: "DistributedVirtualSwitch",
+		}
+
+		ospec.SelectSet = append(ospec.SelectSet, &tspec)
+	}
+
+	childTypes := []string{
+		"DistributedVirtualPortgroup",
+	}
+
+	var pspecs []types.PropertySpec
+	for _, t := range childTypes {
+		pspec := types.PropertySpec{
+			Type: t,
+		}
+
+		if l.All {
+			pspec.All = types.NewBool(true)
+		} else {
+			pspec.PathSet = []string{"name"}
+		}
+
+		pspecs = append(pspecs, pspec)
+	}
+
+	req := types.RetrieveProperties{
+		SpecSet: []types.PropertyFilterSpec{
+			{
+				ObjectSet: []types.ObjectSpec{ospec},
+				PropSet:   pspecs,
+			},
+		},
+	}
+
+	var dst []interface{}
+
+	err := l.retrieveProperties(ctx, req, &dst)
+	if err != nil {
+		return nil, err
+	}
+
+	es := []Element{}
+	for _, v := range dst {
+		es = append(es, ToElement(v.(mo.Reference), l.Prefix))
+	}
+
+	return es, nil
+}
+
 func (l Lister) ListVirtualApp(ctx context.Context) ([]Element, error) {
 	ospec := types.ObjectSpec{
 		Obj:  l.Reference,
diff --git a/simulator/dvs.go b/simulator/dvs.go
index bc61b3d9..d40ae925 100644
--- a/simulator/dvs.go
+++ b/simulator/dvs.go
@@ -18,6 +18,7 @@ package simulator
 
 import (
 	"strconv"
+	"strings"
 
 	"github.com/vmware/govmomi/vim25/methods"
 	"github.com/vmware/govmomi/vim25/mo"
@@ -41,10 +42,14 @@ func (s *DistributedVirtualSwitch) AddDVPortgroupTask(ctx *Context, c *types.Add
 			pg.Name = spec.Name
 			pg.Entity().Name = pg.Name
 
-			if obj := Map.FindByName(pg.Name, f.ChildEntity); obj != nil {
-				return nil, &types.DuplicateName{
-					Name:   pg.Name,
-					Object: obj.Reference(),
+			// Standard AddDVPortgroupTask() doesn't allow duplicate names, but NSX 3.0 does create some DVPGs with the same name.
+			// Allow duplicate names using this prefix so we can reproduce and test this condition.
+			if !strings.HasPrefix(pg.Name, "NSX-") {
+				if obj := Map.FindByName(pg.Name, f.ChildEntity); obj != nil {
+					return nil, &types.DuplicateName{
+						Name:   pg.Name,
+						Object: obj.Reference(),
+					}
 				}
 			}
 
diff --git a/simulator/virtual_machine.go b/simulator/virtual_machine.go
index 2089acde..9068d25e 100644
--- a/simulator/virtual_machine.go
+++ b/simulator/virtual_machine.go
@@ -1894,7 +1894,7 @@ func (vm *VirtualMachine) CreateSnapshotTask(req *types.CreateSnapshot_Task) soa
 		changes = append(changes, types.PropertyChange{Name: "snapshot.currentSnapshot", Val: snapshot.Self})
 		Map.Update(vm, changes)
 
-		return nil, nil
+		return snapshot.Self, nil
 	})
 
 	return &methods.CreateSnapshot_TaskBody{
diff --git a/vapi/appliance/logging/forwarding.go b/vapi/appliance/logging/forwarding.go
new file mode 100644
index 00000000..cab70d31
--- /dev/null
+++ b/vapi/appliance/logging/forwarding.go
@@ -0,0 +1,56 @@
+/*
+Copyright (c) 2019 VMware, Inc. All Rights Reserved.
+
+SPDX-License-Identifier: Apache-2.0.
+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 logging
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/vmware/govmomi/vapi/rest"
+)
+
+const applianceLoggingForwardingPath = "/appliance/logging/forwarding"
+
+// Manager provides convenience methods to configure appliance logging forwarding.
+type Manager struct {
+	*rest.Client
+}
+
+// NewManager creates a new Manager with the given client
+func NewManager(client *rest.Client) *Manager {
+	return &Manager{
+		Client: client,
+	}
+}
+
+// Forwarding represents configuration for log message forwarding.
+type Forwarding struct {
+	Hostname string `json:"hostname,omitempty"`
+	Port     int    `json:"port,omitempty"`
+	Protocol string `json:"protocol,omitempty"`
+}
+
+func (m *Manager) getManagerResource() *rest.Resource {
+	return m.Resource(applianceLoggingForwardingPath)
+}
+
+// Forwarding returns all logging forwarding config.
+func (m *Manager) Forwarding(ctx context.Context) ([]Forwarding, error) {
+	r := m.getManagerResource()
+	var res []Forwarding
+	return res, m.Do(ctx, r.Request(http.MethodGet), &res)
+}


Test patch:
diff --git a/govc/test/import.bats b/govc/test/import.bats
index 8a9a84cd..d0f56c39 100755
--- a/govc/test/import.bats
+++ b/govc/test/import.bats
@@ -148,3 +148,45 @@ load test_helper
   run govc import.vmdk -force "$GOVC_TEST_VMDK_SRC" "$name"
   assert_success # exists, but -force was used
 }
+
+@test "import duplicate dvpg names" {
+  vcsim_env
+
+  run govc dvs.create DVS1 # DVS0 already exists
+  assert_success
+
+  run govc dvs.portgroup.add -dvs DVS0 -type ephemeral NSX-dvpg
+  assert_success
+
+  run govc dvs.portgroup.add -dvs DVS1 -type ephemeral NSX-dvpg
+  assert_success
+
+  ovf="$GOVC_IMAGES/$TTYLINUX_NAME.ovf"
+
+  spec=$(govc import.spec "$GOVC_IMAGES/$TTYLINUX_NAME.ovf")
+
+  run govc import.ovf -name ttylinux -options - "$ovf" <<<"$spec"
+  assert_success # no network specified
+
+  options=$(jq ".NetworkMapping[].Network = \"enoent\"" <<<"$spec")
+
+  run govc import.ovf -options - "$ovf" <<<"$options"
+  assert_failure # network not found
+
+  options=$(jq ".NetworkMapping[].Network = \"NSX-dvpg\"" <<<"$spec")
+
+  run govc import.ovf -options - "$ovf" <<<"$options"
+  assert_failure # 2 networks have the same name
+
+  options=$(jq ".NetworkMapping[].Network = \"DVS0/NSX-dvpg\"" <<<"$spec")
+
+  run govc import.ovf -name ttylinux2 -options - "$ovf" <<<"$options"
+  assert_success # switch_name/portgroup_name is unique
+
+  switch=$(govc find -i network -name DVS0)
+  id=$(govc find -i network -config.distributedVirtualSwitch "$switch" -name NSX-dvpg)
+  options=$(jq ".NetworkMapping[].Network = \"$id\"" <<<"$spec")
+
+  run govc import.ovf -options - "$ovf" <<<"$options"
+  assert_success # using raw MO id
+}
diff --git a/govc/test/network.bats b/govc/test/network.bats
index 655f0699..a5155dd4 100755
--- a/govc/test/network.bats
+++ b/govc/test/network.bats
@@ -38,6 +38,22 @@ load test_helper
 
   run govc device.remove -vm $vm $eth0
   assert_failure "govc: device '$eth0' not found"
+
+  # Test PG's with the same name
+  run govc dvs.create DVS1 # DVS0 already exists
+  assert_success
+
+  run govc dvs.portgroup.add -dvs DVS0 -type ephemeral NSX-dvpg
+  assert_success
+
+  run govc dvs.portgroup.add -dvs DVS1 -type ephemeral NSX-dvpg
+  assert_success
+
+  run govc vm.network.add -vm $vm -net NSX-dvpg
+  assert_failure # resolves to multiple networks
+
+  run govc vm.network.add -vm $vm -net DVS0/NSX-dvpg
+  assert_success # switch_name/portgroup_name is unique
 }
 
 @test "network change backing" {
diff --git a/govc/test/vm.bats b/govc/test/vm.bats
index 553dce30..0af2fc1d 100755
--- a/govc/test/vm.bats
+++ b/govc/test/vm.bats
@@ -375,6 +375,32 @@ load test_helper
   assert_equal "$hv" "true"
 }
 
+@test "vm.info multi dc" {
+  vcsim_start -dc 2
+
+  run govc vm.info /DC1/vm/DC1_H0_VM1
+  assert_success
+
+  run govc vm.info DC1_H0_VM1
+  assert_failure
+
+  run govc vm.info -vm.ipath /DC1/vm/DC1_H0_VM1
+  assert_success
+  uuid=$(grep "UUID:" <<<"$output" | awk '{print $2}')
+
+  run govc vm.info -vm.ipath DC1_H0_VM1
+  assert_failure
+
+  run govc vm.info -vm.uuid enoent
+  assert_failure
+
+  run govc vm.info -vm.uuid "$uuid"
+  assert_failure
+
+  run govc vm.info -dc DC1 -vm.uuid "$uuid"
+  assert_success
+}
+
 @test "vm.create linked ide disk" {
   esx_env
 
diff --git a/simulator/virtual_machine_test.go b/simulator/virtual_machine_test.go
index 90c07990..bb79f6d9 100644
--- a/simulator/virtual_machine_test.go
+++ b/simulator/virtual_machine_test.go
@@ -773,11 +773,21 @@ func TestVmSnapshot(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	err = task.Wait(ctx)
+	info, err := task.WaitForResult(ctx)
 	if err != nil {
 		t.Fatal(err)
 	}
 
+	snapRef, ok := info.Result.(types.ManagedObjectReference)
+	if !ok {
+		t.Fatal("expected ManagedObjectRefrence result for CreateSnapshot")
+	}
+
+	_, err = vm.FindSnapshot(ctx, snapRef.Value)
+	if err != nil {
+		t.Fatal(err, "snapshot should be found by result reference")
+	}
+
 	_, err = fieldValue(reflect.ValueOf(simVm), "snapshot")
 	if err == errEmptyField {
 		t.Fatal("snapshot property should not be 'nil' if there are snapshots")