@@ -42,14 +42,16 @@ const (
4242 ImageFormatRaw ImageFormat = "raw"
4343 // ImageFormatQcow2 is the QEMU copy-on-write v2 format.
4444 ImageFormatQcow2 ImageFormat = "qcow2"
45+ // ImageFormatVhd is the Hyper-V VHD format (QEMU driver: vpc).
46+ ImageFormatVhd ImageFormat = "vhd"
4547 // ImageFormatVhdx is the Hyper-V virtual hard disk format.
4648 ImageFormatVhdx ImageFormat = "vhdx"
4749)
4850
4951// SupportedImageFormats returns the list of supported bootable image formats in priority order.
5052// When multiple formats exist, the first match in this order is selected.
5153func SupportedImageFormats () []string {
52- return []string {string (ImageFormatRaw ), string (ImageFormatQcow2 ), string (ImageFormatVhdx )}
54+ return []string {string (ImageFormatRaw ), string (ImageFormatQcow2 ), string (ImageFormatVhdx ), string ( ImageFormatVhd ) }
5355}
5456
5557// Assert that [ImageFormat] implements the [pflag.Value] interface.
@@ -66,6 +68,8 @@ func (f *ImageFormat) Set(value string) error {
6668 * f = ImageFormatRaw
6769 case string (ImageFormatQcow2 ):
6870 * f = ImageFormatQcow2
71+ case string (ImageFormatVhd ):
72+ * f = ImageFormatVhd
6973 case string (ImageFormatVhdx ):
7074 * f = ImageFormatVhdx
7175 default :
@@ -80,6 +84,18 @@ func (f *ImageFormat) Type() string {
8084 return "format"
8185}
8286
87+ // QEMUDriver returns the QEMU block driver name for the image format.
88+ // Most formats use their string value directly, but some (e.g., vhd → vpc)
89+ // require translation.
90+ func QEMUDriver (format string ) string {
91+ switch format {
92+ case string (ImageFormatVhd ):
93+ return "vpc"
94+ default :
95+ return format
96+ }
97+ }
98+
8399// ImageBootOptions contains options for the boot command.
84100type ImageBootOptions struct {
85101 Arch qemu.Arch
@@ -155,7 +171,7 @@ Requirements:
155171
156172func addBootFlags (cmd * cobra.Command , options * ImageBootOptions ) {
157173 cmd .Flags ().VarP (& options .Format , "format" , "f" ,
158- "Image format to boot (raw, qcow2, vhdx). Auto-detected if not specified." )
174+ "Image format to boot (raw, qcow2, vhdx, vhd ). Auto-detected if not specified." )
159175
160176 cmd .Flags ().StringVar (& options .TestUserName , "test-user" , "test" , "Name for the test account" )
161177 cmd .Flags ().StringVar (& options .TestUserPassword , "test-password" , "" ,
@@ -239,6 +255,19 @@ func bootImage(env *azldev.Env, options *ImageBootOptions) error {
239255 slog .String ("path" , imagePath ),
240256 slog .String ("format" , imageFormat ),
241257 )
258+ } else {
259+ // Infer format from the file extension when --image-path is used.
260+ var err error
261+
262+ imageFormat , err = InferImageFormat (imagePath )
263+ if err != nil {
264+ return err
265+ }
266+
267+ slog .Info ("Inferred image format from file extension" ,
268+ slog .String ("path" , imagePath ),
269+ slog .String ("format" , imageFormat ),
270+ )
242271 }
243272
244273 // Dry-run mode: log what would be executed and return early.
@@ -255,9 +284,20 @@ func bootImage(env *azldev.Env, options *ImageBootOptions) error {
255284 return bootImageUsingDiskFile (env , options , arch , imagePath , imageFormat )
256285}
257286
287+ // fileExtensionsForFormat returns the file extensions to search for the given format.
288+ // Most formats have a single extension matching the format name, but vhd accepts
289+ // both .vhd and .vhdfixed since QEMU treats them identically.
290+ func fileExtensionsForFormat (format string ) []string {
291+ if format == string (ImageFormatVhd ) {
292+ return []string {"vhd" , "vhdfixed" }
293+ }
294+
295+ return []string {format }
296+ }
297+
258298// findBootableImageArtifact locates a bootable image artifact in the output directory for the
259299// given image name. If format is specified, only that format is searched. Otherwise, formats
260- // are searched in priority order (raw, qcow2, vhdx) and the first match is returned.
300+ // are searched in priority order (raw, qcow2, vhdx, vhd ) and the first match is returned.
261301func findBootableImageArtifact (
262302 env * azldev.Env , imageName , format string ,
263303) (imagePath , imageFormat string , err error ) {
@@ -290,16 +330,18 @@ func findBootableImageArtifact(
290330
291331 // Search for bootable artifacts in priority order.
292332 for _ , currentFormat := range formatsToSearch {
293- pattern := filepath .Join (imageOutputDir , "*." + currentFormat )
333+ for _ , ext := range fileExtensionsForFormat (currentFormat ) {
334+ pattern := filepath .Join (imageOutputDir , "*." + ext )
294335
295- matches , globErr := fileutils .Glob (env .FS (), pattern )
296- if globErr != nil {
297- continue
298- }
336+ matches , globErr := fileutils .Glob (env .FS (), pattern )
337+ if globErr != nil {
338+ continue
339+ }
299340
300- if len (matches ) > 0 {
301- // Return the first match for the highest-priority format.
302- return matches [0 ], currentFormat , nil
341+ if len (matches ) > 0 {
342+ // Return the first match for the highest-priority format.
343+ return matches [0 ], currentFormat , nil
344+ }
303345 }
304346 }
305347
@@ -326,6 +368,35 @@ func findBootableImageArtifact(
326368 )
327369}
328370
371+ // InferImageFormat determines the image format from the file extension.
372+ // Returns an error if the extension does not match a supported format.
373+ func InferImageFormat (imagePath string ) (string , error ) {
374+ ext := strings .ToLower (filepath .Ext (imagePath ))
375+ if ext == "" {
376+ return "" , fmt .Errorf (
377+ "cannot infer image format from %#q: no file extension" ,
378+ imagePath ,
379+ )
380+ }
381+
382+ // Strip the leading dot (e.g., ".vhdfixed" → "vhdfixed").
383+ format := ext [1 :]
384+ if format == "vhdfixed" {
385+ format = string (ImageFormatVhd )
386+ }
387+
388+ // Validate the inferred format is supported.
389+ supported := SupportedImageFormats ()
390+ if ! lo .Contains (supported , format ) {
391+ return "" , fmt .Errorf (
392+ "unsupported image format %#q inferred from %#q; supported formats: %v" ,
393+ format , imagePath , supported ,
394+ )
395+ }
396+
397+ return format , nil
398+ }
399+
329400// listImageArtifacts returns a list of image artifact filenames in the given directory.
330401func listImageArtifacts (env * azldev.Env , dir string ) ([]string , error ) {
331402 entries , err := fileutils .ReadDir (env .FS (), dir )
@@ -428,7 +499,7 @@ func bootImageUsingDiskFile(
428499 FirmwarePath : fwPath ,
429500 NVRAMPath : nvramPath ,
430501 DiskPath : selectedDiskPath ,
431- DiskType : imageFormat ,
502+ DiskType : QEMUDriver ( imageFormat ) ,
432503 CloudInitISOPath : cloudInitMetadataIsoPath ,
433504 SecureBoot : options .SecureBoot ,
434505 SSHPort : int (options .SSHPort ),
0 commit comments