|
8 | 8 | "fmt"
|
9 | 9 | "net"
|
10 | 10 | "net/netip"
|
| 11 | + "regexp" |
11 | 12 | "slices"
|
12 | 13 | "strconv"
|
13 | 14 | "strings"
|
@@ -50,6 +51,10 @@ const (
|
50 | 51 | regionKey = "region"
|
51 | 52 | zoneKey = "zone"
|
52 | 53 | minimumHWVersion = 15
|
| 54 | + // maxUnitNumber constant is used to define the maximum number of devices that can be assigned to a virtual machine's controller. |
| 55 | + // Not all controllers support up to 30, but the maximum is 30. |
| 56 | + // xref: https://docs.vmware.com/en/VMware-vSphere/8.0/vsphere-vm-administration/GUID-5872D173-A076-42FE-8D0B-9DB0EB0E7362.html#:~:text=If%20you%20add%20a%20hard,values%20from%200%20to%2014. |
| 57 | + maxUnitNumber = 30 |
53 | 58 | )
|
54 | 59 |
|
55 | 60 | // These are the guestinfo variables used by Ignition.
|
@@ -479,9 +484,12 @@ func (r *Reconciler) delete() error {
|
479 | 484 | if err != nil {
|
480 | 485 | return fmt.Errorf("%v: can not obtain virtual disks attached to the vm: %w", r.machine.GetName(), err)
|
481 | 486 | }
|
482 |
| - // Currently, MAPI does not provide any API knobs to configure additional volumes for a VM. |
483 |
| - // So, we are expecting the VM to have only one disk, which is OS disk. |
484 |
| - if len(disks) > 1 { |
| 487 | + |
| 488 | + additionalDisks := len(r.providerSpec.DataDisks) |
| 489 | + // Currently, MAPI only allows VMs to be configured w/ 1 primary disk in the template and a limited number of additional |
| 490 | + // disks via the data disks configuration. So, we are expecting the VM to have only one disk, which is OS disk, plus |
| 491 | + // the additional disks defined in the DataDisks configuration. |
| 492 | + if len(disks) > 1+additionalDisks { |
485 | 493 | // If node drain was skipped we need to detach disks forcefully to prevent possible data corruption.
|
486 | 494 | if drainSkipped {
|
487 | 495 | klog.V(1).Infof(
|
@@ -996,6 +1004,13 @@ func clone(s *machineScope) (string, error) {
|
996 | 1004 | deviceSpecs = append(deviceSpecs, diskSpec)
|
997 | 1005 | }
|
998 | 1006 |
|
| 1007 | + // Process all DataDisks definitions to dynamically create and add disks to the VM |
| 1008 | + additionalDisks, err := createDataDisks(s, devices) |
| 1009 | + if err != nil { |
| 1010 | + return "", fmt.Errorf("error getting additional disk specs: %w", err) |
| 1011 | + } |
| 1012 | + deviceSpecs = append(deviceSpecs, additionalDisks...) |
| 1013 | + |
999 | 1014 | klog.V(3).Infof("Getting network devices")
|
1000 | 1015 | networkDevices, err := getNetworkDevices(s, resourcepool, devices)
|
1001 | 1016 | if err != nil {
|
@@ -1196,6 +1211,122 @@ func getDiskSpec(s *machineScope, devices object.VirtualDeviceList) (types.BaseV
|
1196 | 1211 | }, nil
|
1197 | 1212 | }
|
1198 | 1213 |
|
| 1214 | +func createDataDisks(s *machineScope, devices object.VirtualDeviceList) ([]types.BaseVirtualDeviceConfigSpec, error) { |
| 1215 | + var diskSpecs []types.BaseVirtualDeviceConfigSpec |
| 1216 | + |
| 1217 | + // Only add additional disks if the feature gate is enabled. |
| 1218 | + if len(s.providerSpec.DataDisks) > 0 && !s.featureGates.Enabled(featuregate.Feature(apifeatures.FeatureGateVSphereMultiDisk)) { |
| 1219 | + return nil, machinecontroller.InvalidMachineConfiguration( |
| 1220 | + "machines cannot contain additional disks due to VSphereMultiDisk feature gate being disabled") |
| 1221 | + } |
| 1222 | + |
| 1223 | + // Get primary disk |
| 1224 | + disks := devices.SelectByType((*types.VirtualDisk)(nil)) |
| 1225 | + if len(disks) == 0 { |
| 1226 | + return nil, fmt.Errorf("invalid disk count: %d", len(disks)) |
| 1227 | + } |
| 1228 | + |
| 1229 | + // There is at least one disk |
| 1230 | + primaryDisk := disks[0].(*types.VirtualDisk) |
| 1231 | + |
| 1232 | + // Get the controller of the primary disk. |
| 1233 | + controller, ok := devices.FindByKey(primaryDisk.ControllerKey).(types.BaseVirtualController) |
| 1234 | + if !ok { |
| 1235 | + return nil, fmt.Errorf("unable to find controller with key=%v", primaryDisk.ControllerKey) |
| 1236 | + } |
| 1237 | + |
| 1238 | + controllerKey := controller.GetVirtualController().Key |
| 1239 | + unitNumberAssigner, err := newUnitNumberAssigner(controller, devices) |
| 1240 | + if err != nil { |
| 1241 | + return nil, fmt.Errorf("unable to create unit number assigner: %v", err) |
| 1242 | + } |
| 1243 | + |
| 1244 | + // Let's create the data disks now |
| 1245 | + for i, dataDisk := range s.providerSpec.DataDisks { |
| 1246 | + klog.V(2).InfoS("Adding disk", "name", dataDisk.Name, "spec", dataDisk) |
| 1247 | + |
| 1248 | + dev := &types.VirtualDisk{ |
| 1249 | + VirtualDevice: types.VirtualDevice{ |
| 1250 | + // Key needs to be unique and cannot match another new disk being added. So we'll use the index as an |
| 1251 | + // input to NewKey. NewKey() will always return same value since our new devices are not part of devices yet. |
| 1252 | + Key: devices.NewKey() - int32(i), |
| 1253 | + Backing: &types.VirtualDiskFlatVer2BackingInfo{ |
| 1254 | + DiskMode: string(types.VirtualDiskModePersistent), |
| 1255 | + ThinProvisioned: types.NewBool(true), |
| 1256 | + VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{ |
| 1257 | + FileName: "", |
| 1258 | + }, |
| 1259 | + }, |
| 1260 | + ControllerKey: controller.GetVirtualController().Key, |
| 1261 | + }, |
| 1262 | + CapacityInKB: int64(dataDisk.SizeGiB) * 1024 * 1024, |
| 1263 | + } |
| 1264 | + |
| 1265 | + vd := dev.GetVirtualDevice() |
| 1266 | + vd.ControllerKey = controllerKey |
| 1267 | + |
| 1268 | + // Assign unit number to the new disk. Should be next available slot on the controller. |
| 1269 | + unitNumber, err := unitNumberAssigner.assign() |
| 1270 | + if err != nil { |
| 1271 | + return nil, err |
| 1272 | + } |
| 1273 | + vd.UnitNumber = &unitNumber |
| 1274 | + |
| 1275 | + klog.V(2).InfoS("Created device for data disk device", "name", dataDisk.Name, "spec", dataDisk, "device", dev) |
| 1276 | + diskSpecs = append(diskSpecs, &types.VirtualDeviceConfigSpec{ |
| 1277 | + Device: dev, |
| 1278 | + Operation: types.VirtualDeviceConfigSpecOperationAdd, |
| 1279 | + FileOperation: types.VirtualDeviceConfigSpecFileOperationCreate, |
| 1280 | + }) |
| 1281 | + } |
| 1282 | + |
| 1283 | + return diskSpecs, nil |
| 1284 | +} |
| 1285 | + |
| 1286 | +type unitNumberAssigner struct { |
| 1287 | + used []bool |
| 1288 | + offset int32 |
| 1289 | +} |
| 1290 | + |
| 1291 | +func newUnitNumberAssigner(controller types.BaseVirtualController, existingDevices object.VirtualDeviceList) (*unitNumberAssigner, error) { |
| 1292 | + if controller == nil { |
| 1293 | + return nil, errors.New("controller parameter cannot be nil") |
| 1294 | + } |
| 1295 | + used := make([]bool, maxUnitNumber) |
| 1296 | + |
| 1297 | + // SCSIControllers also use a unit. |
| 1298 | + if scsiController, ok := controller.(types.BaseVirtualSCSIController); ok { |
| 1299 | + used[scsiController.GetVirtualSCSIController().ScsiCtlrUnitNumber] = true |
| 1300 | + } |
| 1301 | + controllerKey := controller.GetVirtualController().Key |
| 1302 | + |
| 1303 | + // Mark all unit numbers of existing devices as used |
| 1304 | + for _, device := range existingDevices { |
| 1305 | + d := device.GetVirtualDevice() |
| 1306 | + if d.ControllerKey == controllerKey && d.UnitNumber != nil { |
| 1307 | + used[*d.UnitNumber] = true |
| 1308 | + } |
| 1309 | + } |
| 1310 | + |
| 1311 | + // Set offset to 0, it will auto-increment on the first assignment. |
| 1312 | + return &unitNumberAssigner{used: used, offset: 0}, nil |
| 1313 | +} |
| 1314 | + |
| 1315 | +func (a *unitNumberAssigner) assign() (int32, error) { |
| 1316 | + if int(a.offset) > len(a.used) { |
| 1317 | + return -1, fmt.Errorf("all unit numbers are already in-use") |
| 1318 | + } |
| 1319 | + for i, isInUse := range a.used[a.offset:] { |
| 1320 | + unit := int32(i) + a.offset |
| 1321 | + if !isInUse { |
| 1322 | + a.used[unit] = true |
| 1323 | + a.offset++ |
| 1324 | + return unit, nil |
| 1325 | + } |
| 1326 | + } |
| 1327 | + return -1, fmt.Errorf("all unit numbers are already in-use") |
| 1328 | +} |
| 1329 | + |
1199 | 1330 | func getNetworkDevices(s *machineScope, resourcepool *object.ResourcePool, devices object.VirtualDeviceList) ([]types.BaseVirtualDeviceConfigSpec, error) {
|
1200 | 1331 | var networkDevices []types.BaseVirtualDeviceConfigSpec
|
1201 | 1332 | // Remove any existing NICs
|
@@ -1628,15 +1759,16 @@ type attachedDisk struct {
|
1628 | 1759 | diskMode string
|
1629 | 1760 | }
|
1630 | 1761 |
|
1631 |
| -// Filters out disks that look like vm OS disk. |
| 1762 | +// Filters out disks that look like vm OS disk or any of the additional disks. |
1632 | 1763 | // VM os disks filename contains the machine name in it
|
1633 | 1764 | // and has the format like "[DATASTORE] path-within-datastore/machine-name.vmdk".
|
1634 | 1765 | // This is based on vSphere behavior, an OS disk file gets a name that equals the target VM name during the clone operation.
|
1635 | 1766 | func filterOutVmOsDisk(attachedDisks []attachedDisk, machine *machinev1.Machine) []attachedDisk {
|
1636 | 1767 | var disks []attachedDisk
|
| 1768 | + regex, _ := regexp.Compile(fmt.Sprintf(".*\\/%s(_\\d*)?.vmdk", machine.GetName())) |
1637 | 1769 |
|
1638 | 1770 | for _, disk := range attachedDisks {
|
1639 |
| - if strings.HasSuffix(disk.fileName, fmt.Sprintf("/%s.vmdk", machine.GetName())) { |
| 1771 | + if regex.MatchString(disk.fileName) { |
1640 | 1772 | continue
|
1641 | 1773 | }
|
1642 | 1774 | disks = append(disks, disk)
|
|
0 commit comments