From 7d2fe6606e85dd9663a355f460a90a0050bccb69 Mon Sep 17 00:00:00 2001 From: Ondra Machacek Date: Fri, 10 Jan 2025 10:32:34 +0100 Subject: [PATCH] Add default inventory This commit add a Seed() method which will seed the database with default inventory, so the user can do some exploration of what he will get in case he will do the assessment. The default inventory can't be removed and is visible for all users. Signed-off-by: Ondra Machacek --- cmd/planner-api/run.go | 7 ++ internal/service/agent.go | 12 +- internal/service/agent_test.go | 2 + internal/service/mappers/outbound.go | 16 ++- internal/service/source.go | 12 +- internal/store/inventory.go | 172 +++++++++++++++++++++++++++ internal/store/options.go | 19 ++- internal/store/store.go | 47 ++++++++ internal/store/store_test.go | 16 +++ test/e2e/e2e_agent_test.go | 23 +++- 10 files changed, 311 insertions(+), 15 deletions(-) create mode 100644 internal/store/inventory.go diff --git a/cmd/planner-api/run.go b/cmd/planner-api/run.go index 89b67c8..5de6de3 100644 --- a/cmd/planner-api/run.go +++ b/cmd/planner-api/run.go @@ -59,6 +59,13 @@ var runCmd = &cobra.Command{ zap.S().Fatalf("running initial migration: %v", err) } + // Initialize database with basic example report + if v, b := os.LookupEnv("NO_SEED"); !b || v == "false" { + if err := store.Seed(); err != nil { + zap.S().Fatalf("seeding database with default report: %v", err) + } + } + // initilize event writer ep, _ := getEventProducer(cfg) diff --git a/internal/service/agent.go b/internal/service/agent.go index 014c8c8..134a3df 100644 --- a/internal/service/agent.go +++ b/internal/service/agent.go @@ -11,15 +11,23 @@ import ( ) func (h *ServiceHandler) ListAgents(ctx context.Context, request server.ListAgentsRequestObject) (server.ListAgentsResponseObject, error) { + // Get user content: qf := store.NewAgentQueryFilter() if user, found := auth.UserFromContext(ctx); found { qf = qf.ByUsername(user.Username) } - result, err := h.store.Agent().List(ctx, qf, store.NewAgentQueryOptions().WithIncludeSoftDeleted(false)) + userResult, err := h.store.Agent().List(ctx, qf, store.NewAgentQueryOptions().WithIncludeSoftDeleted(false)) if err != nil { return nil, err } - return server.ListAgents200JSONResponse(mappers.AgentListToApi(result)), nil + + // Get default content + defaultResult, err := h.store.Agent().List(ctx, store.NewAgentQueryFilter().ByDefaultInventory(), store.NewAgentQueryOptions().WithIncludeSoftDeleted(false)) + if err != nil { + return nil, err + } + + return server.ListAgents200JSONResponse(mappers.AgentListToApi(userResult, defaultResult)), nil } func (h *ServiceHandler) DeleteAgent(ctx context.Context, request server.DeleteAgentRequestObject) (server.DeleteAgentResponseObject, error) { diff --git a/internal/service/agent_test.go b/internal/service/agent_test.go index cfaf8da..8c1b2b2 100644 --- a/internal/service/agent_test.go +++ b/internal/service/agent_test.go @@ -100,6 +100,7 @@ var _ = Describe("agent handler", Ordered, func() { AfterEach(func() { gormdb.Exec("DELETE FROM agents;") + gormdb.Exec("DELETE FROM sources;") }) }) @@ -182,6 +183,7 @@ var _ = Describe("agent handler", Ordered, func() { AfterEach(func() { gormdb.Exec("DELETE FROM agents;") + gormdb.Exec("DELETE FROM sources;") }) }) }) diff --git a/internal/service/mappers/outbound.go b/internal/service/mappers/outbound.go index b72aae4..f9659f0 100644 --- a/internal/service/mappers/outbound.go +++ b/internal/service/mappers/outbound.go @@ -28,10 +28,12 @@ func SourceToApi(s model.Source) api.Source { return source } -func SourceListToApi(sources model.SourceList) api.SourceList { - sourceList := make([]api.Source, 0, len(sources)) +func SourceListToApi(sources ...model.SourceList) api.SourceList { + sourceList := []api.Source{} for _, source := range sources { - sourceList = append(sourceList, SourceToApi(source)) + for _, s := range source { + sourceList = append(sourceList, SourceToApi(s)) + } } return sourceList } @@ -59,10 +61,12 @@ func AgentToApi(a model.Agent) api.Agent { return agent } -func AgentListToApi(agents model.AgentList) api.AgentList { - agentList := make([]api.Agent, 0, len(agents)) +func AgentListToApi(agents ...model.AgentList) api.AgentList { + agentList := []api.Agent{} for _, agent := range agents { - agentList = append(agentList, AgentToApi(agent)) + for _, a := range agent { + agentList = append(agentList, AgentToApi(a)) + } } return agentList } diff --git a/internal/service/source.go b/internal/service/source.go index 9040eb1..e8bff4c 100644 --- a/internal/service/source.go +++ b/internal/service/source.go @@ -10,15 +10,23 @@ import ( ) func (h *ServiceHandler) ListSources(ctx context.Context, request server.ListSourcesRequestObject) (server.ListSourcesResponseObject, error) { + // Get user content filter := store.NewSourceQueryFilter() if user, found := auth.UserFromContext(ctx); found { filter = filter.ByUsername(user.Username) } - result, err := h.store.Source().List(ctx, filter) + userResult, err := h.store.Source().List(ctx, filter) if err != nil { return nil, err } - return server.ListSources200JSONResponse(mappers.SourceListToApi(result)), nil + + // Get default content + defaultResult, err := h.store.Source().List(ctx, store.NewSourceQueryFilter().ByDefaultInventory()) + if err != nil { + return nil, err + } + + return server.ListSources200JSONResponse(mappers.SourceListToApi(userResult, defaultResult)), nil } func (h *ServiceHandler) DeleteSources(ctx context.Context, request server.DeleteSourcesRequestObject) (server.DeleteSourcesResponseObject, error) { diff --git a/internal/store/inventory.go b/internal/store/inventory.go new file mode 100644 index 0000000..43221e3 --- /dev/null +++ b/internal/store/inventory.go @@ -0,0 +1,172 @@ +package store + +import ( + api "github.com/kubev2v/migration-planner/api/v1alpha1" +) + +func GenerateDefaultInventory() api.Inventory { + n := 107 + dvSwitch := "management" + dvSwitchVm := "vm" + vlanid100 := "100" + vlanid200 := "200" + vlanTrunk := "0-4094" + return api.Inventory{ + Infra: api.Infra{ + Datastores: []struct { + FreeCapacityGB int `json:"freeCapacityGB"` + TotalCapacityGB int `json:"totalCapacityGB"` + Type string `json:"type"` + }{ + {FreeCapacityGB: 615, TotalCapacityGB: 766, Type: "VMFS"}, + {FreeCapacityGB: 650, TotalCapacityGB: 766, Type: "VMFS"}, + {FreeCapacityGB: 167, TotalCapacityGB: 221, Type: "VMFS"}, + {FreeCapacityGB: 424, TotalCapacityGB: 766, Type: "VMFS"}, + {FreeCapacityGB: 1369, TotalCapacityGB: 3321, Type: "VMFS"}, + {FreeCapacityGB: 1252, TotalCapacityGB: 3071, Type: "VMFS"}, + {FreeCapacityGB: 415, TotalCapacityGB: 766, Type: "VMFS"}, + {FreeCapacityGB: 585, TotalCapacityGB: 766, Type: "VMFS"}, + {FreeCapacityGB: 170, TotalCapacityGB: 196, Type: "NFS"}, + {FreeCapacityGB: 606, TotalCapacityGB: 766, Type: "VMFS"}, + {FreeCapacityGB: 740, TotalCapacityGB: 766, Type: "VMFS"}, + }, + HostPowerStates: map[string]int{ + "Green": 8, + }, + HostsPerCluster: []int{1, 7, 0}, + Networks: []struct { + Dvswitch *string `json:"dvswitch,omitempty"` + Name string `json:"name"` + Type api.InfraNetworksType `json:"type"` + VlanId *string `json:"vlanId,omitempty"` + }{ + {Name: dvSwitch, Type: "dvswitch"}, + {Dvswitch: &dvSwitch, Name: "mgmt", Type: "distributed", VlanId: &vlanid100}, + {Name: dvSwitchVm, Type: "dvswitch"}, + {Dvswitch: &dvSwitchVm, Name: "storage", Type: "distributed", VlanId: &vlanid200}, + {Dvswitch: &dvSwitch, Name: "vMotion", Type: "distributed", VlanId: &vlanid100}, + {Dvswitch: &dvSwitch, Name: "trunk", Type: "distributed", VlanId: &vlanTrunk}, + }, + TotalClusters: 3, + TotalHosts: 8, + }, + Vcenter: api.VCenter{ + Id: "00000000-0000-0000-0000-000000000000", + }, + Vms: api.VMs{ + CpuCores: api.VMResourceBreakdown{ + Histogram: struct { + Data []int `json:"data"` + MinValue int `json:"minValue"` + Step int `json:"step"` + }{ + Data: []int{45, 0, 39, 2, 13, 0, 0, 0, 0, 8}, + MinValue: 1, + Step: 2, + }, + Total: 472, + TotalForMigratableWithWarnings: 472, + }, + DiskCount: api.VMResourceBreakdown{ + Histogram: struct { + Data []int `json:"data"` + MinValue int `json:"minValue"` + Step int `json:"step"` + }{ + Data: []int{10, 91, 1, 2, 0, 2, 0, 0, 0, 1}, + MinValue: 0, + Step: 1, + }, + Total: 115, + TotalForMigratableWithWarnings: 115, + }, + NotMigratableReasons: []struct { + Assessment string `json:"assessment"` + Count int `json:"count"` + Label string `json:"label"` + }{}, + DiskGB: api.VMResourceBreakdown{ + Histogram: struct { + Data []int `json:"data"` + MinValue int `json:"minValue"` + Step int `json:"step"` + }{ + Data: []int{32, 23, 31, 14, 0, 2, 2, 1, 0, 2}, + MinValue: 0, + Step: 38, + }, + Total: 7945, + TotalForMigratableWithWarnings: 7945, + }, + RamGB: api.VMResourceBreakdown{ + Histogram: struct { + Data []int `json:"data"` + MinValue int `json:"minValue"` + Step int `json:"step"` + }{ + Data: []int{49, 32, 1, 14, 0, 0, 9, 0, 0, 2}, + MinValue: 1, + Step: 5, + }, + Total: 1031, + TotalForMigratableWithWarnings: 1031, + }, + PowerStates: map[string]int{ + "poweredOff": 78, + "poweredOn": 29, + }, + Os: map[string]int{ + "Amazon Linux 2 (64-bit)": 1, + "CentOS 7 (64-bit)": 1, + "CentOS 8 (64-bit)": 1, + "Debian GNU/Linux 12 (64-bit)": 1, + "FreeBSD (64-bit)": 2, + "Microsoft Windows 10 (64-bit)": 2, + "Microsoft Windows 11 (64-bit)": 2, + "Microsoft Windows Server 2019 (64-bit)": 8, + "Microsoft Windows Server 2022 (64-bit)": 3, + "Microsoft Windows Server 2025 (64-bit)": 2, + "Other (32-bit)": 12, + "Other (64-bit)": 1, + "Other 2.6.x Linux (64-bit)": 13, + "Other Linux (64-bit)": 1, + "Red Hat Enterprise Linux 8 (64-bit)": 5, + "Red Hat Enterprise Linux 9 (64-bit)": 41, + "Red Hat Fedora (64-bit)": 2, + "Rocky Linux (64-bit)": 1, + "Ubuntu Linux (64-bit)": 3, + "VMware ESXi 8.0 or later": 5, + }, + MigrationWarnings: api.MigrationIssues{ + { + Label: "Changed Block Tracking (CBT) not enabled", + Count: 105, + Assessment: "Changed Block Tracking (CBT) has not been enabled on this VM. This feature is a prerequisite for VM warm migration.", + }, + { + Label: "UEFI detected", + Count: 77, + Assessment: "UEFI secure boot will be disabled on OpenShift Virtualization. If the VM was set with UEFI secure boot, manual steps within the guest would be needed for the guest operating system to boot.", + }, + { + Label: "Invalid VM Name", + Count: 31, + Assessment: "The VM name must comply with the DNS subdomain name format defined in RFC 1123. The name can contain lowercase letters (a-z), numbers (0-9), and hyphens (-), up to a maximum of 63 characters. The first and last characters must be alphanumeric. The name must not contain uppercase letters, spaces, periods (.), or special characters. The VM will be renamed automatically during the migration to meet the RFC convention.", + }, + { + Label: "VM configured with a TPM device", + Count: 3, + Assessment: "The VM is configured with a TPM device. TPM data is not transferred during the migration.", + }, + { + Label: "Independent disk detected", + Count: 2, + Assessment: "Independent disks cannot be transferred using recent versions of VDDK. It is recommended to change them in vSphere to 'Dependent' mode, or alternatively, to export the VM to an OVA.", + }, + }, + Total: 107, + TotalMigratable: 107, + TotalMigratableWithWarnings: &n, + }, + } +} diff --git a/internal/store/options.go b/internal/store/options.go index bffe1cc..f28d54d 100644 --- a/internal/store/options.go +++ b/internal/store/options.go @@ -1,6 +1,9 @@ package store -import "gorm.io/gorm" +import ( + "github.com/google/uuid" + "gorm.io/gorm" +) type BaseQuerier struct { QueryFn []func(tx *gorm.DB) *gorm.DB @@ -33,6 +36,13 @@ func (qf *AgentQueryFilter) ByOrgID(id string) *AgentQueryFilter { return qf } +func (qf *AgentQueryFilter) ByDefaultInventory() *AgentQueryFilter { + qf.QueryFn = append(qf.QueryFn, func(tx *gorm.DB) *gorm.DB { + return tx.Where("id = ?", uuid.UUID{}) + }) + return qf +} + func (qf *AgentQueryFilter) BySoftDeleted(isSoftDeleted bool) *AgentQueryFilter { qf.QueryFn = append(qf.QueryFn, func(tx *gorm.DB) *gorm.DB { if isSoftDeleted { @@ -101,3 +111,10 @@ func (sf *SourceQueryFilter) ByOrgID(id string) *SourceQueryFilter { }) return sf } + +func (sf *SourceQueryFilter) ByDefaultInventory() *SourceQueryFilter { + sf.QueryFn = append(sf.QueryFn, func(tx *gorm.DB) *gorm.DB { + return tx.Where("id = ?", uuid.UUID{}) + }) + return sf +} diff --git a/internal/store/store.go b/internal/store/store.go index 547dca0..8363b8d 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -3,13 +3,18 @@ package store import ( "context" + "github.com/google/uuid" + api "github.com/kubev2v/migration-planner/api/v1alpha1" + "github.com/kubev2v/migration-planner/internal/store/model" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type Store interface { NewTransactionContext(ctx context.Context) (context.Context, error) Agent() Agent Source() Source + Seed() error InitialMigration() error Close() error } @@ -58,6 +63,48 @@ func (s *DataStore) InitialMigration() error { return err } +func (s *DataStore) Seed() error { + sourceUuid := uuid.UUID{} + sourceUuidStr := sourceUuid.String() + + tx, err := newTransaction(s.db) + if err != nil { + return err + } + // Create/update default source + source := model.Source{ + ID: sourceUuid, + Inventory: model.MakeJSONField(GenerateDefaultInventory()), + } + + if err := tx.tx.Clauses(clause.OnConflict{ + UpdateAll: true, + }).Create(&source).Error; err != nil { + _ = tx.Rollback() + } + + // Create/update default agent + agent := model.Agent{ + ID: sourceUuidStr, + Status: string(api.AgentStatusUpToDate), + StatusInfo: "Inventory successfully collected", + CredUrl: "Example report", + SourceID: &sourceUuidStr, + Associated: true, + } + if err := tx.tx.Clauses(clause.OnConflict{ + UpdateAll: true, + }).Create(&agent).Error; err != nil { + _ = tx.Rollback() + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} + func (s *DataStore) Close() error { sqlDB, err := s.db.DB() if err != nil { diff --git a/internal/store/store_test.go b/internal/store/store_test.go index c186800..4b6c4c7 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -86,8 +86,24 @@ var _ = Describe("Store", Ordered, func() { Expect(count).To(Equal(0)) }) + It("Seed the databsae", func() { + err := store.Seed() + Expect(err).To(BeNil()) + + count := 0 + err = gormDB.Raw("SELECT COUNT(*) from sources;").Scan(&count).Error + Expect(err).To(BeNil()) + Expect(count).To(Equal(1)) + + count = 0 + err = gormDB.Raw("SELECT COUNT(*) from agents;").Scan(&count).Error + Expect(err).To(BeNil()) + Expect(count).To(Equal(1)) + }) + AfterEach(func() { gormDB.Exec("DELETE from sources;") + gormDB.Exec("DELETE from agents;") }) }) }) diff --git a/test/e2e/e2e_agent_test.go b/test/e2e/e2e_agent_test.go index 9bf2bb6..0be085c 100644 --- a/test/e2e/e2e_agent_test.go +++ b/test/e2e/e2e_agent_test.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" + "github.com/google/uuid" api "github.com/kubev2v/migration-planner/api/v1alpha1" internalclient "github.com/kubev2v/migration-planner/internal/api/client" "github.com/kubev2v/migration-planner/internal/client" @@ -266,11 +267,18 @@ func (s *plannerService) GetAgent() (*api.Agent, error) { return nil, fmt.Errorf("Error listing agents") } - if len(*res.JSON200) == 0 { + if len(*res.JSON200) == 1 { return nil, fmt.Errorf("No agents found") } - return &(*res.JSON200)[0], nil + nullUuid := uuid.UUID{} + for _, agent := range *res.JSON200 { + if agent.Id != nullUuid.String() { + return &agent, nil + } + } + + return nil, fmt.Errorf("No agents found") } func (s *plannerService) GetSource() (*api.Source, error) { @@ -280,11 +288,18 @@ func (s *plannerService) GetSource() (*api.Source, error) { return nil, fmt.Errorf("Error listing sources") } - if len(*res.JSON200) == 0 { + if len(*res.JSON200) == 1 { return nil, fmt.Errorf("No sources found") } - return &(*res.JSON200)[0], nil + nullUuid := uuid.UUID{} + for _, source := range *res.JSON200 { + if source.Id != nullUuid { + return &source, nil + } + } + + return nil, fmt.Errorf("No sources found") } func (s *plannerService) RemoveSources() error {