diff --git a/simulator/folder.go b/simulator/folder.go index 8a59b994d..ac80e6299 100644 --- a/simulator/folder.go +++ b/simulator/folder.go @@ -81,7 +81,9 @@ func folderPutChild(ctx *Context, f *mo.Folder, o mo.Entity) { // Need to update ChildEntity before Map.Put for ContainerView updates to work properly f.ChildEntity = append(f.ChildEntity, ctx.Map.reference(o)) ctx.Map.PutEntity(f, o) - + if ActiveModel != nil { + ActiveModel.MarkDirty() + } folderUpdate(ctx, f, o, ctx.Map.AddReference) switch e := o.(type) { @@ -98,6 +100,9 @@ func folderPutChild(ctx *Context, f *mo.Folder, o mo.Entity) { func folderRemoveChild(ctx *Context, f *mo.Folder, o mo.Reference) { ctx.Map.Remove(ctx, o.Reference()) + if ActiveModel != nil { + ActiveModel.MarkDirty() + } folderRemoveReference(ctx, f, o) } diff --git a/simulator/model.go b/simulator/model.go index 1fd07c41a..650241cab 100644 --- a/simulator/model.go +++ b/simulator/model.go @@ -16,6 +16,7 @@ import ( "reflect" "strings" "time" + "sort" "github.com/google/uuid" @@ -29,6 +30,12 @@ import ( "github.com/vmware/govmomi/vim25/mo" "github.com/vmware/govmomi/vim25/types" "github.com/vmware/govmomi/vim25/xml" + + + "github.com/vmware/govmomi/property" + "github.com/vmware/govmomi/fault" + "github.com/vmware/govmomi/view" + ) type DelayConfig struct { @@ -135,7 +142,14 @@ type Model struct { total int dir string + saveDir string + summary map[string]int + n int + + //track if in-memory state changed since last save + dirty bool } +var ActiveModel *Model // ESX is the default Model for a standalone ESX instance func ESX() *Model { @@ -468,6 +482,391 @@ func (m *Model) Load(dir string) error { return m.resolveReferences(ctx) } +// write encodes data to file name +func (m *Model) write(name string, data any) error { + f, err := os.Create(filepath.Join(m.saveDir, name) + ".xml") + if err != nil { + return err + } + e := xml.NewEncoder(f) + e.Indent("", " ") + if err = e.Encode(data); err != nil { + _ = f.Close() + return err + } + if err = f.Close(); err != nil { + return err + } + return nil +} + +type saveMethod struct { + Name string + Data any +} + +func saveDVS(ctx context.Context, c *vim25.Client, ref types.ManagedObjectReference) ([]saveMethod, error) { + res, err := methods.FetchDVPorts(ctx, c, &types.FetchDVPorts{This: ref}) + if err != nil { + return nil, err + } + return []saveMethod{{"FetchDVPorts", res}}, nil +} + +func saveEnvironmentBrowser(ctx context.Context, c *vim25.Client, ref types.ManagedObjectReference) ([]saveMethod, error) { + var save []saveMethod + { + res, err := methods.QueryConfigOption(ctx, c, &types.QueryConfigOption{This: ref}) + if err != nil { + return nil, err + } + save = append(save, saveMethod{"QueryConfigOption", res}) + } + { + res, err := methods.QueryConfigTarget(ctx, c, &types.QueryConfigTarget{This: ref}) + if err != nil { + return nil, err + } + save = append(save, saveMethod{"QueryConfigTarget", res}) + } + { + res, err := methods.QueryTargetCapabilities(ctx, c, &types.QueryTargetCapabilities{This: ref}) + if err != nil { + return nil, err + } + save = append(save, saveMethod{"QueryTargetCapabilities", res}) + } + return save, nil +} + +func saveHostNetworkSystem(ctx context.Context, c *vim25.Client, ref types.ManagedObjectReference) ([]saveMethod, error) { + res, err := methods.QueryNetworkHint(ctx, c, &types.QueryNetworkHint{This: ref}) + if err != nil { + return nil, err + } + return []saveMethod{{"QueryNetworkHint", res}}, nil +} + +func saveHostSystem(ctx context.Context, c *vim25.Client, ref types.ManagedObjectReference) ([]saveMethod, error) { + res, err := methods.QueryTpmAttestationReport(ctx, c, &types.QueryTpmAttestationReport{This: ref}) + if err != nil { + return nil, err + } + return []saveMethod{{"QueryTpmAttestationReport", res}}, nil +} + +func saveAlarmManager(ctx context.Context, c *vim25.Client, ref types.ManagedObjectReference) ([]saveMethod, error) { + res, err := methods.GetAlarm(ctx, c, &types.GetAlarm{This: ref}) + if err != nil { + return nil, err + } + pc := property.DefaultCollector(c) + var content []types.ObjectContent + if err = pc.Retrieve(ctx, res.Returnval, nil, &content); err != nil { + return nil, err + } + 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, + "EnvironmentBrowser": saveEnvironmentBrowser, + "HostNetworkSystem": saveHostNetworkSystem, + "HostSystem": saveHostSystem, + "AlarmManager": saveAlarmManager, + "LicenseAssignmentManager": saveLicenseAssignmentManager, +} + +func (m *Model) save(content []types.ObjectContent, c *vim25.Client) error { + for _, x := range content { + x.MissingSet = nil // drop any NoPermission faults + m.summary[x.Obj.Type]++ + ref := x.Obj.Encode() + name := fmt.Sprintf("%04d-%s", m.n, ref) + m.n++ + if err := m.write(name, x); err != nil { + return err + } + + if method, ok := saveObjects[x.Obj.Type]; ok { + objs, err := method(context.Background(), c, x.Obj) + if err != nil { + if fault.Is(err, &types.HostNotConnected{}) { + continue + } + if fault.Is(err, &types.NotSupported{}) { + continue + } + return err + } + dir := filepath.Join(m.saveDir, ref) + if err = os.MkdirAll(dir, 0755); err != nil { + return err + } + for _, obj := range objs { + if obj.Name == "" { + err = m.save(obj.Data.([]types.ObjectContent), c) + if err != nil { + return err + } + continue + } + err = m.write(filepath.Join(ref, obj.Name), obj.Data) + if err != nil { + return err + } + } + } + } + return nil +} + +func (m *Model) Save(dir string) error { + if dir == "" { + return nil + } + + m.saveDir = dir + ctx := NewContext() + m.summary = make(map[string]int) + + //soapClient := soap.NewClient(m.Service.Listen, false) + //c, err := vim25.NewClient(ctx, soapClient) + c1, err := govmomi.NewClient(ctx, m.Service.Listen, true) + if err != nil { + return err + } + c := c1.Client + if m.saveDir == "" { + u := c.URL() + name := u.Fragment + if name == "" { + name = u.Hostname() + } + m.saveDir = "vcsim-" + name + } + + if _, err := os.Stat(m.saveDir); err == nil { + if err := os.RemoveAll(m.saveDir); err != nil { + return err + } + } + + mkdir := os.Mkdir + if err := mkdir(m.saveDir, 0755); err != nil { + return err + } + + var content []types.ObjectContent + pc := property.DefaultCollector(c) + root := vim25.ServiceInstance + + req := types.RetrievePropertiesEx{ + This: pc.Reference(), + Options: types.RetrieveOptions{MaxObjects: 10}, + } + + if root == vim25.ServiceInstance { + err := pc.RetrieveOne(ctx, root, []string{"content"}, &content) + if err != nil { + return nil + } + if err = m.save(content, c); err != nil { + return err + } + + root = c.ServiceContent.RootFolder + + 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": + 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 + 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, + SelectSet: selectSet, + }}, + PropSet: append(propSet, types.PropertySpec{ + Type: ref.Type, + All: all, + PathSet: path, + }), + }) + } + break + } + } + } + + mgr := view.NewManager(c) + v, err := mgr.CreateContainerView(ctx, root, []string{}, true) + if err != nil { + return err + } + + defer func() { + _ = v.Destroy(ctx) + }() + + all := types.NewBool(true) + req.SpecSet = append(req.SpecSet, types.PropertyFilterSpec{ + ObjectSet: []types.ObjectSpec{{ + Obj: v.Reference(), + Skip: types.NewBool(false), + SelectSet: []types.BaseSelectionSpec{ + &types.TraversalSpec{ + Type: v.Reference().Type, + Path: "view", + SelectSet: []types.BaseSelectionSpec{ + &types.SelectionSpec{ + Name: "computeTraversalSpec", + }, + &types.SelectionSpec{ + Name: "datastoreTraversalSpec", + }, + &types.SelectionSpec{ + Name: "hostDatastoreSystemTraversalSpec", + }, + &types.SelectionSpec{ + Name: "hostNetworkSystemTraversalSpec", + }, + &types.SelectionSpec{ + Name: "hostVirtualNicManagerTraversalSpec", + }, + &types.SelectionSpec{ + Name: "hostCertificateManagerTraversalSpec", + }, + &types.SelectionSpec{ + Name: "entityTraversalSpec", + }, + }, + }, + &types.TraversalSpec{ + SelectionSpec: types.SelectionSpec{ + Name: "computeTraversalSpec", + }, + Type: "ComputeResource", + Path: "environmentBrowser", + }, + &types.TraversalSpec{ + SelectionSpec: types.SelectionSpec{ + Name: "datastoreTraversalSpec", + }, + Type: "Datastore", + Path: "browser", + }, + &types.TraversalSpec{ + SelectionSpec: types.SelectionSpec{ + Name: "hostNetworkSystemTraversalSpec", + }, + Type: "HostSystem", + Path: "configManager.networkSystem", + }, + &types.TraversalSpec{ + SelectionSpec: types.SelectionSpec{ + Name: "hostVirtualNicManagerTraversalSpec", + }, + Type: "HostSystem", + Path: "configManager.virtualNicManager", + }, + &types.TraversalSpec{ + SelectionSpec: types.SelectionSpec{ + Name: "hostCertificateManagerTraversalSpec", + }, + Type: "HostSystem", + Path: "configManager.certificateManager", + }, + &types.TraversalSpec{ + SelectionSpec: types.SelectionSpec{ + Name: "hostDatastoreSystemTraversalSpec", + }, + Type: "HostSystem", + Path: "configManager.datastoreSystem", + }, + &types.TraversalSpec{ + SelectionSpec: types.SelectionSpec{ + Name: "entityTraversalSpec", + }, + Type: "ManagedEntity", + Path: "recentTask", + }, + }, + }}, + PropSet: []types.PropertySpec{ + {Type: "EnvironmentBrowser", All: all}, + {Type: "HostDatastoreBrowser", All: all}, + {Type: "HostDatastoreSystem", All: all}, + {Type: "HostNetworkSystem", All: all}, + {Type: "HostVirtualNicManager", All: all}, + {Type: "HostCertificateManager", All: all}, + {Type: "ManagedEntity", All: all}, + {Type: "Task", All: all}, + }, + }) + + res, err := methods.RetrievePropertiesEx(ctx, c, &req) + if err != nil { + return err + } + if err = m.save(res.Returnval.Objects, c); err != nil { + return err + } + + token := res.Returnval.Token + for token != "" { + cres, err := methods.ContinueRetrievePropertiesEx(ctx, c, &types.ContinueRetrievePropertiesEx{ + This: req.This, + Token: token, + }) + if err != nil { + return err + } + token = cres.Returnval.Token + if err = m.save(cres.Returnval.Objects, c); err != nil { + return err + } + } + + var summary []string + for k, v := range m.summary { + summary = append(summary, fmt.Sprintf("%s: %d", k, v)) + } + sort.Strings(summary) + + s := ", including" + fmt.Printf("Saved %d total objects to %q%s:\n", m.n, m.saveDir, s) + for i := range summary { + fmt.Println(summary[i]) + } + + return nil +} + // Create populates the Model with the given ModelConfig func (m *Model) Create() error { @@ -989,3 +1388,27 @@ func (dc *DelayConfig) delay(method string) { time.Sleep(time.Duration(d) * time.Millisecond) } } + +// Dirty returns whether the model has unsaved changes. + +func (m *Model) Dirty() bool { + +return m.dirty + +} + +// ClearDirty resets the dirty flag after saving. + +func (m *Model) ClearDirty() { + +m.dirty = false + +} + +// MarkDirty marks the model as having changed state. + +func (m *Model) MarkDirty() { + +m.dirty=true + +} \ No newline at end of file diff --git a/vcsim/go.sum b/vcsim/go.sum index bc7293f6a..956ac7d20 100644 --- a/vcsim/go.sum +++ b/vcsim/go.sum @@ -10,8 +10,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/vcsim/main.go b/vcsim/main.go index 7c89d3405..e2a68d3a0 100644 --- a/vcsim/main.go +++ b/vcsim/main.go @@ -19,6 +19,7 @@ import ( "strconv" "strings" "syscall" + "time" "github.com/google/uuid" @@ -86,6 +87,7 @@ func main() { trace := flag.String("trace-file", "", "Trace output file (defaults to stderr)") stdinExit := flag.Bool("stdinexit", false, "Press any key to exit") dir := flag.String("load", "", "Load model from directory") + saveDir := flag.String("save", "", "Save model into directory") flag.IntVar(&model.DelayConfig.Delay, "delay", model.DelayConfig.Delay, "Method response delay across all methods") methodDelayP := flag.String("method-delay", "", "Delay per method on the form 'method1:delay1,method2:delay2...'") @@ -239,6 +241,38 @@ func main() { }() } + // Auto-save only when model state changes + +go func() { + + ticker := time.NewTicker(3 * time.Hour) + + defer ticker.Stop() + + for range ticker.C { + + if model != nil && model.Dirty() { + + log.Println("[vcsim] Detected in-memory change, saving model...") + + if err := model.Save(*saveDir); err != nil { + + log.Printf("[vcsim] Auto-save failed: %v", err) + + } else { + + model.ClearDirty() + + log.Println("[vcsim] Auto-save completed successfully.") + + } + + } + + } + +}() + <-sig model.Remove()