From 9074e1eabe354f2a9e6138e582c430a76bead936 Mon Sep 17 00:00:00 2001 From: Igor Troyanovsky Date: Fri, 15 May 2026 09:40:56 +0300 Subject: [PATCH] ECOPROJECT-4282 | feat: add cluster drs information into the api Signed-off-by: Igor Troyanovsky rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- api/v1alpha1/agent/spec.gen.go | 97 +++--- api/v1alpha1/openapi.yaml | 19 ++ api/v1alpha1/spec.gen.go | 322 +++++++++--------- api/v1alpha1/types.gen.go | 30 +- pkg/duckdb_parser/builder.go | 6 + pkg/duckdb_parser/inventory_builder.go | 51 ++- pkg/duckdb_parser/inventory_builder_test.go | 218 +++++++++++- pkg/duckdb_parser/models/inventory.go | 16 +- pkg/duckdb_parser/queries.go | 23 ++ .../templates/cluster_features_query.go.tmpl | 9 + .../templates/create_schema.go.tmpl | 5 +- .../templates/ingest_rvtools.go.tmpl | 22 +- pkg/inventory/converters/to_api.go | 32 +- pkg/inventory/model.go | 12 +- 14 files changed, 637 insertions(+), 225 deletions(-) create mode 100644 pkg/duckdb_parser/templates/cluster_features_query.go.tmpl diff --git a/api/v1alpha1/agent/spec.gen.go b/api/v1alpha1/agent/spec.gen.go index 4817e0c8c..1fb51c6ea 100644 --- a/api/v1alpha1/agent/spec.gen.go +++ b/api/v1alpha1/agent/spec.gen.go @@ -19,53 +19,56 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v1alpha1/openapi.yaml b/api/v1alpha1/openapi.yaml index 609a408b1..6a64e3af6 100644 --- a/api/v1alpha1/openapi.yaml +++ b/api/v1alpha1/openapi.yaml @@ -2079,6 +2079,23 @@ components: nullable: true minLength: 1 + ClusterFeatures: + type: object + properties: + drsEnabled: + type: boolean + default: false + description: "Whether DRS (Distributed Resource Scheduler) is enabled for this cluster" + drsMode: + type: string + enum: ["Fully Automated", "Partially Automated", "Manual", "None"] + default: "None" + description: "DRS automation mode for the cluster" + storageDrsEnabled: + type: boolean + default: false + description: "Whether Storage DRS is enabled for this cluster" + SourceCreate: type: object properties: @@ -2210,6 +2227,8 @@ components: deprecated: true nullable: true $ref: "#/components/schemas/VCenter" + clusterFeatures: + $ref: "#/components/schemas/ClusterFeatures" vms: $ref: "#/components/schemas/VMs" infra: diff --git a/api/v1alpha1/spec.gen.go b/api/v1alpha1/spec.gen.go index d62fe3227..056b7c5de 100644 --- a/api/v1alpha1/spec.gen.go +++ b/api/v1alpha1/spec.gen.go @@ -18,166 +18,168 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "jvP45fH/BwAA//9KRYHliycBAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v1alpha1/types.gen.go b/api/v1alpha1/types.gen.go index 7fdaa23ff..33a635b36 100644 --- a/api/v1alpha1/types.gen.go +++ b/api/v1alpha1/types.gen.go @@ -33,6 +33,14 @@ const ( AssessmentSourceTypeSource AssessmentSourceType = "source" ) +// Defines values for ClusterFeaturesDrsMode. +const ( + FullyAutomated ClusterFeaturesDrsMode = "Fully Automated" + Manual ClusterFeaturesDrsMode = "Manual" + None ClusterFeaturesDrsMode = "None" + PartiallyAutomated ClusterFeaturesDrsMode = "Partially Automated" +) + // Defines values for ClusterRequirementsRequestControlPlaneNodeCount. const ( ClusterRequirementsRequestControlPlaneNodeCountN1 ClusterRequirementsRequestControlPlaneNodeCount = 1 @@ -221,6 +229,21 @@ type AssessmentUpdate struct { Name *string `json:"name,omitempty" validate:"required,assessment_name"` } +// ClusterFeatures defines model for ClusterFeatures. +type ClusterFeatures struct { + // DrsEnabled Whether DRS (Distributed Resource Scheduler) is enabled for this cluster + DrsEnabled *bool `json:"drsEnabled,omitempty"` + + // DrsMode DRS automation mode for the cluster + DrsMode *ClusterFeaturesDrsMode `json:"drsMode,omitempty"` + + // StorageDrsEnabled Whether Storage DRS is enabled for this cluster + StorageDrsEnabled *bool `json:"storageDrsEnabled,omitempty"` +} + +// ClusterFeaturesDrsMode DRS automation mode for the cluster +type ClusterFeaturesDrsMode string + // ClusterRequirementsRequest Request payload for calculating cluster requirements type ClusterRequirementsRequest struct { // ClusterId ID of the cluster to calculate requirements for @@ -558,9 +581,10 @@ type Inventory struct { // InventoryData defines model for InventoryData. type InventoryData struct { - Infra Infra `json:"infra"` - Vcenter *VCenter `json:"vcenter,omitempty"` - Vms VMs `json:"vms"` + ClusterFeatures *ClusterFeatures `json:"clusterFeatures,omitempty"` + Infra Infra `json:"infra"` + Vcenter *VCenter `json:"vcenter,omitempty"` + Vms VMs `json:"vms"` } // InventoryTotals Inventory totals for the cluster diff --git a/pkg/duckdb_parser/builder.go b/pkg/duckdb_parser/builder.go index 200d89bce..1a695e0ca 100644 --- a/pkg/duckdb_parser/builder.go +++ b/pkg/duckdb_parser/builder.go @@ -150,6 +150,12 @@ func (b *QueryBuilder) ClusterObjectIDsQuery() (string, error) { return b.buildQuery("cluster_object_ids_query", mustGetTemplate("cluster_object_ids_query"), nil) } +// ClusterFeaturesQuery builds query to get cluster features. +func (b *QueryBuilder) ClusterFeaturesQuery(clusterName string) (string, error) { + params := struct{ ClusterName string }{ClusterName: escapeSQLString(clusterName)} + return b.buildQuery("cluster_features_query", mustGetTemplate("cluster_features_query"), params) +} + // VMCountQuery builds the VM count query with filters. func (b *QueryBuilder) VMCountQuery(filters Filters) (string, error) { params := queryParams{ diff --git a/pkg/duckdb_parser/inventory_builder.go b/pkg/duckdb_parser/inventory_builder.go index 04c571d5e..aac57e4ed 100644 --- a/pkg/duckdb_parser/inventory_builder.go +++ b/pkg/duckdb_parser/inventory_builder.go @@ -8,6 +8,7 @@ import ( "go.uber.org/zap" "github.com/kubev2v/migration-planner/internal/util" + "github.com/kubev2v/migration-planner/pkg/duckdb_parser/models" "github.com/kubev2v/migration-planner/pkg/inventory" ) @@ -95,12 +96,58 @@ func (p *Parser) buildInventoryData(ctx context.Context, filters Filters) (*inve return nil, fmt.Errorf("building infra: %w", err) } + // Build Cluster section (only for cluster-scoped queries) + var cluster *models.Cluster + if filters.Cluster != "" { + clusterData, err := p.buildClusterData(ctx, filters.Cluster) + if err != nil { + // Treat cluster features enrichment as non-fatal - continue without cluster data + zap.S().Named("duckdb_parser").Warnf("Failed to build cluster data for %s, continuing without cluster features: %v", filters.Cluster, err) + } else { + cluster = clusterData + } + } + + // Convert models.Cluster to inventory.ClusterFeatures if present + var clusterFeatures *inventory.ClusterFeatures + if cluster != nil && cluster.ClusterFeatures != nil { + clusterFeatures = &inventory.ClusterFeatures{ + DrsEnabled: cluster.ClusterFeatures.DrsEnabled, + DrsMode: cluster.ClusterFeatures.DrsMode, + StorageDrsEnabled: cluster.ClusterFeatures.StorageDrsEnabled, + } + } + return &inventory.InventoryData{ - VMs: *vms, - Infra: *infra, + VMs: *vms, + Infra: *infra, + ClusterFeatures: clusterFeatures, }, nil } +// buildClusterData constructs cluster-level information. +// Returns nil cluster data if cluster features are not available (for backward compatibility). +func (p *Parser) buildClusterData(ctx context.Context, clusterName string) (*models.Cluster, error) { + clusterFeatures, err := p.ClusterFeatures(ctx, clusterName) + if err != nil { + // For backward compatibility, if cluster features aren't available, + // continue without them rather than failing the entire inventory + zap.S().Named("duckdb_parser").Debugf("Cluster features not available for %s: %v", clusterName, err) + return nil, nil + } + + // Create a cluster with the features + cluster := &models.Cluster{ + Name: clusterName, + ClusterFeatures: clusterFeatures, + Datastores: []models.Datastore{}, + Hosts: []models.Host{}, + Networks: []models.Network{}, + } + + return cluster, nil +} + // buildVMsData constructs the VMs section of InventoryData. func (p *Parser) buildVMsData(ctx context.Context, filters Filters) (*inventory.VMsData, error) { vmsData := &inventory.VMsData{ diff --git a/pkg/duckdb_parser/inventory_builder_test.go b/pkg/duckdb_parser/inventory_builder_test.go index 731df2af6..8f6d3362c 100644 --- a/pkg/duckdb_parser/inventory_builder_test.go +++ b/pkg/duckdb_parser/inventory_builder_test.go @@ -15,6 +15,7 @@ import ( "github.com/xuri/excelize/v2" "github.com/kubev2v/migration-planner/pkg/duckdb_parser/models" + "github.com/kubev2v/migration-planner/pkg/inventory" ) // testValidator returns no concerns for all VMs. @@ -92,7 +93,7 @@ var ( } vHostHeaders = []string{"Datacenter", "Cluster", "# Cores", "# CPU", "Object ID", "# Memory", "Model", "Vendor", "Host", "Config status"} vDatastoreHeaders = []string{"Hosts", "Address", "Name", "Object ID", "Free MiB", "MHA", "Capacity MiB", "Type"} - vClusterHeaders = []string{"Name", "Object ID"} + vClusterHeaders = []string{"Name", "Object ID", "drs"} ) // defaultStandardSheets returns vInfo, vHost, default vDatastore, and vCluster from hosts for createTestExcel. @@ -105,7 +106,7 @@ func defaultStandardSheets(vms, hosts []map[string]string) []ExcelSheet { continue } clustersSeen[cluster] = true - vClustersRows = append(vClustersRows, map[string]string{"Name": cluster, "Object ID": fmt.Sprintf("domain-c%d", i+1)}) + vClustersRows = append(vClustersRows, map[string]string{"Name": cluster, "Object ID": fmt.Sprintf("domain-c%d", i+1), "drs": "false"}) } vDatastoreRows := []map[string]string{ @@ -1475,6 +1476,219 @@ func TestBuildInventory_VMListFilter(t *testing.T) { assert.Equal(t, 0, invNone.VCenter.VMs.Total, "Non-existent VMs should result in 0 count") } +// TestBuildInventory_ClusterFeatures tests that cluster features including DrsEnabled +// are properly parsed from vCluster sheet and populated in the inventory. +func TestBuildInventory_ClusterFeatures(t *testing.T) { + parser, _, cleanup := setupTestParser(t, &testValidator{}) + defer cleanup() + + vms := []map[string]string{ + {"VM": "vm-1", "VM ID": "vm-001", "VI SDK UUID": "uuid-1", "Host": "esxi-host-1", "CPUs": "4", "Memory": "8192", "Powerstate": "poweredOn", "Cluster": "cluster-drs-enabled", "Datacenter": "dc1"}, + {"VM": "vm-2", "VM ID": "vm-002", "VI SDK UUID": "uuid-2", "Host": "esxi-host-2", "CPUs": "2", "Memory": "4096", "Powerstate": "poweredOn", "Cluster": "cluster-drs-disabled", "Datacenter": "dc1"}, + {"VM": "vm-3", "VM ID": "vm-003", "VI SDK UUID": "uuid-3", "Host": "esxi-host-3", "CPUs": "8", "Memory": "16384", "Powerstate": "poweredOn", "Cluster": "cluster-no-drs-data", "Datacenter": "dc1"}, + } + hosts := []map[string]string{ + {"Datacenter": "dc1", "Cluster": "cluster-drs-enabled", "# Cores": "8", "# CPU": "2", "Object ID": "host-001", "# Memory": "32768", "Model": "ESXi", "Vendor": "VMware", "Host": "esxi-host-1", "Config status": "green"}, + {"Datacenter": "dc1", "Cluster": "cluster-drs-disabled", "# Cores": "16", "# CPU": "2", "Object ID": "host-002", "# Memory": "65536", "Model": "ESXi", "Vendor": "VMware", "Host": "esxi-host-2", "Config status": "green"}, + {"Datacenter": "dc1", "Cluster": "cluster-no-drs-data", "# Cores": "12", "# CPU": "2", "Object ID": "host-003", "# Memory": "49152", "Model": "ESXi", "Vendor": "VMware", "Host": "esxi-host-3", "Config status": "green"}, + } + + // Custom vCluster data with specific DRS settings + vClustersRows := []map[string]string{ + {"Name": "cluster-drs-enabled", "Object ID": "domain-c1", "drs": "true"}, + {"Name": "cluster-drs-disabled", "Object ID": "domain-c2", "drs": "false"}, + {"Name": "cluster-no-drs-data", "Object ID": "domain-c3", "drs": ""}, // Empty DRS value should default to false + } + + vDatastoreRows := []map[string]string{ + {"Hosts": "esxi-host-1", "Address": "10.0.0.1", "Name": "datastore1", "Object ID": "datastore-001", "Free MiB": "524288", "MHA": "false", "Capacity MiB": "1048576", "Type": "VMFS"}, + } + + sheets := []ExcelSheet{ + NewExcelSheet("vInfo", vInfoHeaders, vms), + NewExcelSheet("vHost", vHostHeaders, hosts), + NewExcelSheet("vDatastore", vDatastoreHeaders, vDatastoreRows), + NewExcelSheet("vCluster", vClusterHeaders, vClustersRows), + } + tmpFile := createTestExcel(t, sheets...) + + ctx := context.Background() + _, err := parser.IngestRvTools(ctx, tmpFile) + require.NoError(t, err) + + inv, err := parser.BuildInventory(ctx, nil) + require.NoError(t, err) + + // Should have 3 clusters + assert.Len(t, inv.Clusters, 3) + + // Find each cluster and verify their DRS settings + var drsEnabledCluster, drsDisabledCluster, noDrsDataCluster *inventory.InventoryData + for clusterID, cluster := range inv.Clusters { + switch clusterID { + case "domain-c1": // cluster-drs-enabled + drsEnabledCluster = &cluster + case "domain-c2": // cluster-drs-disabled + drsDisabledCluster = &cluster + case "domain-c3": // cluster-no-drs-data + noDrsDataCluster = &cluster + } + } + + // Verify cluster with DRS enabled + require.NotNil(t, drsEnabledCluster, "Should find cluster-drs-enabled") + require.NotNil(t, drsEnabledCluster.ClusterFeatures, "cluster-drs-enabled should have cluster features") + require.NotNil(t, drsEnabledCluster.ClusterFeatures.DrsEnabled, "cluster-drs-enabled should have DrsEnabled set") + assert.True(t, *drsEnabledCluster.ClusterFeatures.DrsEnabled, "cluster-drs-enabled should have DRS enabled") + + // Verify cluster with DRS disabled + require.NotNil(t, drsDisabledCluster, "Should find cluster-drs-disabled") + require.NotNil(t, drsDisabledCluster.ClusterFeatures, "cluster-drs-disabled should have cluster features") + require.NotNil(t, drsDisabledCluster.ClusterFeatures.DrsEnabled, "cluster-drs-disabled should have DrsEnabled set") + assert.False(t, *drsDisabledCluster.ClusterFeatures.DrsEnabled, "cluster-drs-disabled should have DRS disabled") + + // Verify cluster with missing DRS data (should default to false) + require.NotNil(t, noDrsDataCluster, "Should find cluster-no-drs-data") + require.NotNil(t, noDrsDataCluster.ClusterFeatures, "cluster-no-drs-data should have cluster features") + require.NotNil(t, noDrsDataCluster.ClusterFeatures.DrsEnabled, "cluster-no-drs-data should have DrsEnabled set") + assert.False(t, *noDrsDataCluster.ClusterFeatures.DrsEnabled, "cluster-no-drs-data should default to DRS disabled") +} + +// TestBuildInventory_ClusterFeaturesFlexibleParsing tests that the DRS parsing +// handles various input formats correctly. +func TestBuildInventory_ClusterFeaturesFlexibleParsing(t *testing.T) { + testCases := []struct { + name string + drsValue string + expectedEnabled bool + }{ + {"true lowercase", "true", true}, + {"TRUE uppercase", "TRUE", true}, + {"True mixed case", "True", true}, + {"1 numeric", "1", true}, + {"yes lowercase", "yes", true}, + {"YES uppercase", "YES", true}, + {"enabled lowercase", "enabled", true}, + {"ENABLED uppercase", "ENABLED", true}, + {"false lowercase", "false", false}, + {"FALSE uppercase", "FALSE", false}, + {"0 numeric", "0", false}, + {"no lowercase", "no", false}, + {"disabled", "disabled", false}, + {"empty string", "", false}, + {"whitespace only", " ", false}, + {"random text", "random", false}, + {"null-like", "null", false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + parser, _, cleanup := setupTestParser(t, &testValidator{}) + defer cleanup() + + vms := []map[string]string{ + {"VM": "test-vm", "VM ID": "vm-001", "VI SDK UUID": "uuid-1", "Host": "esxi-host-1", "CPUs": "4", "Memory": "8192", "Powerstate": "poweredOn", "Cluster": "test-cluster", "Datacenter": "dc1"}, + } + hosts := []map[string]string{ + {"Datacenter": "dc1", "Cluster": "test-cluster", "# Cores": "8", "# CPU": "2", "Object ID": "host-001", "# Memory": "32768", "Model": "ESXi", "Vendor": "VMware", "Host": "esxi-host-1", "Config status": "green"}, + } + vClustersRows := []map[string]string{ + {"Name": "test-cluster", "Object ID": "domain-c1", "drs": tc.drsValue}, + } + vDatastoreRows := []map[string]string{ + {"Hosts": "esxi-host-1", "Address": "10.0.0.1", "Name": "datastore1", "Object ID": "datastore-001", "Free MiB": "524288", "MHA": "false", "Capacity MiB": "1048576", "Type": "VMFS"}, + } + + sheets := []ExcelSheet{ + NewExcelSheet("vInfo", vInfoHeaders, vms), + NewExcelSheet("vHost", vHostHeaders, hosts), + NewExcelSheet("vDatastore", vDatastoreHeaders, vDatastoreRows), + NewExcelSheet("vCluster", vClusterHeaders, vClustersRows), + } + tmpFile := createTestExcel(t, sheets...) + + ctx := context.Background() + _, err := parser.IngestRvTools(ctx, tmpFile) + require.NoError(t, err) + + inv, err := parser.BuildInventory(ctx, nil) + require.NoError(t, err) + + // Should have 1 cluster + require.Len(t, inv.Clusters, 1) + + // Find the cluster and verify DRS setting + var testCluster *inventory.InventoryData + for clusterID, cluster := range inv.Clusters { + if clusterID == "domain-c1" { // test-cluster + testCluster = &cluster + break + } + } + + require.NotNil(t, testCluster, "Should find test-cluster") + require.NotNil(t, testCluster.ClusterFeatures, "test-cluster should have cluster features") + require.NotNil(t, testCluster.ClusterFeatures.DrsEnabled, "test-cluster should have DrsEnabled set") + assert.Equal(t, tc.expectedEnabled, *testCluster.ClusterFeatures.DrsEnabled, "DRS setting should match expected value for input: %q", tc.drsValue) + }) + } +} + +// TestBuildInventory_BackwardCompatibility_NoClusterFeatures tests that the system +// works correctly when cluster features (vCluster sheet) don't exist at all. +// This ensures backward compatibility with older RVTools exports. +func TestBuildInventory_BackwardCompatibility_NoClusterFeatures(t *testing.T) { + parser, _, cleanup := setupTestParser(t, &testValidator{}) + defer cleanup() + + vms := []map[string]string{ + {"VM": "vm-1", "VM ID": "vm-001", "VI SDK UUID": "uuid-1", "Host": "esxi-host-1", "CPUs": "4", "Memory": "8192", "Powerstate": "poweredOn", "Cluster": "test-cluster", "Datacenter": "dc1"}, + {"VM": "vm-2", "VM ID": "vm-002", "VI SDK UUID": "uuid-2", "Host": "esxi-host-1", "CPUs": "2", "Memory": "4096", "Powerstate": "poweredOn", "Cluster": "test-cluster", "Datacenter": "dc1"}, + } + hosts := []map[string]string{ + {"Datacenter": "dc1", "Cluster": "test-cluster", "# Cores": "8", "# CPU": "2", "Object ID": "host-001", "# Memory": "32768", "Model": "ESXi", "Vendor": "VMware", "Host": "esxi-host-1", "Config status": "green"}, + } + vDatastoreRows := []map[string]string{ + {"Hosts": "esxi-host-1", "Address": "10.0.0.1", "Name": "datastore1", "Object ID": "datastore-001", "Free MiB": "524288", "MHA": "false", "Capacity MiB": "1048576", "Type": "VMFS"}, + } + + // Create sheets WITHOUT vCluster for backward compatibility test + sheets := []ExcelSheet{ + NewExcelSheet("vInfo", vInfoHeaders, vms), + NewExcelSheet("vHost", vHostHeaders, hosts), + NewExcelSheet("vDatastore", vDatastoreHeaders, vDatastoreRows), + // NOTE: No vCluster sheet here - this tests backward compatibility + } + tmpFile := createTestExcel(t, sheets...) + + ctx := context.Background() + _, err := parser.IngestRvTools(ctx, tmpFile) + require.NoError(t, err) + + inv, err := parser.BuildInventory(ctx, nil) + require.NoError(t, err) + + // Should have 1 cluster (generated from VM cluster assignments) + require.Len(t, inv.Clusters, 1) + + // Find the cluster + var testCluster *inventory.InventoryData + for clusterID, cluster := range inv.Clusters { + t.Logf("Found cluster ID: %s", clusterID) + testCluster = &cluster + break + } + + require.NotNil(t, testCluster, "Should find cluster") + + // Cluster should exist but ClusterFeatures should be nil for backward compatibility + assert.Nil(t, testCluster.ClusterFeatures, "ClusterFeatures should be nil when vCluster sheet is missing (backward compatibility)") + + // But VM and Infra data should still be populated + assert.Equal(t, 2, testCluster.VMs.Total, "VMs should be counted correctly") + assert.NotEmpty(t, testCluster.Infra.Hosts, "Hosts should be populated") +} + func TestVCenterVersion(t *testing.T) { tests := []struct { name string diff --git a/pkg/duckdb_parser/models/inventory.go b/pkg/duckdb_parser/models/inventory.go index 5655bcb7a..007bdd871 100644 --- a/pkg/duckdb_parser/models/inventory.go +++ b/pkg/duckdb_parser/models/inventory.go @@ -45,8 +45,16 @@ type Network struct { // Cluster represents a VMware cluster with its resources. type Cluster struct { - Name string `json:"name"` - Datastores []Datastore `json:"datastores"` - Hosts []Host `json:"hosts"` - Networks []Network `json:"networks"` + Name string `json:"name"` + ClusterFeatures *ClusterFeatures `json:"clusterFeatures,omitempty"` + Datastores []Datastore `json:"datastores"` + Hosts []Host `json:"hosts"` + Networks []Network `json:"networks"` +} + +// ClusterFeatures represents VMware cluster feature settings (DRS, etc). +type ClusterFeatures struct { + DrsEnabled *bool `json:"drsEnabled,omitempty"` + DrsMode *string `json:"drsMode,omitempty"` + StorageDrsEnabled *bool `json:"storageDrsEnabled,omitempty"` } diff --git a/pkg/duckdb_parser/queries.go b/pkg/duckdb_parser/queries.go index 903062c9d..b78a665d1 100644 --- a/pkg/duckdb_parser/queries.go +++ b/pkg/duckdb_parser/queries.go @@ -518,6 +518,29 @@ func (p *Parser) ClustersPerDatacenter(ctx context.Context) ([]int, error) { return counts, rows.Err() } +// ClusterFeatures returns cluster features including DRS settings. +func (p *Parser) ClusterFeatures(ctx context.Context, clusterName string) (*models.ClusterFeatures, error) { + q, err := p.builder.ClusterFeaturesQuery(clusterName) + if err != nil { + return nil, fmt.Errorf("building cluster features query: %w", err) + } + + var cluster models.ClusterFeatures + err = p.db.QueryRowContext(ctx, q).Scan( + &cluster.DrsEnabled, + &cluster.DrsMode, + &cluster.StorageDrsEnabled, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, fmt.Errorf("cluster features not found for cluster %q: %w", clusterName, err) + } + return nil, fmt.Errorf("scanning cluster features: %w", err) + } + + return &cluster, nil +} + // readStringIntMap is a helper for reading (string, int) result sets into a map. func (p *Parser) readStringIntMap(ctx context.Context, query string) (map[string]int, error) { result := make(map[string]int) diff --git a/pkg/duckdb_parser/templates/cluster_features_query.go.tmpl b/pkg/duckdb_parser/templates/cluster_features_query.go.tmpl new file mode 100644 index 000000000..adcce5752 --- /dev/null +++ b/pkg/duckdb_parser/templates/cluster_features_query.go.tmpl @@ -0,0 +1,9 @@ +{{- /* +Cluster Features Query Template - Returns cluster features including DRS settings. +Takes cluster name filter to return features for specific cluster. +*/ -}} +SELECT "DrsEnabled", "DrsDefaultVmBehavior" as drsMode, "StorageDrsEnabled" +FROM vcluster +WHERE "Name" = '{{.ClusterName}}' + AND "Name" IS NOT NULL AND "Name" != '' +LIMIT 1; \ No newline at end of file diff --git a/pkg/duckdb_parser/templates/create_schema.go.tmpl b/pkg/duckdb_parser/templates/create_schema.go.tmpl index bce2f7107..374eac16e 100644 --- a/pkg/duckdb_parser/templates/create_schema.go.tmpl +++ b/pkg/duckdb_parser/templates/create_schema.go.tmpl @@ -154,7 +154,10 @@ CREATE TABLE IF NOT EXISTS concerns ( CREATE TABLE IF NOT EXISTS vcluster ( "Name" VARCHAR, - "Object ID" VARCHAR + "Object ID" VARCHAR, + "DrsEnabled" BOOLEAN DEFAULT false, + "DrsDefaultVmBehavior" VARCHAR DEFAULT 'None', + "StorageDrsEnabled" BOOLEAN DEFAULT false ); CREATE TABLE IF NOT EXISTS about ( diff --git a/pkg/duckdb_parser/templates/ingest_rvtools.go.tmpl b/pkg/duckdb_parser/templates/ingest_rvtools.go.tmpl index 8c41c26c9..533fc8426 100644 --- a/pkg/duckdb_parser/templates/ingest_rvtools.go.tmpl +++ b/pkg/duckdb_parser/templates/ingest_rvtools.go.tmpl @@ -287,6 +287,22 @@ INSERT INTO dvswitch ("Name") SELECT DISTINCT "Name" FROM read_xlsx('{{.FilePath}}', sheet='dvSwitch', all_varchar=true); -INSERT INTO vcluster ("Name", "Object ID") -SELECT "Name", "Object ID" -FROM read_xlsx('{{.FilePath}}', sheet='vCluster', all_varchar=true); +CREATE TABLE vcluster_raw AS +SELECT * FROM read_xlsx('{{.FilePath}}', sheet='vCluster', all_varchar=true); + +ALTER TABLE vcluster_raw ADD COLUMN IF NOT EXISTS "drs" VARCHAR; +ALTER TABLE vcluster_raw ADD COLUMN IF NOT EXISTS "DrsDefaultVmBehavior" VARCHAR; +ALTER TABLE vcluster_raw ADD COLUMN IF NOT EXISTS "Storage DRS" VARCHAR; + +INSERT INTO vcluster ("Name", "Object ID", "DrsEnabled", "DrsDefaultVmBehavior", "StorageDrsEnabled") +SELECT "Name", "Object ID", + CASE + WHEN LOWER(TRIM(COALESCE("drs", ''))) IN ('true', '1', 'yes', 'enabled') THEN true + ELSE false + END as "DrsEnabled", + COALESCE(NULLIF(TRIM("DrsDefaultVmBehavior"), ''), 'None') as "DrsDefaultVmBehavior", + CASE + WHEN LOWER(TRIM(COALESCE("Storage DRS", ''))) IN ('true', '1', 'yes', 'enabled') THEN true + ELSE false + END as "StorageDrsEnabled" +FROM vcluster_raw; diff --git a/pkg/inventory/converters/to_api.go b/pkg/inventory/converters/to_api.go index a453f4dc4..ce3d3e716 100644 --- a/pkg/inventory/converters/to_api.go +++ b/pkg/inventory/converters/to_api.go @@ -27,10 +27,40 @@ func ToAPI(inv *inventory.Inventory) *api.Inventory { } func toAPIInventoryData(d *inventory.InventoryData) api.InventoryData { - return api.InventoryData{ + result := api.InventoryData{ Vms: toAPIVMs(&d.VMs), Infra: toAPIInfra(&d.Infra), } + + if d.ClusterFeatures != nil { + clusterFeatures := api.ClusterFeatures{ + DrsEnabled: d.ClusterFeatures.DrsEnabled, + StorageDrsEnabled: d.ClusterFeatures.StorageDrsEnabled, + } + + // Map DrsMode string to enum type + if d.ClusterFeatures.DrsMode != nil { + normalized := strings.ToLower(strings.TrimSpace(*d.ClusterFeatures.DrsMode)) + switch normalized { + case "fullyautomated", "fully automated": + mode := api.FullyAutomated + clusterFeatures.DrsMode = &mode + case "partiallyautomated", "partially automated": + mode := api.PartiallyAutomated + clusterFeatures.DrsMode = &mode + case "manual": + mode := api.Manual + clusterFeatures.DrsMode = &mode + default: + mode := api.None + clusterFeatures.DrsMode = &mode + } + } + + result.ClusterFeatures = &clusterFeatures + } + + return result } func toAPIVMs(v *inventory.VMsData) api.VMs { diff --git a/pkg/inventory/model.go b/pkg/inventory/model.go index e4fae1dbe..793ad5c3a 100644 --- a/pkg/inventory/model.go +++ b/pkg/inventory/model.go @@ -11,8 +11,16 @@ type Inventory struct { // InventoryData contains VM and infrastructure data for a scope (vCenter or cluster). type InventoryData struct { - VMs VMsData - Infra InfraData + VMs VMsData + Infra InfraData + ClusterFeatures *ClusterFeatures +} + +// ClusterFeatures contains cluster-level feature information. +type ClusterFeatures struct { + DrsEnabled *bool `json:"drsEnabled,omitempty"` + DrsMode *string `json:"drsMode,omitempty"` + StorageDrsEnabled *bool `json:"storageDrsEnabled,omitempty"` } // VMsData contains aggregated VM statistics and distribution data.