diff --git a/govc/object/collect.go b/govc/object/collect.go index 0689d3da5..972f1f337 100644 --- a/govc/object/collect.go +++ b/govc/object/collect.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2017-2023 VMware, Inc. All Rights Reserved. +Copyright (c) 2017-2024 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. @@ -416,7 +416,7 @@ func (cmd *collect) Run(ctx context.Context, f *flag.FlagSet) error { } res, err := p.RetrieveProperties(ctx, req) if err != nil { - return nil + return err } content := res.Returnval if len(content) != 1 { diff --git a/govc/test/object.bats b/govc/test/object.bats index bf5232070..2da45061b 100755 --- a/govc/test/object.bats +++ b/govc/test/object.bats @@ -411,6 +411,66 @@ EOF govc object.collect -O -json | jq . } +@test "object.collect index" { + vcsim_env + + export GOVC_VM=/DC0/vm/DC0_H0_VM0 + + # NOTE: '-o' flag uses RetrievePropertiesEx() and mo.ObjectContentToType() + # By default, WaitForUpdatesEx() is used with raw types.ObjectContent + + run govc object.collect -o $GOVC_VM 'config.hardware[4000]' + assert_failure + + run govc object.collect -o $GOVC_VM 'config.hardware.device[4000' + assert_failure + + run govc object.collect -o $GOVC_VM 'config.hardware.device["4000"]' + assert_failure # Key is int, not string + + run govc object.collect -o -json $GOVC_VM 'config.hardware.device[4000]' + assert_success + + run jq -r .config.hardware.device[].deviceInfo.label <<<"$output" + assert_success ethernet-0 + + run govc object.collect -o $GOVC_VM 'config.hardware.device[4000].enoent' + assert_failure # InvalidProperty + + run govc object.collect -o -json $GOVC_VM 'config.hardware.device[4000].deviceInfo.label' + assert_success + + run govc object.collect -s $GOVC_VM 'config.hardware.device[4000].deviceInfo.label' + assert_success ethernet-0 + + run govc object.collect -o $GOVC_VM 'config.extraConfig[guestinfo.a]' + assert_failure # string Key requires quotes + + run govc object.collect -o $GOVC_VM 'config["guestinfo.a"]' + assert_failure + + run govc object.collect -o $GOVC_VM 'config.extraConfig["guestinfo.a"]' + assert_success # Key does not exist, not an error + + run govc vm.change -e "guestinfo.a=1" -e "guestinfo.b=2" + assert_success + + run govc object.collect -json $GOVC_VM 'config.extraConfig["guestinfo.b"]' + assert_success + + run jq -r .[].val.value <<<"$output" + assert_success 2 + + run govc object.collect -o -json $GOVC_VM 'config.extraConfig["guestinfo.b"]' + assert_success + + run jq -r .config.extraConfig[].value <<<"$output" + assert_success 2 + + run govc object.collect -s $GOVC_VM 'config.extraConfig["guestinfo.b"].value' + assert_success 2 +} + @test "object.find" { vcsim_env -ds 2 diff --git a/object/extension_manager_test.go b/object/extension_manager_test.go new file mode 100644 index 000000000..639f8ce9b --- /dev/null +++ b/object/extension_manager_test.go @@ -0,0 +1,121 @@ +/* +Copyright (c) 2024-2024 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_test + +import ( + "context" + "reflect" + "sync" + "testing" + + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/property" + "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/types" +) + +func TestExtensionMangerUpdates(t *testing.T) { + extension := types.Extension{ + Description: &types.Description{ + Label: "govmomi-test", + Summary: "Extension Manager test", + }, + Key: t.Name(), + Version: "0.0.1", + ShownInSolutionManager: types.NewBool(false), + } + + description := extension.Description.GetDescription() + + f := func(item string) string { + return (&mo.Field{Path: "extensionList", Key: extension.Key, Item: item}).String() + } + + tests := []types.PropertyChange{ + {Name: f(""), Val: extension, Op: types.PropertyChangeOpAdd}, + {Name: f(""), Val: extension, Op: types.PropertyChangeOpAssign}, + {Name: f("description"), Val: *description, Op: types.PropertyChangeOpAssign}, + {Name: f("description.label"), Val: description.Label, Op: types.PropertyChangeOpAssign}, + {Name: f(""), Val: nil, Op: types.PropertyChangeOpRemove}, + } + + simulator.Test(func(ctx context.Context, c *vim25.Client) { + m := object.NewExtensionManager(c) + pc := property.DefaultCollector(c) + + for _, test := range tests { + t.Logf("%s: %s", test.Op, test.Name) + update := make(chan bool) + parked := sync.OnceFunc(func() { update <- true }) + + var change *types.PropertyChange + cb := func(p []types.PropertyChange) bool { + parked() + change = &p[0] + if change.Op != test.Op { + t.Logf("ignore: change Op=%s, test Op=%s", change.Op, test.Op) + return false + } + return true + } + + go func() { + werr := property.Wait(ctx, pc, m.Reference(), []string{test.Name}, cb) + if werr != nil { + t.Log(werr) + } + update <- true + }() + <-update // wait until above go func is parked in WaitForUpdatesEx() + + switch test.Op { + case types.PropertyChangeOpAdd: + if err := m.Register(ctx, extension); err != nil { + t.Fatal(err) + } + case types.PropertyChangeOpAssign: + if err := m.Update(ctx, extension); err != nil { + t.Fatal(err) + } + case types.PropertyChangeOpRemove: + if err := m.Unregister(ctx, extension.Key); err != nil { + t.Fatal(err) + } + } + <-update // wait until update is received (cb returns true) + + if change == nil { + t.Fatal("no change") + } + + if change.Name != test.Name { + t.Errorf("Name: %s", change.Name) + } + + if change.Op != test.Op { + t.Errorf("Op: %s", change.Op) + } + + if !reflect.DeepEqual(change.Val, test.Val) { + t.Errorf("change.Val: %#v", change.Val) + t.Errorf("test.Val: %#v", test.Val) + } + } + }) +} diff --git a/simulator/extension_manager.go b/simulator/extension_manager.go index 6b72ae0fd..84b7c6b0f 100644 --- a/simulator/extension_manager.go +++ b/simulator/extension_manager.go @@ -97,6 +97,12 @@ func (m *ExtensionManager) RegisterExtension(ctx *Context, req *types.RegisterEx body.Res = new(types.RegisterExtensionResponse) m.ExtensionList = append(m.ExtensionList, req.Extension) + f := mo.Field{Path: "extensionList", Key: req.Extension.Key} + ctx.Map.Update(m, []types.PropertyChange{ + {Name: f.Path, Val: m.ExtensionList}, + {Name: f.String(), Val: req.Extension, Op: types.PropertyChangeOpAdd}, + }) + return body } @@ -107,6 +113,12 @@ func (m *ExtensionManager) UnregisterExtension(ctx *Context, req *types.Unregist if x.Key == req.ExtensionKey { m.ExtensionList = append(m.ExtensionList[:i], m.ExtensionList[i+1:]...) + f := mo.Field{Path: "extensionList", Key: req.ExtensionKey} + ctx.Map.Update(m, []types.PropertyChange{ + {Name: f.Path, Val: m.ExtensionList}, + {Name: f.String(), Op: types.PropertyChangeOpRemove}, + }) + body.Res = new(types.UnregisterExtensionResponse) return body } @@ -124,6 +136,12 @@ func (m *ExtensionManager) UpdateExtension(ctx *Context, req *types.UpdateExtens if x.Key == req.Extension.Key { m.ExtensionList[i] = req.Extension + f := mo.Field{Path: "extensionList", Key: req.Extension.Key} + ctx.Map.Update(m, []types.PropertyChange{ + {Name: f.Path, Val: m.ExtensionList}, + {Name: f.String(), Val: req.Extension}, + }) + body.Res = new(types.UpdateExtensionResponse) return body } diff --git a/simulator/property_collector.go b/simulator/property_collector.go index c7c608957..b50fc9a91 100644 --- a/simulator/property_collector.go +++ b/simulator/property_collector.go @@ -157,15 +157,19 @@ func wrapValue(rval reflect.Value, rtype reflect.Type) interface{} { return pval } -func fieldValueInterface(f reflect.StructField, rval reflect.Value) interface{} { +func fieldValueInterface(f reflect.StructField, rval reflect.Value, keyed ...bool) interface{} { if rval.Kind() == reflect.Ptr { rval = rval.Elem() } + if len(keyed) == 1 && keyed[0] { + return rval.Interface() // don't wrap keyed fields in ArrayOf* type + } + return wrapValue(rval, f.Type) } -func fieldValue(rval reflect.Value, p string) (interface{}, error) { +func fieldValue(rval reflect.Value, p string, keyed ...bool) (interface{}, error) { var value interface{} fields := strings.Split(p, ".") @@ -204,7 +208,7 @@ func fieldValue(rval reflect.Value, p string) (interface{}, error) { if i == len(fields)-1 { ftype, _ := rval.Type().FieldByName(x) - value = fieldValueInterface(ftype, val) + value = fieldValueInterface(ftype, val, keyed...) break } @@ -214,6 +218,64 @@ func fieldValue(rval reflect.Value, p string) (interface{}, error) { return value, nil } +func fieldValueKey(rval reflect.Value, p mo.Field) (interface{}, error) { + if rval.Kind() != reflect.Slice { + return nil, errInvalidField + } + + zero := reflect.Value{} + + for i := 0; i < rval.Len(); i++ { + item := rval.Index(i) + if item.Kind() == reflect.Interface { + item = item.Elem() + } + if item.Kind() == reflect.Ptr { + item = item.Elem() + } + if item.Kind() != reflect.Struct { + return reflect.Value{}, errInvalidField + } + + field := item.FieldByName("Key") + if field == zero { + return nil, errInvalidField + } + + switch key := field.Interface().(type) { + case string: + s, ok := p.Key.(string) + if !ok { + return nil, errInvalidField + } + if s == key { + return item.Interface(), nil + } + case int32: + s, ok := p.Key.(int32) + if !ok { + return nil, errInvalidField + } + if s == key { + return item.Interface(), nil + } + default: + return nil, errInvalidField + } + } + + return nil, nil +} + +func fieldValueIndex(rval reflect.Value, p mo.Field) (interface{}, error) { + val, err := fieldValueKey(rval, p) + if err != nil || val == nil || p.Item == "" { + return val, err + } + + return fieldValue(reflect.ValueOf(val), p.Item) +} + func fieldRefs(f interface{}) []types.ManagedObjectReference { switch fv := f.(type) { case types.ManagedObjectReference: @@ -329,7 +391,19 @@ func (rr *retrieveResult) collectFields(ctx *Context, rval reflect.Value, fields } seen[name] = true - val, err := fieldValue(rval, name) + var val interface{} + var err error + var field mo.Field + if field.FromString(name) { + keyed := field.Key != nil + + val, err = fieldValue(rval, field.Path, keyed) + if err == nil && keyed { + val, err = fieldValueIndex(reflect.ValueOf(val), field) + } + } else { + err = errInvalidField + } switch err { case nil, errEmptyField: diff --git a/simulator/property_filter.go b/simulator/property_filter.go index b84c85e7d..9a4960559 100644 --- a/simulator/property_filter.go +++ b/simulator/property_filter.go @@ -1,11 +1,11 @@ /* -Copyright (c) 2017 VMware, Inc. All Rights Reserved. +Copyright (c) 2017-2024 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 +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, @@ -70,6 +70,20 @@ func (f *PropertyFilter) matches(ctx *Context, ref types.ManagedObjectReference, return true } + var field mo.Field + if field.FromString(name) && field.Item != "" { + // "field[key].item" -> "field[key]" + field.Item = "" + if field.String() == change.Name { + change.Name = name + return true + } + } + + if field.FromString(change.Name) && field.Key != nil { + continue // case below does not apply to property index + } + // strings.HasPrefix("runtime.powerState", "runtime") == parent field matches if strings.HasPrefix(change.Name, name) { if obj := ctx.Map.Get(ref); obj != nil { // object may have since been deleted diff --git a/simulator/virtual_machine.go b/simulator/virtual_machine.go index 6fd4cc851..7c1ad3b36 100644 --- a/simulator/virtual_machine.go +++ b/simulator/virtual_machine.go @@ -429,12 +429,19 @@ func extraConfigKey(key string) string { } func (vm *VirtualMachine) applyExtraConfig(ctx *Context, spec *types.VirtualMachineConfigSpec) types.BaseMethodFault { + if len(spec.ExtraConfig) == 0 { + return nil + } var removedContainerBacking bool var changes []types.PropertyChange + field := mo.Field{Path: "config.extraConfig"} + for _, c := range spec.ExtraConfig { val := c.GetOptionValue() key := strings.TrimPrefix(extraConfigKey(val.Key), "SET.") if key == val.Key { + field.Key = key + op := types.PropertyChangeOpAssign keyIndex := -1 for i := range vm.Config.ExtraConfig { bov := vm.Config.ExtraConfig[i] @@ -451,26 +458,28 @@ func (vm *VirtualMachine) applyExtraConfig(ctx *Context, spec *types.VirtualMach } } if keyIndex < 0 { + op = types.PropertyChangeOpAdd vm.Config.ExtraConfig = append(vm.Config.ExtraConfig, c) } else { if s, ok := val.Value.(string); ok && s == "" { + op = types.PropertyChangeOpRemove if key == ContainerBackingOptionKey { removedContainerBacking = true } // Remove existing element - l := len(vm.Config.ExtraConfig) - vm.Config.ExtraConfig[keyIndex] = vm.Config.ExtraConfig[l-1] - vm.Config.ExtraConfig[l-1] = nil - vm.Config.ExtraConfig = vm.Config.ExtraConfig[:l-1] + vm.Config.ExtraConfig = append( + vm.Config.ExtraConfig[:keyIndex], + vm.Config.ExtraConfig[keyIndex+1:]...) + val = nil } else { // Update existing element - vm.Config.ExtraConfig[keyIndex].GetOptionValue().Value = val.Value + vm.Config.ExtraConfig[keyIndex] = val } } + changes = append(changes, types.PropertyChange{Name: field.String(), Val: val, Op: op}) continue } - changes = append(changes, types.PropertyChange{Name: key, Val: val.Value}) switch key { case "guest.ipAddress": @@ -529,9 +538,8 @@ func (vm *VirtualMachine) applyExtraConfig(ctx *Context, spec *types.VirtualMach } } - if len(changes) != 0 { - Map.Update(vm, changes) - } + change := types.PropertyChange{Name: field.Path, Val: vm.Config.ExtraConfig} + ctx.Map.Update(vm, append(changes, change)) return fault } @@ -1586,6 +1594,8 @@ func (vm *VirtualMachine) genVmdkPath(p object.DatastorePath) (string, types.Bas } func (vm *VirtualMachine) configureDevices(ctx *Context, spec *types.VirtualMachineConfigSpec) types.BaseMethodFault { + var changes []types.PropertyChange + field := mo.Field{Path: "config.hardware.device"} devices := object.VirtualDeviceList(vm.Config.Hardware.Device) var err types.BaseMethodFault @@ -1593,6 +1603,7 @@ func (vm *VirtualMachine) configureDevices(ctx *Context, spec *types.VirtualMach dspec := change.GetVirtualDeviceConfigSpec() device := dspec.Device.GetVirtualDevice() invalid := &types.InvalidDeviceSpec{DeviceIndex: int32(i)} + change := types.PropertyChange{} switch dspec.FileOperation { case types.VirtualDeviceConfigSpecFileOperationCreate: @@ -1606,6 +1617,8 @@ func (vm *VirtualMachine) configureDevices(ctx *Context, spec *types.VirtualMach switch dspec.Operation { case types.VirtualDeviceConfigSpecOperationAdd: + change.Op = types.PropertyChangeOpAdd + if devices.FindByKey(device.Key) != nil && device.ControllerKey == 0 { // Note: real ESX does not allow adding base controllers (ControllerKey = 0) // after VM is created (returns success but device is not added). @@ -1659,13 +1672,20 @@ func (vm *VirtualMachine) configureDevices(ctx *Context, spec *types.VirtualMach devices = append(devices, dspec.Device) case types.VirtualDeviceConfigSpecOperationRemove: + change.Op = types.PropertyChangeOpRemove + devices = vm.removeDevice(ctx, devices, dspec) } + + field.Key = device.Key + change.Name = field.String() + changes = append(changes, change) } - ctx.Map.Update(vm, []types.PropertyChange{ - {Name: "config.hardware.device", Val: []types.BaseVirtualDevice(devices)}, - }) + if len(changes) != 0 { + change := types.PropertyChange{Name: field.Path, Val: []types.BaseVirtualDevice(devices)} + ctx.Map.Update(vm, append(changes, change)) + } err = vm.updateDiskLayouts() if err != nil { diff --git a/vim25/mo/retrieve.go b/vim25/mo/retrieve.go index 9f2b32486..66a8a9782 100644 --- a/vim25/mo/retrieve.go +++ b/vim25/mo/retrieve.go @@ -1,11 +1,11 @@ /* -Copyright (c) 2014-2015 VMware, Inc. All Rights Reserved. +Copyright (c) 2014-2024 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 +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, @@ -75,16 +75,19 @@ func ApplyPropertyChange(obj Reference, changes []types.PropertyChange) { v := reflect.ValueOf(obj) for _, p := range changes { - rv, ok := t.props[p.Name] + var field Field + if !field.FromString(p.Name) { + panic(p.Name + ": invalid property path") + } + + rv, ok := t.props[field.Path] if !ok { - // For now, skip unknown properties allowing PC updates to be triggered - // for partial updates (e.g. extensionList["my.extension"]). - // Ultimately we should support partial updates by assigning the value - // reflectively in assignValue. - continue + panic(field.Path + ": property not found") } - assignValue(v, rv, reflect.ValueOf(p.Val)) + if field.Key == nil { // Key is only used for notifications + assignValue(v, rv, reflect.ValueOf(p.Val)) + } } } diff --git a/vim25/mo/type_info.go b/vim25/mo/type_info.go index 3b1ccce2d..21f59291e 100644 --- a/vim25/mo/type_info.go +++ b/vim25/mo/type_info.go @@ -1,11 +1,11 @@ /* -Copyright (c) 2014 VMware, Inc. All Rights Reserved. +Copyright (c) 2014-2024 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 +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, @@ -20,6 +20,7 @@ import ( "fmt" "reflect" "regexp" + "strconv" "strings" "sync" @@ -34,6 +35,9 @@ type typeInfo struct { // Map property names to field indices. props map[string][]int + + // Use base type for interface indices. + base bool } var typeInfoLock sync.RWMutex @@ -62,12 +66,22 @@ func typeInfoForType(tname string) *typeInfo { return ti } -func newTypeInfo(typ reflect.Type) *typeInfo { +func baseType(ftyp reflect.Type) reflect.Type { + base := strings.TrimPrefix(ftyp.Name(), "Base") + if kind, ok := types.TypeFunc()(base); ok { + return kind + } + return ftyp +} + +func newTypeInfo(typ reflect.Type, base ...bool) *typeInfo { t := typeInfo{ typ: typ, props: make(map[string][]int), } - + if len(base) == 1 { + t.base = base[0] + } t.build(typ, "", []int{}) return &t @@ -155,6 +169,15 @@ func (t *typeInfo) build(typ reflect.Type, fn string, fi []int) { if ftyp.Kind() == reflect.Struct { t.build(ftyp, fnc, fic) } + + // Indexed property path may traverse into array element fields. + // When interface, use the base type to index fields. + // For example, BaseVirtualDevice: + // config.hardware.device[4000].deviceInfo.label + if t.base && ftyp.Kind() == reflect.Interface { + base := baseType(ftyp) + t.build(base, fnc, fic) + } } } @@ -164,7 +187,14 @@ var nilValue reflect.Value // slice of field indices. It recurses into the struct until it finds the field // specified by the indices. It creates new values for pointer types where // needed. -func assignValue(val reflect.Value, fi []int, pv reflect.Value) { +func assignValue(val reflect.Value, fi []int, pv reflect.Value, field ...string) { + // Indexed property path can only use base types + if val.Kind() == reflect.Interface { + base := baseType(val.Type()) + val.Set(reflect.New(base)) + val = val.Elem() + } + // Create new value if necessary. if val.Kind() == reflect.Ptr { if val.IsNil() { @@ -230,6 +260,43 @@ func assignValue(val reflect.Value, fi []int, pv reflect.Value) { rv.Set(pv) } else if rt.ConvertibleTo(pt) { rv.Set(pv.Convert(rt)) + } else if rt.Kind() == reflect.Slice { + // Indexed array value + path := field[0] + isInterface := rt.Elem().Kind() == reflect.Interface + + if len(path) == 0 { + // Append item (pv) directly to the array, converting to pointer if interface + if isInterface { + npv := reflect.New(pt) + npv.Elem().Set(pv) + pv = npv + pt = pv.Type() + } + } else { + // Construct item to be appended to the array, setting field within to value of pv + var item reflect.Value + if isInterface { + base := baseType(rt.Elem()) + item = reflect.New(base) + } else { + item = reflect.New(rt.Elem()) + } + + field := newTypeInfo(item.Type(), true) + if ix, ok := field.props[path]; ok { + assignValue(item, ix, pv) + } + + if rt.Elem().Kind() == reflect.Struct { + pv = item.Elem() + } else { + pv = item + } + pt = pv.Type() + } + + rv.Set(reflect.Append(rv, pv)) } else { panic(fmt.Sprintf("cannot assign %q (%s) to %q (%s)", rt.Name(), rt.Kind(), pt.Name(), pt.Kind())) } @@ -237,7 +304,7 @@ func assignValue(val reflect.Value, fi []int, pv reflect.Value) { return } - assignValue(rv, fi, pv) + assignValue(rv, fi, pv, field...) } var arrayOfRegexp = regexp.MustCompile("ArrayOf(.*)$") @@ -250,11 +317,14 @@ func (t *typeInfo) LoadFromObjectContent(o types.ObjectContent) (reflect.Value, assignValue(v, t.self, reflect.ValueOf(o.Obj)) for _, p := range o.PropSet { - rv, ok := t.props[p.Name] + var field Field + field.FromString(p.Name) + + rv, ok := t.props[field.Path] if !ok { continue } - assignValue(v, rv, reflect.ValueOf(p.Val)) + assignValue(v, rv, reflect.ValueOf(p.Val), field.Item) } return v, nil @@ -264,3 +334,70 @@ func IsManagedObjectType(kind string) bool { _, ok := t[kind] return ok } + +// Field of a ManagedObject in string form. +type Field struct { + Path string + Key any + Item string +} + +func (f *Field) String() string { + if f.Key == nil { + return f.Path + } + + var key, item string + + switch f.Key.(type) { + case string: + key = fmt.Sprintf("%q", f.Key) + default: + key = fmt.Sprintf("%d", f.Key) + } + + if f.Item != "" { + item = "." + f.Item + } + + return fmt.Sprintf("%s[%s]%s", f.Path, key, item) +} + +func (f *Field) FromString(spec string) bool { + s := strings.SplitN(spec, "[", 2) + f.Path = s[0] + f.Key = nil + f.Item = "" + if len(s) == 1 { + return true + } + + parts := strings.SplitN(s[1], "]", 2) + + if len(parts) != 2 { + return false + } + + ix := strings.Trim(parts[0], `"`) + + if ix == parts[0] { + v, err := strconv.ParseInt(ix, 0, 32) + if err != nil { + return false + } + f.Key = int32(v) + } else { + f.Key = ix + } + + if parts[1] == "" { + return true + } + + if parts[1][0] != '.' { + return false + } + f.Item = parts[1][1:] + + return true +} diff --git a/vim25/mo/type_info_test.go b/vim25/mo/type_info_test.go index 51fc7c484..531168220 100644 --- a/vim25/mo/type_info_test.go +++ b/vim25/mo/type_info_test.go @@ -1,11 +1,11 @@ /* -Copyright (c) 2014 VMware, Inc. All Rights Reserved. +Copyright (c) 2014-2024 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 +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, @@ -49,3 +49,31 @@ func BenchmarkLoadVirtualMachine(b *testing.B) { newTypeInfo(vmtyp) } } + +func TestPropertyPathFromString(t *testing.T) { + tests := []struct { + path string + expect *Field + }{ + {`foo.bar`, &Field{Path: "foo.bar"}}, + {`foo.bar["biz"]`, &Field{Path: "foo.bar", Key: "biz"}}, + {`foo.bar["biz"].baz`, &Field{Path: "foo.bar", Key: "biz", Item: "baz"}}, + {`foo.bar[0]`, &Field{Path: "foo.bar", Key: int32(0)}}, + {`foo.bar[1].baz`, &Field{Path: "foo.bar", Key: int32(1), Item: "baz"}}, + {`foo.bar[1].baz.buz`, &Field{Path: "foo.bar", Key: int32(1), Item: "baz.buz"}}, + } + + for i, tp := range tests { + var field Field + if field.FromString(tp.path) { + if field.String() != tp.expect.String() { + t.Errorf("%d: %s != %s", i, field, tp.expect) + } + if field != *tp.expect { + t.Errorf("%d: %#v != %#v", i, field, *tp.expect) + } + } else { + t.Error(tp.path) + } + } +}