Repository: vmware/govmomi
Pull request #2230 (2021-01-07 18:48:38)

IPv6 support for govc
I screwed up my branches; here is a new PR for IPv6 support for govc. See also #2151.


Patch:
diff --git a/govc/about/command.go b/govc/about/command.go
index 054969b3..19ce538c 100644
--- a/govc/about/command.go
+++ b/govc/about/command.go
@@ -104,6 +104,7 @@ type infoResult struct {
 
 func (r *infoResult) Write(w io.Writer) error {
 	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
+	fmt.Fprintf(tw, "FullName:\t%s\n", r.a.FullName)
 	fmt.Fprintf(tw, "Name:\t%s\n", r.a.Name)
 	fmt.Fprintf(tw, "Vendor:\t%s\n", r.a.Vendor)
 	fmt.Fprintf(tw, "Version:\t%s\n", r.a.Version)
diff --git a/govc/object/save.go b/govc/object/save.go
index 9c17f50e..ce0dc3df 100644
--- a/govc/object/save.go
+++ b/govc/object/save.go
@@ -112,9 +112,18 @@ func saveDVS(ctx context.Context, c *vim25.Client, ref types.ManagedObjectRefere
 	return []saveMethod{{"FetchDVPorts", res}}, nil
 }
 
+func saveEnvironmentBrowser(ctx context.Context, c *vim25.Client, ref types.ManagedObjectReference) ([]saveMethod, error) {
+	res, err := methods.QueryConfigOption(ctx, c, &types.QueryConfigOption{This: ref})
+	if err != nil {
+		return nil, err
+	}
+	return []saveMethod{{"QueryConfigOption", res}}, nil
+}
+
 // saveObjects maps object types to functions that can save data that isn't available via the PropertyCollector
 var saveObjects = map[string]func(context.Context, *vim25.Client, types.ManagedObjectReference) ([]saveMethod, error){
 	"VmwareDistributedVirtualSwitch": saveDVS,
+	"EnvironmentBrowser":             saveEnvironmentBrowser,
 }
 
 func (cmd *save) save(content []types.ObjectContent) error {
diff --git a/govc/vm/customize.go b/govc/vm/customize.go
index b6d918e6..c726dbc1 100644
--- a/govc/vm/customize.go
+++ b/govc/vm/customize.go
@@ -39,6 +39,7 @@ type customize struct {
 	host      types.CustomizationFixedName
 	mac       flags.StringList
 	ip        flags.StringList
+	ip6       flags.StringList
 	gateway   flags.StringList
 	netmask   flags.StringList
 	dnsserver flags.StringList
@@ -60,8 +61,10 @@ func (cmd *customize) Register(ctx context.Context, f *flag.FlagSet) {
 	f.StringVar(&cmd.host.Name, "name", "", "Host name")
 	f.Var(&cmd.mac, "mac", "MAC address")
 	cmd.mac = nil
-	f.Var(&cmd.ip, "ip", "IP address")
+	f.Var(&cmd.ip, "ip", "IPv4 address")
 	cmd.ip = nil
+	f.Var(&cmd.ip6, "ip6", "IPv6 addresses with optional netmask (defaults to /64), separated by comma")
+	cmd.ip6 = nil
 	f.Var(&cmd.gateway, "gateway", "Gateway")
 	cmd.gateway = nil
 	f.Var(&cmd.netmask, "netmask", "Netmask")
@@ -93,11 +96,58 @@ Examples:
   govc vm.customize -vm VM -ip 10.0.0.178 -netmask 255.255.255.0 -ip 10.0.0.162 -netmask 255.255.255.0
   # Multiple -ip with -mac are applied by vCenter to the NIC with the given MAC address
   govc vm.customize -vm VM -mac 00:50:56:be:dd:f8 -ip 10.0.0.178 -netmask 255.255.255.0 -mac 00:50:56:be:60:cf -ip 10.0.0.162 -netmask 255.255.255.0
+  # Dual stack IPv4/IPv6, single NIC
+  govc vm.customize -vm VM -ip 10.0.0.1 -netmask 255.255.255.0 -ip6 '2001:db8::1/64' -name my-hostname NAME
+  # DHCPv6, single NIC
+  govc vm.customize -vm VM -ip6 dhcp6 NAME
+  # Static IPv6, three NICs, last one with two addresses
+  govc vm.customize -vm VM -ip6 2001:db8::1/64 -ip6 2001:db8::2/64 -ip6 2001:db8::3/64,2001:db8::4/64 NAME
   govc vm.customize -vm VM -auto-login 3 NAME
   govc vm.customize -vm VM -prefix demo NAME
   govc vm.customize -vm VM -tz America/New_York NAME`
 }
 
+// Parse a string of multiple IPv6 addresses with optional netmask; separated by comma
+func parseIPv6Argument(argv string) (ipconf []types.BaseCustomizationIpV6Generator, err error) {
+	for _, substring := range strings.Split(argv, ",") {
+		// remove leading and trailing white space
+		substring = strings.TrimSpace(substring)
+		// handle "dhcp6" and lists of static IPv6 addresses
+		switch substring {
+		case "dhcp6":
+			ipconf = append(
+				ipconf,
+				&types.CustomizationDhcpIpV6Generator{},
+			)
+		default:
+			// check if subnet mask was specified
+			switch strings.Count(substring, "/") {
+			// no mask, set default
+			case 0:
+				ipconf = append(ipconf, &types.CustomizationFixedIpV6{
+					IpAddress:  substring,
+					SubnetMask: 64,
+				})
+			// a single forward slash was found: parse and use subnet mask
+			case 1:
+				parts := strings.Split(substring, "/")
+				mask, err := strconv.Atoi(parts[1])
+				if err != nil {
+					return nil, fmt.Errorf("unable to convert subnet mask to int: %w", err)
+				}
+				ipconf = append(ipconf, &types.CustomizationFixedIpV6{
+					IpAddress:  parts[0],
+					SubnetMask: int32(mask),
+				})
+			// too many forward slashes; return error
+			default:
+				return nil, fmt.Errorf("unable to parse IPv6 address (too many subnet separators): %s", substring)
+			}
+		}
+	}
+	return ipconf, nil
+}
+
 func (cmd *customize) Run(ctx context.Context, f *flag.FlagSet) error {
 	vm, err := cmd.VirtualMachineFlag.VirtualMachine()
 	if err != nil {
@@ -234,6 +284,23 @@ func (cmd *customize) Run(ctx context.Context, f *flag.FlagSet) error {
 		}
 	}
 
+	for i, ip6 := range cmd.ip6 {
+		ipconfig, err := parseIPv6Argument(ip6)
+		if err != nil {
+			return err
+		}
+		// use the same logic as the ip switch: the first occurrence of the ip6 switch is assigned to the first nic,
+		// the second to the second nic and so forth.
+		if spec.NicSettingMap == nil || len(spec.NicSettingMap) < i {
+			return fmt.Errorf("unable to find a network adapter for IPv6 settings %d (%s)", i, ip6)
+		}
+		nic := &spec.NicSettingMap[i]
+		if nic.Adapter.IpV6Spec == nil {
+			nic.Adapter.IpV6Spec = new(types.CustomizationIPSettingsIpV6AddressSpec)
+		}
+		nic.Adapter.IpV6Spec.Ip = append(nic.Adapter.IpV6Spec.Ip, ipconfig...)
+	}
+
 	task, err := vm.Customize(ctx, *spec)
 	if err != nil {
 		return err
diff --git a/simulator/datastore.go b/simulator/datastore.go
index dfe2a35a..1a6318f7 100644
--- a/simulator/datastore.go
+++ b/simulator/datastore.go
@@ -40,7 +40,7 @@ func (ds *Datastore) model(m *Model) error {
 		// rewrite saved vmfs path to a local temp dir
 		u.Path = path.Clean(u.Path)
 		parent := strings.ReplaceAll(path.Dir(u.Path), "/", "_")
-		name := path.Base(u.Path)
+		name := strings.ReplaceAll(path.Base(u.Path), ":", "_")
 
 		dir, err := m.createTempDir(parent, name)
 		if err != nil {
diff --git a/simulator/dvs_manager.go b/simulator/dvs_manager.go
new file mode 100644
index 00000000..8643027c
--- /dev/null
+++ b/simulator/dvs_manager.go
@@ -0,0 +1,51 @@
+/*
+   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 simulator
+
+import (
+	"github.com/vmware/govmomi/vim25/methods"
+	"github.com/vmware/govmomi/vim25/mo"
+	"github.com/vmware/govmomi/vim25/soap"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+type DistributedVirtualSwitchManager struct {
+	mo.DistributedVirtualSwitchManager
+}
+
+func (m *DistributedVirtualSwitchManager) DVSManagerLookupDvPortGroup(ctx *Context, req *types.DVSManagerLookupDvPortGroup) soap.HasFault {
+	body := &methods.DVSManagerLookupDvPortGroupBody{}
+
+	for _, obj := range ctx.Map.All("DistributedVirtualSwitch") {
+		dvs := obj.(*DistributedVirtualSwitch)
+		if dvs.Uuid == req.SwitchUuid {
+			for _, ref := range dvs.Portgroup {
+				pg := ctx.Map.Get(ref).(*DistributedVirtualPortgroup)
+				if pg.Key == req.PortgroupKey {
+					body.Res = &types.DVSManagerLookupDvPortGroupResponse{
+						Returnval: &ref,
+					}
+					return body
+				}
+			}
+		}
+	}
+
+	body.Fault_ = Fault("", new(types.NotFound))
+
+	return body
+}
diff --git a/simulator/environment_browser.go b/simulator/environment_browser.go
index 58816681..4f8ba8ed 100644
--- a/simulator/environment_browser.go
+++ b/simulator/environment_browser.go
@@ -28,6 +28,8 @@ import (
 
 type EnvironmentBrowser struct {
 	mo.EnvironmentBrowser
+
+	types.QueryConfigOptionResponse
 }
 
 func newEnvironmentBrowser() *types.ManagedObjectReference {
@@ -57,9 +59,12 @@ func (b *EnvironmentBrowser) hosts(ctx *Context) []types.ManagedObjectReference
 func (b *EnvironmentBrowser) QueryConfigOption(req *types.QueryConfigOption) soap.HasFault {
 	body := new(methods.QueryConfigOptionBody)
 
-	opt := &types.VirtualMachineConfigOption{
-		Version:       esx.HardwareVersion,
-		DefaultDevice: esx.VirtualDevice,
+	opt := b.QueryConfigOptionResponse.Returnval
+	if opt == nil {
+		opt = &types.VirtualMachineConfigOption{
+			Version:       esx.HardwareVersion,
+			DefaultDevice: esx.VirtualDevice,
+		}
 	}
 
 	body.Res = &types.QueryConfigOptionResponse{
@@ -85,9 +90,12 @@ func guestFamily(id string) string {
 func (b *EnvironmentBrowser) QueryConfigOptionEx(req *types.QueryConfigOptionEx) soap.HasFault {
 	body := new(methods.QueryConfigOptionExBody)
 
-	opt := &types.VirtualMachineConfigOption{
-		Version:       esx.HardwareVersion,
-		DefaultDevice: esx.VirtualDevice,
+	opt := b.QueryConfigOptionResponse.Returnval
+	if opt == nil {
+		opt = &types.VirtualMachineConfigOption{
+			Version:       esx.HardwareVersion,
+			DefaultDevice: esx.VirtualDevice,
+		}
 	}
 
 	if req.Spec != nil {
diff --git a/simulator/esx/event_manager.go b/simulator/esx/event_manager.go
index 7024b404..4fc4f4f9 100644
--- a/simulator/esx/event_manager.go
+++ b/simulator/esx/event_manager.go
@@ -233,6 +233,24 @@ var EventInfo = []types.EventDescriptionEventDetail{
 		Category:    "info",
 		FullFormat:  "Completed the relocation of the virtual machine",
 	},
+	{
+		Key:         "CustomizationFailed",
+		Description: "An error occurred during customization",
+		Category:    "info",
+		FullFormat:  "An error occurred during customization on VM {{.Vm.Name}}",
+	},
+	{
+		Key:         "CustomizationStartedEvent",
+		Description: "Started customization",
+		Category:    "info",
+		FullFormat:  "Started customization of VM {{.Vm.Name}}",
+	},
+	{
+		Key:         "CustomizationSucceeded",
+		Description: "Customization succeeded",
+		Category:    "info",
+		FullFormat:  "Customization of VM {{.Vm.Name}} succeeded",
+	},
 	{
 		Key:         "DrsVmMigratedEvent",
 		Description: "DRS VM migrated",
diff --git a/simulator/event_manager.go b/simulator/event_manager.go
index 49eda136..4d6905af 100644
--- a/simulator/event_manager.go
+++ b/simulator/event_manager.go
@@ -19,7 +19,6 @@ package simulator
 import (
 	"bytes"
 	"container/list"
-	"container/ring"
 	"log"
 	"reflect"
 	"text/template"
@@ -72,10 +71,11 @@ func (m *EventManager) createCollector(ctx *Context, req *types.CreateCollectorF
 
 	collector := &EventHistoryCollector{
 		m:    m,
-		page: ring.New(size),
+		page: list.New(),
+		size: size,
 	}
 	collector.Filter = req.Filter
-	collector.fillPage(size)
+	collector.fillPage()
 
 	return collector, nil
 }
@@ -148,6 +148,13 @@ func (m *EventManager) formatMessage(event types.BaseEvent) {
 	}
 }
 
+func pushEvent(l *list.List, event types.BaseEvent) {
+	if l.Len() > maxPageSize*5 {
+		l.Remove(l.Front()) // Prune history
+	}
+	l.PushBack(event)
+}
+
 func (m *EventManager) PostEvent(ctx *Context, req *types.PostEvent) soap.HasFault {
 	m.key++
 	event := req.EventToPost.GetEvent()
@@ -158,18 +165,12 @@ func (m *EventManager) PostEvent(ctx *Context, req *types.PostEvent) soap.HasFau
 
 	m.formatMessage(req.EventToPost)
 
-	if m.history.Len() > maxPageSize*5 {
-		// Prune history
-		m.history.Remove(m.history.Front())
-	}
-
-	m.history.PushBack(req.EventToPost)
+	pushEvent(m.history, req.EventToPost)
 
 	for _, c := range m.collectors {
 		ctx.WithLock(c, func() {
 			if c.eventMatches(req.EventToPost) {
-				c.page = c.page.Prev()
-				c.page.Value = req.EventToPost
+				pushEvent(c.page, req.EventToPost)
 				Map.Update(c, []types.PropertyChange{{Name: "latestPage", Val: c.GetLatestPage()}})
 			}
 		})
@@ -184,7 +185,8 @@ type EventHistoryCollector struct {
 	mo.EventHistoryCollector
 
 	m    *EventManager
-	page *ring.Ring
+	size int
+	page *list.List
 	pos  *list.Element
 }
 
@@ -331,21 +333,13 @@ func (c *EventHistoryCollector) eventMatches(event types.BaseEvent) bool {
 	return c.entityMatches(event, &spec)
 }
 
-// filePage copies the manager's latest events into the collector's page with Filter applied.
-func (c *EventHistoryCollector) fillPage(size int) {
-	matches := 0
-	page := c.page
-
-	for e := c.m.history.Back(); e != nil; e = e.Prev() {
+// fillPage copies the manager's latest events into the collector's page with Filter applied.
+func (c *EventHistoryCollector) fillPage() {
+	for e := c.m.history.Front(); e != nil; e = e.Next() {
 		event := e.Value.(types.BaseEvent)
 
 		if c.eventMatches(event) {
-			page.Value = event
-			page = page.Prev()
-			matches++
-			if matches == size {
-				break
-			}
+			c.page.PushBack(event)
 		}
 	}
 }
@@ -370,17 +364,16 @@ func (c *EventHistoryCollector) SetCollectorPageSize(ctx *Context, req *types.Se
 		return body
 	}
 
-	ctx.WithLock(c.m, func() {
-		c.page = ring.New(size)
-		c.fillPage(size)
-	})
+	c.size = size
+	c.page = list.New()
+	ctx.WithLock(c.m, c.fillPage)
 
 	body.Res = new(types.SetCollectorPageSizeResponse)
 	return body
 }
 
 func (c *EventHistoryCollector) ResetCollector(ctx *Context, req *types.ResetCollector) soap.HasFault {
-	c.pos = c.m.history.Back()
+	c.pos = c.page.Back()
 
 	return &methods.ResetCollectorBody{
 		Res: new(types.ResetCollectorResponse),
@@ -388,7 +381,7 @@ func (c *EventHistoryCollector) ResetCollector(ctx *Context, req *types.ResetCol
 }
 
 func (c *EventHistoryCollector) RewindCollector(ctx *Context, req *types.RewindCollector) soap.HasFault {
-	c.pos = c.m.history.Front()
+	c.pos = c.page.Front()
 
 	return &methods.RewindCollectorBody{
 		Res: new(types.RewindCollectorResponse),
@@ -424,7 +417,7 @@ func (c *EventHistoryCollector) ReadNextEvents(ctx *Context, req *types.ReadNext
 		if c.pos != nil {
 			return c.pos.Next()
 		}
-		return c.m.history.Front()
+		return c.page.Front()
 	}
 
 	body.Res.Returnval = c.readEvents(ctx, req.MaxCount, next)
@@ -444,7 +437,7 @@ func (c *EventHistoryCollector) ReadPreviousEvents(ctx *Context, req *types.Read
 		if c.pos != nil {
 			return c.pos.Prev()
 		}
-		return c.m.history.Back()
+		return c.page.Back()
 	}
 
 	body.Res.Returnval = c.readEvents(ctx, req.MaxCount, next)
@@ -467,12 +460,14 @@ func (c *EventHistoryCollector) DestroyCollector(ctx *Context, req *types.Destro
 func (c *EventHistoryCollector) GetLatestPage() []types.BaseEvent {
 	var latestPage []types.BaseEvent
 
-	c.page.Do(func(val interface{}) {
-		if val == nil {
-			return
+	e := c.page.Back()
+	for i := 0; i < c.size; i++ {
+		if e == nil {
+			break
 		}
-		latestPage = append(latestPage, val.(types.BaseEvent))
-	})
+		latestPage = append(latestPage, e.Value.(types.BaseEvent))
+		e = e.Prev()
+	}
 
 	return latestPage
 }
diff --git a/simulator/model.go b/simulator/model.go
index f238f858..3cdd6668 100644
--- a/simulator/model.go
+++ b/simulator/model.go
@@ -223,42 +223,43 @@ func (*Model) fmtName(prefix string, num int) string {
 
 // kinds maps managed object types to their vcsim wrapper types
 var kinds = map[string]reflect.Type{
-	"AuthorizationManager":           reflect.TypeOf((*AuthorizationManager)(nil)).Elem(),
-	"ClusterComputeResource":         reflect.TypeOf((*ClusterComputeResource)(nil)).Elem(),
-	"CustomFieldsManager":            reflect.TypeOf((*CustomFieldsManager)(nil)).Elem(),
-	"CustomizationSpecManager":       reflect.TypeOf((*CustomizationSpecManager)(nil)).Elem(),
-	"Datacenter":                     reflect.TypeOf((*Datacenter)(nil)).Elem(),
-	"Datastore":                      reflect.TypeOf((*Datastore)(nil)).Elem(),
-	"DistributedVirtualPortgroup":    reflect.TypeOf((*DistributedVirtualPortgroup)(nil)).Elem(),
-	"DistributedVirtualSwitch":       reflect.TypeOf((*DistributedVirtualSwitch)(nil)).Elem(),
-	"EnvironmentBrowser":             reflect.TypeOf((*EnvironmentBrowser)(nil)).Elem(),
-	"EventManager":                   reflect.TypeOf((*EventManager)(nil)).Elem(),
-	"FileManager":                    reflect.TypeOf((*FileManager)(nil)).Elem(),
-	"Folder":                         reflect.TypeOf((*Folder)(nil)).Elem(),
-	"GuestOperationsManager":         reflect.TypeOf((*GuestOperationsManager)(nil)).Elem(),
-	"HostDatastoreBrowser":           reflect.TypeOf((*HostDatastoreBrowser)(nil)).Elem(),
-	"HostLocalAccountManager":        reflect.TypeOf((*HostLocalAccountManager)(nil)).Elem(),
-	"HostNetworkSystem":              reflect.TypeOf((*HostNetworkSystem)(nil)).Elem(),
-	"HostSystem":                     reflect.TypeOf((*HostSystem)(nil)).Elem(),
-	"IpPoolManager":                  reflect.TypeOf((*IpPoolManager)(nil)).Elem(),
-	"LicenseManager":                 reflect.TypeOf((*LicenseManager)(nil)).Elem(),
-	"OptionManager":                  reflect.TypeOf((*OptionManager)(nil)).Elem(),
-	"OvfManager":                     reflect.TypeOf((*OvfManager)(nil)).Elem(),
-	"PerformanceManager":             reflect.TypeOf((*PerformanceManager)(nil)).Elem(),
-	"PropertyCollector":              reflect.TypeOf((*PropertyCollector)(nil)).Elem(),
-	"ResourcePool":                   reflect.TypeOf((*ResourcePool)(nil)).Elem(),
-	"SearchIndex":                    reflect.TypeOf((*SearchIndex)(nil)).Elem(),
-	"SessionManager":                 reflect.TypeOf((*SessionManager)(nil)).Elem(),
-	"StoragePod":                     reflect.TypeOf((*StoragePod)(nil)).Elem(),
-	"StorageResourceManager":         reflect.TypeOf((*StorageResourceManager)(nil)).Elem(),
-	"TaskManager":                    reflect.TypeOf((*TaskManager)(nil)).Elem(),
-	"UserDirectory":                  reflect.TypeOf((*UserDirectory)(nil)).Elem(),
-	"VcenterVStorageObjectManager":   reflect.TypeOf((*VcenterVStorageObjectManager)(nil)).Elem(),
-	"ViewManager":                    reflect.TypeOf((*ViewManager)(nil)).Elem(),
-	"VirtualApp":                     reflect.TypeOf((*VirtualApp)(nil)).Elem(),
-	"VirtualDiskManager":             reflect.TypeOf((*VirtualDiskManager)(nil)).Elem(),
-	"VirtualMachine":                 reflect.TypeOf((*VirtualMachine)(nil)).Elem(),
-	"VmwareDistributedVirtualSwitch": reflect.TypeOf((*DistributedVirtualSwitch)(nil)).Elem(),
+	"AuthorizationManager":            reflect.TypeOf((*AuthorizationManager)(nil)).Elem(),
+	"ClusterComputeResource":          reflect.TypeOf((*ClusterComputeResource)(nil)).Elem(),
+	"CustomFieldsManager":             reflect.TypeOf((*CustomFieldsManager)(nil)).Elem(),
+	"CustomizationSpecManager":        reflect.TypeOf((*CustomizationSpecManager)(nil)).Elem(),
+	"Datacenter":                      reflect.TypeOf((*Datacenter)(nil)).Elem(),
+	"Datastore":                       reflect.TypeOf((*Datastore)(nil)).Elem(),
+	"DistributedVirtualPortgroup":     reflect.TypeOf((*DistributedVirtualPortgroup)(nil)).Elem(),
+	"DistributedVirtualSwitch":        reflect.TypeOf((*DistributedVirtualSwitch)(nil)).Elem(),
+	"DistributedVirtualSwitchManager": reflect.TypeOf((*DistributedVirtualSwitchManager)(nil)).Elem(),
+	"EnvironmentBrowser":              reflect.TypeOf((*EnvironmentBrowser)(nil)).Elem(),
+	"EventManager":                    reflect.TypeOf((*EventManager)(nil)).Elem(),
+	"FileManager":                     reflect.TypeOf((*FileManager)(nil)).Elem(),
+	"Folder":                          reflect.TypeOf((*Folder)(nil)).Elem(),
+	"GuestOperationsManager":          reflect.TypeOf((*GuestOperationsManager)(nil)).Elem(),
+	"HostDatastoreBrowser":            reflect.TypeOf((*HostDatastoreBrowser)(nil)).Elem(),
+	"HostLocalAccountManager":         reflect.TypeOf((*HostLocalAccountManager)(nil)).Elem(),
+	"HostNetworkSystem":               reflect.TypeOf((*HostNetworkSystem)(nil)).Elem(),
+	"HostSystem":                      reflect.TypeOf((*HostSystem)(nil)).Elem(),
+	"IpPoolManager":                   reflect.TypeOf((*IpPoolManager)(nil)).Elem(),
+	"LicenseManager":                  reflect.TypeOf((*LicenseManager)(nil)).Elem(),
+	"OptionManager":                   reflect.TypeOf((*OptionManager)(nil)).Elem(),
+	"OvfManager":                      reflect.TypeOf((*OvfManager)(nil)).Elem(),
+	"PerformanceManager":              reflect.TypeOf((*PerformanceManager)(nil)).Elem(),
+	"PropertyCollector":               reflect.TypeOf((*PropertyCollector)(nil)).Elem(),
+	"ResourcePool":                    reflect.TypeOf((*ResourcePool)(nil)).Elem(),
+	"SearchIndex":                     reflect.TypeOf((*SearchIndex)(nil)).Elem(),
+	"SessionManager":                  reflect.TypeOf((*SessionManager)(nil)).Elem(),
+	"StoragePod":                      reflect.TypeOf((*StoragePod)(nil)).Elem(),
+	"StorageResourceManager":          reflect.TypeOf((*StorageResourceManager)(nil)).Elem(),
+	"TaskManager":                     reflect.TypeOf((*TaskManager)(nil)).Elem(),
+	"UserDirectory":                   reflect.TypeOf((*UserDirectory)(nil)).Elem(),
+	"VcenterVStorageObjectManager":    reflect.TypeOf((*VcenterVStorageObjectManager)(nil)).Elem(),
+	"ViewManager":                     reflect.TypeOf((*ViewManager)(nil)).Elem(),
+	"VirtualApp":                      reflect.TypeOf((*VirtualApp)(nil)).Elem(),
+	"VirtualDiskManager":              reflect.TypeOf((*VirtualDiskManager)(nil)).Elem(),
+	"VirtualMachine":                  reflect.TypeOf((*VirtualMachine)(nil)).Elem(),
+	"VmwareDistributedVirtualSwitch":  reflect.TypeOf((*DistributedVirtualSwitch)(nil)).Elem(),
 }
 
 func loadObject(content types.ObjectContent) (mo.Reference, error) {
diff --git a/simulator/virtual_disk_manager.go b/simulator/virtual_disk_manager.go
index 52e3c702..e597b8b5 100644
--- a/simulator/virtual_disk_manager.go
+++ b/simulator/virtual_disk_manager.go
@@ -187,6 +187,13 @@ func (m *VirtualDiskManager) CopyVirtualDiskTask(_ *Context, req *types.CopyVirt
 	}
 }
 
+func virtualDiskUUID(dc *types.ManagedObjectReference, file string) string {
+	if dc != nil {
+		file = dc.String() + file
+	}
+	return uuid.NewSHA1(uuid.NameSpaceOID, []byte(file)).String()
+}
+
 func (m *VirtualDiskManager) QueryVirtualDiskUuid(_ *Context, req *types.QueryVirtualDiskUuid) soap.HasFault {
 	body := new(methods.QueryVirtualDiskUuidBody)
 
@@ -206,7 +213,7 @@ func (m *VirtualDiskManager) QueryVirtualDiskUuid(_ *Context, req *types.QueryVi
 	}
 
 	body.Res = &types.QueryVirtualDiskUuidResponse{
-		Returnval: uuid.NewSHA1(uuid.NameSpaceOID, []byte(file)).String(),
+		Returnval: virtualDiskUUID(req.Datacenter, file),
 	}
 
 	return body
diff --git a/simulator/virtual_machine.go b/simulator/virtual_machine.go
index 3b53cafc..7eeba7b7 100644
--- a/simulator/virtual_machine.go
+++ b/simulator/virtual_machine.go
@@ -1143,6 +1143,7 @@ func (vm *VirtualMachine) configureDevice(devices object.VirtualDeviceList, spec
 						*prop = types.NewBool(false)
 					}
 				}
+				disk.Uuid = virtualDiskUUID(&dc.Self, info.FileName)
 			}
 		}
 	case *types.VirtualCdrom:
@@ -1414,7 +1415,7 @@ func (c *powerVMTask) Run(task *Task) (types.AnyType, types.BaseMethodFault) {
 			&types.VmStartingEvent{VmEvent: event},
 			&types.VmPoweredOnEvent{VmEvent: event},
 		)
-		c.customize()
+		c.customize(c.ctx)
 	case types.VirtualMachinePowerStatePoweredOff:
 		c.run.stop(c.VirtualMachine)
 		c.ctx.postEvent(
@@ -1822,11 +1823,14 @@ func (vm *VirtualMachine) RelocateVMTask(ctx *Context, req *types.RelocateVM_Tas
 	}
 }
 
-func (vm *VirtualMachine) customize() {
+func (vm *VirtualMachine) customize(ctx *Context) {
 	if vm.imc == nil {
 		return
 	}
 
+	event := types.CustomizationEvent{VmEvent: vm.event()}
+	ctx.postEvent(&types.CustomizationStartedEvent{CustomizationEvent: event})
+
 	changes := []types.PropertyChange{
 		{Name: "config.tools.pendingCustomization", Val: ""},
 	}
@@ -1892,6 +1896,7 @@ func (vm *VirtualMachine) customize() {
 
 	vm.imc = nil
 	Map.Update(vm, changes)
+	ctx.postEvent(&types.CustomizationSucceeded{CustomizationEvent: event})
 }
 
 func (vm *VirtualMachine) CustomizeVMTask(req *types.CustomizeVM_Task) soap.HasFault {
diff --git a/vcsim/main.go b/vcsim/main.go
index e4e210df..d4570c89 100644
--- a/vcsim/main.go
+++ b/vcsim/main.go
@@ -155,7 +155,7 @@ func main() {
 	}
 
 	tag := " (govmomi simulator)"
-	model.ServiceContent.About.Name += tag
+	model.ServiceContent.About.FullName += tag
 	model.ServiceContent.About.OsType = runtime.GOOS + "-" + runtime.GOARCH
 
 	esx.HostSystem.Summary.Hardware.Vendor += tag


Test patch:
diff --git a/govc/test/vcsim.bats b/govc/test/vcsim.bats
index 72bc9934..a72c71a2 100755
--- a/govc/test/vcsim.bats
+++ b/govc/test/vcsim.bats
@@ -57,6 +57,10 @@ EOF
   [ "$(jq .Cluster <<<"$model")" == "6" ]
   [ "$(jq .Machine <<<"$model")" == "0" ]
   [ "$(jq .Datastore <<<"$model")" == "0" ]
+
+  run govc about
+  assert_success
+  assert_matches "govmomi simulator"
 }
 
 @test "vcsim host placement" {
diff --git a/simulator/event_manager_test.go b/simulator/event_manager_test.go
index b95d38c5..5414a89b 100644
--- a/simulator/event_manager_test.go
+++ b/simulator/event_manager_test.go
@@ -171,13 +171,14 @@ func TestEventManagerRead(t *testing.T) {
 	tests := []struct {
 		max    int
 		rewind bool
+		order  bool
 		read   func(context.Context, int32) ([]types.BaseEvent, error)
 	}{
-		{nevents, true, c.ReadNextEvents},
-		{nevents / 3, true, c.ReadNextEvents},
-		{nevents * 3, false, c.ReadNextEvents},
-		{3, false, c.ReadPreviousEvents},
-		{nevents * 3, false, c.ReadNextEvents},
+		{nevents, true, true, c.ReadNextEvents},
+		{nevents / 3, true, true, c.ReadNextEvents},
+		{nevents * 3, false, true, c.ReadNextEvents},
+		{3, false, false, c.ReadPreviousEvents},
+		{nevents * 3, false, true, c.ReadNextEvents},
 	}
 
 	for _, test := range tests {
@@ -205,6 +206,21 @@ func TestEventManagerRead(t *testing.T) {
 		if count < len(page) {
 			t.Errorf("expected at least %d events, got: %d", len(page), count)
 		}
+
+		for i := 1; i < len(all); i++ {
+			prev := all[i-1].GetEvent().Key
+			key := all[i].GetEvent().Key
+			if test.order {
+				if prev > key {
+					t.Errorf("key %d > %d", prev, key)
+				}
+			} else {
+				if prev < key {
+					t.Errorf("key %d < %d", prev, key)
+				}
+			}
+		}
+
 		if test.rewind {
 			if err = c.Rewind(ctx); err != nil {
 				t.Error(err)