Support import of VM/template for Managed block storage - #1138
Conversation
|
Hello, similar context again as on oVirt/vdsm#459 - testing on LINSTOR/DRBD. While single-disk OVA import to an MBS storage domain worked on our cluster, multi-disk OVA imports always fail at extract time with The We took a swing at refactoring for reduced complexity and reduced LoC here: refactor branch (single commit on top of With the refactor, we verified the imports with LINSTOR managed block storage by exporting then re-importing:
Happy to open a follow up PR with the branch above after this is merged. Or, if desired, pull the changes into this PR or let it inspire a similar refactoring? Everything else works well, just a hiccup on multi-disk imports. |
There was a problem hiding this comment.
I mainly have structural suggestions/remarks. The current implementation weaves MBS-specific branches throughout existing classes using ifs etc. (e.g. if (isManagedBlockStorage()) checks. This makes each class harder to reason about,
you always have to mentally filter out the "other" path. My suggestion is to keep each class focused on one storage type and put MBS logic in dedicated places. A patch illustrating all of the below is attached: mbs-refactor-suggestion.patch
(mind you; generated with help of AI, not tested nor compiled, so just use it as a general guide 😅 )
Also, you don't HAVE to blindly agree with me, I'm open for discussion 👼
StorageDomainValidator, one predicate instead of six scattered checks (minor)
Both Cinder and MBS are externally managed backends where oVirt has no reliable free-space data. Right now each space-check method has
its own isCinderDomain() || isManagedBlockStorage() bypass, and three methods are already missing the MBS bypass (inconsistent, intentional?). Add a
single protected method:
protected boolean isExternallyManagedStorage() {
return storageDomain.getStorageType().isCinderDomain()
|| storageDomain.getStorageType().isManagedBlockStorage();
} Replace all six bypass conditions with it.
ExtractOvaCommand + new MbsExtractOvaCommand, subclass instead of branching
In this PR ExtractOvaCommand imports ManagedBlockStorageCommandUtil, DisksFilter, ManagedBlockStorageDisk, and
DeviceInfoReturn, none of which belong in a general extraction command. The four things that actually differ between IRS and MBS are:
pre-image setup, how a disk path is resolved, how a disk is torn down, and how the image-mapping YAML is built. Extract those as
protected template methods with IRS defaults in the base class, and put all MBS-specific overrides in a new MbsExtractOvaCommand. The
parent import commands pick which action type to dispatch via a single overridable extractOvaActionType() method.
The same can be looked at for the other commands in bll/exportimport
DiskProfileHelper, the MBS check becomes redundant with the subclass approach
The PR adds an isManagedBlockStorageDomain() check inside DiskProfileHelper to skip profile validation for MBS destinations. This
works, but it means a general-purpose helper now needs to know about storage domain types, and it queries storageDomainDao once per
disk to do so.
With the MbsImportVmFromOvaCommand subclass approach this becomes unnecessary. Each MBS command simply overrides
setAndValidateDiskProfiles() to return true:
// MbsImportVmFromOvaCommand
@Override
protected boolean setAndValidateDiskProfiles() {
return true; // MBS domains have no disk profiles
} DiskProfileHelper is then never called from an MBS command path in the first place, making the bypass inside it a no-op that can be
removed. The helper goes back to knowing nothing about storage domain types, and the StorageDomainDao injection added by this PR can be
dropped along with it.
The same override could again be added other Mbs...Commands where needed.
AddDiskCommand, fix the type mismatch at source instead of patching around it
isDiskImageTargetingManagedBlockStorageDomain() exists because during OVA import, disk objects arrive at AddDisk typed as IMAGE
even though they are going to an MBS domain. The command then has to inspect the storage domain in four separate places to route
correctly. The fix is to correct the type earlier: in adjustDisk() inside the import command, convert the DiskImage to a
ManagedBlockStorageDisk when the destination is MBS (add a static ManagedBlockStorageDisk.copyFrom(DiskImage)). AddDiskCommand then
sees MANAGED_BLOCK_STORAGE and the existing routing handles it — isDiskImageTargetingManagedBlockStorageDomain() and all four call
sites can be deleted.
The relevant changes:
- ManagedBlockStorageDisk.java, the new copyFrom() static factory
- MbsImportVmFromOvaCommand.java, the adjustDisk() override that calls copyFrom()
- AddDiskCommand.java, the four removed call sites and the deleted isDiskImageTargetingManagedBlockStorageDomain() method
|
Thanks for the review, @peter-boden — I've addressed the structural points:
Ryan's multi-disk OVA extract fix is included too. Please take another look when you can. |
Both PRs branched off master independently and each claimed the then-next-free ActionType id 1054: MbsTransferDiskImage from oVirt#1125 (upload-msb-disk) and MbsExtractOva from oVirt#1138 (import-ova-mbs). They add their entries in different parts of the enum, so git merges them without a textual conflict and only ActionTypeTest notices. This is not merely a failing test: ActionType.forValue(1054) returns whichever entry comes first, so a command rebuilt from command_entities.command_type could dispatch to the wrong one. Move oVirt#1125's single entry to 1058 rather than oVirt#1138's contiguous 1054-1057 block, as the smaller change. Nothing references the numeric value (lookups go through the enum name) and no dbscript mentions it. Upstream still needs one of the two PRs renumbered; until that happens this is carried as olvm/patches/ovirt-engine/ so each re-roll re-applies it. Generated-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both PRs branched off master independently and each claimed the then-next-free ActionType id 1054: MbsTransferDiskImage from oVirt#1125 (upload-msb-disk) and MbsExtractOva from oVirt#1138 (import-ova-mbs). They add their entries in different parts of the enum, so git merges them without a textual conflict and only ActionTypeTest notices. This is not merely a failing test: ActionType.forValue(1054) returns whichever entry comes first, so a command rebuilt from command_entities.command_type could dispatch to the wrong one. Move oVirt#1125's single entry to 1058 rather than oVirt#1138's contiguous 1054-1057 block, as the smaller change. Nothing references the numeric value (lookups go through the enum name) and no dbscript mentions it. Upstream still needs one of the two PRs renumbered; until that happens this is carried as olvm/patches/ovirt-engine/ so each re-roll re-applies it. Generated-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
peter-boden
left a comment
There was a problem hiding this comment.
I found one more issue that I missed in the previous review, and also a new issue that was introduced due to the refactor (kind-off my fault 🙈 )
| .collect(Collectors.toMap(i -> diskPaths.get(i), | ||
| i -> diskList.get(i).getVolumeFormat() == VolumeFormat.COW ? "qcow2" : "raw")); | ||
| String json; | ||
| private String prepareDisksJson(List<DiskImage> diskList, List<String> diskPaths) { |
There was a problem hiding this comment.
This method pairs ovaTarNamesByIndex with diskPaths/diskList by index. These two lists come from different places and are not guaranteed to share the same order:
ovaTarNamesByIndexis built in the parent (ImportVmFromOvaCommand.buildOvaTarNamesByIndex,ImportVmTemplateFromOvaCommandequivalent) fromimageMappings, aMap<diskId, tarName>that is already keyed by disk id.diskListin the child is reloaded viaupdateDisksFromDb() -> diskDao.getAllForVm -> GetDisksVmGuid, which has no ORDER BY and joins a multi-table view. Order is not guaranteed.
When the two orders diverge, disk i's tar name gets paired with disk j's path, so the wrong OVA member is extracted into the wrong volume. Silent, no error. Single-disk imports can't hit this (index 0 to index 0 always matches),
which is likely why the 1/2/3-disk LINSTOR test passed but real-world multi-disk still risks corruption depending on plan/heap order.
Compounding issue: extract_ova.py has no coverage check. If a tar name the engine hands over never matches a member in the OVA, that disk is silently skipped and the playbook returns rc 0 with an empty volume.
Suggested fix: stop pairing by index.
- On
ConvertOvaParameters, replaceList<String> ovaTarNamesByIndexwithMap<Guid, String> diskIdToTarName. The parent already has the disk-id keyed map (imageMappings), just pass it through instead of flattening to a list. - In
prepareDisksJson, look up the tar name viadiskIdToTarName.get(diskList.get(i).getId())instead oftarNames.get(i). Throw if a lookup misses or a tar name is duplicated, don't silently drop a disk. - In
extract_ova.py, track a remaining = set(disks) and discard on match,sys.exit(1)if anything is left after the loop.
This removes the ordering dependency entirely, no ORDER BY/sort needed anywhere, and adds loud-failure behavior for a mismatch instead of a silent bad import.
There was a problem hiding this comment.
Thanks for catching both issues @peter-boden! For extract, we now pass diskIdToTarName (Map<Guid, String>) instead of pairing by index, fail on miss/duplicate in prepareDisksJson, and extract_ova.py exits 1 if any tar name is unmatched. For mixed template import, MbsImportVmTemplateFromOvaCommand no longer overrides setAndValidateDiskProfiles() with return true; it supplies a per-disk skip predicate so only MBS-destined disks skip profiles, using destination-based lookup in validate() (before adjustDisk/copyFrom). Destination resolution goes through getDestinationDomainIdForDisk() keyed by original OVF disk id so non-MBS disks in a mixed template still get profiles and the correct storage domain. Tested with mixed MBS (with StorPool) + NFS template OVA import.
| @Override | ||
| public void executeImport(IFrontendMultipleActionAsyncCallback callback) { | ||
| List<ActionParametersBase> parameters = buildImportTemplateFromOvaParameters(); | ||
| ActionType actionType = importsToManagedBlockStorage(parameters) |
There was a problem hiding this comment.
Correction first: my earlier "subclass per storage type" suggestion works for VMs but does not
work for templates, and that assumption is why this gap exists.
VM import targets one storage domain for the whole import (getStorage().getSelectedItem()
on the frontend), so all disks are MBS or none. A blanket setAndValidateDiskProfiles() { return true; }
in MbsImportVmFromOvaCommand is correct there.
Template import is different. Per-disk destination is an established feature (predates this PR):
ImportDiskData carries its own selectedStorageDomain per disk, and buildImportTemplateFromOvaParameters
writes it to imageToDestinationDomainMap keyed per disk. So a single template can have
disk1 -> MBS and disk2 -> iSCSI, and a batch can mix an all-iSCSI template with an MBS template.
importsToManagedBlockStorage returns true if any disk of any template in the batch targets MBS,
so a mixed template or mixed batch runs the whole batch through MbsImportVmTemplateFromOvaCommand.
Its setAndValidateDiskProfiles() returns true unconditionally, so it skips disk-profile assignment
and the isDiskProfilePermitted check for every disk, including non-MBS disks. Before this refactor
the template command did not override setAndValidateDiskProfiles (it used the base per-disk
validation), so this is a regression. Non-MBS disks in a mixed template or batch then:
- get no default disk profile set when
getDiskProfileId() == null, which is required on non-MBS domains - skip the profile permission check, so a user without
ATTACH_DISK_PROFILErights on the
destination domain can import a disk there unvalidated
On the fix, the per-disk MBS decision is a destination-domain property, not a command property,
and OvaImportManagedBlockSupport.diskTargetsManagedBlockStorage already computes it per disk.
Have DiskProfileHelper accept a skip predicate (keeps it storage-type-agnostic), the base
ImportVmTemplateCommandBase.setAndValidateDiskProfiles pass "skip nothing", and
MbsImportVmTemplateFromOvaCommand supply diskTargetsManagedBlockStorage as the predicate
instead of return true. That covers the mixed-batch and single-mixed-template cases and keeps
the discrimination out of the command body.
One lifecycle note: setAndValidateDiskProfiles runs in validate(), before adjustDisk/copyFrom
(which runs in executeCommand()/processImages). So at validation time MBS-destined disks are
still DiskImage typed as IMAGE, and DiskProfileHelper's existing != IMAGE skip does not catch
them. The discrimination has to be destination-based at this phase, not type-based. removeVmImages
runs post-adjustDisk/persist and already partitions correctly by getDiskStorageType() inside
removeDisksAfterFailedOvaImport, so that path does not need the same change.
OVA import for virtual machines and templates is improved so imports work when the destination is managed block storage: the engine treats MBS like a first-class target for OVA extract/convert/import, disk handling, and validation, instead of only traditional storage domains. Signed-off-by: Slavka Peleva <slavkap@storpool.com>
- enables multi-disk OVA imports to managed block storage domains - extract_ova.py joins disks by tar member name directly, removing the pathToImageId sub-field and image_mappings argv that bridged OVF and engine-fresh UUID universes - drops the now-orphaned imageMappings field on ConvertOvaParameters (the OVA path stashes the diskId-to-tar-name map on the parent ImportVmFromOvaParameters and resolves it once into ovaTarNamesByIndex before handing off to ConvertOva / ExtractOva) - tested on LINSTOR by exporting then re-importing: VMs with 1, 2, and 3 disks on managed block storage, mixed iSCSI + managed block storage, iSCSI-only, and a template Signed-off-by: Ryan Ronnander <rronnander@linbit.com>
Signed-off-by: Slavka Peleva <slavkap@storpool.com>
Addressed comments: isolate MBS logic in OVA extract/convert/import commands Signed-off-by: Slavka Peleva <slavkap@storpool.com>
Replace index-based ovaTarNamesByIndex with diskIdToTarName so multi-disk extract pairs OVA tar members to the correct disk after DB reload. Fail explicitly on missing/duplicate mappings and unmatched tar names in extract_ova.py. For mixed template imports, skip disk profile validation only for disks targeting MBS (predicate in DiskProfileHelper), not all disks in the batch. Resolve per-disk destination domains via original OVF disk IDs so NFS disks still get profiles and a storage domain after clone ID minting. Signed-off-by: Slavka Peleva <slavkap@storpool.com>
02f9ef9 to
e28e475
Compare
peter-boden
left a comment
There was a problem hiding this comment.
Changes look good to me now, props 👍
I'm however not able to test these changes, so if e.g. @ryan-ronnander you could take another look? The changes are quite substantial. Especially a test with multiple disk could be interesting I think.
|
Sure thing, just tested the recent changes against LINSTOR/DRBD MBS backend. All operations checked with sha256 against the original disk contents. Looks good from our side 👍 , thanks @slavkap. Test results table:
|
|
@peter-boden, @ryan-ronnander, thank you for your time to review and test this! |
Changes introduced with this PR
This has been tested with MBS - StorPool and Ceph
Are you the owner of the code you are sending in, or do you have permission of the owner?
yes