Skip to content

Commit b1babde

Browse files
authored
Merge branch 'main' into private/geet/main/cleanup-builds-final
2 parents dcac68d + 03ab937 commit b1babde

File tree

27 files changed

+720
-167
lines changed

27 files changed

+720
-167
lines changed

deploy/00crds.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,8 @@ spec:
985985
description: NICOverride defines per-NIC overrides for IP and
986986
MAC preservation during migration
987987
properties:
988+
UserAssignedIP:
989+
type: string
988990
interfaceIndex:
989991
description: InterfaceIndex is the zero-based index of the
990992
NIC

deploy/installer.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,8 @@ spec:
985985
description: NICOverride defines per-NIC overrides for IP and
986986
MAC preservation during migration
987987
properties:
988+
UserAssignedIP:
989+
type: string
988990
interfaceIndex:
989991
description: InterfaceIndex is the zero-based index of the
990992
NIC

k8s/migration/api/v1alpha1/migrationplan_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ type NICOverride struct {
8686
// PreserveMAC indicates whether to preserve the source VM's MAC address for this NIC.
8787
// When nil the migration default (preserve) is used.
8888
PreserveMAC *bool `json:"preserveMAC,omitempty"`
89+
90+
UserAssignedIP string `json:"UserAssignedIP,omitempty"`
8991
}
9092

9193
// MigrationPlanSpec defines the desired state of MigrationPlan including

k8s/migration/config/crd/bases/vjailbreak.k8s.pf9.io_migrationplans.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ spec:
156156
description: NICOverride defines per-NIC overrides for IP and
157157
MAC preservation during migration
158158
properties:
159+
UserAssignedIP:
160+
type: string
159161
interfaceIndex:
160162
description: InterfaceIndex is the zero-based index of the
161163
NIC

k8s/migration/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
github.com/prometheus/client_golang v1.22.0
2323
github.com/vmware/govmomi v0.51.0
2424
go.uber.org/zap v1.27.0
25+
golang.org/x/crypto v0.44.0
2526
gopkg.in/yaml.v3 v3.0.1
2627
k8s.io/api v0.33.3
2728
k8s.io/apimachinery v0.33.3
@@ -86,7 +87,6 @@ require (
8687
go.uber.org/automaxprocs v1.6.0 // indirect
8788
go.uber.org/multierr v1.11.0 // indirect
8889
go.yaml.in/yaml/v2 v2.4.2 // indirect
89-
golang.org/x/crypto v0.44.0 // indirect
9090
golang.org/x/net v0.47.0 // indirect
9191
golang.org/x/oauth2 v0.30.0 // indirect
9292
golang.org/x/sync v0.18.0 // indirect

k8s/migration/internal/controller/migrationplan_controller.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,7 +1704,8 @@ func (r *MigrationPlanReconciler) reconcileMapping(ctx context.Context,
17041704
// Skip storage mapping reconciliation for StorageCopyMethod storage copy method
17051705
// as it uses ArrayCredsMapping instead of StorageMapping
17061706
if migrationtemplate.Spec.StorageCopyMethod != StorageCopyMethod {
1707-
openstackvolumetypes, err = r.reconcileStorage(ctx, migrationtemplate, openstackcreds, vmMachine.Spec.VMInfo.Datastores)
1707+
vmDatastores := getDatastoresForVolumeMapping(vmMachine)
1708+
openstackvolumetypes, err = r.reconcileStorage(ctx, migrationtemplate, openstackcreds, vmDatastores)
17081709
if err != nil {
17091710
return nil, nil, errors.Wrap(err, "failed to reconcile storage")
17101711
}
@@ -1713,6 +1714,22 @@ func (r *MigrationPlanReconciler) reconcileMapping(ctx context.Context,
17131714
return openstacknws, openstackvolumetypes, nil
17141715
}
17151716

1717+
// getDatastoresForVolumeMapping returns datastores in per-disk order so
1718+
// StorageMapping produces one Cinder volume type for each virtual disk.
1719+
func getDatastoresForVolumeMapping(vmMachine *vjailbreakv1alpha1.VMwareMachine) []string {
1720+
if len(vmMachine.Spec.VMInfo.Disks) > 0 {
1721+
vmds := make([]string, 0, len(vmMachine.Spec.VMInfo.Disks))
1722+
for _, disk := range vmMachine.Spec.VMInfo.Disks {
1723+
vmds = append(vmds, disk.Datastore)
1724+
}
1725+
if len(vmds) > 0 {
1726+
return vmds
1727+
}
1728+
}
1729+
1730+
return vmMachine.Spec.VMInfo.Datastores
1731+
}
1732+
17161733
//nolint:dupl // Similar logic to storages reconciliation, excluding from linting to keep it readable
17171734
func (r *MigrationPlanReconciler) reconcileNetwork(ctx context.Context,
17181735
migrationtemplate *vjailbreakv1alpha1.MigrationTemplate,
@@ -1785,15 +1802,21 @@ func (r *MigrationPlanReconciler) reconcileStorage(ctx context.Context,
17851802
}
17861803

17871804
openstackvolumetypes := []string{}
1788-
for _, vmdatastore := range vmds {
1805+
for diskIdx, vmdatastore := range vmds {
1806+
if vmdatastore == "" {
1807+
return nil, errors.Errorf("VMware datastore is empty for disk index %d", diskIdx)
1808+
}
1809+
found := false
17891810
for _, storagemaptype := range storagemap.Spec.Storages {
17901811
if vmdatastore == storagemaptype.Source {
17911812
openstackvolumetypes = append(openstackvolumetypes, storagemaptype.Target)
1813+
found = true
1814+
break
17921815
}
17931816
}
1794-
}
1795-
if len(openstackvolumetypes) != len(vmds) {
1796-
return nil, errors.Errorf("VMware Datastore(s) not found in StorageMapping vm(%d) openstack(%d)", len(vmds), len(openstackvolumetypes))
1817+
if !found {
1818+
return nil, errors.Errorf("VMware datastore %q not found in StorageMapping for disk index %d", vmdatastore, diskIdx)
1819+
}
17971820
}
17981821
if storagemap.Status.StoragemappingValidationStatus != string(corev1.PodSucceeded) {
17991822
err = utils.VerifyStorage(ctx, r.Client, openstackcreds, openstackvolumetypes)

k8s/migration/internal/controller/migrationplan_controller_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package controller
1818

1919
import (
2020
"context"
21+
"reflect"
22+
"testing"
2123

2224
"github.com/onsi/ginkgo/v2"
2325
"github.com/onsi/gomega"
@@ -80,3 +82,61 @@ var _ = ginkgo.Describe("MigrationPlan Controller", func() {
8082
})
8183
})
8284
})
85+
86+
func TestGetDatastoresForVolumeMapping_UsesPerDiskOrderWithDuplicates(t *testing.T) {
87+
vmMachine := &vjailbreakv1alpha1.VMwareMachine{
88+
Spec: vjailbreakv1alpha1.VMwareMachineSpec{
89+
VMInfo: vjailbreakv1alpha1.VMInfo{
90+
Datastores: []string{"nfs"},
91+
Disks: []vjailbreakv1alpha1.Disk{
92+
{Name: "Hard disk 1", Datastore: "nfs"},
93+
{Name: "Hard disk 2", Datastore: "nfs"},
94+
{Name: "Hard disk 3", Datastore: "ssd"},
95+
},
96+
},
97+
},
98+
}
99+
100+
got := getDatastoresForVolumeMapping(vmMachine)
101+
want := []string{"nfs", "nfs", "ssd"}
102+
if !reflect.DeepEqual(got, want) {
103+
t.Fatalf("unexpected datastore mapping: got %v, want %v", got, want)
104+
}
105+
}
106+
107+
func TestGetDatastoresForVolumeMapping_FallsBackToLegacyDatastores(t *testing.T) {
108+
vmMachine := &vjailbreakv1alpha1.VMwareMachine{
109+
Spec: vjailbreakv1alpha1.VMwareMachineSpec{
110+
VMInfo: vjailbreakv1alpha1.VMInfo{
111+
Datastores: []string{"nfs", "ssd"},
112+
Disks: nil,
113+
},
114+
},
115+
}
116+
117+
got := getDatastoresForVolumeMapping(vmMachine)
118+
want := []string{"nfs", "ssd"}
119+
if !reflect.DeepEqual(got, want) {
120+
t.Fatalf("unexpected fallback datastore mapping: got %v, want %v", got, want)
121+
}
122+
}
123+
124+
func TestGetDatastoresForVolumeMapping_PreservesBlankDiskDatastore(t *testing.T) {
125+
vmMachine := &vjailbreakv1alpha1.VMwareMachine{
126+
Spec: vjailbreakv1alpha1.VMwareMachineSpec{
127+
VMInfo: vjailbreakv1alpha1.VMInfo{
128+
Datastores: []string{"legacy-ds"},
129+
Disks: []vjailbreakv1alpha1.Disk{
130+
{Name: "Hard disk 1", Datastore: ""},
131+
{Name: "Hard disk 2", Datastore: "nfs"},
132+
},
133+
},
134+
},
135+
}
136+
137+
got := getDatastoresForVolumeMapping(vmMachine)
138+
want := []string{"", "nfs"}
139+
if !reflect.DeepEqual(got, want) {
140+
t.Fatalf("unexpected datastore mapping for blank datastore disks: got %v, want %v", got, want)
141+
}
142+
}

pkg/common/validation/openstack/validate.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,15 @@ func Validate(ctx context.Context, k8sClient client.Client, openstackcreds *vjai
149149
}
150150

151151
if openstackCredential.VJBInstanceID != "" {
152+
// Verify the provided instance ID exists in the OpenStack environment
153+
_, err = verifyInstanceExists(providerClient, openstackCredential.RegionName, openstackCredential.VJBInstanceID)
154+
if err != nil {
155+
return ValidationResult{
156+
Valid: false,
157+
Message: fmt.Sprintf("Failed to verify instance ID '%s': %s", openstackCredential.VJBInstanceID, err.Error()),
158+
Error: err,
159+
}
160+
}
152161
return successfulValidationObject
153162
}
154163

@@ -284,6 +293,47 @@ func verifyCredentialsMatchCurrentEnvironment(providerClient *gophercloud.Provid
284293
return true, nil
285294
}
286295

296+
// verifyInstanceExists checks if the provided instance ID exists in the OpenStack environment
297+
func verifyInstanceExists(providerClient *gophercloud.ProviderClient, regionName string, instanceID string) (ok bool, err error) {
298+
defer func() {
299+
if r := recover(); r != nil {
300+
ok = false
301+
err = fmt.Errorf("panic while verifying instance existence: %v", r)
302+
}
303+
}()
304+
305+
if providerClient == nil {
306+
return false, fmt.Errorf("provider client is nil")
307+
}
308+
if providerClient.EndpointLocator == nil {
309+
return false, fmt.Errorf("OpenStack client is not authenticated (endpoint locator is not initialized)")
310+
}
311+
if strings.TrimSpace(regionName) == "" {
312+
return false, fmt.Errorf("region name is empty")
313+
}
314+
if strings.TrimSpace(instanceID) == "" {
315+
return false, fmt.Errorf("instance ID is empty")
316+
}
317+
318+
computeClient, err := openstack.NewComputeV2(providerClient, gophercloud.EndpointOpts{
319+
Region: regionName,
320+
})
321+
if err != nil {
322+
return false, fmt.Errorf("failed to create OpenStack compute client: %w", err)
323+
}
324+
325+
_, err = servers.Get(context.TODO(), computeClient, instanceID).Extract()
326+
if err != nil {
327+
if strings.Contains(err.Error(), "Resource not found") ||
328+
strings.Contains(err.Error(), "No server with a name or ID") ||
329+
strings.Contains(err.Error(), "404") {
330+
return false, fmt.Errorf("instance ID '%s' not found in the OpenStack environment. Please verify the instance ID and ensure the credentials are for the correct OpenStack environment", instanceID)
331+
}
332+
return false, fmt.Errorf("failed to verify instance access: %w. Please check if the provided credentials have compute:get_server permission", err)
333+
}
334+
return true, nil
335+
}
336+
287337
// PostValidationResources holds resources fetched after successful validation
288338
type PostValidationResources struct {
289339
Flavors []flavors.Flavor

0 commit comments

Comments
 (0)