Repository: osbuild/osbuild-composer
Pull request #2521 (2022-04-06 11:56:49)

osbuild2: QEMU stage implementation enhancement and code de-duplication
- Enhance the QEMU stage implementation to allow specifying VMDK subformat.
- De-duplicate the `qemuStageOptions()` function from many distro implementations and move it to the `osbuild2` package as `NewQEMUStageOptions()`. In addition, make it more generic by not taking any format-specific arguments.

This PR is a preparation for eventually changing the VMDK image to be produced as stream-optimized, instead of being converted before uploading.

This pull request includes:

- [x] adequate testing for the new functionality or fixed issue
- [ ] adequate documentation informing people about the change such as
  - [ ] submit a PR for the [guides](https://github.com/osbuild/guides) repository if this PR changed any behavior described there: https://www.osbuild.org/guides/

<!--
Thanks for proposing a change to osbuild-composer!

Please don't remove the above check list. These are things that each pull
request must have before it is merged. It helps maintainers to not forget
anything.

If the reason for ticking any of the boxes is ambiguous, please add a short
note explaining why.

In addition, if this pull request fixes a downstream issue, please refer to
test/README.md and add these additional items:

- [ ] 1st commit of any `rhbz#` related PR contains bug reproducer; CI reports FAIL or
- [ ] PR contains automated tests for new functionality and
- [ ] QE has approved reproducer/new tests and
- [ ] Subsequent commits provide bug fixes without modifying the reproducer; CI reports PASS and
- [ ] QE approves this PR; RHBZ status is set to `MODIFIED + Verified=Tested`

Information regarding our GitLab pipeline (Schutzbot):

CI will not be ran automatically if WIP label is applied or the PR is in DRAFT state, instead only
a link will be provided to the pipeline which can then be triggered manually if desired. To run the
CI automatically either switch the PR to ready or apply WIP+test label.

Outside contributors need manual approval from one of the osbuild-composer maintainers.

Schutzbot will only be triggered if all Tests jobs in GitHub workflow succeed.
-->



Patch:
diff --git a/internal/distro/rhel85/pipelines.go b/internal/distro/rhel85/pipelines.go
index 1d4e7da25..abce49732 100644
--- a/internal/distro/rhel85/pipelines.go
+++ b/internal/distro/rhel85/pipelines.go
@@ -55,7 +55,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "qcow2", "0.10")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatQCOW2, osbuild.QCOW2Options{Compat: "0.10"})
 	pipelines = append(pipelines, *qemuPipeline)
 
 	return pipelines, nil
@@ -99,7 +99,7 @@ func vhdPipelines(t *imageType, customizations *blueprint.Customizations, option
 		return nil, err
 	}
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "vpc", "")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatVPC, nil)
 	pipelines = append(pipelines, *qemuPipeline)
 	return pipelines, nil
 }
@@ -130,7 +130,7 @@ func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, optio
 		return nil, err
 	}
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "vmdk", "")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatVMDK, nil)
 	pipelines = append(pipelines, *qemuPipeline)
 	return pipelines, nil
 }
@@ -161,7 +161,7 @@ func openstackPipelines(t *imageType, customizations *blueprint.Customizations,
 		return nil, err
 	}
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "qcow2", "")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatQCOW2, nil)
 	pipelines = append(pipelines, *qemuPipeline)
 	return pipelines, nil
 }
@@ -1271,12 +1271,15 @@ func xzArchivePipeline(inputPipelineName, inputFilename, outputFilename string)
 	return p
 }
 
-func qemuPipeline(inputPipelineName, inputFilename, outputFilename, format, qcow2Compat string) *osbuild.Pipeline {
+func qemuPipeline(inputPipelineName, inputFilename, outputFilename string, format osbuild.QEMUFormat, formatOptions osbuild.QEMUFormatOptions) *osbuild.Pipeline {
 	p := new(osbuild.Pipeline)
-	p.Name = format
+	p.Name = string(format)
 	p.Build = "name:build"
 
-	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename))
+	qemuStage := osbuild.NewQEMUStage(
+		osbuild.NewQEMUStageOptions(outputFilename, format, formatOptions),
+		osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename),
+	)
 	p.AddStage(qemuStage)
 	return p
 }
diff --git a/internal/distro/rhel85/stage_options.go b/internal/distro/rhel85/stage_options.go
index 8a912c800..fceffaa00 100644
--- a/internal/distro/rhel85/stage_options.go
+++ b/internal/distro/rhel85/stage_options.go
@@ -229,32 +229,6 @@ func xorrisofsStageOptions(filename string, arch string, isolinux bool) *osbuild
 	return options
 }
 
-func qemuStageOptions(filename, format, compat string) *osbuild.QEMUStageOptions {
-	var options osbuild.QEMUFormatOptions
-	switch format {
-	case "qcow2":
-		options = osbuild.Qcow2Options{
-			Type:   "qcow2",
-			Compat: compat,
-		}
-	case "vpc":
-		options = osbuild.VPCOptions{
-			Type: "vpc",
-		}
-	case "vmdk":
-		options = osbuild.VMDKOptions{
-			Type: "vmdk",
-		}
-	default:
-		panic("unknown format in qemu stage: " + format)
-	}
-
-	return &osbuild.QEMUStageOptions{
-		Filename: filename,
-		Format:   options,
-	}
-}
-
 func nginxConfigStageOptions(path, htmlRoot, listen string) *osbuild.NginxConfigStageOptions {
 	// configure nginx to work in an unprivileged container
 	cfg := &osbuild.NginxConfig{
diff --git a/internal/distro/rhel86/pipelines.go b/internal/distro/rhel86/pipelines.go
index f7e73c012..258381dfb 100644
--- a/internal/distro/rhel86/pipelines.go
+++ b/internal/distro/rhel86/pipelines.go
@@ -34,7 +34,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "qcow2", "0.10")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatQCOW2, osbuild.QCOW2Options{Compat: "0.10"})
 	pipelines = append(pipelines, *qemuPipeline)
 
 	return pipelines, nil
@@ -75,7 +75,7 @@ func vhdPipelines(t *imageType, customizations *blueprint.Customizations, option
 		return nil, err
 	}
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "vpc", "")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatVPC, nil)
 	pipelines = append(pipelines, *qemuPipeline)
 	return pipelines, nil
 }
@@ -103,7 +103,7 @@ func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, optio
 		return nil, err
 	}
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "vmdk", "")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatVMDK, nil)
 	pipelines = append(pipelines, *qemuPipeline)
 	return pipelines, nil
 }
@@ -131,7 +131,7 @@ func openstackPipelines(t *imageType, customizations *blueprint.Customizations,
 		return nil, err
 	}
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "qcow2", "")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatQCOW2, nil)
 	pipelines = append(pipelines, *qemuPipeline)
 	return pipelines, nil
 }
@@ -1033,12 +1033,15 @@ func xzArchivePipeline(inputPipelineName, inputFilename, outputFilename string)
 	return p
 }
 
-func qemuPipeline(inputPipelineName, inputFilename, outputFilename, format, qcow2Compat string) *osbuild.Pipeline {
+func qemuPipeline(inputPipelineName, inputFilename, outputFilename string, format osbuild.QEMUFormat, formatOptions osbuild.QEMUFormatOptions) *osbuild.Pipeline {
 	p := new(osbuild.Pipeline)
-	p.Name = format
+	p.Name = string(format)
 	p.Build = "name:build"
 
-	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename))
+	qemuStage := osbuild.NewQEMUStage(
+		osbuild.NewQEMUStageOptions(outputFilename, format, formatOptions),
+		osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename),
+	)
 	p.AddStage(qemuStage)
 	return p
 }
diff --git a/internal/distro/rhel86/stage_options.go b/internal/distro/rhel86/stage_options.go
index 9ddc70e18..911dbcab7 100644
--- a/internal/distro/rhel86/stage_options.go
+++ b/internal/distro/rhel86/stage_options.go
@@ -278,32 +278,6 @@ func xorrisofsStageOptions(filename, isolabel, arch string, isolinux bool) *osbu
 	return options
 }
 
-func qemuStageOptions(filename, format, compat string) *osbuild.QEMUStageOptions {
-	var options osbuild.QEMUFormatOptions
-	switch format {
-	case "qcow2":
-		options = osbuild.Qcow2Options{
-			Type:   "qcow2",
-			Compat: compat,
-		}
-	case "vpc":
-		options = osbuild.VPCOptions{
-			Type: "vpc",
-		}
-	case "vmdk":
-		options = osbuild.VMDKOptions{
-			Type: "vmdk",
-		}
-	default:
-		panic("unknown format in qemu stage: " + format)
-	}
-
-	return &osbuild.QEMUStageOptions{
-		Filename: filename,
-		Format:   options,
-	}
-}
-
 func nginxConfigStageOptions(path, htmlRoot, listen string) *osbuild.NginxConfigStageOptions {
 	// configure nginx to work in an unprivileged container
 	cfg := &osbuild.NginxConfig{
diff --git a/internal/distro/rhel90/pipelines.go b/internal/distro/rhel90/pipelines.go
index 48c15a66e..b0aab697e 100644
--- a/internal/distro/rhel90/pipelines.go
+++ b/internal/distro/rhel90/pipelines.go
@@ -35,7 +35,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "qcow2", "1.1")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatQCOW2, osbuild.QCOW2Options{Compat: "1.1"})
 	pipelines = append(pipelines, *qemuPipeline)
 
 	return pipelines, nil
@@ -72,7 +72,7 @@ func vhdPipelines(t *imageType, customizations *blueprint.Customizations, option
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "vpc", "")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatVPC, nil)
 	pipelines = append(pipelines, *qemuPipeline)
 	return pipelines, nil
 }
@@ -97,7 +97,7 @@ func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, optio
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "vmdk", "")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatVMDK, nil)
 	pipelines = append(pipelines, *qemuPipeline)
 	return pipelines, nil
 }
@@ -122,7 +122,7 @@ func openstackPipelines(t *imageType, customizations *blueprint.Customizations,
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "qcow2", "")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatQCOW2, nil)
 	pipelines = append(pipelines, *qemuPipeline)
 	return pipelines, nil
 }
@@ -1025,12 +1025,15 @@ func xzArchivePipeline(inputPipelineName, inputFilename, outputFilename string)
 	return p
 }
 
-func qemuPipeline(inputPipelineName, inputFilename, outputFilename, format, qcow2Compat string) *osbuild.Pipeline {
+func qemuPipeline(inputPipelineName, inputFilename, outputFilename string, format osbuild.QEMUFormat, formatOptions osbuild.QEMUFormatOptions) *osbuild.Pipeline {
 	p := new(osbuild.Pipeline)
-	p.Name = format
+	p.Name = string(format)
 	p.Build = "name:build"
 
-	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename))
+	qemuStage := osbuild.NewQEMUStage(
+		osbuild.NewQEMUStageOptions(outputFilename, format, formatOptions),
+		osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename),
+	)
 	p.AddStage(qemuStage)
 	return p
 }
diff --git a/internal/distro/rhel90/stage_options.go b/internal/distro/rhel90/stage_options.go
index 949d891bc..cb33cc793 100644
--- a/internal/distro/rhel90/stage_options.go
+++ b/internal/distro/rhel90/stage_options.go
@@ -278,32 +278,6 @@ func xorrisofsStageOptions(filename, isolabel, arch string, isolinux bool) *osbu
 	return options
 }
 
-func qemuStageOptions(filename, format, compat string) *osbuild.QEMUStageOptions {
-	var options osbuild.QEMUFormatOptions
-	switch format {
-	case "qcow2":
-		options = osbuild.Qcow2Options{
-			Type:   "qcow2",
-			Compat: compat,
-		}
-	case "vpc":
-		options = osbuild.VPCOptions{
-			Type: "vpc",
-		}
-	case "vmdk":
-		options = osbuild.VMDKOptions{
-			Type: "vmdk",
-		}
-	default:
-		panic("unknown format in qemu stage: " + format)
-	}
-
-	return &osbuild.QEMUStageOptions{
-		Filename: filename,
-		Format:   options,
-	}
-}
-
 func nginxConfigStageOptions(path, htmlRoot, listen string) *osbuild.NginxConfigStageOptions {
 	// configure nginx to work in an unprivileged container
 	cfg := &osbuild.NginxConfig{
diff --git a/internal/distro/rhel90beta/pipelines.go b/internal/distro/rhel90beta/pipelines.go
index 2ef052060..e8b989f04 100644
--- a/internal/distro/rhel90beta/pipelines.go
+++ b/internal/distro/rhel90beta/pipelines.go
@@ -51,7 +51,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "qcow2", "1.1")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatQCOW2, osbuild.QCOW2Options{Compat: "1.1"})
 	pipelines = append(pipelines, *qemuPipeline)
 
 	return pipelines, nil
@@ -92,7 +92,7 @@ func vhdPipelines(t *imageType, customizations *blueprint.Customizations, option
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "vpc", "")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatVPC, nil)
 	pipelines = append(pipelines, *qemuPipeline)
 	return pipelines, nil
 }
@@ -121,7 +121,7 @@ func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, optio
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "vmdk", "")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatVMDK, nil)
 	pipelines = append(pipelines, *qemuPipeline)
 	return pipelines, nil
 }
@@ -149,7 +149,7 @@ func openstackPipelines(t *imageType, customizations *blueprint.Customizations,
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
-	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, "qcow2", "")
+	qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename, osbuild.QEMUFormatQCOW2, nil)
 	pipelines = append(pipelines, *qemuPipeline)
 	return pipelines, nil
 }
@@ -1087,12 +1087,15 @@ func xzArchivePipeline(inputPipelineName, inputFilename, outputFilename string)
 	return p
 }
 
-func qemuPipeline(inputPipelineName, inputFilename, outputFilename, format, qcow2Compat string) *osbuild.Pipeline {
+func qemuPipeline(inputPipelineName, inputFilename, outputFilename string, format osbuild.QEMUFormat, formatOptions osbuild.QEMUFormatOptions) *osbuild.Pipeline {
 	p := new(osbuild.Pipeline)
-	p.Name = format
+	p.Name = string(format)
 	p.Build = "name:build"
 
-	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename))
+	qemuStage := osbuild.NewQEMUStage(
+		osbuild.NewQEMUStageOptions(outputFilename, format, formatOptions),
+		osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename),
+	)
 	p.AddStage(qemuStage)
 	return p
 }
diff --git a/internal/distro/rhel90beta/stage_options.go b/internal/distro/rhel90beta/stage_options.go
index 2eaa4409d..25660f0fd 100644
--- a/internal/distro/rhel90beta/stage_options.go
+++ b/internal/distro/rhel90beta/stage_options.go
@@ -207,29 +207,3 @@ func xorrisofsStageOptions(filename string, arch string) *osbuild.XorrisofsStage
 		IsohybridMBR: "/usr/share/syslinux/isohdpfx.bin",
 	}
 }
-
-func qemuStageOptions(filename, format, compat string) *osbuild.QEMUStageOptions {
-	var options osbuild.QEMUFormatOptions
-	switch format {
-	case "qcow2":
-		options = osbuild.Qcow2Options{
-			Type:   "qcow2",
-			Compat: compat,
-		}
-	case "vpc":
-		options = osbuild.VPCOptions{
-			Type: "vpc",
-		}
-	case "vmdk":
-		options = osbuild.VMDKOptions{
-			Type: "vmdk",
-		}
-	default:
-		panic("unknown format in qemu stage: " + format)
-	}
-
-	return &osbuild.QEMUStageOptions{
-		Filename: filename,
-		Format:   options,
-	}
-}
diff --git a/internal/osbuild2/qemu_stage.go b/internal/osbuild2/qemu_stage.go
index 8093babe5..bd7e91dc9 100644
--- a/internal/osbuild2/qemu_stage.go
+++ b/internal/osbuild2/qemu_stage.go
@@ -20,34 +20,145 @@ type QEMUStageOptions struct {
 
 func (QEMUStageOptions) isStageOptions() {}
 
+type QEMUFormat string
+type VMDKSubformat string
+
+const (
+	QEMUFormatQCOW2 QEMUFormat = "qcow2"
+	QEMUFormatVDI   QEMUFormat = "vdi"
+	QEMUFormatVMDK  QEMUFormat = "vmdk"
+	QEMUFormatVPC   QEMUFormat = "vpc"
+	QEMUFormatVHDX  QEMUFormat = "vhdx"
+
+	VMDKSubformatMonolithicSparse     VMDKSubformat = "monolithicSparse"
+	VMDKSubformatMonolithicFlat       VMDKSubformat = "monolithicFlat"
+	VMDKSubformatTwoGbMaxExtentSparse VMDKSubformat = "twoGbMaxExtentSparse"
+	VMDKSubformatTwoGbMaxExtentFlat   VMDKSubformat = "twoGbMaxExtentFlat"
+	VMDKSubformatStreamOptimized      VMDKSubformat = "streamOptimized"
+)
+
 type QEMUFormatOptions interface {
 	isQEMUFormatOptions()
+	validate() error
+	formatType() QEMUFormat
 }
 
-type Qcow2Options struct {
+type QCOW2Options struct {
 	// The type of the format must be 'qcow2'
-	Type string `json:"type"`
+	Type QEMUFormat `json:"type"`
 
 	// The qcow2-compatibility-version to use
 	Compat string `json:"compat"`
 }
 
-func (Qcow2Options) isQEMUFormatOptions() {}
+func (QCOW2Options) isQEMUFormatOptions() {}
+
+func (o QCOW2Options) validate() error {
+	if o.Type != QEMUFormatQCOW2 {
+		return fmt.Errorf("invalid format type %q for %q options", o.Type, QEMUFormatQCOW2)
+	}
+	return nil
+}
+
+func (o QCOW2Options) formatType() QEMUFormat {
+	return o.Type
+}
+
+type VDIOptions struct {
+	// The type of the format must be 'vdi'
+	Type QEMUFormat `json:"type"`
+}
+
+func (VDIOptions) isQEMUFormatOptions() {}
+
+func (o VDIOptions) validate() error {
+	if o.Type != QEMUFormatVDI {
+		return fmt.Errorf("invalid format type %q for %q options", o.Type, QEMUFormatVDI)
+	}
+	return nil
+}
+
+func (o VDIOptions) formatType() QEMUFormat {
+	return o.Type
+}
 
 type VPCOptions struct {
 	// The type of the format must be 'vpc'
-	Type string `json:"type"`
+	Type QEMUFormat `json:"type"`
 }
 
 func (VPCOptions) isQEMUFormatOptions() {}
 
+func (o VPCOptions) validate() error {
+	if o.Type != QEMUFormatVPC {
+		return fmt.Errorf("invalid format type %q for %q options", o.Type, QEMUFormatVPC)
+	}
+	return nil
+}
+
+func (o VPCOptions) formatType() QEMUFormat {
+	return o.Type
+}
+
 type VMDKOptions struct {
-	// The type of the format must be 'vpc'
-	Type string `json:"type"`
+	// The type of the format must be 'vmdk'
+	Type QEMUFormat `json:"type"`
+
+	Subformat VMDKSubformat `json:"subformat,omitempty"`
 }
 
 func (VMDKOptions) isQEMUFormatOptions() {}
 
+func (o VMDKOptions) validate() error {
+	if o.Type != QEMUFormatVMDK {
+		return fmt.Errorf("invalid format type %q for %q options", o.Type, QEMUFormatVMDK)
+	}
+
+	if o.Subformat != "" {
+		allowedVMDKSubformats := []VMDKSubformat{
+			VMDKSubformatMonolithicFlat,
+			VMDKSubformatMonolithicSparse,
+			VMDKSubformatTwoGbMaxExtentFlat,
+			VMDKSubformatTwoGbMaxExtentSparse,
+			VMDKSubformatStreamOptimized,
+		}
+		valid := false
+		for _, value := range allowedVMDKSubformats {
+			if o.Subformat == value {
+				valid = true
+				break
+			}
+		}
+		if !valid {
+			return fmt.Errorf("'subformat' option does not allow %q as a value", o.Subformat)
+		}
+	}
+
+	return nil
+}
+
+func (o VMDKOptions) formatType() QEMUFormat {
+	return o.Type
+}
+
+type VHDXOptions struct {
+	// The type of the format must be 'vhdx'
+	Type QEMUFormat `json:"type"`
+}
+
+func (VHDXOptions) isQEMUFormatOptions() {}
+
+func (o VHDXOptions) validate() error {
+	if o.Type != QEMUFormatVHDX {
+		return fmt.Errorf("invalid format type %q for %q options", o.Type, QEMUFormatVHDX)
+	}
+	return nil
+}
+
+func (o VHDXOptions) formatType() QEMUFormat {
+	return o.Type
+}
+
 type QEMUStageInputs struct {
 	Image *QEMUStageInput `json:"image"`
 }
@@ -81,26 +192,77 @@ func NewQEMUStage(options *QEMUStageOptions, inputs *QEMUStageInputs) *Stage {
 	}
 }
 
+// NewQEMUStageOptions creates a new QEMU Stage options object.
+//
+// In case the format-specific options are provided, they are used for
+// the Stage options.
+func NewQEMUStageOptions(filename string, format QEMUFormat, formatOptions QEMUFormatOptions) *QEMUStageOptions {
+	if formatOptions != nil {
+		// If the format type is not set explicitly in the provided format
+		// options, set it to the appropriate value based on the format options
+		// object type.
+		if formatOptions.formatType() == "" {
+			switch o := formatOptions.(type) {
+			case QCOW2Options:
+				o.Type = QEMUFormatQCOW2
+				formatOptions = o
+			case VDIOptions:
+				o.Type = QEMUFormatVDI
+				formatOptions = o
+			case VPCOptions:
+				o.Type = QEMUFormatVPC
+				formatOptions = o
+			case VMDKOptions:
+				o.Type = QEMUFormatVMDK
+				formatOptions = o
+			case VHDXOptions:
+				o.Type = QEMUFormatVHDX
+				formatOptions = o
+			default:
+				panic(fmt.Sprintf("unknown format options type in qemu stage: %t", o))
+			}
+		}
+
+		// Ensure that the explicitly provided QEMU format and the format set
+		// in the format options structure (set by user or by this function
+		// above) are matching.
+		if t := formatOptions.formatType(); t != format {
+			panic(fmt.Sprintf("mismatch between passed format type %q and format options type %q", format, t))
+		}
+
+		if err := formatOptions.validate(); err != nil {
+			panic(err)
+		}
+	} else {
+		switch format {
+		case QEMUFormatQCOW2:
+			formatOptions = QCOW2Options{Type: format}
+		case QEMUFormatVDI:
+			formatOptions = VDIOptions{Type: format}
+		case QEMUFormatVPC:
+			formatOptions = VPCOptions{Type: format}
+		case QEMUFormatVMDK:
+			formatOptions = VMDKOptions{Type: format}
+		case QEMUFormatVHDX:
+			formatOptions = VHDXOptions{Type: format}
+		default:
+			panic("unknown format in qemu stage: " + format)
+		}
+	}
+
+	return &QEMUStageOptions{
+		Filename: filename,
+		Format:   formatOptions,
+	}
+}
+
 // alias for custom marshaller
 type qemuStageOptions QEMUStageOptions
 
 // Custom marshaller for validating
 func (options QEMUStageOptions) MarshalJSON() ([]byte, error) {
-	switch o := options.Format.(type) {
-	case Qcow2Options:
-		if o.Type != "qcow2" {
-			return nil, fmt.Errorf("invalid format type %q for qcow2 options", o.Type)
-		}
-	case VPCOptions:
-		if o.Type != "vpc" {
-			return nil, fmt.Errorf("invalid format type %q for vpc options", o.Type)
-		}
-	case VMDKOptions:
-		if o.Type != "vmdk" {
-			return nil, fmt.Errorf("invalid format type %q for vmdk options", o.Type)
-		}
-	default:
-		return nil, fmt.Errorf("unknown format options in QEMU stage: %#v", options.Format)
+	if err := options.Format.validate(); err != nil {
+		return nil, err
 	}
 
 	return json.Marshal(qemuStageOptions(options))


Test patch:
diff --git a/internal/osbuild2/qemu_stage_test.go b/internal/osbuild2/qemu_stage_test.go
index 996993a24..7921115cc 100644
--- a/internal/osbuild2/qemu_stage_test.go
+++ b/internal/osbuild2/qemu_stage_test.go
@@ -1,6 +1,7 @@
 package osbuild2
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -9,15 +10,21 @@ import (
 func TestNewQemuStage(t *testing.T) {
 
 	formatOptionsList := []QEMUFormatOptions{
-		Qcow2Options{
-			Type:   "qcow2",
+		QCOW2Options{
+			Type:   QEMUFormatQCOW2,
 			Compat: "0.10",
 		},
+		VDIOptions{
+			Type: QEMUFormatVDI,
+		},
 		VPCOptions{
-			Type: "vpc",
+			Type: QEMUFormatVPC,
 		},
 		VMDKOptions{
-			Type: "vmdk",
+			Type: QEMUFormatVMDK,
+		},
+		VHDXOptions{
+			Type: QEMUFormatVHDX,
 		},
 	}
 
@@ -46,3 +53,163 @@ func TestNewQemuStage(t *testing.T) {
 		assert.Equal(t, expectedStage, actualStage)
 	}
 }
+
+func TestNewQEMUStageOptions(t *testing.T) {
+	tests := []struct {
+		Filename        string
+		Format          QEMUFormat
+		FormatOptions   QEMUFormatOptions
+		ExpectedOptions *QEMUStageOptions
+		Error           bool
+	}{
+		{
+			Filename: "image.qcow2",
+			Format:   QEMUFormatQCOW2,
+			FormatOptions: QCOW2Options{
+				Compat: "1.1",
+			},
+			ExpectedOptions: &QEMUStageOptions{
+				Filename: "image.qcow2",
+				Format: QCOW2Options{
+					Type:   QEMUFormatQCOW2,
+					Compat: "1.1",
+				},
+			},
+		},
+		{
+			Filename: "image.qcow2",
+			Format:   QEMUFormatQCOW2,
+			FormatOptions: QCOW2Options{
+				Type: QEMUFormatQCOW2,
+			},
+			ExpectedOptions: &QEMUStageOptions{
+				Filename: "image.qcow2",
+				Format: QCOW2Options{
+					Type: QEMUFormatQCOW2,
+				},
+			},
+		},
+		{
+			Filename:      "image.qcow2",
+			Format:        QEMUFormatQCOW2,
+			FormatOptions: QCOW2Options{},
+			ExpectedOptions: &QEMUStageOptions{
+				Filename: "image.qcow2",
+				Format: QCOW2Options{
+					Type: QEMUFormatQCOW2,
+				},
+			},
+		},
+		{
+			Filename:      "image.qcow2",
+			Format:        QEMUFormatQCOW2,
+			FormatOptions: nil,
+			ExpectedOptions: &QEMUStageOptions{
+				Filename: "image.qcow2",
+				Format: QCOW2Options{
+					Type: QEMUFormatQCOW2,
+				},
+			},
+		},
+		{
+			Filename:      "image.vdi",
+			Format:        QEMUFormatVDI,
+			FormatOptions: nil,
+			ExpectedOptions: &QEMUStageOptions{
+				Filename: "image.vdi",
+				Format: VDIOptions{
+					Type: QEMUFormatVDI,
+				},
+			},
+		},
+		{
+			Filename:      "image.vpc",
+			Format:        QEMUFormatVPC,
+			FormatOptions: nil,
+			ExpectedOptions: &QEMUStageOptions{
+				Filename: "image.vpc",
+				Format: VPCOptions{
+					Type: QEMUFormatVPC,
+				},
+			},
+		},
+		{
+			Filename:      "image.vmdk",
+			Format:        QEMUFormatVMDK,
+			FormatOptions: nil,
+			ExpectedOptions: &QEMUStageOptions{
+				Filename: "image.vmdk",
+				Format: VMDKOptions{
+					Type: QEMUFormatVMDK,
+				},
+			},
+		},
+		{
+			Filename: "image.vmdk",
+			Format:   QEMUFormatVMDK,
+			FormatOptions: VMDKOptions{
+				Subformat: VMDKSubformatStreamOptimized,
+			},
+			ExpectedOptions: &QEMUStageOptions{
+				Filename: "image.vmdk",
+				Format: VMDKOptions{
+					Type:      QEMUFormatVMDK,
+					Subformat: VMDKSubformatStreamOptimized,
+				},
+			},
+		},
+		{
+			Filename:      "image.vhdx",
+			Format:        QEMUFormatVHDX,
+			FormatOptions: nil,
+			ExpectedOptions: &QEMUStageOptions{
+				Filename: "image.vhdx",
+				Format: VHDXOptions{
+					Type: QEMUFormatVHDX,
+				},
+			},
+		},
+		// mismatch between format and format options type
+		{
+			Filename:      "image.qcow2",
+			Format:        QEMUFormatQCOW2,
+			FormatOptions: VMDKOptions{},
+			Error:         true,
+		},
+		// mismatch between format and format options type
+		{
+			Filename: "image.qcow2",
+			Format:   QEMUFormatQCOW2,
+			FormatOptions: VMDKOptions{
+				Type: QEMUFormatQCOW2,
+			},
+			Error: true,
+		},
+		// mismatch between format and format options type
+		{
+			Filename: "image.qcow2",
+			Format:   QEMUFormatQCOW2,
+			FormatOptions: VMDKOptions{
+				Type: QEMUFormatVMDK,
+			},
+			Error: true,
+		},
+		// unknown format
+		{
+			Filename:      "image.qcow2",
+			Format:        "",
+			FormatOptions: nil,
+			Error:         true,
+		},
+	}
+	for idx, test := range tests {
+		t.Run(fmt.Sprintf("test-(%d/%d)", idx, len(tests)), func(t *testing.T) {
+			if test.Error {
+				assert.Panics(t, func() { NewQEMUStageOptions(test.Filename, test.Format, test.FormatOptions) })
+			} else {
+				stageOptions := NewQEMUStageOptions(test.Filename, test.Format, test.FormatOptions)
+				assert.EqualValues(t, test.ExpectedOptions, stageOptions)
+			}
+		})
+	}
+}
diff --git a/test/cases/api.sh b/test/cases/api.sh
index bf67764e7..e1801b8bc 100755
--- a/test/cases/api.sh
+++ b/test/cases/api.sh
@@ -166,7 +166,7 @@ function checkEnvAzure() {
 
 # Check that needed variables are set to register to RHSM (RHEL only)
 function checkEnvSubscription() {
-  printenv API_TEST_SUBSCRIPTION_ORG_ID API_TEST_SUBSCRIPTION_ACTIVATION_KEY > /dev/null
+  printenv API_TEST_SUBSCRIPTION_ORG_ID API_TEST_SUBSCRIPTION_ACTIVATION_KEY_V2 > /dev/null
 }
 
 function checkEnvVSphere() {
@@ -541,7 +541,7 @@ if [[ "$ID" == "rhel" ]]; then
 ,
     "subscription": {
       "organization": "${API_TEST_SUBSCRIPTION_ORG_ID:-}",
-      "activation_key": "${API_TEST_SUBSCRIPTION_ACTIVATION_KEY:-}",
+      "activation_key": "${API_TEST_SUBSCRIPTION_ACTIVATION_KEY_V2:-}",
       "base_url": "https://cdn.redhat.com/",
       "server_url": "subscription.rhsm.redhat.com",
       "insights": true