diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go index 89e8b6943..90969f38c 100644 --- a/builder/qemu/builder.go +++ b/builder/qemu/builder.go @@ -96,34 +96,35 @@ type Config struct { Comm communicator.Config `mapstructure:",squash"` common.FloppyConfig `mapstructure:",squash"` - ISOSkipCache bool `mapstructure:"iso_skip_cache"` - Accelerator string `mapstructure:"accelerator"` - CpuCount int `mapstructure:"cpus"` - DiskInterface string `mapstructure:"disk_interface"` - DiskSize uint `mapstructure:"disk_size"` - DiskCache string `mapstructure:"disk_cache"` - DiskDiscard string `mapstructure:"disk_discard"` - DetectZeroes string `mapstructure:"disk_detect_zeroes"` - SkipCompaction bool `mapstructure:"skip_compaction"` - DiskCompression bool `mapstructure:"disk_compression"` - Format string `mapstructure:"format"` - Headless bool `mapstructure:"headless"` - DiskImage bool `mapstructure:"disk_image"` - UseBackingFile bool `mapstructure:"use_backing_file"` - MachineType string `mapstructure:"machine_type"` - MemorySize int `mapstructure:"memory"` - NetDevice string `mapstructure:"net_device"` - OutputDir string `mapstructure:"output_directory"` - QemuArgs [][]string `mapstructure:"qemuargs"` - QemuBinary string `mapstructure:"qemu_binary"` - ShutdownCommand string `mapstructure:"shutdown_command"` - SSHHostPortMin int `mapstructure:"ssh_host_port_min"` - SSHHostPortMax int `mapstructure:"ssh_host_port_max"` - UseDefaultDisplay bool `mapstructure:"use_default_display"` - VNCBindAddress string `mapstructure:"vnc_bind_address"` - VNCPortMin int `mapstructure:"vnc_port_min"` - VNCPortMax int `mapstructure:"vnc_port_max"` - VMName string `mapstructure:"vm_name"` + ISOSkipCache bool `mapstructure:"iso_skip_cache"` + Accelerator string `mapstructure:"accelerator"` + CpuCount int `mapstructure:"cpus"` + AdditionalDiskSize []string `mapstructure:"disk_additional_size"` + DiskInterface string `mapstructure:"disk_interface"` + DiskSize uint `mapstructure:"disk_size"` + DiskCache string `mapstructure:"disk_cache"` + DiskDiscard string `mapstructure:"disk_discard"` + DetectZeroes string `mapstructure:"disk_detect_zeroes"` + SkipCompaction bool `mapstructure:"skip_compaction"` + DiskCompression bool `mapstructure:"disk_compression"` + Format string `mapstructure:"format"` + Headless bool `mapstructure:"headless"` + DiskImage bool `mapstructure:"disk_image"` + UseBackingFile bool `mapstructure:"use_backing_file"` + MachineType string `mapstructure:"machine_type"` + MemorySize int `mapstructure:"memory"` + NetDevice string `mapstructure:"net_device"` + OutputDir string `mapstructure:"output_directory"` + QemuArgs [][]string `mapstructure:"qemuargs"` + QemuBinary string `mapstructure:"qemu_binary"` + ShutdownCommand string `mapstructure:"shutdown_command"` + SSHHostPortMin int `mapstructure:"ssh_host_port_min"` + SSHHostPortMax int `mapstructure:"ssh_host_port_max"` + UseDefaultDisplay bool `mapstructure:"use_default_display"` + VNCBindAddress string `mapstructure:"vnc_bind_address"` + VNCPortMin int `mapstructure:"vnc_port_min"` + VNCPortMax int `mapstructure:"vnc_port_max"` + VMName string `mapstructure:"vm_name"` // These are deprecated, but we keep them around for BC // TODO(@mitchellh): remove @@ -286,6 +287,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { errs, errors.New("use_backing_file can only be enabled for QCOW2 images and when disk_image is true")) } + if b.config.DiskImage && len(b.config.AdditionalDiskSize) > 0 { + errs = packer.MultiErrorAppend( + errs, errors.New("disk_additional_size can only be used when disk_image is false")) + } + if _, ok := accels[b.config.Accelerator]; !ok { errs = packer.MultiErrorAppend( errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', 'whpx', or 'none' are allowed")) @@ -500,7 +506,11 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack state: make(map[string]interface{}), } - artifact.state["diskName"] = state.Get("disk_filename").(string) + artifact.state["diskName"] = b.config.VMName + diskpaths, ok := state.Get("qemu_disk_paths").([]string) + if ok { + artifact.state["diskPaths"] = diskpaths + } artifact.state["diskType"] = b.config.Format artifact.state["diskSize"] = uint64(b.config.DiskSize) artifact.state["domainType"] = b.config.Accelerator diff --git a/builder/qemu/builder_test.go b/builder/qemu/builder_test.go index d0d403666..e726ce456 100644 --- a/builder/qemu/builder_test.go +++ b/builder/qemu/builder_test.go @@ -187,6 +187,36 @@ func TestBuilderPrepare_DiskSize(t *testing.T) { } } +func TestBuilderPrepare_AdditionalDiskSize(t *testing.T) { + var b Builder + config := testConfig() + + config["disk_additional_size"] = []string{"1M"} + config["disk_image"] = true + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatalf("should have error") + } + + delete(config, "disk_image") + config["disk_additional_size"] = []string{"1M"} + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.AdditionalDiskSize[0] != "1M" { + t.Fatalf("bad size: %s", b.config.AdditionalDiskSize) + } +} + func TestBuilderPrepare_Format(t *testing.T) { var b Builder config := testConfig() diff --git a/builder/qemu/step_convert_disk.go b/builder/qemu/step_convert_disk.go index 067722be1..249efb414 100644 --- a/builder/qemu/step_convert_disk.go +++ b/builder/qemu/step_convert_disk.go @@ -22,7 +22,7 @@ type stepConvertDisk struct{} func (s *stepConvertDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) driver := state.Get("driver").(Driver) - diskName := state.Get("disk_filename").(string) + diskName := config.VMName ui := state.Get("ui").(packer.Ui) if config.SkipCompaction && !config.DiskCompression { diff --git a/builder/qemu/step_copy_disk.go b/builder/qemu/step_copy_disk.go index e18e59657..e5fa4de46 100644 --- a/builder/qemu/step_copy_disk.go +++ b/builder/qemu/step_copy_disk.go @@ -19,7 +19,6 @@ func (s *stepCopyDisk) Run(ctx context.Context, state multistep.StateBag) multis isoPath := state.Get("iso_path").(string) ui := state.Get("ui").(packer.Ui) path := filepath.Join(config.OutputDir, fmt.Sprintf("%s", config.VMName)) - name := config.VMName command := []string{ "convert", @@ -40,8 +39,6 @@ func (s *stepCopyDisk) Run(ctx context.Context, state multistep.StateBag) multis return multistep.ActionHalt } - state.Put("disk_filename", name) - return multistep.ActionContinue } diff --git a/builder/qemu/step_create_disk.go b/builder/qemu/step_create_disk.go index e7a6db046..8fb18d025 100644 --- a/builder/qemu/step_create_disk.go +++ b/builder/qemu/step_create_disk.go @@ -3,6 +3,7 @@ package qemu import ( "context" "fmt" + "log" "path/filepath" "github.com/hashicorp/packer/helper/multistep" @@ -18,36 +19,54 @@ func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) mult driver := state.Get("driver").(Driver) ui := state.Get("ui").(packer.Ui) name := config.VMName - path := filepath.Join(config.OutputDir, name) - command := []string{ - "create", - "-f", config.Format, + if config.DiskImage && !config.UseBackingFile { + return multistep.ActionContinue } - if config.UseBackingFile { - isoPath := state.Get("iso_path").(string) - command = append(command, "-b", isoPath) + var diskFullPaths, diskSizes []string + + ui.Say("Creating required virtual machine disks") + // The 'main' or 'default' disk + diskFullPaths = append(diskFullPaths, filepath.Join(config.OutputDir, name)) + diskSizes = append(diskSizes, fmt.Sprintf("%dM", uint64(config.DiskSize))) + // Additional disks + if len(config.AdditionalDiskSize) > 0 { + for i, diskSize := range config.AdditionalDiskSize { + path := filepath.Join(config.OutputDir, fmt.Sprintf("%s-%d", name, i+1)) + diskFullPaths = append(diskFullPaths, path) + size := fmt.Sprintf("%s", diskSize) + diskSizes = append(diskSizes, size) + } } - command = append(command, - path, - fmt.Sprintf("%vM", config.DiskSize), - ) + // Create all required disks + for i, diskFullPath := range diskFullPaths { + log.Printf("[INFO] Creating disk with Path: %s and Size: %s", diskFullPath, diskSizes[i]) + command := []string{ + "create", + "-f", config.Format, + } - if config.DiskImage && !config.UseBackingFile { - return multistep.ActionContinue - } + if config.UseBackingFile && i == 0 { + isoPath := state.Get("iso_path").(string) + command = append(command, "-b", isoPath) + } + + command = append(command, + diskFullPath, + diskSizes[i]) - ui.Say("Creating hard drive...") - if err := driver.QemuImg(command...); err != nil { - err := fmt.Errorf("Error creating hard drive: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt + if err := driver.QemuImg(command...); err != nil { + err := fmt.Errorf("Error creating hard drive: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } } - state.Put("disk_filename", name) + // Stash the disk paths so we can retrieve later + state.Put("qemu_disk_paths", diskFullPaths) return multistep.ActionContinue } diff --git a/builder/qemu/step_run.go b/builder/qemu/step_run.go index b0e10e4a1..de22a94b8 100644 --- a/builder/qemu/step_run.go +++ b/builder/qemu/step_run.go @@ -94,19 +94,42 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error if qemuVersion.GreaterThanOrEqual(v2) { if config.DiskInterface == "virtio-scsi" { - deviceArgs = append(deviceArgs, "virtio-scsi-pci,id=scsi0", "scsi-hd,bus=scsi0.0,drive=drive0") - driveArgumentString := fmt.Sprintf("if=none,file=%s,id=drive0,cache=%s,discard=%s,format=%s", imgPath, config.DiskCache, config.DiskDiscard, config.Format) - if config.DetectZeroes != "off" { - driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes) + if config.DiskImage { + deviceArgs = append(deviceArgs, "virtio-scsi-pci,id=scsi0", "scsi-hd,bus=scsi0.0,drive=drive0") + driveArgumentString := fmt.Sprintf("if=none,file=%s,id=drive0,cache=%s,discard=%s,format=%s", imgPath, config.DiskCache, config.DiskDiscard, config.Format) + if config.DetectZeroes != "off" { + driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes) + } + driveArgs = append(driveArgs, driveArgumentString) + } else { + deviceArgs = append(deviceArgs, "virtio-scsi-pci,id=scsi0") + diskFullPaths := state.Get("qemu_disk_paths").([]string) + for i, diskFullPath := range diskFullPaths { + deviceArgs = append(deviceArgs, fmt.Sprintf("scsi-hd,bus=scsi0.0,drive=drive%d", i)) + driveArgumentString := fmt.Sprintf("if=none,file=%s,id=drive%d,cache=%s,discard=%s,format=%s", diskFullPath, i, config.DiskCache, config.DiskDiscard, config.Format) + if config.DetectZeroes != "off" { + driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes) + } + driveArgs = append(driveArgs, driveArgumentString) + } } - driveArgs = append(driveArgs, driveArgumentString) } else { - driveArgumentString := fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s,format=%s", imgPath, config.DiskInterface, config.DiskCache, config.DiskDiscard, config.Format) - if config.DetectZeroes != "off" { - driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes) + if config.DiskImage { + driveArgumentString := fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s,format=%s", imgPath, config.DiskInterface, config.DiskCache, config.DiskDiscard, config.Format) + if config.DetectZeroes != "off" { + driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes) + } + driveArgs = append(driveArgs, driveArgumentString) + } else { + diskFullPaths := state.Get("qemu_disk_paths").([]string) + for _, diskFullPath := range diskFullPaths { + driveArgumentString := fmt.Sprintf("file=%s,if=%s,cache=%s,discard=%s,format=%s", diskFullPath, config.DiskInterface, config.DiskCache, config.DiskDiscard, config.Format) + if config.DetectZeroes != "off" { + driveArgumentString = fmt.Sprintf("%s,detect-zeroes=%s", driveArgumentString, config.DetectZeroes) + } + driveArgs = append(driveArgs, driveArgumentString) + } } - driveArgs = append(driveArgs, driveArgumentString) - } } else { driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,cache=%s,format=%s", imgPath, config.DiskInterface, config.DiskCache, config.Format)) diff --git a/website/source/docs/builders/qemu.html.md.erb b/website/source/docs/builders/qemu.html.md.erb index e611dd77e..251aab8e4 100644 --- a/website/source/docs/builders/qemu.html.md.erb +++ b/website/source/docs/builders/qemu.html.md.erb @@ -142,6 +142,16 @@ Linux server and have not enabled X11 forwarding (`ssh -X`). - `cpus` (number) - The number of cpus to use when building the VM. The default is `1` CPU. +- `disk_additional_size` (array of strings) - Additional disks to create. + Uses `vm_name` as the disk name template and appends `-#` where `#` is + the position in the array. `#` starts at 1 since 0 is the default disk. + Each string represents the disk image size in bytes. Optional suffixes + 'k' or 'K' (kilobyte, 1024), 'M' (megabyte, 1024k), 'G' (gigabyte, 1024M), + 'T' (terabyte, 1024G), 'P' (petabyte, 1024T) and 'E' (exabyte, 1024P) are + supported. 'b' is ignored. Per qemu-img documentation. + Each additional disk uses the same disk parameters as the default disk. + Unset by default. + - `disk_cache` (string) - The cache mode to use for disk. Allowed values include any of `writethrough`, `writeback`, `none`, `unsafe` or `directsync`. By default, this is set to `writeback`.