packer-cn/builder/hyperv/vmcx/builder.go

395 lines
14 KiB
Go
Raw Normal View History

//go:generate struct-markdown
//go:generate mapstructure-to-hcl2 -type Config
package vmcx
import (
"context"
"errors"
"fmt"
"os"
"strings"
build using HCL2 (#8423) This follows #8232 which added the code to generate the code required to parse HCL files for each packer component. All old config files of packer will keep on working the same. Packer takes one argument. When a directory is passed, all files in the folder with a name ending with “.pkr.hcl” or “.pkr.json” will be parsed using the HCL2 format. When a file ending with “.pkr.hcl” or “.pkr.json” is passed it will be parsed using the HCL2 format. For every other case; the old packer style will be used. ## 1. the hcl2template pkg can create a packer.Build from a set of HCL (v2) files I had to make the packer.coreBuild (which is our one and only packer.Build ) a public struct with public fields ## 2. Components interfaces get a new ConfigSpec Method to read a file from an HCL file. This is a breaking change for packer plugins. a packer component can be a: builder/provisioner/post-processor each component interface now gets a `ConfigSpec() hcldec.ObjectSpec` which allows packer to tell what is the layout of the hcl2 config meant to configure that specific component. This ObjectSpec is sent through the wire (RPC) and a cty.Value is now sent through the already existing configuration entrypoints: Provisioner.Prepare(raws ...interface{}) error Builder.Prepare(raws ...interface{}) ([]string, error) PostProcessor.Configure(raws ...interface{}) error close #1768 Example hcl files: ```hcl // file amazon-ebs-kms-key/run.pkr.hcl build { sources = [ "source.amazon-ebs.first", ] provisioner "shell" { inline = [ "sleep 5" ] } post-processor "shell-local" { inline = [ "sleep 5" ] } } // amazon-ebs-kms-key/source.pkr.hcl source "amazon-ebs" "first" { ami_name = "hcl2-test" region = "us-east-1" instance_type = "t2.micro" kms_key_id = "c729958f-c6ba-44cd-ab39-35ab68ce0a6c" encrypt_boot = true source_ami_filter { filters { virtualization-type = "hvm" name = "amzn-ami-hvm-????.??.?.????????-x86_64-gp2" root-device-type = "ebs" } most_recent = true owners = ["amazon"] } launch_block_device_mappings { device_name = "/dev/xvda" volume_size = 20 volume_type = "gp2" delete_on_termination = "true" } launch_block_device_mappings { device_name = "/dev/xvdf" volume_size = 500 volume_type = "gp2" delete_on_termination = true encrypted = true } ami_regions = ["eu-central-1"] run_tags { Name = "packer-solr-something" stack-name = "DevOps Tools" } communicator = "ssh" ssh_pty = true ssh_username = "ec2-user" associate_public_ip_address = true } ```
2019-12-17 05:25:56 -05:00
"github.com/hashicorp/hcl/v2/hcldec"
hypervcommon "github.com/hashicorp/packer/builder/hyperv/common"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/common/bootcommand"
powershell "github.com/hashicorp/packer/common/powershell"
"github.com/hashicorp/packer/common/shutdowncommand"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
const (
2019-06-24 19:39:27 -04:00
DefaultRamSize = 1 * 1024 // 1GB
MinRamSize = 32 // 32MB
MaxRamSize = 1024 * 1024 // 1TB
MinNestedVirtualizationRamSize = 4 * 1024 // 4GB
LowRam = 256 // 256MB
DefaultUsername = ""
DefaultPassword = ""
)
// Builder implements packer.Builder and builds the actual Hyperv
// images.
type Builder struct {
config Config
runner multistep.Runner
}
type Config struct {
common.PackerConfig `mapstructure:",squash"`
common.HTTPConfig `mapstructure:",squash"`
common.ISOConfig `mapstructure:",squash"`
bootcommand.BootConfig `mapstructure:",squash"`
hypervcommon.OutputConfig `mapstructure:",squash"`
hypervcommon.SSHConfig `mapstructure:",squash"`
hypervcommon.CommonConfig `mapstructure:",squash"`
shutdowncommand.ShutdownConfig `mapstructure:",squash"`
// This is the path to a directory containing an exported virtual machine.
CloneFromVMCXPath string `mapstructure:"clone_from_vmcx_path"`
// This is the name of the virtual machine to clone from.
CloneFromVMName string `mapstructure:"clone_from_vm_name"`
// The name of a snapshot in the
2019-06-06 10:29:25 -04:00
// source machine to use as a starting point for the clone. If the value
// given is an empty string, the last snapshot present in the source will
// be chosen as the starting point for the new VM.
CloneFromSnapshotName string `mapstructure:"clone_from_snapshot_name" required:"false"`
// If set to true all snapshots
2019-06-06 10:29:25 -04:00
// present in the source machine will be copied when the machine is
// cloned. The final result of the build will be an exported virtual
// machine that contains all the snapshots of the parent.
CloneAllSnapshots bool `mapstructure:"clone_all_snapshots" required:"false"`
// If true enables differencing disks. Only
2019-06-06 10:29:25 -04:00
// the changes will be written to the new disk. This is especially useful if
// your source is a VHD/VHDX. This defaults to false.
DifferencingDisk bool `mapstructure:"differencing_disk" required:"false"`
// When cloning a vm to build from, we run a powershell
2019-06-06 10:29:25 -04:00
// Compare-VM command, which, depending on your version of Windows, may need
// the "Copy" flag to be set to true or false. Defaults to "false". Command:
CompareCopy bool `mapstructure:"copy_in_compare" required:"false"`
ctx interpolate.Context
}
build using HCL2 (#8423) This follows #8232 which added the code to generate the code required to parse HCL files for each packer component. All old config files of packer will keep on working the same. Packer takes one argument. When a directory is passed, all files in the folder with a name ending with “.pkr.hcl” or “.pkr.json” will be parsed using the HCL2 format. When a file ending with “.pkr.hcl” or “.pkr.json” is passed it will be parsed using the HCL2 format. For every other case; the old packer style will be used. ## 1. the hcl2template pkg can create a packer.Build from a set of HCL (v2) files I had to make the packer.coreBuild (which is our one and only packer.Build ) a public struct with public fields ## 2. Components interfaces get a new ConfigSpec Method to read a file from an HCL file. This is a breaking change for packer plugins. a packer component can be a: builder/provisioner/post-processor each component interface now gets a `ConfigSpec() hcldec.ObjectSpec` which allows packer to tell what is the layout of the hcl2 config meant to configure that specific component. This ObjectSpec is sent through the wire (RPC) and a cty.Value is now sent through the already existing configuration entrypoints: Provisioner.Prepare(raws ...interface{}) error Builder.Prepare(raws ...interface{}) ([]string, error) PostProcessor.Configure(raws ...interface{}) error close #1768 Example hcl files: ```hcl // file amazon-ebs-kms-key/run.pkr.hcl build { sources = [ "source.amazon-ebs.first", ] provisioner "shell" { inline = [ "sleep 5" ] } post-processor "shell-local" { inline = [ "sleep 5" ] } } // amazon-ebs-kms-key/source.pkr.hcl source "amazon-ebs" "first" { ami_name = "hcl2-test" region = "us-east-1" instance_type = "t2.micro" kms_key_id = "c729958f-c6ba-44cd-ab39-35ab68ce0a6c" encrypt_boot = true source_ami_filter { filters { virtualization-type = "hvm" name = "amzn-ami-hvm-????.??.?.????????-x86_64-gp2" root-device-type = "ebs" } most_recent = true owners = ["amazon"] } launch_block_device_mappings { device_name = "/dev/xvda" volume_size = 20 volume_type = "gp2" delete_on_termination = "true" } launch_block_device_mappings { device_name = "/dev/xvdf" volume_size = 500 volume_type = "gp2" delete_on_termination = true encrypted = true } ami_regions = ["eu-central-1"] run_tags { Name = "packer-solr-something" stack-name = "DevOps Tools" } communicator = "ssh" ssh_pty = true ssh_username = "ec2-user" associate_public_ip_address = true } ```
2019-12-17 05:25:56 -05:00
func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() }
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
2017-11-05 09:16:47 -05:00
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"boot_command",
},
},
}, raws...)
if err != nil {
return nil, nil, err
}
// Accumulate any errors and warnings
var errs *packer.MultiError
warnings := make([]string, 0)
if b.config.RawSingleISOUrl != "" || len(b.config.ISOUrls) > 0 {
isoWarnings, isoErrs := b.config.ISOConfig.Prepare(&b.config.ctx)
warnings = append(warnings, isoWarnings...)
errs = packer.MultiErrorAppend(errs, isoErrs...)
}
errs = packer.MultiErrorAppend(errs, b.config.BootConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.OutputConfig.Prepare(&b.config.ctx, &b.config.PackerConfig)...)
errs = packer.MultiErrorAppend(errs, b.config.SSHConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.ShutdownConfig.Prepare(&b.config.ctx)...)
commonErrs, commonWarns := b.config.CommonConfig.Prepare(&b.config.ctx, &b.config.PackerConfig)
packer.MultiErrorAppend(errs, commonErrs...)
warnings = append(warnings, commonWarns...)
if b.config.Cpu < 1 {
b.config.Cpu = 1
}
if b.config.CloneFromVMName == "" {
if b.config.CloneFromVMCXPath == "" {
2018-07-09 12:20:38 -04:00
errs = packer.MultiErrorAppend(errs, fmt.Errorf("The clone_from_vm_name must be specified if "+
"clone_from_vmcx_path is not specified."))
}
} else {
virtualMachineExists, err := powershell.DoesVirtualMachineExist(b.config.CloneFromVMName)
if err != nil {
2018-07-09 12:20:38 -04:00
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed detecting if virtual machine to clone "+
"from exists: %s", err))
} else {
if !virtualMachineExists {
2018-07-09 12:20:38 -04:00
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Virtual machine '%s' to clone from does not "+
"exist.", b.config.CloneFromVMName))
} else {
b.config.Generation, err = powershell.GetVirtualMachineGeneration(b.config.CloneFromVMName)
if err != nil {
2018-07-09 12:20:38 -04:00
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed detecting virtual machine to clone "+
"from generation: %s", err))
}
if b.config.CloneFromSnapshotName != "" {
2018-07-09 12:20:38 -04:00
virtualMachineSnapshotExists, err := powershell.DoesVirtualMachineSnapshotExist(
b.config.CloneFromVMName, b.config.CloneFromSnapshotName)
if err != nil {
2018-07-09 12:20:38 -04:00
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed detecting if virtual machine "+
"snapshot to clone from exists: %s", err))
} else {
if !virtualMachineSnapshotExists {
2018-07-09 12:20:38 -04:00
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Virtual machine snapshot '%s' on "+
"virtual machine '%s' to clone from does not exist.",
b.config.CloneFromSnapshotName, b.config.CloneFromVMName))
}
}
}
virtualMachineOn, err := powershell.IsVirtualMachineOn(b.config.CloneFromVMName)
if err != nil {
2018-07-09 12:20:38 -04:00
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed detecting if virtual machine to "+
"clone is running: %s", err))
} else {
if virtualMachineOn {
warning := fmt.Sprintf("Cloning from a virtual machine that is running.")
warnings = hypervcommon.Appendwarns(warnings, warning)
}
}
}
}
}
if b.config.CloneFromVMCXPath == "" {
if b.config.CloneFromVMName == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("The clone_from_vmcx_path be specified if "+
2018-07-09 12:20:38 -04:00
"clone_from_vm_name must is not specified."))
}
} else {
if _, err := os.Stat(b.config.CloneFromVMCXPath); os.IsNotExist(err) {
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("CloneFromVMCXPath does not exist: %s", err))
}
}
if strings.HasSuffix(strings.ToLower(b.config.CloneFromVMCXPath), ".vmcx") {
// User has provided the vmcx file itself rather than the containing
// folder.
if strings.Contains(b.config.CloneFromVMCXPath, "Virtual Machines") {
keep := strings.Split(b.config.CloneFromVMCXPath, "Virtual Machines")
b.config.CloneFromVMCXPath = keep[0]
} else {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Unable to "+
"parse the clone_from_vmcx_path to find the vm directory. "+
"Please provide the path to the folder containing the "+
"vmcx file, not the file itself. Example: instead of "+
"C:\\path\\to\\output-hyperv-iso\\Virtual Machines\\filename.vmcx"+
", provide C:\\path\\to\\output-hyperv-iso\\."))
}
}
}
// Warnings
if b.config.ShutdownCommand == "" {
warnings = hypervcommon.Appendwarns(warnings,
"A shutdown_command was not specified. Without a shutdown command, Packer\n"+
"will forcibly halt the virtual machine, which may result in data loss.")
}
if errs != nil && len(errs.Errors) > 0 {
return nil, warnings, errs
}
return nil, warnings, nil
}
// Run executes a Packer build and returns a packer.Artifact representing
// a Hyperv appliance.
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
// Create the driver that we'll use to communicate with Hyperv
driver, err := hypervcommon.NewHypervPS4Driver()
if err != nil {
return nil, fmt.Errorf("Failed creating Hyper-V driver: %s", err)
}
// Set up the state.
state := new(multistep.BasicStateBag)
state.Put("debug", b.config.PackerDebug)
state.Put("driver", driver)
state.Put("hook", hook)
state.Put("ui", ui)
steps := []multistep.Step{
&hypervcommon.StepCreateBuildDir{
TempPath: b.config.TempPath,
},
&common.StepOutputDir{
Force: b.config.PackerForce,
Path: b.config.OutputDir,
},
&common.StepDownload{
Checksum: b.config.ISOChecksum,
ChecksumType: b.config.ISOChecksumType,
Description: "ISO",
ResultKey: "iso_path",
Url: b.config.ISOUrls,
Extension: b.config.TargetExtension,
TargetPath: b.config.TargetPath,
},
&common.StepCreateFloppy{
2017-09-11 13:11:45 -04:00
Files: b.config.FloppyFiles,
Directories: b.config.FloppyConfig.FloppyDirectories,
2019-09-12 08:25:22 -04:00
Label: b.config.FloppyConfig.FloppyLabel,
},
&common.StepHTTPServer{
HTTPDir: b.config.HTTPDir,
HTTPPortMin: b.config.HTTPPortMin,
HTTPPortMax: b.config.HTTPPortMax,
},
&hypervcommon.StepCreateSwitch{
SwitchName: b.config.SwitchName,
},
&hypervcommon.StepCloneVM{
CloneFromVMCXPath: b.config.CloneFromVMCXPath,
CloneFromVMName: b.config.CloneFromVMName,
CloneFromSnapshotName: b.config.CloneFromSnapshotName,
CloneAllSnapshots: b.config.CloneAllSnapshots,
VMName: b.config.VMName,
SwitchName: b.config.SwitchName,
CompareCopy: b.config.CompareCopy,
RamSize: b.config.RamSize,
Cpu: b.config.Cpu,
EnableMacSpoofing: b.config.EnableMacSpoofing,
EnableDynamicMemory: b.config.EnableDynamicMemory,
EnableSecureBoot: b.config.EnableSecureBoot,
SecureBootTemplate: b.config.SecureBootTemplate,
EnableVirtualizationExtensions: b.config.EnableVirtualizationExtensions,
MacAddress: b.config.MacAddress,
2019-04-12 18:59:09 -04:00
KeepRegistered: b.config.KeepRegistered,
AdditionalDiskSize: b.config.AdditionalDiskSize,
DiskBlockSize: b.config.DiskBlockSize,
},
&hypervcommon.StepEnableIntegrationService{},
&hypervcommon.StepMountDvdDrive{
2020-02-21 02:07:58 -05:00
Generation: b.config.Generation,
FirstBootDevice: b.config.FirstBootDevice,
},
&hypervcommon.StepMountFloppydrive{
Generation: b.config.Generation,
},
&hypervcommon.StepMountGuestAdditions{
GuestAdditionsMode: b.config.GuestAdditionsMode,
GuestAdditionsPath: b.config.GuestAdditionsPath,
Generation: b.config.Generation,
},
&hypervcommon.StepMountSecondaryDvdImages{
IsoPaths: b.config.SecondaryDvdImages,
Generation: b.config.Generation,
},
&hypervcommon.StepConfigureVlan{
VlanId: b.config.VlanId,
SwitchVlanId: b.config.SwitchVlanId,
},
&hypervcommon.StepSetFirstBootDevice{
Generation: b.config.Generation,
FirstBootDevice: b.config.FirstBootDevice,
},
&hypervcommon.StepRun{
Headless: b.config.Headless,
SwitchName: b.config.SwitchName,
},
&hypervcommon.StepTypeBootCommand{
BootCommand: b.config.FlatBootCommand(),
BootWait: b.config.BootWait,
SwitchName: b.config.SwitchName,
Ctx: b.config.ctx,
GroupInterval: b.config.BootConfig.BootGroupInterval,
},
// configure the communicator ssh, winrm
&communicator.StepConnect{
Config: &b.config.SSHConfig.Comm,
2018-12-27 03:33:58 -05:00
Host: hypervcommon.CommHost(b.config.SSHConfig.Comm.SSHHost),
SSHConfig: b.config.SSHConfig.Comm.SSHConfigFunc(),
},
// provision requires communicator to be setup
&common.StepProvision{},
// Remove ephemeral SSH keys, if using
&common.StepCleanupTempKeys{
Comm: &b.config.SSHConfig.Comm,
},
&hypervcommon.StepShutdown{
Command: b.config.ShutdownCommand,
Timeout: b.config.ShutdownTimeout,
},
// wait for the vm to be powered off
&hypervcommon.StepWaitForPowerOff{},
// remove the secondary dvd images
// after we power down
&hypervcommon.StepUnmountSecondaryDvdImages{},
&hypervcommon.StepUnmountGuestAdditions{},
&hypervcommon.StepUnmountDvdDrive{},
&hypervcommon.StepUnmountFloppyDrive{
Generation: b.config.Generation,
},
&hypervcommon.StepCompactDisk{
SkipCompaction: b.config.SkipCompaction,
},
&hypervcommon.StepExportVm{
OutputDir: b.config.OutputDir,
SkipExport: b.config.SkipExport,
},
&hypervcommon.StepCollateArtifacts{
OutputDir: b.config.OutputDir,
SkipExport: b.config.SkipExport,
},
}
// the clean up actions for each step will be executed reverse order
// Run the steps.
2017-09-11 13:38:47 -04:00
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(ctx, state)
// Report any errors.
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
// If we were interrupted or cancelled, then just exit.
if _, ok := state.GetOk(multistep.StateCancelled); ok {
return nil, errors.New("Build was cancelled.")
}
if _, ok := state.GetOk(multistep.StateHalted); ok {
return nil, errors.New("Build was halted.")
}
generatedData := map[string]interface{}{"generated_data": state.Get("generated_data")}
return hypervcommon.NewArtifact(b.config.OutputDir, generatedData)
}
// Cancel.