diff --git a/cli/object/save.go b/cli/object/save.go index f43edb44d..42478f2a8 100644 --- a/cli/object/save.go +++ b/cli/object/save.go @@ -45,6 +45,7 @@ type save struct { verbose bool recurse bool one bool + license bool kind kinds summary map[string]int } @@ -60,6 +61,7 @@ func (cmd *save) Register(ctx context.Context, f *flag.FlagSet) { f.BoolVar(&cmd.one, "1", false, "Save ROOT only, without its children") f.StringVar(&cmd.dir, "d", "", "Save objects in directory") f.BoolVar(&cmd.force, "f", false, "Remove existing object directory") + f.BoolVar(&cmd.license, "l", false, "Include license properties") f.BoolVar(&cmd.recurse, "r", true, "Include children of the container view root") f.Var(&cmd.kind, "type", "Resource types to save. Defaults to all types") f.BoolVar(&cmd.verbose, "v", false, "Verbose output") @@ -168,6 +170,14 @@ func saveAlarmManager(ctx context.Context, c *vim25.Client, ref types.ManagedObj return []saveMethod{{"GetAlarm", res}, {"", content}}, nil } +func saveLicenseAssignmentManager(ctx context.Context, c *vim25.Client, ref types.ManagedObjectReference) ([]saveMethod, error) { + res, err := methods.QueryAssignedLicenses(ctx, c, &types.QueryAssignedLicenses{This: ref}) + if err != nil { + return nil, err + } + return []saveMethod{{"QueryAssignedLicenses", 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, @@ -175,6 +185,7 @@ var saveObjects = map[string]func(context.Context, *vim25.Client, types.ManagedO "HostNetworkSystem": saveHostNetworkSystem, "HostSystem": saveHostSystem, "AlarmManager": saveAlarmManager, + "LicenseAssignmentManager": saveLicenseAssignmentManager, } func (cmd *save) save(content []types.ObjectContent) error { @@ -226,6 +237,10 @@ func (cmd *save) save(content []types.ObjectContent) error { } func (cmd *save) Run(ctx context.Context, f *flag.FlagSet) error { + if f.NArg() > 1 { + return flag.ErrHelp + } + cmd.summary = make(map[string]int) c, err := cmd.Client() if err != nil { @@ -291,25 +306,35 @@ func (cmd *save) Run(ctx context.Context, f *flag.FlagSet) error { for _, p := range content[0].PropSet { if c, ok := p.Val.(types.ServiceContent); ok { var path []string + var selectSet []types.BaseSelectionSpec + var propSet []types.PropertySpec for _, ref := range mo.References(c) { all := types.NewBool(true) switch ref.Type { case "LicenseManager": - // avoid saving "licenses" property as it includes the keys - path = []string{"licenseAssignmentManager"} - all = nil + selectSet = []types.BaseSelectionSpec{&types.TraversalSpec{ + Type: ref.Type, + Path: "licenseAssignmentManager", + }} + propSet = []types.PropertySpec{{Type: "LicenseAssignmentManager", All: all}} + // avoid saving "licenses" property by default as it includes the keys + if cmd.license == false { + path = []string{selectSet[0].(*types.TraversalSpec).Path} + all, selectSet, propSet = nil, nil, nil + } case "ServiceManager": all = nil } req.SpecSet = append(req.SpecSet, types.PropertyFilterSpec{ ObjectSet: []types.ObjectSpec{{ - Obj: ref, + Obj: ref, + SelectSet: selectSet, }}, - PropSet: []types.PropertySpec{{ + PropSet: append(propSet, types.PropertySpec{ Type: ref.Type, All: all, PathSet: path, - }}, + }), }) } break diff --git a/govc/USAGE.md b/govc/USAGE.md index 0f03033f8..b8ffcdcdc 100644 --- a/govc/USAGE.md +++ b/govc/USAGE.md @@ -5000,6 +5000,7 @@ Options: -d= Save objects in directory -f=false Remove existing object directory -folder= Inventory folder [GOVC_FOLDER] + -l=false Include license properties -r=true Include children of the container view root -type=[] Resource types to save. Defaults to all types -v=false Verbose output diff --git a/govc/test/license.bats b/govc/test/license.bats index e50dd6aa7..50c15ee07 100755 --- a/govc/test/license.bats +++ b/govc/test/license.bats @@ -66,11 +66,18 @@ get_nlabel() { assert_success # Expect the test instance to run in evaluation mode - assert_equal "Evaluation Mode" "$(get_key 00000-00000-00000-00000-00000 <<<$output | jq -r ".name")" + mode="$(get_key 00000-00000-00000-00000-00000 <<<"$output" | jq -r ".name")" + assert_equal "Evaluation Mode" "$mode" + + name=$(jq -r '.[].properties[] | select(.key == "ProductName") | .value' <<<"$output") + assert_equal "$(govc about -json | jq -r .about.licenseProductName)" "$name" + + name=$(jq -r '.[].properties[] | select(.key == "ProductVersion") | .value' <<<"$output") + assert_equal "$(govc about -json | jq -r .about.licenseProductVersion)" "$name" } @test "license.decode" { - esx_env + vcsim_env verify_evaluation diff --git a/govc/test/object.bats b/govc/test/object.bats index 1b6ea7fd3..8330b8936 100755 --- a/govc/test/object.bats +++ b/govc/test/object.bats @@ -734,6 +734,20 @@ EOF n=$(ls "$dir"/*.xml | wc -l) rm -rf "$dir" assert_equal 10 "$n" + + run govc object.save -v -d "$dir" + assert_success + + n=$(ls "$dir"/*License*.xml | wc -l) + rm -rf "$dir" + assert_equal 1 "$n" # LicenseManager + + run govc object.save -l -v -d "$dir" + assert_success + + n=$(ls "$dir"/*License*.xml | wc -l) + rm -rf "$dir" + assert_equal 2 "$n" # LicenseManager + LicenseAssignmentManager } @test "tree" { diff --git a/simulator/license_manager.go b/simulator/license_manager.go index 749215955..0546cf8de 100644 --- a/simulator/license_manager.go +++ b/simulator/license_manager.go @@ -17,6 +17,8 @@ limitations under the License. package simulator import ( + "slices" + "github.com/vmware/govmomi/vim25/methods" "github.com/vmware/govmomi/vim25/mo" "github.com/vmware/govmomi/vim25/soap" @@ -51,7 +53,23 @@ type LicenseManager struct { } func (m *LicenseManager) init(r *Registry) { - m.Licenses = []types.LicenseManagerLicenseInfo{EvalLicense} + if len(m.Licenses) == 0 { + about := r.content().About + product := []types.KeyAnyValue{ + { + Key: "ProductName", + Value: about.LicenseProductName, + }, + { + Key: "ProductVersion", + Value: about.LicenseProductVersion, + }, + } + + EvalLicense.Properties = append(EvalLicense.Properties, product...) + + m.Licenses = []types.LicenseManagerLicenseInfo{EvalLicense} + } if r.IsVPX() { if m.LicenseAssignmentManager == nil { @@ -60,9 +78,16 @@ func (m *LicenseManager) init(r *Registry) { Value: "LicenseAssignmentManager", } } - r.Put(&LicenseAssignmentManager{ - mo.LicenseAssignmentManager{Self: *m.LicenseAssignmentManager}, - }) + + lam := new(LicenseAssignmentManager) + lam.Self = *m.LicenseAssignmentManager + lam.QueryAssignedLicensesResponse.Returnval = []types.LicenseAssignmentManagerLicenseAssignment{{ + EntityId: r.content().About.InstanceUuid, + EntityDisplayName: "vcsim", + AssignedLicense: EvalLicense, + }} + r.Put(lam) + r.AddHandler(lam) } } @@ -102,6 +127,21 @@ func (m *LicenseManager) RemoveLicense(ctx *Context, req *types.RemoveLicense) s return body } +func (m *LicenseManager) DecodeLicense(ctx *Context, req *types.DecodeLicense) soap.HasFault { + body := &methods.DecodeLicenseBody{ + Res: &types.DecodeLicenseResponse{}, + } + + for _, license := range m.Licenses { + if req.LicenseKey == license.LicenseKey { + body.Res.Returnval = license + break + } + } + + return body +} + func (m *LicenseManager) UpdateLicenseLabel(ctx *Context, req *types.UpdateLicenseLabel) soap.HasFault { body := &methods.UpdateLicenseLabelBody{} @@ -141,6 +181,55 @@ func (m *LicenseManager) UpdateLicenseLabel(ctx *Context, req *types.UpdateLicen type LicenseAssignmentManager struct { mo.LicenseAssignmentManager + + types.QueryAssignedLicensesResponse +} + +var licensedTypes = []string{"HostSystem", "ClusterComputeResource"} + +// PutObject assigns a license when a host or cluster is created. +func (m *LicenseAssignmentManager) PutObject(obj mo.Reference) { + ref := obj.Reference() + + if !slices.Contains(licensedTypes, ref.Type) { + return + } + + if slices.ContainsFunc(m.QueryAssignedLicensesResponse.Returnval, + func(am types.LicenseAssignmentManagerLicenseAssignment) bool { + return am.EntityId == ref.Value + }) { + return // via vcsim -load + } + + la := types.LicenseAssignmentManagerLicenseAssignment{ + EntityId: ref.Value, + Scope: Map.content().About.InstanceUuid, + EntityDisplayName: obj.(mo.Entity).Entity().Name, + AssignedLicense: EvalLicense, + } + + m.QueryAssignedLicensesResponse.Returnval = + append(m.QueryAssignedLicensesResponse.Returnval, la) +} + +// RemoveObject removes the license assignment when a host or cluster is removed. +func (m *LicenseAssignmentManager) RemoveObject(ctx *Context, ref types.ManagedObjectReference) { + if !slices.Contains(licensedTypes, ref.Type) { + return + } + + m.QueryAssignedLicensesResponse.Returnval = + slices.DeleteFunc(m.QueryAssignedLicensesResponse.Returnval, + func(am types.LicenseAssignmentManagerLicenseAssignment) bool { + return am.EntityId == ref.Value + }) +} + +func (*LicenseAssignmentManager) UpdateObject(*Context, mo.Reference, []types.PropertyChange) {} + +func (m *LicenseAssignmentManager) init(r *Registry) { + r.AddHandler(m) } func (m *LicenseAssignmentManager) QueryAssignedLicenses(ctx *Context, req *types.QueryAssignedLicenses) soap.HasFault { @@ -148,35 +237,52 @@ func (m *LicenseAssignmentManager) QueryAssignedLicenses(ctx *Context, req *type Res: &types.QueryAssignedLicensesResponse{}, } - // EntityId can be a HostSystem or the vCenter InstanceUuid - if req.EntityId != "" { - if req.EntityId != ctx.Map.content().About.InstanceUuid { - id := types.ManagedObjectReference{ - Type: "HostSystem", - Value: req.EntityId, - } - - if ctx.Map.Get(id) == nil { - return body + if req.EntityId == "" { + body.Res = &m.QueryAssignedLicensesResponse + } else { + for _, r := range m.QueryAssignedLicensesResponse.Returnval { + if r.EntityId == req.EntityId { + body.Res.Returnval = append(body.Res.Returnval, r) } } } - body.Res.Returnval = []types.LicenseAssignmentManagerLicenseAssignment{ - { - EntityId: req.EntityId, - AssignedLicense: EvalLicense, - }, - } - return body } func (m *LicenseAssignmentManager) UpdateAssignedLicense(ctx *Context, req *types.UpdateAssignedLicense) soap.HasFault { - body := &methods.UpdateAssignedLicenseBody{ - Res: &types.UpdateAssignedLicenseResponse{ - Returnval: licenseInfo(req.LicenseKey, nil), - }, + body := new(methods.UpdateAssignedLicenseBody) + + var license *types.LicenseManagerLicenseInfo + lm := ctx.Map.Get(*ctx.Map.content().LicenseManager).(*LicenseManager) + + for i, l := range lm.Licenses { + if l.LicenseKey == req.LicenseKey { + license = &lm.Licenses[i] + } + } + + if license == nil { + body.Fault_ = Fault("", &types.InvalidArgument{InvalidProperty: "entityId"}) + return body + } + + for i, r := range m.QueryAssignedLicensesResponse.Returnval { + if r.EntityId == req.Entity { + r.AssignedLicense = *license + + if req.EntityDisplayName != "" { + r.EntityDisplayName = req.EntityDisplayName + } + + m.QueryAssignedLicensesResponse.Returnval[i] = r + + body.Res = &types.UpdateAssignedLicenseResponse{ + Returnval: r.AssignedLicense, + } + + break + } } return body diff --git a/simulator/license_manager_test.go b/simulator/license_manager_test.go index 6a3e5186c..d272ddf7d 100644 --- a/simulator/license_manager_test.go +++ b/simulator/license_manager_test.go @@ -75,8 +75,13 @@ func TestLicenseManagerVPX(t *testing.T) { t.Fatal(err) } - if len(la) != 1 { - t.Fatal("no licenses") + expect := 1 + if name == "" { + count := m.Count() + expect = count.Host + count.ClusterHost + count.Cluster + 1 // (1 == vCenter) + } + if len(la) != expect { + t.Fatalf("%d licenses", len(la)) } if !reflect.DeepEqual(la[0].AssignedLicense, EvalLicense) { diff --git a/simulator/model.go b/simulator/model.go index f16efc2e5..e590838e6 100644 --- a/simulator/model.go +++ b/simulator/model.go @@ -257,6 +257,7 @@ var kinds = map[string]reflect.Type{ "HostSystem": reflect.TypeOf((*HostSystem)(nil)).Elem(), "IpPoolManager": reflect.TypeOf((*IpPoolManager)(nil)).Elem(), "LicenseManager": reflect.TypeOf((*LicenseManager)(nil)).Elem(), + "LicenseAssignmentManager": reflect.TypeOf((*LicenseAssignmentManager)(nil)).Elem(), "OptionManager": reflect.TypeOf((*OptionManager)(nil)).Elem(), "OvfManager": reflect.TypeOf((*OvfManager)(nil)).Elem(), "PerformanceManager": reflect.TypeOf((*PerformanceManager)(nil)).Elem(),