diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..7230f36f4b4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ + +common/test-fixtures/root/* eol=lf \ No newline at end of file diff --git a/builder/hyperv/common/artifact.go b/builder/hyperv/common/artifact.go new file mode 100644 index 00000000000..2baeb2d62bf --- /dev/null +++ b/builder/hyperv/common/artifact.go @@ -0,0 +1,65 @@ +package common + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/mitchellh/packer/packer" +) + +// This is the common builder ID to all of these artifacts. +const BuilderId = "MSOpenTech.hyperv" + +// Artifact is the result of running the hyperv builder, namely a set +// of files associated with the resulting machine. +type artifact struct { + dir string + f []string +} + +// NewArtifact returns a hyperv artifact containing the files +// in the given directory. +func NewArtifact(dir string) (packer.Artifact, error) { + files := make([]string, 0, 5) + visit := func(path string, info os.FileInfo, err error) error { + if !info.IsDir() { + files = append(files, path) + } + + return err + } + + if err := filepath.Walk(dir, visit); err != nil { + return nil, err + } + + return &artifact{ + dir: dir, + f: files, + }, nil +} + +func (*artifact) BuilderId() string { + return BuilderId +} + +func (a *artifact) Files() []string { + return a.f +} + +func (*artifact) Id() string { + return "VM" +} + +func (a *artifact) String() string { + return fmt.Sprintf("VM files in directory: %s", a.dir) +} + +func (a *artifact) State(name string) interface{} { + return nil +} + +func (a *artifact) Destroy() error { + return os.RemoveAll(a.dir) +} diff --git a/builder/hyperv/common/artifact_test.go b/builder/hyperv/common/artifact_test.go new file mode 100644 index 00000000000..f9ddc5dbfc8 --- /dev/null +++ b/builder/hyperv/common/artifact_test.go @@ -0,0 +1,43 @@ +package common + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/mitchellh/packer/packer" +) + +func TestArtifact_impl(t *testing.T) { + var _ packer.Artifact = new(artifact) +} + +func TestNewArtifact(t *testing.T) { + td, err := ioutil.TempDir("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(td) + + err = ioutil.WriteFile(filepath.Join(td, "a"), []byte("foo"), 0644) + if err != nil { + t.Fatalf("err: %s", err) + } + + if err := os.Mkdir(filepath.Join(td, "b"), 0755); err != nil { + t.Fatalf("err: %s", err) + } + + a, err := NewArtifact(td) + if err != nil { + t.Fatalf("err: %s", err) + } + + if a.BuilderId() != BuilderId { + t.Fatalf("bad: %#v", a.BuilderId()) + } + if len(a.Files()) != 1 { + t.Fatalf("should length 1: %d", len(a.Files())) + } +} diff --git a/builder/hyperv/common/config_test.go b/builder/hyperv/common/config_test.go new file mode 100644 index 00000000000..eeeda864a3d --- /dev/null +++ b/builder/hyperv/common/config_test.go @@ -0,0 +1,11 @@ +package common + +import ( + "testing" + + "github.com/mitchellh/packer/template/interpolate" +) + +func testConfigTemplate(t *testing.T) *interpolate.Context { + return &interpolate.Context{} +} diff --git a/builder/hyperv/common/driver.go b/builder/hyperv/common/driver.go new file mode 100644 index 00000000000..038c855e642 --- /dev/null +++ b/builder/hyperv/common/driver.go @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +// A driver is able to talk to HyperV and perform certain +// operations with it. Some of the operations on here may seem overly +// specific, but they were built specifically in mind to handle features +// of the HyperV builder for Packer, and to abstract differences in +// versions out of the builder steps, so sometimes the methods are +// extremely specific. +type Driver interface { + + // Checks if the VM named is running. + IsRunning(string) (bool, error) + + // Checks if the VM named is off. + IsOff(string) (bool, error) + + //How long has VM been on + Uptime(vmName string) (uint64, error) + + // Start starts a VM specified by the name given. + Start(string) error + + // Stop stops a VM specified by the name given. + Stop(string) error + + // Verify checks to make sure that this driver should function + // properly. If there is any indication the driver can't function, + // this will return an error. + Verify() error + + // Finds the MAC address of the NIC nic0 + Mac(string) (string, error) + + // Finds the IP address of a VM connected that uses DHCP by its MAC address + IpAddress(string) (string, error) + + // Finds the hostname for the ip address + GetHostName(string) (string, error) + + // Finds the IP address of a host adapter connected to switch + GetHostAdapterIpAddressForSwitch(string) (string, error) + + // Type scan codes to virtual keyboard of vm + TypeScanCodes(string, string) error + + //Get the ip address for network adaptor + GetVirtualMachineNetworkAdapterAddress(string) (string, error) + + //Set the vlan to use for switch + SetNetworkAdapterVlanId(string, string) error + + //Set the vlan to use for machine + SetVirtualMachineVlanId(string, string) error + + UntagVirtualMachineNetworkAdapterVlan(string, string) error + + CreateExternalVirtualSwitch(string, string) error + + GetVirtualMachineSwitchName(string) (string, error) + + ConnectVirtualMachineNetworkAdapterToSwitch(string, string) error + + CreateVirtualSwitch(string, string) (bool, error) + + DeleteVirtualSwitch(string) error + + CreateVirtualMachine(string, string, int64, int64, string, uint) error + + DeleteVirtualMachine(string) error + + SetVirtualMachineCpuCount(string, uint) error + + SetVirtualMachineMacSpoofing(string, bool) error + + SetVirtualMachineDynamicMemory(string, bool) error + + SetVirtualMachineSecureBoot(string, bool) error + + SetVirtualMachineVirtualizationExtensions(string, bool) error + + EnableVirtualMachineIntegrationService(string, string) error + + ExportVirtualMachine(string, string) error + + CompactDisks(string, string) error + + CopyExportedVirtualMachine(string, string, string, string) error + + RestartVirtualMachine(string) error + + CreateDvdDrive(string, string, uint) (uint, uint, error) + + MountDvdDrive(string, string, uint, uint) error + + SetBootDvdDrive(string, uint, uint, uint) error + + UnmountDvdDrive(string, uint, uint) error + + DeleteDvdDrive(string, uint, uint) error + + MountFloppyDrive(string, string) error + + UnmountFloppyDrive(string) error + + SetVirtualMachineIPNetworkConfiguration(string, string, string, string, string) error +} diff --git a/builder/hyperv/common/driver_ps_4.go b/builder/hyperv/common/driver_ps_4.go new file mode 100644 index 00000000000..6ece080d2b6 --- /dev/null +++ b/builder/hyperv/common/driver_ps_4.go @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "log" + "runtime" + "strconv" + "strings" + + "github.com/mitchellh/packer/powershell" + "github.com/mitchellh/packer/powershell/hyperv" +) + +type HypervPS4Driver struct { +} + +func NewHypervPS4Driver() (Driver, error) { + appliesTo := "Applies to Windows 8.1, Windows PowerShell 4.0, Windows Server 2012 R2 only" + + // Check this is Windows + if runtime.GOOS != "windows" { + err := fmt.Errorf("%s", appliesTo) + return nil, err + } + + ps4Driver := &HypervPS4Driver{} + + if err := ps4Driver.Verify(); err != nil { + return nil, err + } + + return ps4Driver, nil +} + +func (d *HypervPS4Driver) IsRunning(vmName string) (bool, error) { + return hyperv.IsRunning(vmName) +} + +func (d *HypervPS4Driver) IsOff(vmName string) (bool, error) { + return hyperv.IsOff(vmName) +} + +func (d *HypervPS4Driver) Uptime(vmName string) (uint64, error) { + return hyperv.Uptime(vmName) +} + +// Start starts a VM specified by the name given. +func (d *HypervPS4Driver) Start(vmName string) error { + return hyperv.StartVirtualMachine(vmName) +} + +// Stop stops a VM specified by the name given. +func (d *HypervPS4Driver) Stop(vmName string) error { + return hyperv.StopVirtualMachine(vmName) +} + +func (d *HypervPS4Driver) Verify() error { + + if err := d.verifyPSVersion(); err != nil { + return err + } + + if err := d.verifyPSHypervModule(); err != nil { + return err + } + + if err := d.verifyElevatedMode(); err != nil { + return err + } + + return nil +} + +// Get mac address for VM. +func (d *HypervPS4Driver) Mac(vmName string) (string, error) { + res, err := hyperv.Mac(vmName) + + if err != nil { + return res, err + } + + if res == "" { + err := fmt.Errorf("%s", "No mac address.") + return res, err + } + + return res, err +} + +// Get ip address for mac address. +func (d *HypervPS4Driver) IpAddress(mac string) (string, error) { + res, err := hyperv.IpAddress(mac) + + if err != nil { + return res, err + } + + if res == "" { + err := fmt.Errorf("%s", "No ip address.") + return res, err + } + return res, err +} + +// Get host name from ip address +func (d *HypervPS4Driver) GetHostName(ip string) (string, error) { + return powershell.GetHostName(ip) +} + +// Finds the IP address of a host adapter connected to switch +func (d *HypervPS4Driver) GetHostAdapterIpAddressForSwitch(switchName string) (string, error) { + res, err := hyperv.GetHostAdapterIpAddressForSwitch(switchName) + + if err != nil { + return res, err + } + + if res == "" { + err := fmt.Errorf("%s", "No ip address.") + return res, err + } + return res, err +} + +// Type scan codes to virtual keyboard of vm +func (d *HypervPS4Driver) TypeScanCodes(vmName string, scanCodes string) error { + return hyperv.TypeScanCodes(vmName, scanCodes) +} + +// Get network adapter address +func (d *HypervPS4Driver) GetVirtualMachineNetworkAdapterAddress(vmName string) (string, error) { + return hyperv.GetVirtualMachineNetworkAdapterAddress(vmName) +} + +//Set the vlan to use for switch +func (d *HypervPS4Driver) SetNetworkAdapterVlanId(switchName string, vlanId string) error { + return hyperv.SetNetworkAdapterVlanId(switchName, vlanId) +} + +//Set the vlan to use for machine +func (d *HypervPS4Driver) SetVirtualMachineVlanId(vmName string, vlanId string) error { + return hyperv.SetVirtualMachineVlanId(vmName, vlanId) +} + +func (d *HypervPS4Driver) UntagVirtualMachineNetworkAdapterVlan(vmName string, switchName string) error { + return hyperv.UntagVirtualMachineNetworkAdapterVlan(vmName, switchName) +} + +func (d *HypervPS4Driver) CreateExternalVirtualSwitch(vmName string, switchName string) error { + return hyperv.CreateExternalVirtualSwitch(vmName, switchName) +} + +func (d *HypervPS4Driver) GetVirtualMachineSwitchName(vmName string) (string, error) { + return hyperv.GetVirtualMachineSwitchName(vmName) +} + +func (d *HypervPS4Driver) ConnectVirtualMachineNetworkAdapterToSwitch(vmName string, switchName string) error { + return hyperv.ConnectVirtualMachineNetworkAdapterToSwitch(vmName, switchName) +} + +func (d *HypervPS4Driver) DeleteVirtualSwitch(switchName string) error { + return hyperv.DeleteVirtualSwitch(switchName) +} + +func (d *HypervPS4Driver) CreateVirtualSwitch(switchName string, switchType string) (bool, error) { + return hyperv.CreateVirtualSwitch(switchName, switchType) +} + +func (d *HypervPS4Driver) CreateVirtualMachine(vmName string, path string, ram int64, diskSize int64, switchName string, generation uint) error { + return hyperv.CreateVirtualMachine(vmName, path, ram, diskSize, switchName, generation) +} + +func (d *HypervPS4Driver) DeleteVirtualMachine(vmName string) error { + return hyperv.DeleteVirtualMachine(vmName) +} + +func (d *HypervPS4Driver) SetVirtualMachineCpuCount(vmName string, cpu uint) error { + return hyperv.SetVirtualMachineCpuCount(vmName, cpu) +} + +func (d *HypervPS4Driver) SetVirtualMachineMacSpoofing(vmName string, enable bool) error { + return hyperv.SetVirtualMachineMacSpoofing(vmName, enable) +} + +func (d *HypervPS4Driver) SetVirtualMachineDynamicMemory(vmName string, enable bool) error { + return hyperv.SetVirtualMachineDynamicMemory(vmName, enable) +} + +func (d *HypervPS4Driver) SetVirtualMachineSecureBoot(vmName string, enable bool) error { + return hyperv.SetVirtualMachineSecureBoot(vmName, enable) +} + +func (d *HypervPS4Driver) SetVirtualMachineVirtualizationExtensions(vmName string, enable bool) error { + return hyperv.SetVirtualMachineVirtualizationExtensions(vmName, enable) +} + +func (d *HypervPS4Driver) EnableVirtualMachineIntegrationService(vmName string, integrationServiceName string) error { + return hyperv.EnableVirtualMachineIntegrationService(vmName, integrationServiceName) +} + +func (d *HypervPS4Driver) ExportVirtualMachine(vmName string, path string) error { + return hyperv.ExportVirtualMachine(vmName, path) +} + +func (d *HypervPS4Driver) CompactDisks(expPath string, vhdDir string) error { + return hyperv.CompactDisks(expPath, vhdDir) +} + +func (d *HypervPS4Driver) CopyExportedVirtualMachine(expPath string, outputPath string, vhdDir string, vmDir string) error { + return hyperv.CopyExportedVirtualMachine(expPath, outputPath, vhdDir, vmDir) +} + +func (d *HypervPS4Driver) RestartVirtualMachine(vmName string) error { + return hyperv.RestartVirtualMachine(vmName) +} + +func (d *HypervPS4Driver) CreateDvdDrive(vmName string, isoPath string, generation uint) (uint, uint, error) { + return hyperv.CreateDvdDrive(vmName, isoPath, generation) +} + +func (d *HypervPS4Driver) MountDvdDrive(vmName string, path string, controllerNumber uint, controllerLocation uint) error { + return hyperv.MountDvdDrive(vmName, path, controllerNumber, controllerLocation) +} + +func (d *HypervPS4Driver) SetBootDvdDrive(vmName string, controllerNumber uint, controllerLocation uint, generation uint) error { + return hyperv.SetBootDvdDrive(vmName, controllerNumber, controllerLocation, generation) +} + +func (d *HypervPS4Driver) UnmountDvdDrive(vmName string, controllerNumber uint, controllerLocation uint) error { + return hyperv.UnmountDvdDrive(vmName, controllerNumber, controllerLocation) +} + +func (d *HypervPS4Driver) DeleteDvdDrive(vmName string, controllerNumber uint, controllerLocation uint) error { + return hyperv.DeleteDvdDrive(vmName, controllerNumber, controllerLocation) +} + +func (d *HypervPS4Driver) MountFloppyDrive(vmName string, path string) error { + return hyperv.MountFloppyDrive(vmName, path) +} + +func (d *HypervPS4Driver) UnmountFloppyDrive(vmName string) error { + return hyperv.UnmountFloppyDrive(vmName) +} + +func (d *HypervPS4Driver) SetVirtualMachineIPNetworkConfiguration(vmName string, IPAddress string, Gateway string, DNSServer string, Subnet string) error { + return hyperv.SetVirtualMachineIPNetworkConfiguration(vmName, IPAddress, Gateway, DNSServer, Subnet) +} + +func (d *HypervPS4Driver) verifyPSVersion() error { + + log.Printf("Enter method: %s", "verifyPSVersion") + // check PS is available and is of proper version + versionCmd := "$host.version.Major" + + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(versionCmd) + if err != nil { + return err + } + + versionOutput := strings.TrimSpace(string(cmdOut)) + log.Printf("%s output: %s", versionCmd, versionOutput) + + ver, err := strconv.ParseInt(versionOutput, 10, 32) + + if err != nil { + return err + } + + if ver < 4 { + err := fmt.Errorf("%s", "Windows PowerShell version 4.0 or higher is expected") + return err + } + + return nil +} + +func (d *HypervPS4Driver) verifyPSHypervModule() error { + + log.Printf("Enter method: %s", "verifyPSHypervModule") + + versionCmd := "function foo(){try{ $commands = Get-Command -Module Hyper-V;if($commands.Length -eq 0){return $false} }catch{return $false}; return $true} foo" + + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(versionCmd) + if err != nil { + return err + } + + res := strings.TrimSpace(string(cmdOut)) + + if res == "False" { + err := fmt.Errorf("%s", "PS Hyper-V module is not loaded. Make sure Hyper-V feature is on.") + return err + } + + return nil +} + +func (d *HypervPS4Driver) verifyElevatedMode() error { + + log.Printf("Enter method: %s", "verifyElevatedMode") + + isAdmin, _ := powershell.IsCurrentUserAnAdministrator() + + if !isAdmin { + err := fmt.Errorf("%s", "Please restart your shell in elevated mode") + return err + } + + return nil +} diff --git a/builder/hyperv/common/floppy_config.go b/builder/hyperv/common/floppy_config.go new file mode 100644 index 00000000000..d656e103a8f --- /dev/null +++ b/builder/hyperv/common/floppy_config.go @@ -0,0 +1,19 @@ +package common + +import ( + "github.com/mitchellh/packer/template/interpolate" +) + +// FloppyConfig is configuration related to created floppy disks and attaching +// them to a Parallels virtual machine. +type FloppyConfig struct { + FloppyFiles []string `mapstructure:"floppy_files"` +} + +func (c *FloppyConfig) Prepare(ctx *interpolate.Context) []error { + if c.FloppyFiles == nil { + c.FloppyFiles = make([]string, 0) + } + + return nil +} diff --git a/builder/hyperv/common/floppy_config_test.go b/builder/hyperv/common/floppy_config_test.go new file mode 100644 index 00000000000..3e4fc55db3a --- /dev/null +++ b/builder/hyperv/common/floppy_config_test.go @@ -0,0 +1,18 @@ +package common + +import ( + "testing" +) + +func TestFloppyConfigPrepare(t *testing.T) { + c := new(FloppyConfig) + + errs := c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("err: %#v", errs) + } + + if len(c.FloppyFiles) > 0 { + t.Fatal("should not have floppy files") + } +} diff --git a/builder/hyperv/common/output_config.go b/builder/hyperv/common/output_config.go new file mode 100644 index 00000000000..40b10e6e552 --- /dev/null +++ b/builder/hyperv/common/output_config.go @@ -0,0 +1,28 @@ +package common + +import ( + "fmt" + "os" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/template/interpolate" +) + +type OutputConfig struct { + OutputDir string `mapstructure:"output_directory"` +} + +func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig) []error { + if c.OutputDir == "" { + c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName) + } + + var errs []error + if !pc.PackerForce { + if _, err := os.Stat(c.OutputDir); err == nil { + errs = append(errs, fmt.Errorf( + "Output directory '%s' already exists. It must not exist.", c.OutputDir)) + } + } + + return errs +} diff --git a/builder/hyperv/common/output_config_test.go b/builder/hyperv/common/output_config_test.go new file mode 100644 index 00000000000..ebd91eab13d --- /dev/null +++ b/builder/hyperv/common/output_config_test.go @@ -0,0 +1,45 @@ +package common + +import ( + "github.com/mitchellh/packer/common" + "io/ioutil" + "os" + "testing" +) + +func TestOutputConfigPrepare(t *testing.T) { + c := new(OutputConfig) + if c.OutputDir != "" { + t.Fatalf("what: %s", c.OutputDir) + } + + pc := &common.PackerConfig{PackerBuildName: "foo"} + errs := c.Prepare(testConfigTemplate(t), pc) + if len(errs) > 0 { + t.Fatalf("err: %#v", errs) + } + + if c.OutputDir == "" { + t.Fatal("should have output dir") + } +} + +func TestOutputConfigPrepare_exists(t *testing.T) { + td, err := ioutil.TempDir("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(td) + + c := new(OutputConfig) + c.OutputDir = td + + pc := &common.PackerConfig{ + PackerBuildName: "foo", + PackerForce: false, + } + errs := c.Prepare(testConfigTemplate(t), pc) + if len(errs) == 0 { + t.Fatal("should have errors") + } +} diff --git a/builder/hyperv/common/run_config.go b/builder/hyperv/common/run_config.go new file mode 100644 index 00000000000..3ac72471cd5 --- /dev/null +++ b/builder/hyperv/common/run_config.go @@ -0,0 +1,33 @@ +package common + +import ( + "fmt" + "github.com/mitchellh/packer/template/interpolate" + "time" +) + +type RunConfig struct { + Headless bool `mapstructure:"headless"` + RawBootWait string `mapstructure:"boot_wait"` + + BootWait time.Duration `` +} + +func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { + if c.RawBootWait == "" { + c.RawBootWait = "10s" + } + + var errs []error + var err error + + if c.RawBootWait != "" { + c.BootWait, err = time.ParseDuration(c.RawBootWait) + if err != nil { + errs = append( + errs, fmt.Errorf("Failed parsing boot_wait: %s", err)) + } + } + + return errs +} diff --git a/builder/hyperv/common/run_config_test.go b/builder/hyperv/common/run_config_test.go new file mode 100644 index 00000000000..8068fe625e3 --- /dev/null +++ b/builder/hyperv/common/run_config_test.go @@ -0,0 +1,37 @@ +package common + +import ( + "testing" +) + +func TestRunConfigPrepare_BootWait(t *testing.T) { + var c *RunConfig + var errs []error + + // Test a default boot_wait + c = new(RunConfig) + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %s", errs) + } + + if c.RawBootWait != "10s" { + t.Fatalf("bad value: %s", c.RawBootWait) + } + + // Test with a bad boot_wait + c = new(RunConfig) + c.RawBootWait = "this is not good" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) == 0 { + t.Fatalf("bad: %#v", errs) + } + + // Test with a good one + c = new(RunConfig) + c.RawBootWait = "5s" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("should not have error: %s", errs) + } +} diff --git a/builder/hyperv/common/shutdown_config.go b/builder/hyperv/common/shutdown_config.go new file mode 100644 index 00000000000..83d2224c386 --- /dev/null +++ b/builder/hyperv/common/shutdown_config.go @@ -0,0 +1,30 @@ +package common + +import ( + "fmt" + "time" + + "github.com/mitchellh/packer/template/interpolate" +) + +type ShutdownConfig struct { + ShutdownCommand string `mapstructure:"shutdown_command"` + RawShutdownTimeout string `mapstructure:"shutdown_timeout"` + + ShutdownTimeout time.Duration `` +} + +func (c *ShutdownConfig) Prepare(ctx *interpolate.Context) []error { + if c.RawShutdownTimeout == "" { + c.RawShutdownTimeout = "5m" + } + + var errs []error + var err error + c.ShutdownTimeout, err = time.ParseDuration(c.RawShutdownTimeout) + if err != nil { + errs = append(errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err)) + } + + return errs +} diff --git a/builder/hyperv/common/shutdown_config_test.go b/builder/hyperv/common/shutdown_config_test.go new file mode 100644 index 00000000000..5da613a19a2 --- /dev/null +++ b/builder/hyperv/common/shutdown_config_test.go @@ -0,0 +1,45 @@ +package common + +import ( + "testing" + "time" +) + +func testShutdownConfig() *ShutdownConfig { + return &ShutdownConfig{} +} + +func TestShutdownConfigPrepare_ShutdownCommand(t *testing.T) { + var c *ShutdownConfig + var errs []error + + c = testShutdownConfig() + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("err: %#v", errs) + } +} + +func TestShutdownConfigPrepare_ShutdownTimeout(t *testing.T) { + var c *ShutdownConfig + var errs []error + + // Test with a bad value + c = testShutdownConfig() + c.RawShutdownTimeout = "this is not good" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) == 0 { + t.Fatalf("should have error") + } + + // Test with a good one + c = testShutdownConfig() + c.RawShutdownTimeout = "5s" + errs = c.Prepare(testConfigTemplate(t)) + if len(errs) > 0 { + t.Fatalf("err: %#v", errs) + } + if c.ShutdownTimeout != 5*time.Second { + t.Fatalf("bad: %s", c.ShutdownTimeout) + } +} diff --git a/builder/hyperv/common/ssh.go b/builder/hyperv/common/ssh.go new file mode 100644 index 00000000000..1bb06bfd250 --- /dev/null +++ b/builder/hyperv/common/ssh.go @@ -0,0 +1,49 @@ +package common + +import ( + "github.com/mitchellh/multistep" + commonssh "github.com/mitchellh/packer/common/ssh" + "github.com/mitchellh/packer/communicator/ssh" + gossh "golang.org/x/crypto/ssh" +) + +func CommHost(state multistep.StateBag) (string, error) { + vmName := state.Get("vmName").(string) + driver := state.Get("driver").(Driver) + + mac, err := driver.Mac(vmName) + if err != nil { + return "", err + } + + ip, err := driver.IpAddress(mac) + if err != nil { + return "", err + } + + return ip, nil +} + +func SSHConfigFunc(config *SSHConfig) func(multistep.StateBag) (*gossh.ClientConfig, error) { + return func(state multistep.StateBag) (*gossh.ClientConfig, error) { + auth := []gossh.AuthMethod{ + gossh.Password(config.Comm.SSHPassword), + gossh.KeyboardInteractive( + ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)), + } + + if config.Comm.SSHPrivateKey != "" { + signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey) + if err != nil { + return nil, err + } + + auth = append(auth, gossh.PublicKeys(signer)) + } + + return &gossh.ClientConfig{ + User: config.Comm.SSHUsername, + Auth: auth, + }, nil + } +} diff --git a/builder/hyperv/common/ssh_config.go b/builder/hyperv/common/ssh_config.go new file mode 100644 index 00000000000..bea164b0683 --- /dev/null +++ b/builder/hyperv/common/ssh_config.go @@ -0,0 +1,29 @@ +package common + +import ( + "time" + + "github.com/mitchellh/packer/helper/communicator" + "github.com/mitchellh/packer/template/interpolate" +) + +type SSHConfig struct { + Comm communicator.Config `mapstructure:",squash"` + + // These are deprecated, but we keep them around for BC + // TODO(@mitchellh): remove + SSHKeyPath string `mapstructure:"ssh_key_path"` + SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"` +} + +func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error { + // TODO: backwards compatibility, write fixer instead + if c.SSHKeyPath != "" { + c.Comm.SSHPrivateKey = c.SSHKeyPath + } + if c.SSHWaitTimeout != 0 { + c.Comm.SSHTimeout = c.SSHWaitTimeout + } + + return c.Comm.Prepare(ctx) +} diff --git a/builder/hyperv/common/step_configure_ip.go b/builder/hyperv/common/step_configure_ip.go new file mode 100644 index 00000000000..96ea5300b59 --- /dev/null +++ b/builder/hyperv/common/step_configure_ip.go @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepConfigureIP struct { + IPAddress string + Gateway string + DNSServer string + Subnet string +} + +func (s *StepConfigureIP) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + errorMsg := "Error configuring ip address: %s" + vmName := state.Get("vmName").(string) + + ui.Say("Configuring ip address...") + err := driver.SetVirtualMachineIPNetworkConfiguration(vmName, s.IPAddress, s.Gateway, s.DNSServer, s.Subnet) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + count := 60 + var duration time.Duration = 1 + sleepTime := time.Minute * duration + var ip string + + for count != 0 { + cmdOut, err := driver.GetVirtualMachineNetworkAdapterAddress(vmName) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ip = strings.TrimSpace(string(cmdOut)) + + if ip != "False" { + break + } + + log.Println(fmt.Sprintf("Waiting for another %v minutes...", uint(duration))) + time.Sleep(sleepTime) + count-- + } + + if count == 0 { + err := fmt.Errorf(errorMsg, "IP address assigned to the adapter is empty") + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say("ip address is " + ip) + + hostName, err := driver.GetHostName(ip) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say("hostname is " + hostName) + + state.Put("ip", ip) + state.Put("hostname", hostName) + + return multistep.ActionContinue +} + +func (s *StepConfigureIP) Cleanup(state multistep.StateBag) { + // do nothing +} diff --git a/builder/hyperv/common/step_configure_vlan.go b/builder/hyperv/common/step_configure_vlan.go new file mode 100644 index 00000000000..bfd24b49034 --- /dev/null +++ b/builder/hyperv/common/step_configure_vlan.go @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepConfigureVlan struct { + VlanId string + SwitchVlanId string +} + +func (s *StepConfigureVlan) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + errorMsg := "Error configuring vlan: %s" + vmName := state.Get("vmName").(string) + switchName := state.Get("SwitchName").(string) + vlanId := s.VlanId + switchVlanId := s.SwitchVlanId + + ui.Say("Configuring vlan...") + + if switchVlanId != "" { + err := driver.SetNetworkAdapterVlanId(switchName, vlanId) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + if vlanId != "" { + err := driver.SetVirtualMachineVlanId(vmName, vlanId) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + return multistep.ActionContinue +} + +func (s *StepConfigureVlan) Cleanup(state multistep.StateBag) { + //do nothing +} diff --git a/builder/hyperv/common/step_create_external_switch.go b/builder/hyperv/common/step_create_external_switch.go new file mode 100644 index 00000000000..75249b8b450 --- /dev/null +++ b/builder/hyperv/common/step_create_external_switch.go @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/common/uuid" + "github.com/mitchellh/packer/packer" +) + +// This step creates switch for VM. +// +// Produces: +// SwitchName string - The name of the Switch +type StepCreateExternalSwitch struct { + SwitchName string + oldSwitchName string +} + +func (s *StepCreateExternalSwitch) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + vmName := state.Get("vmName").(string) + errorMsg := "Error createing external switch: %s" + var err error + + ui.Say("Creating external switch...") + + packerExternalSwitchName := "paes_" + uuid.TimeOrderedUUID() + + err = driver.CreateExternalVirtualSwitch(vmName, packerExternalSwitchName) + if err != nil { + err := fmt.Errorf("Error creating switch: %s", err) + state.Put(errorMsg, err) + ui.Error(err.Error()) + s.SwitchName = "" + return multistep.ActionHalt + } + + switchName, err := driver.GetVirtualMachineSwitchName(vmName) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if len(switchName) == 0 { + err := fmt.Errorf(errorMsg, err) + state.Put("error", "Can't get the VM switch name") + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say("External switch name is: '" + switchName + "'") + + if switchName != packerExternalSwitchName { + s.SwitchName = "" + } else { + s.SwitchName = packerExternalSwitchName + s.oldSwitchName = state.Get("SwitchName").(string) + } + + // Set the final name in the state bag so others can use it + state.Put("SwitchName", switchName) + + return multistep.ActionContinue +} + +func (s *StepCreateExternalSwitch) Cleanup(state multistep.StateBag) { + if s.SwitchName == "" { + return + } + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + ui.Say("Unregistering and deleting external switch...") + + var err error = nil + + errMsg := "Error deleting external switch: %s" + + // connect the vm to the old switch + if s.oldSwitchName == "" { + ui.Error(fmt.Sprintf(errMsg, "the old switch name is empty")) + return + } + + err = driver.ConnectVirtualMachineNetworkAdapterToSwitch(vmName, s.oldSwitchName) + if err != nil { + ui.Error(fmt.Sprintf(errMsg, err)) + return + } + + state.Put("SwitchName", s.oldSwitchName) + + err = driver.DeleteVirtualSwitch(s.SwitchName) + if err != nil { + ui.Error(fmt.Sprintf(errMsg, err)) + } +} diff --git a/builder/hyperv/common/step_create_switch.go b/builder/hyperv/common/step_create_switch.go new file mode 100644 index 00000000000..b8eb029626b --- /dev/null +++ b/builder/hyperv/common/step_create_switch.go @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +const ( + SwitchTypeInternal = "Internal" + SwitchTypePrivate = "Private" + DefaultSwitchType = SwitchTypeInternal +) + +// This step creates switch for VM. +// +// Produces: +// SwitchName string - The name of the Switch +type StepCreateSwitch struct { + // Specifies the name of the switch to be created. + SwitchName string + // Specifies the type of the switch to be created. Allowed values are Internal and Private. To create an External + // virtual switch, specify either the NetAdapterInterfaceDescription or the NetAdapterName parameter, which + // implicitly set the type of the virtual switch to External. + SwitchType string + // Specifies the name of the network adapter to be bound to the switch to be created. + NetAdapterName string + // Specifies the interface description of the network adapter to be bound to the switch to be created. + NetAdapterInterfaceDescription string + + createdSwitch bool +} + +func (s *StepCreateSwitch) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + if len(s.SwitchType) == 0 { + s.SwitchType = DefaultSwitchType + } + + ui.Say(fmt.Sprintf("Creating switch '%v' if required...", s.SwitchName)) + + createdSwitch, err := driver.CreateVirtualSwitch(s.SwitchName, s.SwitchType) + if err != nil { + err := fmt.Errorf("Error creating switch: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + s.SwitchName = "" + return multistep.ActionHalt + } + + s.createdSwitch = createdSwitch + + if !s.createdSwitch { + ui.Say(fmt.Sprintf(" switch '%v' already exists. Will not delete on cleanup...", s.SwitchName)) + } + + // Set the final name in the state bag so others can use it + state.Put("SwitchName", s.SwitchName) + + return multistep.ActionContinue +} + +func (s *StepCreateSwitch) Cleanup(state multistep.StateBag) { + if len(s.SwitchName) == 0 || !s.createdSwitch { + return + } + + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + ui.Say("Unregistering and deleting switch...") + + err := driver.DeleteVirtualSwitch(s.SwitchName) + if err != nil { + ui.Error(fmt.Sprintf("Error deleting switch: %s", err)) + } +} diff --git a/builder/hyperv/common/step_create_tempdir.go b/builder/hyperv/common/step_create_tempdir.go new file mode 100644 index 00000000000..c4682a852ac --- /dev/null +++ b/builder/hyperv/common/step_create_tempdir.go @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "io/ioutil" + "os" +) + +type StepCreateTempDir struct { + dirPath string +} + +func (s *StepCreateTempDir) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + ui.Say("Creating temporary directory...") + + tempDir := os.TempDir() + packerTempDir, err := ioutil.TempDir(tempDir, "packerhv") + if err != nil { + err := fmt.Errorf("Error creating temporary directory: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.dirPath = packerTempDir; + state.Put("packerTempDir", packerTempDir) + +// ui.Say("packerTempDir = '" + packerTempDir + "'") + + return multistep.ActionContinue +} + +func (s *StepCreateTempDir) Cleanup(state multistep.StateBag) { + if s.dirPath == "" { + return + } + + ui := state.Get("ui").(packer.Ui) + + ui.Say("Deleting temporary directory...") + + err := os.RemoveAll(s.dirPath) + + if err != nil { + ui.Error(fmt.Sprintf("Error deleting temporary directory: %s", err)) + } +} diff --git a/builder/hyperv/common/step_create_vm.go b/builder/hyperv/common/step_create_vm.go new file mode 100644 index 00000000000..54a5a7103b5 --- /dev/null +++ b/builder/hyperv/common/step_create_vm.go @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// This step creates the actual virtual machine. +// +// Produces: +// VMName string - The name of the VM +type StepCreateVM struct { + VMName string + SwitchName string + RamSizeMB uint + DiskSize uint + Generation uint + Cpu uint + EnableMacSpoofing bool + EnableDynamicMemory bool + EnableSecureBoot bool + EnableVirtualizationExtensions bool +} + +func (s *StepCreateVM) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + ui.Say("Creating virtual machine...") + + path := state.Get("packerTempDir").(string) + + // convert the MB to bytes + ram := int64(s.RamSizeMB * 1024 * 1024) + diskSize := int64(s.DiskSize * 1024 * 1024) + + err := driver.CreateVirtualMachine(s.VMName, path, ram, diskSize, s.SwitchName, s.Generation) + if err != nil { + err := fmt.Errorf("Error creating virtual machine: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + err = driver.SetVirtualMachineCpuCount(s.VMName, s.Cpu) + if err != nil { + err := fmt.Errorf("Error creating setting virtual machine cpu: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if s.EnableDynamicMemory { + err = driver.SetVirtualMachineDynamicMemory(s.VMName, s.EnableDynamicMemory) + if err != nil { + err := fmt.Errorf("Error creating setting virtual machine dynamic memory: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + if s.EnableMacSpoofing { + err = driver.SetVirtualMachineMacSpoofing(s.VMName, s.EnableMacSpoofing) + if err != nil { + err := fmt.Errorf("Error creating setting virtual machine mac spoofing: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + if s.Generation == 2 { + err = driver.SetVirtualMachineSecureBoot(s.VMName, s.EnableSecureBoot) + if err != nil { + err := fmt.Errorf("Error setting secure boot: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + if s.EnableVirtualizationExtensions { + //This is only supported on Windows 10 and Windows Server 2016 onwards + err = driver.SetVirtualMachineVirtualizationExtensions(s.VMName, s.EnableVirtualizationExtensions) + if err != nil { + err := fmt.Errorf("Error creating setting virtual machine virtualization extensions: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + // Set the final name in the state bag so others can use it + state.Put("vmName", s.VMName) + + return multistep.ActionContinue +} + +func (s *StepCreateVM) Cleanup(state multistep.StateBag) { + if s.VMName == "" { + return + } + + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + ui.Say("Unregistering and deleting virtual machine...") + + err := driver.DeleteVirtualMachine(s.VMName) + if err != nil { + ui.Error(fmt.Sprintf("Error deleting virtual machine: %s", err)) + } +} diff --git a/builder/hyperv/common/step_disable_vlan.go b/builder/hyperv/common/step_disable_vlan.go new file mode 100644 index 00000000000..631c941d207 --- /dev/null +++ b/builder/hyperv/common/step_disable_vlan.go @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepDisableVlan struct { +} + +func (s *StepDisableVlan) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + errorMsg := "Error disabling vlan: %s" + vmName := state.Get("vmName").(string) + switchName := state.Get("SwitchName").(string) + + ui.Say("Disabling vlan...") + + err := driver.UntagVirtualMachineNetworkAdapterVlan(vmName, switchName) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepDisableVlan) Cleanup(state multistep.StateBag) { + //do nothing +} diff --git a/builder/hyperv/common/step_enable_integration_service.go b/builder/hyperv/common/step_enable_integration_service.go new file mode 100644 index 00000000000..05b148232e2 --- /dev/null +++ b/builder/hyperv/common/step_enable_integration_service.go @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepEnableIntegrationService struct { + name string +} + +func (s *StepEnableIntegrationService) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + ui.Say("Enabling Integration Service...") + + vmName := state.Get("vmName").(string) + s.name = "Guest Service Interface" + + err := driver.EnableVirtualMachineIntegrationService(vmName, s.name) + + if err != nil { + err := fmt.Errorf("Error enabling Integration Service: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepEnableIntegrationService) Cleanup(state multistep.StateBag) { + // do nothing +} diff --git a/builder/hyperv/common/step_export_vm.go b/builder/hyperv/common/step_export_vm.go new file mode 100644 index 00000000000..2ee10e16c8f --- /dev/null +++ b/builder/hyperv/common/step_export_vm.go @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "io/ioutil" + "path/filepath" +) + +const ( + vhdDir string = "Virtual Hard Disks" + vmDir string = "Virtual Machines" +) + +type StepExportVm struct { + OutputDir string + SkipCompaction bool +} + +func (s *StepExportVm) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + var err error + var errorMsg string + + vmName := state.Get("vmName").(string) + tmpPath := state.Get("packerTempDir").(string) + outputPath := s.OutputDir + + // create temp path to export vm + errorMsg = "Error creating temp export path: %s" + vmExportPath, err := ioutil.TempDir(tmpPath, "export") + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say("Exporting vm...") + + err = driver.ExportVirtualMachine(vmName, vmExportPath) + if err != nil { + errorMsg = "Error exporting vm: %s" + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // copy to output dir + expPath := filepath.Join(vmExportPath, vmName) + + if s.SkipCompaction { + ui.Say("Skipping disk compaction...") + } else { + ui.Say("Compacting disks...") + err = driver.CompactDisks(expPath, vhdDir) + if err != nil { + errorMsg = "Error compacting disks: %s" + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + ui.Say("Coping to output dir...") + err = driver.CopyExportedVirtualMachine(expPath, outputPath, vhdDir, vmDir) + if err != nil { + errorMsg = "Error exporting vm: %s" + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepExportVm) Cleanup(state multistep.StateBag) { + // do nothing +} diff --git a/builder/hyperv/common/step_mount_dvddrive.go b/builder/hyperv/common/step_mount_dvddrive.go new file mode 100644 index 00000000000..cff69627e73 --- /dev/null +++ b/builder/hyperv/common/step_mount_dvddrive.go @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +type StepMountDvdDrive struct { + Generation uint +} + +func (s *StepMountDvdDrive) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + errorMsg := "Error mounting dvd drive: %s" + vmName := state.Get("vmName").(string) + isoPath := state.Get("iso_path").(string) + + // should be able to mount up to 60 additional iso images using SCSI + // but Windows would only allow a max of 22 due to available drive letters + // Will Windows assign DVD drives to A: and B: ? + + // For IDE, there are only 2 controllers (0,1) with 2 locations each (0,1) + + var dvdControllerProperties DvdControllerProperties + controllerNumber, controllerLocation, err := driver.CreateDvdDrive(vmName, isoPath, s.Generation) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + dvdControllerProperties.ControllerNumber = controllerNumber + dvdControllerProperties.ControllerLocation = controllerLocation + dvdControllerProperties.Existing = false + + state.Put("os.dvd.properties", dvdControllerProperties) + + ui.Say(fmt.Sprintf("Setting boot drive to os dvd drive %s ...", isoPath)) + err = driver.SetBootDvdDrive(vmName, controllerNumber, controllerLocation, s.Generation) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf("Mounting os dvd drive %s ...", isoPath)) + err = driver.MountDvdDrive(vmName, isoPath, controllerNumber, controllerLocation) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepMountDvdDrive) Cleanup(state multistep.StateBag) { + dvdControllerState := state.Get("os.dvd.properties") + + if dvdControllerState == nil { + return + } + + dvdController := dvdControllerState.(DvdControllerProperties) + driver := state.Get("driver").(Driver) + vmName := state.Get("vmName").(string) + ui := state.Get("ui").(packer.Ui) + errorMsg := "Error unmounting os dvd drive: %s" + + ui.Say("Clean up os dvd drive...") + + if dvdController.Existing { + err := driver.UnmountDvdDrive(vmName, dvdController.ControllerNumber, dvdController.ControllerLocation) + if err != nil { + err := fmt.Errorf("Error unmounting dvd drive: %s", err) + log.Print(fmt.Sprintf(errorMsg, err)) + } + } else { + err := driver.DeleteDvdDrive(vmName, dvdController.ControllerNumber, dvdController.ControllerLocation) + if err != nil { + err := fmt.Errorf("Error deleting dvd drive: %s", err) + log.Print(fmt.Sprintf(errorMsg, err)) + } + } +} diff --git a/builder/hyperv/common/step_mount_floppydrive.go b/builder/hyperv/common/step_mount_floppydrive.go new file mode 100644 index 00000000000..8e8254083f6 --- /dev/null +++ b/builder/hyperv/common/step_mount_floppydrive.go @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" +) + +const ( + FloppyFileName = "assets.vfd" +) + +type StepMountFloppydrive struct { + Generation uint + floppyPath string +} + +func (s *StepMountFloppydrive) Run(state multistep.StateBag) multistep.StepAction { + if s.Generation > 1 { + return multistep.ActionContinue + } + + driver := state.Get("driver").(Driver) + + // Determine if we even have a floppy disk to attach + var floppyPath string + if floppyPathRaw, ok := state.GetOk("floppy_path"); ok { + floppyPath = floppyPathRaw.(string) + } else { + log.Println("No floppy disk, not attaching.") + return multistep.ActionContinue + } + + // Hyper-V is really dumb and can't figure out the format of the file + // without an extension, so we need to add the "vfd" extension to the + // floppy. + floppyPath, err := s.copyFloppy(floppyPath) + if err != nil { + state.Put("error", fmt.Errorf("Error preparing floppy: %s", err)) + return multistep.ActionHalt + } + + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + ui.Say("Mounting floppy drive...") + + err = driver.MountFloppyDrive(vmName, floppyPath) + if err != nil { + state.Put("error", fmt.Errorf("Error mounting floppy drive: %s", err)) + return multistep.ActionHalt + } + + // Track the path so that we can unregister it from Hyper-V later + s.floppyPath = floppyPath + + return multistep.ActionContinue +} + +func (s *StepMountFloppydrive) Cleanup(state multistep.StateBag) { + if s.Generation > 1 { + return + } + driver := state.Get("driver").(Driver) + if s.floppyPath == "" { + return + } + + errorMsg := "Error unmounting floppy drive: %s" + + vmName := state.Get("vmName").(string) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Cleanup floppy drive...") + + err := driver.UnmountFloppyDrive(vmName) + if err != nil { + log.Print(fmt.Sprintf(errorMsg, err)) + } + + err = os.Remove(s.floppyPath) + + if err != nil { + log.Print(fmt.Sprintf(errorMsg, err)) + } +} + +func (s *StepMountFloppydrive) copyFloppy(path string) (string, error) { + tempdir, err := ioutil.TempDir("", "packer") + if err != nil { + return "", err + } + + floppyPath := filepath.Join(tempdir, "floppy.vfd") + f, err := os.Create(floppyPath) + if err != nil { + return "", err + } + defer f.Close() + + sourceF, err := os.Open(path) + if err != nil { + return "", err + } + defer sourceF.Close() + + log.Printf("Copying floppy to temp location: %s", floppyPath) + if _, err := io.Copy(f, sourceF); err != nil { + return "", err + } + + return floppyPath, nil +} diff --git a/builder/hyperv/common/step_mount_guest_additions.go b/builder/hyperv/common/step_mount_guest_additions.go new file mode 100644 index 00000000000..dff8b90c360 --- /dev/null +++ b/builder/hyperv/common/step_mount_guest_additions.go @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +type StepMountGuestAdditions struct { + GuestAdditionsMode string + GuestAdditionsPath string + Generation uint +} + +func (s *StepMountGuestAdditions) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + if s.GuestAdditionsMode != "attach" { + ui.Say("Skipping mounting Integration Services Setup Disk...") + return multistep.ActionContinue + } + + driver := state.Get("driver").(Driver) + ui.Say("Mounting Integration Services Setup Disk...") + + vmName := state.Get("vmName").(string) + + // should be able to mount up to 60 additional iso images using SCSI + // but Windows would only allow a max of 22 due to available drive letters + // Will Windows assign DVD drives to A: and B: ? + + // For IDE, there are only 2 controllers (0,1) with 2 locations each (0,1) + + var dvdControllerProperties DvdControllerProperties + + controllerNumber, controllerLocation, err := driver.CreateDvdDrive(vmName, s.GuestAdditionsPath, s.Generation) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + dvdControllerProperties.ControllerNumber = controllerNumber + dvdControllerProperties.ControllerLocation = controllerLocation + dvdControllerProperties.Existing = false + state.Put("guest.dvd.properties", dvdControllerProperties) + + ui.Say(fmt.Sprintf("Mounting Integration Services dvd drive %s ...", s.GuestAdditionsPath)) + err = driver.MountDvdDrive(vmName, s.GuestAdditionsPath, controllerNumber, controllerLocation) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + log.Println(fmt.Sprintf("ISO %s mounted on DVD controller %v, location %v", s.GuestAdditionsPath, controllerNumber, controllerLocation)) + + return multistep.ActionContinue +} + +func (s *StepMountGuestAdditions) Cleanup(state multistep.StateBag) { + if s.GuestAdditionsMode != "attach" { + return + } + + dvdControllerState := state.Get("guest.dvd.properties") + + if dvdControllerState == nil { + return + } + + dvdController := dvdControllerState.(DvdControllerProperties) + ui := state.Get("ui").(packer.Ui) + driver := state.Get("driver").(Driver) + vmName := state.Get("vmName").(string) + errorMsg := "Error unmounting Integration Services dvd drive: %s" + + ui.Say("Cleanup Integration Services dvd drive...") + + if dvdController.Existing { + err := driver.UnmountDvdDrive(vmName, dvdController.ControllerNumber, dvdController.ControllerLocation) + if err != nil { + log.Print(fmt.Sprintf(errorMsg, err)) + } + } else { + err := driver.DeleteDvdDrive(vmName, dvdController.ControllerNumber, dvdController.ControllerLocation) + if err != nil { + log.Print(fmt.Sprintf(errorMsg, err)) + } + } +} \ No newline at end of file diff --git a/builder/hyperv/common/step_mount_secondary_dvd_images.go b/builder/hyperv/common/step_mount_secondary_dvd_images.go new file mode 100644 index 00000000000..eb5ce25bc31 --- /dev/null +++ b/builder/hyperv/common/step_mount_secondary_dvd_images.go @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +type StepMountSecondaryDvdImages struct { + IsoPaths []string + Generation uint +} + +type DvdControllerProperties struct { + ControllerNumber uint + ControllerLocation uint + Existing bool +} + +func (s *StepMountSecondaryDvdImages) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + ui.Say("Mounting secondary DVD images...") + + vmName := state.Get("vmName").(string) + + // should be able to mount up to 60 additional iso images using SCSI + // but Windows would only allow a max of 22 due to available drive letters + // Will Windows assign DVD drives to A: and B: ? + + // For IDE, there are only 2 controllers (0,1) with 2 locations each (0,1) + var dvdProperties []DvdControllerProperties + + for _, isoPath := range s.IsoPaths { + var properties DvdControllerProperties + + controllerNumber, controllerLocation, err := driver.CreateDvdDrive(vmName, isoPath, s.Generation) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + properties.ControllerNumber = controllerNumber + properties.ControllerLocation = controllerLocation + properties.Existing = false + dvdProperties = append(dvdProperties, properties) + state.Put("secondary.dvd.properties", dvdProperties) + + ui.Say(fmt.Sprintf("Mounting secondary dvd drive %s ...", isoPath)) + err = driver.MountDvdDrive(vmName, isoPath, controllerNumber, controllerLocation) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + log.Println(fmt.Sprintf("ISO %s mounted on DVD controller %v, location %v", isoPath, controllerNumber, controllerLocation)) + } + + return multistep.ActionContinue +} + +func (s *StepMountSecondaryDvdImages) Cleanup(state multistep.StateBag) { + dvdControllersState := state.Get("secondary.dvd.properties") + + if dvdControllersState == nil { + return + } + + dvdControllers := dvdControllersState.([]DvdControllerProperties) + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + errorMsg := "Error unmounting secondary dvd drive: %s" + + ui.Say("Clean up secondary dvd drives...") + + for _, dvdController := range dvdControllers { + + if dvdController.Existing { + err := driver.UnmountDvdDrive(vmName, dvdController.ControllerNumber, dvdController.ControllerLocation) + if err != nil { + log.Print(fmt.Sprintf(errorMsg, err)) + } + } else { + err := driver.DeleteDvdDrive(vmName, dvdController.ControllerNumber, dvdController.ControllerLocation) + if err != nil { + log.Print(fmt.Sprintf(errorMsg, err)) + } + } + } +} diff --git a/builder/hyperv/common/step_output_dir.go b/builder/hyperv/common/step_output_dir.go new file mode 100644 index 00000000000..602ce265496 --- /dev/null +++ b/builder/hyperv/common/step_output_dir.go @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "log" + "os" + "path/filepath" + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// StepOutputDir sets up the output directory by creating it if it does +// not exist, deleting it if it does exist and we're forcing, and cleaning +// it up when we're done with it. +type StepOutputDir struct { + Force bool + Path string +} + +func (s *StepOutputDir) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + if _, err := os.Stat(s.Path); err == nil && s.Force { + ui.Say("Deleting previous output directory...") + os.RemoveAll(s.Path) + } + + // Create the directory + if err := os.MkdirAll(s.Path, 0755); err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + // Make sure we can write in the directory + f, err := os.Create(filepath.Join(s.Path, "_packer_perm_check")) + if err != nil { + err = fmt.Errorf("Couldn't write to output directory: %s", err) + state.Put("error", err) + return multistep.ActionHalt + } + f.Close() + os.Remove(f.Name()) + + return multistep.ActionContinue +} + +func (s *StepOutputDir) Cleanup(state multistep.StateBag) { + _, cancelled := state.GetOk(multistep.StateCancelled) + _, halted := state.GetOk(multistep.StateHalted) + + if cancelled || halted { + ui := state.Get("ui").(packer.Ui) + + ui.Say("Deleting output directory...") + for i := 0; i < 5; i++ { + err := os.RemoveAll(s.Path) + if err == nil { + break + } + + log.Printf("Error removing output dir: %s", err) + time.Sleep(2 * time.Second) + } + } +} diff --git a/builder/hyperv/common/step_polling_installation.go b/builder/hyperv/common/step_polling_installation.go new file mode 100644 index 00000000000..34577de541f --- /dev/null +++ b/builder/hyperv/common/step_polling_installation.go @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "bytes" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "os/exec" + "strings" + "time" +) + +const port string = "13000" + +type StepPollingInstalation struct { + step int +} + +func (s *StepPollingInstalation) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + errorMsg := "Error polling VM: %s" + vmIp := state.Get("ip").(string) + + ui.Say("Start polling VM to check the installation is complete...") + host := "'" + vmIp + "'," + port + + var blockBuffer bytes.Buffer + blockBuffer.WriteString("Invoke-Command -scriptblock {function foo(){try{$client=New-Object System.Net.Sockets.TcpClient(") + blockBuffer.WriteString(host) + blockBuffer.WriteString(") -ErrorAction SilentlyContinue;if($client -eq $null){return $false}}catch{return $false}return $true} foo}") + + count := 60 + var duration time.Duration = 20 + sleepTime := time.Second * duration + + var res string + + for count > 0 { + log.Println(fmt.Sprintf("Connecting vm (%s)...", host)) + cmd := exec.Command("powershell", blockBuffer.String()) + cmdOut, err := cmd.Output() + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + res = strings.TrimSpace(string(cmdOut)) + + if res != "False" { + ui.Say("Signal was received from the VM") + // Sleep before starting provision + time.Sleep(time.Second * 30) + break + } + + log.Println(fmt.Sprintf("Slipping for more %v seconds...", uint(duration))) + time.Sleep(sleepTime) + count-- + } + + if count == 0 { + err := fmt.Errorf(errorMsg, "a signal from vm was not received in a given time period ") + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say("The installation complete") + + return multistep.ActionContinue +} + +func (s *StepPollingInstalation) Cleanup(state multistep.StateBag) { + +} diff --git a/builder/hyperv/common/step_reboot_vm.go b/builder/hyperv/common/step_reboot_vm.go new file mode 100644 index 00000000000..7be0f3517b2 --- /dev/null +++ b/builder/hyperv/common/step_reboot_vm.go @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "time" +) + +type StepRebootVm struct { +} + +func (s *StepRebootVm) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + errorMsg := "Error rebooting vm: %s" + vmName := state.Get("vmName").(string) + + ui.Say("Rebooting vm...") + + err := driver.RestartVirtualMachine(vmName) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say("Waiting the VM to complete rebooting (2 minutes)...") + + sleepTime := time.Minute * 2 + time.Sleep(sleepTime) + + return multistep.ActionContinue +} + +func (s *StepRebootVm) Cleanup(state multistep.StateBag) { + // do nothing +} diff --git a/builder/hyperv/common/step_run.go b/builder/hyperv/common/step_run.go new file mode 100644 index 00000000000..c0271469202 --- /dev/null +++ b/builder/hyperv/common/step_run.go @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "time" +) + +type StepRun struct { + BootWait time.Duration + Headless bool + + vmName string +} + +func (s *StepRun) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + ui.Say("Starting the virtual machine...") + + err := driver.Start(vmName) + if err != nil { + err := fmt.Errorf("Error starting vm: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.vmName = vmName + + if int64(s.BootWait) > 0 { + ui.Say(fmt.Sprintf("Waiting %s for boot...", s.BootWait)) + wait := time.After(s.BootWait) + WAITLOOP: + for { + select { + case <-wait: + break WAITLOOP + case <-time.After(1 * time.Second): + if _, ok := state.GetOk(multistep.StateCancelled); ok { + return multistep.ActionHalt + } + } + } + } + + return multistep.ActionContinue +} + +func (s *StepRun) Cleanup(state multistep.StateBag) { + if s.vmName == "" { + return + } + + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + if running, _ := driver.IsRunning(s.vmName); running { + if err := driver.Stop(s.vmName); err != nil { + ui.Error(fmt.Sprintf("Error shutting down VM: %s", err)) + } + } +} diff --git a/builder/hyperv/common/step_shutdown.go b/builder/hyperv/common/step_shutdown.go new file mode 100644 index 00000000000..db60a15176b --- /dev/null +++ b/builder/hyperv/common/step_shutdown.go @@ -0,0 +1,104 @@ +package common + +import ( + "bytes" + "errors" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "time" +) + +// This step shuts down the machine. It first attempts to do so gracefully, +// but ultimately forcefully shuts it down if that fails. +// +// Uses: +// communicator packer.Communicator +// dir OutputDir +// driver Driver +// ui packer.Ui +// vmx_path string +// +// Produces: +// +type StepShutdown struct { + Command string + Timeout time.Duration +} + +func (s *StepShutdown) Run(state multistep.StateBag) multistep.StepAction { + + comm := state.Get("communicator").(packer.Communicator) + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + if s.Command != "" { + ui.Say("Gracefully halting virtual machine...") + log.Printf("Executing shutdown command: %s", s.Command) + + var stdout, stderr bytes.Buffer + cmd := &packer.RemoteCmd{ + Command: s.Command, + Stdout: &stdout, + Stderr: &stderr, + } + if err := comm.Start(cmd); err != nil { + err := fmt.Errorf("Failed to send shutdown command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Wait for the command to run + cmd.Wait() + + stderrString := stderr.String() + stdoutString := stdout.String() + + // If the command failed to run, notify the user in some way. + if !(cmd.ExitStatus == 0 || (cmd.ExitStatus == 1 && len(stderrString) == 0)) { + state.Put("error", fmt.Errorf( + "Shutdown command has not successful.\n\nExitStatus: %d\n\nStdout: %s\n\nStderr: %s", + cmd.ExitStatus, stdoutString, stderrString)) + return multistep.ActionHalt + } + + log.Printf("Shutdown stdout: %s", stdoutString) + log.Printf("Shutdown stderr: %s", stderrString) + + // Wait for the machine to actually shut down + log.Printf("Waiting max %s for shutdown to complete", s.Timeout) + shutdownTimer := time.After(s.Timeout) + for { + running, _ := driver.IsRunning(vmName) + if !running { + break + } + + select { + case <-shutdownTimer: + err := errors.New("Timeout while waiting for machine to shut down.") + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + default: + time.Sleep(150 * time.Millisecond) + } + } + } else { + ui.Say("Forcibly halting virtual machine...") + if err := driver.Stop(vmName); err != nil { + err := fmt.Errorf("Error stopping VM: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + log.Println("VM shut down.") + return multistep.ActionContinue +} + +func (s *StepShutdown) Cleanup(state multistep.StateBag) {} diff --git a/builder/hyperv/common/step_sleep.go b/builder/hyperv/common/step_sleep.go new file mode 100644 index 00000000000..2d0f0053a0f --- /dev/null +++ b/builder/hyperv/common/step_sleep.go @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "time" +) + +type StepSleep struct { + Minutes time.Duration + ActionName string +} + +func (s *StepSleep) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + if(len(s.ActionName)>0){ + ui.Say(s.ActionName + "! Waiting for "+ fmt.Sprintf("%v",uint(s.Minutes)) + " minutes to let the action to complete...") + } + time.Sleep(time.Minute*s.Minutes); + + return multistep.ActionContinue +} + +func (s *StepSleep) Cleanup(state multistep.StateBag) { + +} diff --git a/builder/hyperv/common/step_type_boot_command.go b/builder/hyperv/common/step_type_boot_command.go new file mode 100644 index 00000000000..e9a1bc4a44b --- /dev/null +++ b/builder/hyperv/common/step_type_boot_command.go @@ -0,0 +1,288 @@ +package common + +import ( + "fmt" + "log" + "strings" + "unicode" + "unicode/utf8" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/template/interpolate" +) + +type bootCommandTemplateData struct { + HTTPIP string + HTTPPort uint + Name string +} + +// This step "types" the boot command into the VM via the prltype script, built on the +// Parallels Virtualization SDK - Python API. +// +// Uses: +// driver Driver +// http_port int +// ui packer.Ui +// vmName string +// +// Produces: +// +type StepTypeBootCommand struct { + BootCommand []string + SwitchName string + Ctx interpolate.Context +} + +func (s *StepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction { + httpPort := state.Get("http_port").(uint) + ui := state.Get("ui").(packer.Ui) + driver := state.Get("driver").(Driver) + vmName := state.Get("vmName").(string) + + hostIp, err := driver.GetHostAdapterIpAddressForSwitch(s.SwitchName) + + if err != nil { + err := fmt.Errorf("Error getting host adapter ip address: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf("Host IP for the HyperV machine: %s", hostIp)) + + s.Ctx.Data = &bootCommandTemplateData{ + hostIp, + httpPort, + vmName, + } + + ui.Say("Typing the boot command...") + scanCodesToSend := []string{} + + for _, command := range s.BootCommand { + command, err := interpolate.Render(command, &s.Ctx) + + if err != nil { + err := fmt.Errorf("Error preparing boot command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + scanCodesToSend = append(scanCodesToSend, scancodes(command)...) + } + + scanCodesToSendString := strings.Join(scanCodesToSend, " ") + + if err := driver.TypeScanCodes(vmName, scanCodesToSendString); err != nil { + err := fmt.Errorf("Error sending boot command: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (*StepTypeBootCommand) Cleanup(multistep.StateBag) {} + +func scancodes(message string) []string { + // Scancodes reference: http://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html + // + // Scancodes represent raw keyboard output and are fed to the VM by the + // VBoxManage controlvm keyboardputscancode program. + // + // Scancodes are recorded here in pairs. The first entry represents + // the key press and the second entry represents the key release and is + // derived from the first by the addition of 0x80. + special := make(map[string][]string) + special[""] = []string{"0e", "8e"} + special[""] = []string{"53", "d3"} + special[""] = []string{"1c", "9c"} + special[""] = []string{"01", "81"} + special[""] = []string{"3b", "bb"} + special[""] = []string{"3c", "bc"} + special[""] = []string{"3d", "bd"} + special[""] = []string{"3e", "be"} + special[""] = []string{"3f", "bf"} + special[""] = []string{"40", "c0"} + special[""] = []string{"41", "c1"} + special[""] = []string{"42", "c2"} + special[""] = []string{"43", "c3"} + special[""] = []string{"44", "c4"} + special[""] = []string{"1c", "9c"} + special[""] = []string{"0f", "8f"} + special[""] = []string{"48", "c8"} + special[""] = []string{"50", "d0"} + special[""] = []string{"4b", "cb"} + special[""] = []string{"4d", "cd"} + special[""] = []string{"39", "b9"} + special[""] = []string{"52", "d2"} + special[""] = []string{"47", "c7"} + special[""] = []string{"4f", "cf"} + special[""] = []string{"49", "c9"} + special[""] = []string{"51", "d1"} + special[""] = []string{"38", "b8"} + special[""] = []string{"1d", "9d"} + special[""] = []string{"2a", "aa"} + special[""] = []string{"e038", "e0b8"} + special[""] = []string{"e01d", "e09d"} + special[""] = []string{"36", "b6"} + + shiftedChars := "~!@#$%^&*()_+{}|:\"<>?" + + scancodeIndex := make(map[string]uint) + scancodeIndex["1234567890-="] = 0x02 + scancodeIndex["!@#$%^&*()_+"] = 0x02 + scancodeIndex["qwertyuiop[]"] = 0x10 + scancodeIndex["QWERTYUIOP{}"] = 0x10 + scancodeIndex["asdfghjkl;'`"] = 0x1e + scancodeIndex[`ASDFGHJKL:"~`] = 0x1e + scancodeIndex[`\zxcvbnm,./`] = 0x2b + scancodeIndex["|ZXCVBNM<>?"] = 0x2b + scancodeIndex[" "] = 0x39 + + scancodeMap := make(map[rune]uint) + for chars, start := range scancodeIndex { + var i uint = 0 + for len(chars) > 0 { + r, size := utf8.DecodeRuneInString(chars) + chars = chars[size:] + scancodeMap[r] = start + i + i += 1 + } + } + + result := make([]string, 0, len(message)*2) + for len(message) > 0 { + var scancode []string + + if strings.HasPrefix(message, "") { + scancode = []string{"38"} + message = message[len(""):] + log.Printf("Special code '' found, replacing with: 38") + } + + if strings.HasPrefix(message, "") { + scancode = []string{"1d"} + message = message[len(""):] + log.Printf("Special code '' found, replacing with: 1d") + } + + if strings.HasPrefix(message, "") { + scancode = []string{"2a"} + message = message[len(""):] + log.Printf("Special code '' found, replacing with: 2a") + } + + if strings.HasPrefix(message, "") { + scancode = []string{"b8"} + message = message[len(""):] + log.Printf("Special code '' found, replacing with: b8") + } + + if strings.HasPrefix(message, "") { + scancode = []string{"9d"} + message = message[len(""):] + log.Printf("Special code '' found, replacing with: 9d") + } + + if strings.HasPrefix(message, "") { + scancode = []string{"aa"} + message = message[len(""):] + log.Printf("Special code '' found, replacing with: aa") + } + + if strings.HasPrefix(message, "") { + scancode = []string{"e038"} + message = message[len(""):] + log.Printf("Special code '' found, replacing with: e038") + } + + if strings.HasPrefix(message, "") { + scancode = []string{"e01d"} + message = message[len(""):] + log.Printf("Special code '' found, replacing with: e01d") + } + + if strings.HasPrefix(message, "") { + scancode = []string{"36"} + message = message[len(""):] + log.Printf("Special code '' found, replacing with: 36") + } + + if strings.HasPrefix(message, "") { + scancode = []string{"e0b8"} + message = message[len(""):] + log.Printf("Special code '' found, replacing with: e0b8") + } + + if strings.HasPrefix(message, "") { + scancode = []string{"e09d"} + message = message[len(""):] + log.Printf("Special code '' found, replacing with: e09d") + } + + if strings.HasPrefix(message, "") { + scancode = []string{"b6"} + message = message[len(""):] + log.Printf("Special code '' found, replacing with: b6") + } + + if strings.HasPrefix(message, "") { + log.Printf("Special code found, will sleep 1 second at this point.") + scancode = []string{"wait"} + message = message[len(""):] + } + + if strings.HasPrefix(message, "") { + log.Printf("Special code found, will sleep 5 seconds at this point.") + scancode = []string{"wait5"} + message = message[len(""):] + } + + if strings.HasPrefix(message, "") { + log.Printf("Special code found, will sleep 10 seconds at this point.") + scancode = []string{"wait10"} + message = message[len(""):] + } + + if scancode == nil { + for specialCode, specialValue := range special { + if strings.HasPrefix(message, specialCode) { + log.Printf("Special code '%s' found, replacing with: %s", specialCode, specialValue) + scancode = specialValue + message = message[len(specialCode):] + break + } + } + } + + if scancode == nil { + r, size := utf8.DecodeRuneInString(message) + message = message[size:] + scancodeInt := scancodeMap[r] + keyShift := unicode.IsUpper(r) || strings.ContainsRune(shiftedChars, r) + + scancode = make([]string, 0, 4) + if keyShift { + scancode = append(scancode, "2a") + } + + scancode = append(scancode, fmt.Sprintf("%02x", scancodeInt)) + + if keyShift { + scancode = append(scancode, "aa") + } + + scancode = append(scancode, fmt.Sprintf("%02x", scancodeInt+0x80)) + log.Printf("Sending char '%c', code '%v', shift %v", r, scancode, keyShift) + } + + result = append(result, scancode...) + } + + return result +} diff --git a/builder/hyperv/common/step_unmount_dvddrive.go b/builder/hyperv/common/step_unmount_dvddrive.go new file mode 100644 index 00000000000..26f327cc87c --- /dev/null +++ b/builder/hyperv/common/step_unmount_dvddrive.go @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepUnmountDvdDrive struct { +} + +func (s *StepUnmountDvdDrive) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + vmName := state.Get("vmName").(string) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Unmount/delete os dvd drive...") + + dvdControllerState := state.Get("os.dvd.properties") + + if dvdControllerState == nil { + return multistep.ActionContinue + } + + dvdController := dvdControllerState.(DvdControllerProperties) + + if dvdController.Existing { + ui.Say(fmt.Sprintf("Unmounting os dvd drives controller %d location %d ...", dvdController.ControllerNumber, dvdController.ControllerLocation)) + err := driver.UnmountDvdDrive(vmName, dvdController.ControllerNumber, dvdController.ControllerLocation) + if err != nil { + err := fmt.Errorf("Error unmounting os dvd drive: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } else { + ui.Say(fmt.Sprintf("Delete os dvd drives controller %d location %d ...", dvdController.ControllerNumber, dvdController.ControllerLocation)) + err := driver.DeleteDvdDrive(vmName, dvdController.ControllerNumber, dvdController.ControllerLocation) + if err != nil { + err := fmt.Errorf("Error deleting os dvd drive: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + state.Put("os.dvd.properties", nil) + + return multistep.ActionContinue +} + +func (s *StepUnmountDvdDrive) Cleanup(state multistep.StateBag) { +} diff --git a/builder/hyperv/common/step_unmount_floppydrive.go b/builder/hyperv/common/step_unmount_floppydrive.go new file mode 100644 index 00000000000..5ae92073008 --- /dev/null +++ b/builder/hyperv/common/step_unmount_floppydrive.go @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepUnmountFloppyDrive struct { + Generation uint +} + +func (s *StepUnmountFloppyDrive) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + if s.Generation > 1 { + return multistep.ActionContinue + } + + vmName := state.Get("vmName").(string) + ui.Say("Unmount/delete floppy drive (Run)...") + + errorMsg := "Error Unmounting floppy drive: %s" + + err := driver.UnmountFloppyDrive(vmName) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + } + + return multistep.ActionContinue +} + +func (s *StepUnmountFloppyDrive) Cleanup(state multistep.StateBag) { + // do nothing +} diff --git a/builder/hyperv/common/step_unmount_guest_additions.go b/builder/hyperv/common/step_unmount_guest_additions.go new file mode 100644 index 00000000000..56c98791a29 --- /dev/null +++ b/builder/hyperv/common/step_unmount_guest_additions.go @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepUnmountGuestAdditions struct { +} + +func (s *StepUnmountGuestAdditions) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + vmName := state.Get("vmName").(string) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Unmount/delete Integration Services dvd drive...") + + dvdControllerState := state.Get("guest.dvd.properties") + + if dvdControllerState == nil { + return multistep.ActionContinue + } + + dvdController := dvdControllerState.(DvdControllerProperties) + + if dvdController.Existing { + ui.Say(fmt.Sprintf("Unmounting Integration Services dvd drives controller %d location %d ...", dvdController.ControllerNumber, dvdController.ControllerLocation)) + err := driver.UnmountDvdDrive(vmName, dvdController.ControllerNumber, dvdController.ControllerLocation) + if err != nil { + err := fmt.Errorf("Error unmounting Integration Services dvd drive: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } else { + ui.Say(fmt.Sprintf("Delete Integration Services dvd drives controller %d location %d ...", dvdController.ControllerNumber, dvdController.ControllerLocation)) + err := driver.DeleteDvdDrive(vmName, dvdController.ControllerNumber, dvdController.ControllerLocation) + if err != nil { + err := fmt.Errorf("Error deleting Integration Services dvd drive: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + state.Put("guest.dvd.properties", nil) + + return multistep.ActionContinue +} + +func (s *StepUnmountGuestAdditions) Cleanup(state multistep.StateBag) { +} diff --git a/builder/hyperv/common/step_unmount_secondary_dvd_images.go b/builder/hyperv/common/step_unmount_secondary_dvd_images.go new file mode 100644 index 00000000000..7c5b27781c9 --- /dev/null +++ b/builder/hyperv/common/step_unmount_secondary_dvd_images.go @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepUnmountSecondaryDvdImages struct { +} + +func (s *StepUnmountSecondaryDvdImages) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + ui.Say("Unmount/delete secondary dvd drives...") + + dvdControllersState := state.Get("secondary.dvd.properties") + + if dvdControllersState == nil { + return multistep.ActionContinue + } + + dvdControllers := dvdControllersState.([]DvdControllerProperties) + + for _, dvdController := range dvdControllers { + if dvdController.Existing { + ui.Say(fmt.Sprintf("Unmounting secondary dvd drives controller %d location %d ...", dvdController.ControllerNumber, dvdController.ControllerLocation)) + err := driver.UnmountDvdDrive(vmName, dvdController.ControllerNumber, dvdController.ControllerLocation) + if err != nil { + err := fmt.Errorf("Error unmounting secondary dvd drive: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } else { + ui.Say(fmt.Sprintf("Delete secondary dvd drives controller %d location %d ...", dvdController.ControllerNumber, dvdController.ControllerLocation)) + err := driver.DeleteDvdDrive(vmName, dvdController.ControllerNumber, dvdController.ControllerLocation) + if err != nil { + err := fmt.Errorf("Error deleting secondary dvd drive: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + } + + state.Put("secondary.dvd.properties", nil) + + return multistep.ActionContinue +} + +func (s *StepUnmountSecondaryDvdImages) Cleanup(state multistep.StateBag) { +} diff --git a/builder/hyperv/common/step_wait_for_install_to_complete.go b/builder/hyperv/common/step_wait_for_install_to_complete.go new file mode 100644 index 00000000000..46fc7e5cd50 --- /dev/null +++ b/builder/hyperv/common/step_wait_for_install_to_complete.go @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package common + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "time" +) + +const ( + SleepSeconds = 10 +) + +type StepWaitForPowerOff struct { +} + +func (s *StepWaitForPowerOff) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + ui.Say("Waiting for vm to be powered down...") + + for { + isOff, err := driver.IsOff(vmName) + + if err != nil { + err := fmt.Errorf("Error checking if vm is off: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if isOff { + break + } else { + time.Sleep(time.Second * SleepSeconds) + } + } + + return multistep.ActionContinue +} + +func (s *StepWaitForPowerOff) Cleanup(state multistep.StateBag) { +} + +type StepWaitForInstallToComplete struct { + ExpectedRebootCount uint + ActionName string +} + +func (s *StepWaitForInstallToComplete) Run(state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + if len(s.ActionName) > 0 { + ui.Say(fmt.Sprintf("%v ! Waiting for VM to reboot %v times...", s.ActionName, s.ExpectedRebootCount)) + } + + var rebootCount uint + var lastUptime uint64 + + for rebootCount < s.ExpectedRebootCount { + uptime, err := driver.Uptime(vmName) + + if err != nil { + err := fmt.Errorf("Error checking uptime: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if uptime < lastUptime { + rebootCount++ + ui.Say(fmt.Sprintf("%v -> Detected reboot %v after %v seconds...", s.ActionName, rebootCount, lastUptime)) + } + + lastUptime = uptime + + if rebootCount < s.ExpectedRebootCount { + time.Sleep(time.Second * SleepSeconds) + } + } + + return multistep.ActionContinue +} + +func (s *StepWaitForInstallToComplete) Cleanup(state multistep.StateBag) { + +} diff --git a/builder/hyperv/iso/builder.go b/builder/hyperv/iso/builder.go new file mode 100644 index 00000000000..ac390eba102 --- /dev/null +++ b/builder/hyperv/iso/builder.go @@ -0,0 +1,527 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package iso + +import ( + "errors" + "fmt" + "log" + "os" + "strings" + "time" + + "github.com/mitchellh/multistep" + hypervcommon "github.com/mitchellh/packer/builder/hyperv/common" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/helper/communicator" + "github.com/mitchellh/packer/helper/config" + "github.com/mitchellh/packer/packer" + powershell "github.com/mitchellh/packer/powershell" + "github.com/mitchellh/packer/powershell/hyperv" + "github.com/mitchellh/packer/template/interpolate" +) + +const ( + DefaultDiskSize = 40 * 1024 // ~40GB + MinDiskSize = 256 // 256MB + MaxDiskSize = 64 * 1024 * 1024 // 64TB + + DefaultRamSize = 1 * 1024 // 1GB + MinRamSize = 32 // 32MB + MaxRamSize = 32 * 1024 // 32GB + MinNestedVirtualizationRamSize = 4 * 1024 // 4GB + + LowRam = 256 // 256MB + + DefaultUsername = "vagrant" + DefaultPassword = "vagrant" +) + +// 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"` + hypervcommon.FloppyConfig `mapstructure:",squash"` + hypervcommon.OutputConfig `mapstructure:",squash"` + hypervcommon.SSHConfig `mapstructure:",squash"` + hypervcommon.RunConfig `mapstructure:",squash"` + hypervcommon.ShutdownConfig `mapstructure:",squash"` + + // The size, in megabytes, of the hard disk to create for the VM. + // By default, this is 130048 (about 127 GB). + DiskSize uint `mapstructure:"disk_size"` + // The size, in megabytes, of the computer memory in the VM. + // By default, this is 1024 (about 1 GB). + RamSizeMB uint `mapstructure:"ram_size_mb"` + // A list of files to place onto a floppy disk that is attached when the + // VM is booted. This is most useful for unattended Windows installs, + // which look for an Autounattend.xml file on removable media. By default, + // no floppy will be attached. All files listed in this setting get + // placed into the root directory of the floppy and the floppy is attached + // as the first floppy device. Currently, no support exists for creating + // sub-directories on the floppy. Wildcard characters (*, ?, and []) + // are allowed. Directory names are also allowed, which will add all + // the files found in the directory to the floppy. + FloppyFiles []string `mapstructure:"floppy_files"` + // + SecondaryDvdImages []string `mapstructure:"secondary_iso_images"` + + // Should integration services iso be mounted + GuestAdditionsMode string `mapstructure:"guest_additions_mode"` + + // The path to the integration services iso + GuestAdditionsPath string `mapstructure:"guest_additions_path"` + + // This is the name of the new virtual machine. + // By default this is "packer-BUILDNAME", where "BUILDNAME" is the name of the build. + VMName string `mapstructure:"vm_name"` + + BootCommand []string `mapstructure:"boot_command"` + SwitchName string `mapstructure:"switch_name"` + SwitchVlanId string `mapstructure:"switch_vlan_id"` + VlanId string `mapstructure:"vlan_id"` + Cpu uint `mapstructure:"cpu"` + Generation uint `mapstructure:"generation"` + EnableMacSpoofing bool `mapstructure:"enable_mac_spoofing"` + EnableDynamicMemory bool `mapstructure:"enable_dynamic_memory"` + EnableSecureBoot bool `mapstructure:"enable_secure_boot"` + EnableVirtualizationExtensions bool `mapstructure:"enable_virtualization_extensions"` + + Communicator string `mapstructure:"communicator"` + + // The time in seconds to wait for the virtual machine to report an IP address. + // This defaults to 120 seconds. This may have to be increased if your VM takes longer to boot. + IPAddressTimeout time.Duration `mapstructure:"ip_address_timeout"` + + SSHWaitTimeout time.Duration + + SkipCompaction bool `mapstructure:"skip_compaction"` + + ctx interpolate.Context +} + +// Prepare processes the build configuration parameters. +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + err := config.Decode(&b.config, &config.DecodeOpts{ + Interpolate: true, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "boot_command", + }, + }, + }, raws...) + if err != nil { + return nil, err + } + + // Accumulate any errors and warnings + var errs *packer.MultiError + warnings := make([]string, 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.FloppyConfig.Prepare(&b.config.ctx)...) + errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...) + errs = packer.MultiErrorAppend(errs, b.config.RunConfig.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)...) + + err = b.checkDiskSize() + if err != nil { + errs = packer.MultiErrorAppend(errs, err) + } + + err = b.checkRamSize() + if err != nil { + errs = packer.MultiErrorAppend(errs, err) + } + + if b.config.VMName == "" { + b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) + } + + log.Println(fmt.Sprintf("%s: %v", "VMName", b.config.VMName)) + + if b.config.SwitchName == "" { + b.config.SwitchName = b.detectSwitchName() + } + + if b.config.Cpu < 1 { + b.config.Cpu = 1 + } + + if b.config.Generation != 2 { + b.config.Generation = 1 + } + + if b.config.Generation == 2 { + if len(b.config.FloppyFiles) > 0 { + err = errors.New("Generation 2 vms don't support floppy drives. Use ISO image instead.") + errs = packer.MultiErrorAppend(errs, err) + } + } + + log.Println(fmt.Sprintf("Using switch %s", b.config.SwitchName)) + log.Println(fmt.Sprintf("%s: %v", "SwitchName", b.config.SwitchName)) + + if b.config.Communicator == "" { + b.config.Communicator = "ssh" + } else if b.config.Communicator == "ssh" || b.config.Communicator == "winrm" { + // good + } else { + err = errors.New("communicator must be either ssh or winrm") + errs = packer.MultiErrorAppend(errs, err) + } + + log.Println(fmt.Sprintf("%s: %v", "Communicator", b.config.Communicator)) + + // Errors + if b.config.GuestAdditionsMode == "" { + b.config.GuestAdditionsMode = "attach" + } + + if b.config.GuestAdditionsPath == "" { + b.config.GuestAdditionsPath = os.Getenv("WINDIR") + "\\system32\\vmguest.iso" + } + + for _, isoPath := range b.config.SecondaryDvdImages { + if _, err := os.Stat(isoPath); os.IsNotExist(err) { + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Secondary Dvd image does not exist: %s", err)) + } + } + } + + numberOfIsos := len(b.config.SecondaryDvdImages) + + if b.config.GuestAdditionsMode == "attach" { + if _, err := os.Stat(b.config.GuestAdditionsPath); os.IsNotExist(err) { + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Guest additions iso does not exist: %s", err)) + } + } + + numberOfIsos = numberOfIsos + 1 + } + + if b.config.Generation < 2 && numberOfIsos > 2 { + if b.config.GuestAdditionsMode == "attach" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("There are only 2 ide controllers available, so we can't support guest additions and these secondary dvds: %s", strings.Join(b.config.SecondaryDvdImages, ", "))) + } else { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("There are only 2 ide controllers available, so we can't support these secondary dvds: %s", strings.Join(b.config.SecondaryDvdImages, ", "))) + } + } else if b.config.Generation > 1 && len(b.config.SecondaryDvdImages) > 16 { + if b.config.GuestAdditionsMode == "attach" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("There are not enough drive letters available for scsi (limited to 16), so we can't support guest additions and these secondary dvds: %s", strings.Join(b.config.SecondaryDvdImages, ", "))) + } else { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("There are not enough drive letters available for scsi (limited to 16), so we can't support these secondary dvds: %s", strings.Join(b.config.SecondaryDvdImages, ", "))) + } + } + + if b.config.EnableVirtualizationExtensions { + hasVirtualMachineVirtualizationExtensions, err := powershell.HasVirtualMachineVirtualizationExtensions() + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Failed detecting virtual machine virtualization extensions support: %s", err)) + } else { + if !hasVirtualMachineVirtualizationExtensions { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("This version of Hyper-V does not support virtual machine virtualization extension. Please use Windows 10 or Windows Server 2016 or newer.")) + } + } + } + + // Warnings + + if b.config.ShutdownCommand == "" { + warnings = append(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.") + } + + warning := b.checkHostAvailableMemory() + if warning != "" { + warnings = appendWarnings(warnings, warning) + } + + if b.config.EnableVirtualizationExtensions { + if b.config.EnableDynamicMemory { + warning = fmt.Sprintf("For nested virtualization, when virtualization extension is enabled, dynamic memory should not be allowed.") + warnings = appendWarnings(warnings, warning) + } + + if !b.config.EnableMacSpoofing { + warning = fmt.Sprintf("For nested virtualization, when virtualization extension is enabled, mac spoofing should be allowed.") + warnings = appendWarnings(warnings, warning) + } + + if b.config.RamSizeMB < MinNestedVirtualizationRamSize { + warning = fmt.Sprintf("For nested virtualization, when virtualization extension is enabled, there should be 4GB or more memory set for the vm, otherwise Hyper-V may fail to start any nested VMs.") + warnings = appendWarnings(warnings, warning) + } + } + + if b.config.SwitchVlanId != "" { + if b.config.SwitchVlanId != b.config.VlanId { + warning = fmt.Sprintf("Switch network adaptor vlan should match virtual machine network adaptor vlan. The switch will not be able to see traffic from the VM.") + warnings = appendWarnings(warnings, warning) + } + } + + if errs != nil && len(errs.Errors) > 0 { + return warnings, errs + } + + return warnings, nil +} + +// Run executes a Packer build and returns a packer.Artifact representing +// a Hyperv appliance. +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (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("cache", cache) + state.Put("config", &b.config) + state.Put("debug", b.config.PackerDebug) + state.Put("driver", driver) + state.Put("hook", hook) + state.Put("ui", ui) + + steps := []multistep.Step{ + &hypervcommon.StepCreateTempDir{}, + &hypervcommon.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: "iso", + TargetPath: b.config.TargetPath, + }, + &common.StepCreateFloppy{ + Files: b.config.FloppyFiles, + }, + &common.StepHTTPServer{ + HTTPDir: b.config.HTTPDir, + HTTPPortMin: b.config.HTTPPortMin, + HTTPPortMax: b.config.HTTPPortMax, + }, + &hypervcommon.StepCreateSwitch{ + SwitchName: b.config.SwitchName, + }, + &hypervcommon.StepCreateVM{ + VMName: b.config.VMName, + SwitchName: b.config.SwitchName, + RamSizeMB: b.config.RamSizeMB, + DiskSize: b.config.DiskSize, + Generation: b.config.Generation, + Cpu: b.config.Cpu, + EnableMacSpoofing: b.config.EnableMacSpoofing, + EnableDynamicMemory: b.config.EnableDynamicMemory, + EnableSecureBoot: b.config.EnableSecureBoot, + EnableVirtualizationExtensions: b.config.EnableVirtualizationExtensions, + }, + &hypervcommon.StepEnableIntegrationService{}, + + &hypervcommon.StepMountDvdDrive{ + Generation: b.config.Generation, + }, + &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.StepRun{ + BootWait: b.config.BootWait, + Headless: b.config.Headless, + }, + + &hypervcommon.StepTypeBootCommand{ + BootCommand: b.config.BootCommand, + SwitchName: b.config.SwitchName, + Ctx: b.config.ctx, + }, + + // configure the communicator ssh, winrm + &communicator.StepConnect{ + Config: &b.config.SSHConfig.Comm, + Host: hypervcommon.CommHost, + SSHConfig: hypervcommon.SSHConfigFunc(&b.config.SSHConfig), + }, + + // provision requires communicator to be setup + &common.StepProvision{}, + + &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.StepExportVm{ + OutputDir: b.config.OutputDir, + SkipCompaction: b.config.SkipCompaction, + }, + + // the clean up actions for each step will be executed reverse order + } + + // Run the steps. + if b.config.PackerDebug { + pauseFn := common.MultistepDebugFn(ui) + state.Put("pauseFn", pauseFn) + b.runner = &multistep.DebugRunner{ + Steps: steps, + PauseFn: pauseFn, + } + } else { + b.runner = &multistep.BasicRunner{Steps: steps} + } + + b.runner.Run(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.") + } + + return hypervcommon.NewArtifact(b.config.OutputDir) +} + +// Cancel. +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} + +func appendWarnings(slice []string, data ...string) []string { + m := len(slice) + n := m + len(data) + if n > cap(slice) { // if necessary, reallocate + // allocate double what's needed, for future growth. + newSlice := make([]string, (n+1)*2) + copy(newSlice, slice) + slice = newSlice + } + slice = slice[0:n] + copy(slice[m:n], data) + return slice +} + +func (b *Builder) checkDiskSize() error { + if b.config.DiskSize == 0 { + b.config.DiskSize = DefaultDiskSize + } + + log.Println(fmt.Sprintf("%s: %v", "DiskSize", b.config.DiskSize)) + + if b.config.DiskSize < MinDiskSize { + return fmt.Errorf("disk_size_gb: Virtual machine requires disk space >= %v GB, but defined: %v", MinDiskSize, b.config.DiskSize/1024) + } else if b.config.DiskSize > MaxDiskSize { + return fmt.Errorf("disk_size_gb: Virtual machine requires disk space <= %v GB, but defined: %v", MaxDiskSize, b.config.DiskSize/1024) + } + + return nil +} + +func (b *Builder) checkRamSize() error { + if b.config.RamSizeMB == 0 { + b.config.RamSizeMB = DefaultRamSize + } + + log.Println(fmt.Sprintf("%s: %v", "RamSize", b.config.RamSizeMB)) + + if b.config.RamSizeMB < MinRamSize { + return fmt.Errorf("ram_size_mb: Virtual machine requires memory size >= %v MB, but defined: %v", MinRamSize, b.config.RamSizeMB) + } else if b.config.RamSizeMB > MaxRamSize { + return fmt.Errorf("ram_size_mb: Virtual machine requires memory size <= %v MB, but defined: %v", MaxRamSize, b.config.RamSizeMB) + } + + return nil +} + +func (b *Builder) checkHostAvailableMemory() string { + powershellAvailable, _, _ := powershell.IsPowershellAvailable() + + if powershellAvailable { + freeMB := powershell.GetHostAvailableMemory() + + if (freeMB - float64(b.config.RamSizeMB)) < LowRam { + return fmt.Sprintf("Hyper-V might fail to create a VM if there is not enough free memory in the system.") + } + } + + return "" +} + +func (b *Builder) detectSwitchName() string { + powershellAvailable, _, _ := powershell.IsPowershellAvailable() + + if powershellAvailable { + // no switch name, try to get one attached to a online network adapter + onlineSwitchName, err := hyperv.GetExternalOnlineVirtualSwitch() + if onlineSwitchName != "" && err == nil { + return onlineSwitchName + } + } + + return fmt.Sprintf("packer-%s", b.config.PackerBuildName) +} diff --git a/builder/hyperv/iso/builder_test.go b/builder/hyperv/iso/builder_test.go new file mode 100644 index 00000000000..4d8f193a036 --- /dev/null +++ b/builder/hyperv/iso/builder_test.go @@ -0,0 +1,250 @@ +package iso + +import ( + "reflect" + "testing" + + "github.com/mitchellh/packer/packer" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "iso_checksum": "foo", + "iso_checksum_type": "md5", + "iso_url": "http://www.packer.io", + "shutdown_command": "yes", + "ssh_username": "foo", + "ram_size_mb": 64, + "disk_size": 256, + "guest_additions_mode": "none", + packer.BuildNameConfigKey: "foo", + } +} + +func TestBuilder_ImplementsBuilder(t *testing.T) { + var raw interface{} + raw = &Builder{} + if _, ok := raw.(packer.Builder); !ok { + t.Error("Builder must implement builder.") + } +} + +func TestBuilderPrepare_Defaults(t *testing.T) { + var b Builder + config := testConfig() + + 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.VMName != "packer-foo" { + t.Errorf("bad vm name: %s", b.config.VMName) + } +} + +func TestBuilderPrepare_DiskSize(t *testing.T) { + var b Builder + config := testConfig() + + delete(config, "disk_size") + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("bad err: %s", err) + } + + if b.config.DiskSize != 40*1024 { + t.Fatalf("bad size: %d", b.config.DiskSize) + } + + config["disk_size"] = 256 + 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.DiskSize != 256 { + t.Fatalf("bad size: %d", b.config.DiskSize) + } +} + +func TestBuilderPrepare_InvalidKey(t *testing.T) { + var b Builder + config := testConfig() + + // Add a random key + config["i_should_not_be_valid"] = true + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } +} + +func TestBuilderPrepare_ISOChecksum(t *testing.T) { + var b Builder + config := testConfig() + + // Test bad + config["iso_checksum"] = "" + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test good + config["iso_checksum"] = "FOo" + 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.ISOChecksum != "foo" { + t.Fatalf("should've lowercased: %s", b.config.ISOChecksum) + } +} + +func TestBuilderPrepare_ISOChecksumType(t *testing.T) { + var b Builder + config := testConfig() + + // Test bad + config["iso_checksum_type"] = "" + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test good + config["iso_checksum_type"] = "mD5" + 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.ISOChecksumType != "md5" { + t.Fatalf("should've lowercased: %s", b.config.ISOChecksumType) + } + + // Test unknown + config["iso_checksum_type"] = "fake" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test none + config["iso_checksum_type"] = "none" + 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.ISOChecksumType != "none" { + t.Fatalf("should've lowercased: %s", b.config.ISOChecksumType) + } +} + +func TestBuilderPrepare_ISOUrl(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "iso_url") + delete(config, "iso_urls") + + // Test both epty + config["iso_url"] = "" + b = Builder{} + warns, err := b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test iso_url set + config["iso_url"] = "http://www.packer.io" + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Errorf("should not have error: %s", err) + } + + expected := []string{"http://www.packer.io"} + if !reflect.DeepEqual(b.config.ISOUrls, expected) { + t.Fatalf("bad: %#v", b.config.ISOUrls) + } + + // Test both set + config["iso_url"] = "http://www.packer.io" + config["iso_urls"] = []string{"http://www.packer.io"} + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should have error") + } + + // Test just iso_urls set + delete(config, "iso_url") + config["iso_urls"] = []string{ + "http://www.packer.io", + "http://www.hashicorp.com", + } + + b = Builder{} + warns, err = b.Prepare(config) + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Errorf("should not have error: %s", err) + } + + expected = []string{ + "http://www.packer.io", + "http://www.hashicorp.com", + } + if !reflect.DeepEqual(b.config.ISOUrls, expected) { + t.Fatalf("bad: %#v", b.config.ISOUrls) + } +} diff --git a/builder/parallels/common/output_config.go b/builder/parallels/common/output_config.go index 2b7c7c94c3f..00370d372f1 100644 --- a/builder/parallels/common/output_config.go +++ b/builder/parallels/common/output_config.go @@ -3,7 +3,7 @@ package common import ( "fmt" "os" - "path" + "path/filepath" "github.com/mitchellh/packer/common" "github.com/mitchellh/packer/template/interpolate" @@ -19,18 +19,21 @@ func (c *OutputConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig } var errs []error + fmt.Println("OutputDirBefore=", c.OutputDir) - if path.IsAbs(c.OutputDir) { - c.OutputDir = path.Clean(c.OutputDir) - } else { - wd, err := os.Getwd() + if !filepath.IsAbs(c.OutputDir) { + outputDir, err := filepath.Abs(c.OutputDir) if err != nil { errs = append(errs, err) + return errs + } else { + c.OutputDir = outputDir } - c.OutputDir = path.Clean(path.Join(wd, c.OutputDir)) } if !pc.PackerForce { + fmt.Println("OutputDirAfter=", c.OutputDir) + if _, err := os.Stat(c.OutputDir); err == nil { errs = append(errs, fmt.Errorf( "Output directory '%s' already exists. It must not exist.", c.OutputDir)) diff --git a/builder/virtualbox/common/ssh.go b/builder/virtualbox/common/ssh.go index b4a05c54987..a2bf1bfda1a 100644 --- a/builder/virtualbox/common/ssh.go +++ b/builder/virtualbox/common/ssh.go @@ -18,6 +18,11 @@ func SSHPort(state multistep.StateBag) (int, error) { return int(sshHostPort), nil } +func WinRMPort(state multistep.StateBag) (int, error) { + winRMHostPort := state.Get("sshHostPort").(uint) + return int(winRMHostPort), nil +} + func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConfig, error) { return func(state multistep.StateBag) (*gossh.ClientConfig, error) { auth := []gossh.AuthMethod{ diff --git a/builder/virtualbox/common/step_export.go b/builder/virtualbox/common/step_export.go index 26f587875b6..f6b97369c39 100644 --- a/builder/virtualbox/common/step_export.go +++ b/builder/virtualbox/common/step_export.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "time" + "os" ) // This step cleans up forwarded ports and exports the VM to an OVF. @@ -68,6 +69,28 @@ func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionHalt } + //Drop VDI if there is a VMDK + format := "VMDK" + path := filepath.Join(s.OutputDir, fmt.Sprintf("%s.%s", vmName, strings.ToLower(format))) + + if _, err := os.Stat(path); err == nil { + format = "VDI" + path = filepath.Join(s.OutputDir, fmt.Sprintf("%s.%s", vmName, strings.ToLower(format))) + + if _, err := os.Stat(path); err == nil { + ui.Say(fmt.Sprintf("Removing VDI as VMDK exists... %s", path)) + + err := os.Remove(path) + + if err != nil { + err := fmt.Errorf("Error deleting VDI: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + } + state.Put("exportPath", outputPath) return multistep.ActionContinue diff --git a/builder/virtualbox/common/step_forward_ssh.go b/builder/virtualbox/common/step_forward_ssh.go index 4d77449cc07..aa1680fe896 100644 --- a/builder/virtualbox/common/step_forward_ssh.go +++ b/builder/virtualbox/common/step_forward_ssh.go @@ -33,23 +33,23 @@ func (s *StepForwardSSH) Run(state multistep.StateBag) multistep.StepAction { vmName := state.Get("vmName").(string) guestPort := s.CommConfig.Port() - sshHostPort := guestPort + sshHostPort := uint(guestPort) if !s.SkipNatMapping { log.Printf("Looking for available communicator (SSH, WinRM, etc) port between %d and %d", s.HostPortMin, s.HostPortMax) - offset := 0 + offset := uint(0) portRange := int(s.HostPortMax - s.HostPortMin) if portRange > 0 { // Have to check if > 0 to avoid a panic - offset = rand.Intn(portRange) + offset = uint(rand.Intn(portRange)) } for { - sshHostPort = offset + int(s.HostPortMin) - if sshHostPort >= int(s.HostPortMax) { + sshHostPort = offset + s.HostPortMin + if sshHostPort >= s.HostPortMax { offset = 0 - sshHostPort = int(s.HostPortMin) + sshHostPort = s.HostPortMin } log.Printf("Trying port: %d", sshHostPort) l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort)) diff --git a/builder/virtualbox/common/step_type_boot_command.go b/builder/virtualbox/common/step_type_boot_command.go index ab9cd5c7054..3cab20fa461 100644 --- a/builder/virtualbox/common/step_type_boot_command.go +++ b/builder/virtualbox/common/step_type_boot_command.go @@ -33,7 +33,6 @@ type bootCommandTemplateData struct { // type StepTypeBootCommand struct { BootCommand []string - VMName string Ctx interpolate.Context } @@ -52,7 +51,7 @@ func (s *StepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction s.Ctx.Data = &bootCommandTemplateData{ "10.0.2.2", httpPort, - s.VMName, + vmName, } ui.Say("Typing the boot command...") diff --git a/builder/virtualbox/iso/builder.go b/builder/virtualbox/iso/builder.go index a69ad83eac2..1a43975b0ff 100644 --- a/builder/virtualbox/iso/builder.go +++ b/builder/virtualbox/iso/builder.go @@ -202,7 +202,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe HTTPPortMax: b.config.HTTPPortMax, }, new(vboxcommon.StepSuppressMessages), - new(stepCreateVM), + &StepCreateVM{ + VMName : b.config.VMName, + Ctx : b.config.ctx, + }, new(stepCreateDisk), new(stepAttachISO), &vboxcommon.StepAttachGuestAdditions{ @@ -230,7 +233,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe }, &vboxcommon.StepTypeBootCommand{ BootCommand: b.config.BootCommand, - VMName: b.config.VMName, Ctx: b.config.ctx, }, &communicator.StepConnect{ @@ -238,6 +240,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe Host: vboxcommon.CommHost(b.config.SSHConfig.Comm.SSHHost), SSHConfig: vboxcommon.SSHConfigFunc(b.config.SSHConfig), SSHPort: vboxcommon.SSHPort, + WinRMPort: vboxcommon.WinRMPort, }, &vboxcommon.StepUploadVersion{ Path: b.config.VBoxVersionFile, diff --git a/builder/virtualbox/iso/step_create_disk.go b/builder/virtualbox/iso/step_create_disk.go index 22b171f44f9..87015b8468f 100644 --- a/builder/virtualbox/iso/step_create_disk.go +++ b/builder/virtualbox/iso/step_create_disk.go @@ -21,7 +21,7 @@ func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction { vmName := state.Get("vmName").(string) format := "VDI" - path := filepath.Join(config.OutputDir, fmt.Sprintf("%s.%s", config.VMName, strings.ToLower(format))) + path := filepath.Join(config.OutputDir, fmt.Sprintf("%s.%s", vmName, strings.ToLower(format))) command := []string{ "createhd", diff --git a/builder/virtualbox/iso/step_create_vm.go b/builder/virtualbox/iso/step_create_vm.go index adee364113c..c7081d8c799 100644 --- a/builder/virtualbox/iso/step_create_vm.go +++ b/builder/virtualbox/iso/step_create_vm.go @@ -6,23 +6,31 @@ import ( vboxcommon "github.com/mitchellh/packer/builder/virtualbox/common" "github.com/mitchellh/packer/packer" "time" + "github.com/mitchellh/packer/template/interpolate" ) // This step creates the actual virtual machine. // // Produces: -// vmName string - The name of the VM -type stepCreateVM struct { - vmName string +// VMName string - The name of the VM +type StepCreateVM struct { + VMName string + Ctx interpolate.Context } -func (s *stepCreateVM) Run(state multistep.StateBag) multistep.StepAction { +func (s *StepCreateVM) Run(state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) driver := state.Get("driver").(vboxcommon.Driver) ui := state.Get("ui").(packer.Ui) - name := config.VMName - + name, err := interpolate.Render(s.VMName, &s.Ctx) + if err != nil { + err := fmt.Errorf("Error preparing vm name: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + commands := make([][]string, 4) commands[0] = []string{ "createvm", "--name", name, @@ -46,19 +54,19 @@ func (s *stepCreateVM) Run(state multistep.StateBag) multistep.StepAction { } // Set the VM name property on the first command - if s.vmName == "" { - s.vmName = name + if s.VMName == "" { + s.VMName = name } } // Set the final name in the state bag so others can use it - state.Put("vmName", s.vmName) + state.Put("vmName", name) return multistep.ActionContinue } -func (s *stepCreateVM) Cleanup(state multistep.StateBag) { - if s.vmName == "" { +func (s *StepCreateVM) Cleanup(state multistep.StateBag) { + if s.VMName == "" { return } @@ -68,7 +76,7 @@ func (s *stepCreateVM) Cleanup(state multistep.StateBag) { ui.Say("Unregistering and deleting virtual machine...") var err error = nil for i := 0; i < 5; i++ { - err = driver.VBoxManage("unregistervm", s.vmName, "--delete") + err = driver.VBoxManage("unregistervm", s.VMName, "--delete") if err == nil { break } diff --git a/builder/virtualbox/ovf/builder.go b/builder/virtualbox/ovf/builder.go index 56e349b8304..397a35ace3d 100644 --- a/builder/virtualbox/ovf/builder.go +++ b/builder/virtualbox/ovf/builder.go @@ -99,7 +99,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe }, &vboxcommon.StepTypeBootCommand{ BootCommand: b.config.BootCommand, - VMName: b.config.VMName, Ctx: b.config.ctx, }, &communicator.StepConnect{ diff --git a/command/plugin.go b/command/plugin.go index 4def4582b47..31a78329652 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -15,50 +15,50 @@ import ( amazonchrootbuilder "github.com/mitchellh/packer/builder/amazon/chroot" amazonebsbuilder "github.com/mitchellh/packer/builder/amazon/ebs" - amazonimportpostprocessor "github.com/mitchellh/packer/post-processor/amazon-import" amazoninstancebuilder "github.com/mitchellh/packer/builder/amazon/instance" - ansiblelocalprovisioner "github.com/mitchellh/packer/provisioner/ansible-local" - ansibleprovisioner "github.com/mitchellh/packer/provisioner/ansible" - artificepostprocessor "github.com/mitchellh/packer/post-processor/artifice" - atlaspostprocessor "github.com/mitchellh/packer/post-processor/atlas" azurearmbuilder "github.com/mitchellh/packer/builder/azure/arm" - checksumpostprocessor "github.com/mitchellh/packer/post-processor/checksum" - chefclientprovisioner "github.com/mitchellh/packer/provisioner/chef-client" - chefsoloprovisioner "github.com/mitchellh/packer/provisioner/chef-solo" - compresspostprocessor "github.com/mitchellh/packer/post-processor/compress" digitaloceanbuilder "github.com/mitchellh/packer/builder/digitalocean" dockerbuilder "github.com/mitchellh/packer/builder/docker" - dockerimportpostprocessor "github.com/mitchellh/packer/post-processor/docker-import" - dockerpushpostprocessor "github.com/mitchellh/packer/post-processor/docker-push" - dockersavepostprocessor "github.com/mitchellh/packer/post-processor/docker-save" - dockertagpostprocessor "github.com/mitchellh/packer/post-processor/docker-tag" filebuilder "github.com/mitchellh/packer/builder/file" - fileprovisioner "github.com/mitchellh/packer/provisioner/file" googlecomputebuilder "github.com/mitchellh/packer/builder/googlecompute" - googlecomputeexportpostprocessor "github.com/mitchellh/packer/post-processor/googlecompute-export" - manifestpostprocessor "github.com/mitchellh/packer/post-processor/manifest" + hypervbuilder "github.com/mitchellh/packer/builder/hyperv/iso" nullbuilder "github.com/mitchellh/packer/builder/null" openstackbuilder "github.com/mitchellh/packer/builder/openstack" parallelsisobuilder "github.com/mitchellh/packer/builder/parallels/iso" parallelspvmbuilder "github.com/mitchellh/packer/builder/parallels/pvm" - powershellprovisioner "github.com/mitchellh/packer/provisioner/powershell" - puppetmasterlessprovisioner "github.com/mitchellh/packer/provisioner/puppet-masterless" - puppetserverprovisioner "github.com/mitchellh/packer/provisioner/puppet-server" qemubuilder "github.com/mitchellh/packer/builder/qemu" - saltmasterlessprovisioner "github.com/mitchellh/packer/provisioner/salt-masterless" - shelllocalpostprocessor "github.com/mitchellh/packer/post-processor/shell-local" - shelllocalprovisioner "github.com/mitchellh/packer/provisioner/shell-local" - shellprovisioner "github.com/mitchellh/packer/provisioner/shell" - vagrantcloudpostprocessor "github.com/mitchellh/packer/post-processor/vagrant-cloud" - vagrantpostprocessor "github.com/mitchellh/packer/post-processor/vagrant" virtualboxisobuilder "github.com/mitchellh/packer/builder/virtualbox/iso" virtualboxovfbuilder "github.com/mitchellh/packer/builder/virtualbox/ovf" vmwareisobuilder "github.com/mitchellh/packer/builder/vmware/iso" vmwarevmxbuilder "github.com/mitchellh/packer/builder/vmware/vmx" + amazonimportpostprocessor "github.com/mitchellh/packer/post-processor/amazon-import" + artificepostprocessor "github.com/mitchellh/packer/post-processor/artifice" + atlaspostprocessor "github.com/mitchellh/packer/post-processor/atlas" + checksumpostprocessor "github.com/mitchellh/packer/post-processor/checksum" + compresspostprocessor "github.com/mitchellh/packer/post-processor/compress" + dockerimportpostprocessor "github.com/mitchellh/packer/post-processor/docker-import" + dockerpushpostprocessor "github.com/mitchellh/packer/post-processor/docker-push" + dockersavepostprocessor "github.com/mitchellh/packer/post-processor/docker-save" + dockertagpostprocessor "github.com/mitchellh/packer/post-processor/docker-tag" + googlecomputeexportpostprocessor "github.com/mitchellh/packer/post-processor/googlecompute-export" + manifestpostprocessor "github.com/mitchellh/packer/post-processor/manifest" + shelllocalpostprocessor "github.com/mitchellh/packer/post-processor/shell-local" + vagrantpostprocessor "github.com/mitchellh/packer/post-processor/vagrant" + vagrantcloudpostprocessor "github.com/mitchellh/packer/post-processor/vagrant-cloud" vspherepostprocessor "github.com/mitchellh/packer/post-processor/vsphere" + ansibleprovisioner "github.com/mitchellh/packer/provisioner/ansible" + ansiblelocalprovisioner "github.com/mitchellh/packer/provisioner/ansible-local" + chefclientprovisioner "github.com/mitchellh/packer/provisioner/chef-client" + chefsoloprovisioner "github.com/mitchellh/packer/provisioner/chef-solo" + fileprovisioner "github.com/mitchellh/packer/provisioner/file" + powershellprovisioner "github.com/mitchellh/packer/provisioner/powershell" + puppetmasterlessprovisioner "github.com/mitchellh/packer/provisioner/puppet-masterless" + puppetserverprovisioner "github.com/mitchellh/packer/provisioner/puppet-server" + saltmasterlessprovisioner "github.com/mitchellh/packer/provisioner/salt-masterless" + shellprovisioner "github.com/mitchellh/packer/provisioner/shell" + shelllocalprovisioner "github.com/mitchellh/packer/provisioner/shell-local" windowsrestartprovisioner "github.com/mitchellh/packer/provisioner/windows-restart" windowsshellprovisioner "github.com/mitchellh/packer/provisioner/windows-shell" - ) type PluginCommand struct { @@ -67,61 +67,59 @@ type PluginCommand struct { var Builders = map[string]packer.Builder{ "amazon-chroot": new(amazonchrootbuilder.Builder), - "amazon-ebs": new(amazonebsbuilder.Builder), - "amazon-instance": new(amazoninstancebuilder.Builder), - "azure-arm": new(azurearmbuilder.Builder), - "digitalocean": new(digitaloceanbuilder.Builder), - "docker": new(dockerbuilder.Builder), - "file": new(filebuilder.Builder), + "amazon-ebs": new(amazonebsbuilder.Builder), + "amazon-instance": new(amazoninstancebuilder.Builder), + "azure-arm": new(azurearmbuilder.Builder), + "digitalocean": new(digitaloceanbuilder.Builder), + "docker": new(dockerbuilder.Builder), + "file": new(filebuilder.Builder), "googlecompute": new(googlecomputebuilder.Builder), - "null": new(nullbuilder.Builder), - "openstack": new(openstackbuilder.Builder), + "hyperv-iso": new(hypervbuilder.Builder), + "null": new(nullbuilder.Builder), + "openstack": new(openstackbuilder.Builder), "parallels-iso": new(parallelsisobuilder.Builder), "parallels-pvm": new(parallelspvmbuilder.Builder), - "qemu": new(qemubuilder.Builder), - "virtualbox-iso": new(virtualboxisobuilder.Builder), - "virtualbox-ovf": new(virtualboxovfbuilder.Builder), - "vmware-iso": new(vmwareisobuilder.Builder), - "vmware-vmx": new(vmwarevmxbuilder.Builder), + "qemu": new(qemubuilder.Builder), + "virtualbox-iso": new(virtualboxisobuilder.Builder), + "virtualbox-ovf": new(virtualboxovfbuilder.Builder), + "vmware-iso": new(vmwareisobuilder.Builder), + "vmware-vmx": new(vmwarevmxbuilder.Builder), } - var Provisioners = map[string]packer.Provisioner{ - "ansible": new(ansibleprovisioner.Provisioner), - "ansible-local": new(ansiblelocalprovisioner.Provisioner), - "chef-client": new(chefclientprovisioner.Provisioner), - "chef-solo": new(chefsoloprovisioner.Provisioner), - "file": new(fileprovisioner.Provisioner), - "powershell": new(powershellprovisioner.Provisioner), - "puppet-masterless": new(puppetmasterlessprovisioner.Provisioner), - "puppet-server": new(puppetserverprovisioner.Provisioner), + "ansible": new(ansibleprovisioner.Provisioner), + "ansible-local": new(ansiblelocalprovisioner.Provisioner), + "chef-client": new(chefclientprovisioner.Provisioner), + "chef-solo": new(chefsoloprovisioner.Provisioner), + "file": new(fileprovisioner.Provisioner), + "powershell": new(powershellprovisioner.Provisioner), + "puppet-masterless": new(puppetmasterlessprovisioner.Provisioner), + "puppet-server": new(puppetserverprovisioner.Provisioner), "salt-masterless": new(saltmasterlessprovisioner.Provisioner), - "shell": new(shellprovisioner.Provisioner), - "shell-local": new(shelllocalprovisioner.Provisioner), + "shell": new(shellprovisioner.Provisioner), + "shell-local": new(shelllocalprovisioner.Provisioner), "windows-restart": new(windowsrestartprovisioner.Provisioner), - "windows-shell": new(windowsshellprovisioner.Provisioner), + "windows-shell": new(windowsshellprovisioner.Provisioner), } - var PostProcessors = map[string]packer.PostProcessor{ - "amazon-import": new(amazonimportpostprocessor.PostProcessor), - "artifice": new(artificepostprocessor.PostProcessor), - "atlas": new(atlaspostprocessor.PostProcessor), - "checksum": new(checksumpostprocessor.PostProcessor), - "compress": new(compresspostprocessor.PostProcessor), - "docker-import": new(dockerimportpostprocessor.PostProcessor), - "docker-push": new(dockerpushpostprocessor.PostProcessor), - "docker-save": new(dockersavepostprocessor.PostProcessor), - "docker-tag": new(dockertagpostprocessor.PostProcessor), - "googlecompute-export": new(googlecomputeexportpostprocessor.PostProcessor), - "manifest": new(manifestpostprocessor.PostProcessor), - "shell-local": new(shelllocalpostprocessor.PostProcessor), - "vagrant": new(vagrantpostprocessor.PostProcessor), - "vagrant-cloud": new(vagrantcloudpostprocessor.PostProcessor), - "vsphere": new(vspherepostprocessor.PostProcessor), + "amazon-import": new(amazonimportpostprocessor.PostProcessor), + "artifice": new(artificepostprocessor.PostProcessor), + "atlas": new(atlaspostprocessor.PostProcessor), + "checksum": new(checksumpostprocessor.PostProcessor), + "compress": new(compresspostprocessor.PostProcessor), + "docker-import": new(dockerimportpostprocessor.PostProcessor), + "docker-push": new(dockerpushpostprocessor.PostProcessor), + "docker-save": new(dockersavepostprocessor.PostProcessor), + "docker-tag": new(dockertagpostprocessor.PostProcessor), + "googlecompute-export": new(googlecomputeexportpostprocessor.PostProcessor), + "manifest": new(manifestpostprocessor.PostProcessor), + "shell-local": new(shelllocalpostprocessor.PostProcessor), + "vagrant": new(vagrantpostprocessor.PostProcessor), + "vagrant-cloud": new(vagrantcloudpostprocessor.PostProcessor), + "vsphere": new(vspherepostprocessor.PostProcessor), } - var pluginRegexp = regexp.MustCompile("packer-(builder|post-processor|provisioner)-(.+)") func (c *PluginCommand) Run(args []string) int { diff --git a/common/config_test.go b/common/config_test.go index 92a7316a314..8bd68473919 100644 --- a/common/config_test.go +++ b/common/config_test.go @@ -120,7 +120,7 @@ func TestDownloadableURL_FilePaths(t *testing.T) { }() // Test some cases with and without a schema prefix - for _, prefix := range []string{"", "file://"} { + for _, prefix := range []string{"", filePrefix} { // Nonexistent file _, err = DownloadableURL(prefix + "i/dont/exist") if err != nil { diff --git a/common/download_test.go b/common/download_test.go index 51f6f270c82..497a0564990 100644 --- a/common/download_test.go +++ b/common/download_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "os" + "runtime" "testing" ) @@ -56,6 +57,7 @@ func TestDownloadClient_basic(t *testing.T) { Url: ts.URL + "/basic.txt", TargetPath: tf.Name(), }) + path, err := client.Get() if err != nil { t.Fatalf("err: %s", err) @@ -352,7 +354,13 @@ func TestDownloadFileUrl(t *testing.T) { // source_path is a file path and source is a network path sourcePath := fmt.Sprintf("%s/test-fixtures/fileurl/%s", cwd, "cake") - source := fmt.Sprintf("file://" + sourcePath) + + filePrefix := "file://" + if runtime.GOOS == "windows" { + filePrefix += "/" + } + + source := fmt.Sprintf(filePrefix + sourcePath) t.Logf("Trying to download %s", source) config := &DownloadConfig{ diff --git a/common/iso_config.go b/common/iso_config.go index ce7e7b223c1..75147da4f67 100644 --- a/common/iso_config.go +++ b/common/iso_config.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "path/filepath" + "runtime" "strings" "github.com/mitchellh/packer/template/interpolate" @@ -80,7 +81,13 @@ func (c *ISOConfig) Prepare(ctx *interpolate.Context) ([]string, []error) { return warnings, errs } case "file": - file, err := os.Open(u.Path) + path := u.Path + + if runtime.GOOS == "windows" && len(path) > 2 && path[0] == '/' && path[2] == ':' { + path = strings.TrimLeft(path, "/") + } + + file, err := os.Open(path) if err != nil { errs = append(errs, err) return warnings, errs diff --git a/common/iso_config_test.go b/common/iso_config_test.go index b7860d5f86f..661edee3988 100644 --- a/common/iso_config_test.go +++ b/common/iso_config_test.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "reflect" + "runtime" "testing" ) @@ -78,7 +79,11 @@ func TestISOConfigPrepare_ISOChecksumURL(t *testing.T) { i.ISOChecksum = "" cs_file, _ := ioutil.TempFile("", "packer-test-") ioutil.WriteFile(cs_file.Name(), []byte(cs_bsd_style), 0666) - i.ISOChecksumURL = fmt.Sprintf("file://%s", cs_file.Name()) + filePrefix := "file://" + if runtime.GOOS == "windows" { + filePrefix += "/" + } + i.ISOChecksumURL = fmt.Sprintf("%s%s", filePrefix, cs_file.Name()) warns, err = i.Prepare(nil) if len(warns) > 0 { t.Fatalf("bad: %#v", warns) @@ -96,7 +101,7 @@ func TestISOConfigPrepare_ISOChecksumURL(t *testing.T) { i.ISOChecksum = "" cs_file, _ = ioutil.TempFile("", "packer-test-") ioutil.WriteFile(cs_file.Name(), []byte(cs_gnu_style), 0666) - i.ISOChecksumURL = fmt.Sprintf("file://%s", cs_file.Name()) + i.ISOChecksumURL = fmt.Sprintf("%s%s", filePrefix, cs_file.Name()) warns, err = i.Prepare(nil) if len(warns) > 0 { t.Fatalf("bad: %#v", warns) diff --git a/helper/communicator/step_connect.go b/helper/communicator/step_connect.go index f4d1499b7c8..b427534526e 100644 --- a/helper/communicator/step_connect.go +++ b/helper/communicator/step_connect.go @@ -33,6 +33,7 @@ type StepConnect struct { // WinRMConfig should return the default configuration for // connecting via WinRM. WinRMConfig func(multistep.StateBag) (*WinRMConfig, error) + WinRMPort func(multistep.StateBag) (int, error) // CustomConnect can be set to have custom connectors for specific // types. These take highest precedence so you can also override @@ -55,7 +56,7 @@ func (s *StepConnect) Run(state multistep.StateBag) multistep.StepAction { Config: s.Config, Host: s.Host, WinRMConfig: s.WinRMConfig, - WinRMPort: s.SSHPort, + WinRMPort: s.WinRMPort, }, } for k, v := range s.CustomConnect { diff --git a/helper/communicator/step_connect_winrm.go b/helper/communicator/step_connect_winrm.go index 49936ad7b54..60522d79bb7 100644 --- a/helper/communicator/step_connect_winrm.go +++ b/helper/communicator/step_connect_winrm.go @@ -96,6 +96,7 @@ func (s *StepConnectWinRM) waitForWinRM(state multistep.StateBag, cancel <-chan log.Printf("[DEBUG] Error getting WinRM host: %s", err) continue } + port := s.Config.WinRMPort if s.WinRMPort != nil { port, err = s.WinRMPort(state) @@ -103,7 +104,7 @@ func (s *StepConnectWinRM) waitForWinRM(state multistep.StateBag, cancel <-chan log.Printf("[DEBUG] Error getting WinRM port: %s", err) continue } - } + } user := s.Config.WinRMUser password := s.Config.WinRMPassword diff --git a/post-processor/vagrant/hyperv.go b/post-processor/vagrant/hyperv.go index d5fa0ce5234..2f7dc144059 100644 --- a/post-processor/vagrant/hyperv.go +++ b/post-processor/vagrant/hyperv.go @@ -3,7 +3,9 @@ package vagrant import ( "fmt" "github.com/mitchellh/packer/packer" + "os" "path/filepath" + "strings" ) type HypervProvider struct{} @@ -16,14 +18,48 @@ func (p *HypervProvider) Process(ui packer.Ui, artifact packer.Artifact, dir str // Create the metadata metadata = map[string]interface{}{"provider": "hyperv"} + // ui.Message(fmt.Sprintf("artifacts all: %+v", artifact)) + var outputDir string + + // Vargant requires specific dir structure for hyperv + // hyperv builder creates the structure in the output dir + // we have to keep the structure in a temp dir + // hack little bit but string in artifact usually have output dir + artifactString := artifact.String() + d := strings.Split(artifactString, ": ") + outputDir = d[1] + // ui.Message(fmt.Sprintf("artifact dir from string: %s", outputDir)) + // Copy all of the original contents into the temporary directory for _, path := range artifact.Files() { ui.Message(fmt.Sprintf("Copying: %s", path)) - dstPath := filepath.Join(dir, filepath.Base(path)) + var rel string + + rel, err = filepath.Rel(outputDir, filepath.Dir(path)) + // ui.Message(fmt.Sprintf("rel is: %s", rel)) + + if err != nil { + ui.Message(fmt.Sprintf("err in: %s", rel)) + return + } + + dstDir := filepath.Join(dir, rel) + // ui.Message(fmt.Sprintf("dstdir is: %s", dstDir)) + if _, err = os.Stat(dstDir); err != nil { + if err = os.MkdirAll(dstDir, 0755); err != nil { + ui.Message(fmt.Sprintf("err in creating: %s", dstDir)) + return + } + } + + dstPath := filepath.Join(dstDir, filepath.Base(path)) + if err = CopyContents(dstPath, path); err != nil { + ui.Message(fmt.Sprintf("err in copying: %s to %s", path, dstPath)) return } + ui.Message(fmt.Sprintf("Copyed %s to %s", path, dstPath)) } return diff --git a/powershell/hyperv/hyperv.go b/powershell/hyperv/hyperv.go new file mode 100644 index 00000000000..eeae4d727cf --- /dev/null +++ b/powershell/hyperv/hyperv.go @@ -0,0 +1,1046 @@ +package hyperv + +import ( + "errors" + "strconv" + "strings" + + "github.com/mitchellh/packer/powershell" +) + +func GetHostAdapterIpAddressForSwitch(switchName string) (string, error) { + var script = ` +param([string]$switchName, [int]$addressIndex) + +$HostVMAdapter = Get-VMNetworkAdapter -ManagementOS -SwitchName $switchName +if ($HostVMAdapter){ + $HostNetAdapter = Get-NetAdapter | ?{ $_.DeviceID -eq $HostVMAdapter.DeviceId } + if ($HostNetAdapter){ + $HostNetAdapterConfiguration = @(get-wmiobject win32_networkadapterconfiguration -filter "IPEnabled = 'TRUE' AND InterfaceIndex=$($HostNetAdapter.ifIndex)") + if ($HostNetAdapterConfiguration){ + return @($HostNetAdapterConfiguration.IpAddress)[$addressIndex] + } + } +} +return $false +` + + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(script, switchName, "0") + + return cmdOut, err +} + +func GetVirtualMachineNetworkAdapterAddress(vmName string) (string, error) { + + var script = ` +param([string]$vmName, [int]$addressIndex) +try { + $adapter = Get-VMNetworkAdapter -VMName $vmName -ErrorAction SilentlyContinue + $ip = $adapter.IPAddresses[$addressIndex] + if($ip -eq $null) { + return $false + } +} catch { + return $false +} +$ip +` + + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(script, vmName, "0") + + return cmdOut, err +} + +func CreateDvdDrive(vmName string, isoPath string, generation uint) (uint, uint, error) { + var ps powershell.PowerShellCmd + var script string + + script = ` +param([string]$vmName, [string]$isoPath) +$dvdController = Add-VMDvdDrive -VMName $vmName -path $isoPath -Passthru +$dvdController | Set-VMDvdDrive -path $null +$result = "$($dvdController.ControllerNumber),$($dvdController.ControllerLocation)" +$result +` + + cmdOut, err := ps.Output(script, vmName, isoPath) + if err != nil { + return 0, 0, err + } + + cmdOutArray := strings.Split(cmdOut, ",") + if len(cmdOutArray) != 2 { + return 0, 0, errors.New("Did not return controller number and controller location") + } + + controllerNumberTemp, err := strconv.ParseUint(strings.TrimSpace(cmdOutArray[0]), 10, 64) + if err != nil { + return 0, 0, err + } + controllerNumber := uint(controllerNumberTemp) + + controllerLocationTemp, err := strconv.ParseUint(strings.TrimSpace(cmdOutArray[1]), 10, 64) + if err != nil { + return controllerNumber, 0, err + } + controllerLocation := uint(controllerLocationTemp) + + return controllerNumber, controllerLocation, err +} + +func MountDvdDrive(vmName string, path string, controllerNumber uint, controllerLocation uint) error { + + var script = ` +param([string]$vmName,[string]$path,[string]$controllerNumber,[string]$controllerLocation) +$vmDvdDrive = Get-VMDvdDrive -VMName $vmName -ControllerNumber $controllerNumber -ControllerLocation $controllerLocation +if (!$vmDvdDrive) {throw 'unable to find dvd drive'} +Set-VMDvdDrive -VMName $vmName -ControllerNumber $controllerNumber -ControllerLocation $controllerLocation -Path $path +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, path, strconv.FormatInt(int64(controllerNumber), 10), strconv.FormatInt(int64(controllerLocation), 10)) + return err +} + +func UnmountDvdDrive(vmName string, controllerNumber uint, controllerLocation uint) error { + var script = ` +param([string]$vmName,[int]$controllerNumber,[int]$controllerLocation) +$vmDvdDrive = Get-VMDvdDrive -VMName $vmName -ControllerNumber $controllerNumber -ControllerLocation $controllerLocation +if (!$vmDvdDrive) {throw 'unable to find dvd drive'} +Set-VMDvdDrive -VMName $vmName -ControllerNumber $controllerNumber -ControllerLocation $controllerLocation -Path $null +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, strconv.FormatInt(int64(controllerNumber), 10), strconv.FormatInt(int64(controllerLocation), 10)) + return err +} + +func SetBootDvdDrive(vmName string, controllerNumber uint, controllerLocation uint, generation uint) error { + + if generation < 2 { + script := ` +param([string]$vmName) +Set-VMBios -VMName $vmName -StartupOrder @("CD", "IDE","LegacyNetworkAdapter","Floppy") +` + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName) + return err + } else { + script := ` +param([string]$vmName,[int]$controllerNumber,[int]$controllerLocation) +$vmDvdDrive = Get-VMDvdDrive -VMName $vmName -ControllerNumber $controllerNumber -ControllerLocation $controllerLocation +if (!$vmDvdDrive) {throw 'unable to find dvd drive'} +Set-VMFirmware -VMName $vmName -FirstBootDevice $vmDvdDrive -ErrorAction SilentlyContinue +` + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, strconv.FormatInt(int64(controllerNumber), 10), strconv.FormatInt(int64(controllerLocation), 10)) + return err + } +} + +func DeleteDvdDrive(vmName string, controllerNumber uint, controllerLocation uint) error { + var script = ` +param([string]$vmName,[int]$controllerNumber,[int]$controllerLocation) +$vmDvdDrive = Get-VMDvdDrive -VMName $vmName -ControllerNumber $controllerNumber -ControllerLocation $controllerLocation +if (!$vmDvdDrive) {throw 'unable to find dvd drive'} +Remove-VMDvdDrive -VMName $vmName -ControllerNumber $controllerNumber -ControllerLocation $controllerLocation +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, strconv.FormatInt(int64(controllerNumber), 10), strconv.FormatInt(int64(controllerLocation), 10)) + return err +} + +func DeleteAllDvdDrives(vmName string) error { + var script = ` +param([string]$vmName) +Get-VMDvdDrive -VMName $vmName | Remove-VMDvdDrive +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName) + return err +} + +func MountFloppyDrive(vmName string, path string) error { + var script = ` +param([string]$vmName, [string]$path) +Set-VMFloppyDiskDrive -VMName $vmName -Path $path +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, path) + return err +} + +func UnmountFloppyDrive(vmName string) error { + + var script = ` +param([string]$vmName) +Set-VMFloppyDiskDrive -VMName $vmName -Path $null +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName) + return err +} + +func CreateVirtualMachine(vmName string, path string, ram int64, diskSize int64, switchName string, generation uint) error { + + if generation == 2 { + var script = ` +param([string]$vmName, [string]$path, [long]$memoryStartupBytes, [long]$newVHDSizeBytes, [string]$switchName, [int]$generation) +$vhdx = $vmName + '.vhdx' +$vhdPath = Join-Path -Path $path -ChildPath $vhdx +New-VM -Name $vmName -Path $path -MemoryStartupBytes $memoryStartupBytes -NewVHDPath $vhdPath -NewVHDSizeBytes $newVHDSizeBytes -SwitchName $switchName -Generation $generation +` + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, path, strconv.FormatInt(ram, 10), strconv.FormatInt(diskSize, 10), switchName, strconv.FormatInt(int64(generation), 10)) + return err + } else { + var script = ` +param([string]$vmName, [string]$path, [long]$memoryStartupBytes, [long]$newVHDSizeBytes, [string]$switchName) +$vhdx = $vmName + '.vhdx' +$vhdPath = Join-Path -Path $path -ChildPath $vhdx +New-VM -Name $vmName -Path $path -MemoryStartupBytes $memoryStartupBytes -NewVHDPath $vhdPath -NewVHDSizeBytes $newVHDSizeBytes -SwitchName $switchName +` + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, path, strconv.FormatInt(ram, 10), strconv.FormatInt(diskSize, 10), switchName) + + if err != nil { + return err + } + + return DeleteAllDvdDrives(vmName) + } +} + +func SetVirtualMachineCpuCount(vmName string, cpu uint) error { + + var script = ` +param([string]$vmName, [int]$cpu) +Set-VMProcessor -VMName $vmName -Count $cpu +` + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, strconv.FormatInt(int64(cpu), 10)) + return err +} + +func SetVirtualMachineVirtualizationExtensions(vmName string, enableVirtualizationExtensions bool) error { + + var script = ` +param([string]$vmName, [string]$exposeVirtualizationExtensionsString) +$exposeVirtualizationExtensions = [System.Boolean]::Parse($exposeVirtualizationExtensionsString) +Set-VMProcessor -VMName $vmName -ExposeVirtualizationExtensions $exposeVirtualizationExtensions +` + exposeVirtualizationExtensionsString := "False" + if enableVirtualizationExtensions { + exposeVirtualizationExtensionsString = "True" + } + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, exposeVirtualizationExtensionsString) + return err +} + +func SetVirtualMachineDynamicMemory(vmName string, enableDynamicMemory bool) error { + + var script = ` +param([string]$vmName, [string]$enableDynamicMemoryString) +$enableDynamicMemory = [System.Boolean]::Parse($enableDynamicMemoryString) +Set-VMMemory -VMName $vmName -DynamicMemoryEnabled $enableDynamicMemory +` + enableDynamicMemoryString := "False" + if enableDynamicMemory { + enableDynamicMemoryString = "True" + } + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, enableDynamicMemoryString) + return err +} + +func SetVirtualMachineMacSpoofing(vmName string, enableMacSpoofing bool) error { + var script = ` +param([string]$vmName, $enableMacSpoofing) +Set-VMNetworkAdapter -VMName $vmName -MacAddressSpoofing $enableMacSpoofing +` + + var ps powershell.PowerShellCmd + + enableMacSpoofingString := "Off" + if enableMacSpoofing { + enableMacSpoofingString = "On" + } + + err := ps.Run(script, vmName, enableMacSpoofingString) + return err +} + +func SetVirtualMachineSecureBoot(vmName string, enableSecureBoot bool) error { + var script = ` +param([string]$vmName, $enableSecureBoot) +Set-VMFirmware -VMName $vmName -EnableSecureBoot $enableSecureBoot +` + + var ps powershell.PowerShellCmd + + enableSecureBootString := "Off" + if enableSecureBoot { + enableSecureBootString = "On" + } + + err := ps.Run(script, vmName, enableSecureBootString) + return err +} + +func DeleteVirtualMachine(vmName string) error { + + var script = ` +param([string]$vmName) + +$vm = Get-VM -Name $vmName +if (($vm.State -ne [Microsoft.HyperV.PowerShell.VMState]::Off) -and ($vm.State -ne [Microsoft.HyperV.PowerShell.VMState]::OffCritical)) { + Stop-VM -VM $vm -TurnOff -Force -Confirm:$false +} + +Remove-VM -Name $vmName -Force -Confirm:$false +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName) + return err +} + +func ExportVirtualMachine(vmName string, path string) error { + + var script = ` +param([string]$vmName, [string]$path) +Export-VM -Name $vmName -Path $path + +if (Test-Path -Path ([IO.Path]::Combine($path, $vmName, 'Virtual Machines', '*.VMCX'))) +{ + $vm = Get-VM -Name $vmName + $vm_adapter = Get-VMNetworkAdapter -VM $vm | Select -First 1 + + $config = [xml]@" + + + + $($vm.Generation - 1) + $($vm.Name) + + + + $($vm.ProcessorCount) + + + + $($vm.DynamicMemoryEnabled) + $($vm.MemoryMaximum / 1MB) + $($vm.MemoryMinimum / 1MB) + $($vm.MemoryStartup / 1MB) + + + + $($vm_adapter.SwitchName) + + Optical + + False + $($vm.Notes) + + +"@ + + if ($vm.Generation -eq 1) + { + $vm_controllers = Get-VMIdeController -VM $vm + $controller_type = $config.SelectSingleNode('/configuration/vm-controllers') + # IDE controllers are not stored in a special XML container + } + else + { + $vm_controllers = Get-VMScsiController -VM $vm + $controller_type = $config.CreateElement('scsi') + $controller_type.SetAttribute('ChannelInstanceGuid', 'x') + # SCSI controllers are stored in the scsi XML container + if ((Get-VMFirmware -VM $vm).SecureBoot -eq [Microsoft.HyperV.PowerShell.OnOffState]::On) + { + $config.configuration.secure_boot_enabled.'#text' = 'True' + } + else + { + $config.configuration.secure_boot_enabled.'#text' = 'False' + } + } + + $vm_controllers | ForEach { + $controller = $config.CreateElement('controller' + $_.ControllerNumber) + $_.Drives | ForEach { + $drive = $config.CreateElement('drive' + ($_.DiskNumber + 0)) + $drive_path = $config.CreateElement('pathname') + $drive_path.SetAttribute('type', 'string') + $drive_path.AppendChild($config.CreateTextNode($_.Path)) + $drive_type = $config.CreateElement('type') + $drive_type.SetAttribute('type', 'string') + if ($_ -is [Microsoft.HyperV.PowerShell.HardDiskDrive]) + { + $drive_type.AppendChild($config.CreateTextNode('VHD')) + } + elseif ($_ -is [Microsoft.HyperV.PowerShell.DvdDrive]) + { + $drive_type.AppendChild($config.CreateTextNode('ISO')) + } + else + { + $drive_type.AppendChild($config.CreateTextNode('NONE')) + } + $drive.AppendChild($drive_path) + $drive.AppendChild($drive_type) + $controller.AppendChild($drive) + } + $controller_type.AppendChild($controller) + } + if ($controller_type.Name -ne 'vm-controllers') + { + $config.SelectSingleNode('/configuration/vm-controllers').AppendChild($controller_type) + } + + $config.Save([IO.Path]::Combine($path, $vm.Name, 'Virtual Machines', 'box.xml')) +} +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, path) + return err +} + +func CompactDisks(expPath string, vhdDir string) error { + var script = ` +param([string]$srcPath, [string]$vhdDirName) +Get-ChildItem "$srcPath/$vhdDirName" -Filter *.vhd* | %{ + Optimize-VHD -Path $_.FullName -Mode Full +} +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, expPath, vhdDir) + return err +} + +func CopyExportedVirtualMachine(expPath string, outputPath string, vhdDir string, vmDir string) error { + + var script = ` +param([string]$srcPath, [string]$dstPath, [string]$vhdDirName, [string]$vmDir) +Move-Item -Path $srcPath/*.* -Destination $dstPath +Move-Item -Path $srcPath/$vhdDirName -Destination $dstPath +Move-Item -Path $srcPath/$vmDir -Destination $dstPath +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, expPath, outputPath, vhdDir, vmDir) + return err +} + +func CreateVirtualSwitch(switchName string, switchType string) (bool, error) { + + var script = ` +param([string]$switchName,[string]$switchType) +$switches = Get-VMSwitch -Name $switchName -ErrorAction SilentlyContinue +if ($switches.Count -eq 0) { + New-VMSwitch -Name $switchName -SwitchType $switchType + return $true +} +return $false +` + + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(script, switchName, switchType) + var created = strings.TrimSpace(cmdOut) == "True" + return created, err +} + +func DeleteVirtualSwitch(switchName string) error { + + var script = ` +param([string]$switchName) +$switch = Get-VMSwitch -Name $switchName -ErrorAction SilentlyContinue +if ($switch -ne $null) { + $switch | Remove-VMSwitch -Force -Confirm:$false +} +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, switchName) + return err +} + +func StartVirtualMachine(vmName string) error { + + var script = ` +param([string]$vmName) +$vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue +if ($vm.State -eq [Microsoft.HyperV.PowerShell.VMState]::Off) { + Start-VM -Name $vmName -Confirm:$false +} +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName) + return err +} + +func RestartVirtualMachine(vmName string) error { + + var script = ` +param([string]$vmName) +Restart-VM $vmName -Force -Confirm:$false +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName) + return err +} + +func StopVirtualMachine(vmName string) error { + + var script = ` +param([string]$vmName) +$vm = Get-VM -Name $vmName +if ($vm.State -eq [Microsoft.HyperV.PowerShell.VMState]::Running) { + Stop-VM -VM $vm -Force -Confirm:$false +} +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName) + return err +} + +func EnableVirtualMachineIntegrationService(vmName string, integrationServiceName string) error { + + integrationServiceId := "" + switch integrationServiceName { + case "Time Synchronization": + integrationServiceId = "2497F4DE-E9FA-4204-80E4-4B75C46419C0" + case "Heartbeat": + integrationServiceId = "84EAAE65-2F2E-45F5-9BB5-0E857DC8EB47" + case "Key-Value Pair Exchange": + integrationServiceId = "2A34B1C2-FD73-4043-8A5B-DD2159BC743F" + case "Shutdown": + integrationServiceId = "9F8233AC-BE49-4C79-8EE3-E7E1985B2077" + case "VSS": + integrationServiceId = "5CED1297-4598-4915-A5FC-AD21BB4D02A4" + case "Guest Service Interface": + integrationServiceId = "6C09BB55-D683-4DA0-8931-C9BF705F6480" + default: + panic("unrecognized Integration Service Name") + } + + var script = ` +param([string]$vmName,[string]$integrationServiceId) +Get-VMIntegrationService -VmName $vmName | ?{$_.Id -match $integrationServiceId} | Enable-VMIntegrationService +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, integrationServiceId) + return err +} + +func SetNetworkAdapterVlanId(switchName string, vlanId string) error { + + var script = ` +param([string]$networkAdapterName,[string]$vlanId) +Set-VMNetworkAdapterVlan -ManagementOS -VMNetworkAdapterName $networkAdapterName -Access -VlanId $vlanId +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, switchName, vlanId) + return err +} + +func SetVirtualMachineVlanId(vmName string, vlanId string) error { + + var script = ` +param([string]$vmName,[string]$vlanId) +Set-VMNetworkAdapterVlan -VMName $vmName -Access -VlanId $vlanId +` + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, vlanId) + return err +} + +func GetExternalOnlineVirtualSwitch() (string, error) { + + var script = ` +$adapters = Get-NetAdapter -Physical -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq 'Up' } | Sort-Object -Descending -Property Speed +foreach ($adapter in $adapters) { + $switch = Get-VMSwitch -SwitchType External | Where-Object { $_.NetAdapterInterfaceDescription -eq $adapter.InterfaceDescription } + + if ($switch -ne $null) { + $switch.Name + break + } +} +` + + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(script) + if err != nil { + return "", err + } + + var switchName = strings.TrimSpace(cmdOut) + return switchName, nil +} + +func CreateExternalVirtualSwitch(vmName string, switchName string) error { + + var script = ` +param([string]$vmName,[string]$switchName) +$switch = $null +$names = @('ethernet','wi-fi','lan') +$adapters = foreach ($name in $names) { + Get-NetAdapter -Physical -Name $name -ErrorAction SilentlyContinue | where status -eq 'up' +} + +foreach ($adapter in $adapters) { + $switch = Get-VMSwitch -SwitchType External | where { $_.NetAdapterInterfaceDescription -eq $adapter.InterfaceDescription } + + if ($switch -eq $null) { + $switch = New-VMSwitch -Name $switchName -NetAdapterName $adapter.Name -AllowManagementOS $true -Notes 'Parent OS, VMs, WiFi' + } + + if ($switch -ne $null) { + break + } +} + +if($switch -ne $null) { + Get-VMNetworkAdapter -VMName $vmName | Connect-VMNetworkAdapter -VMSwitch $switch +} else { + Write-Error 'No internet adapters found' +} +` + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, switchName) + return err +} + +func GetVirtualMachineSwitchName(vmName string) (string, error) { + + var script = ` +param([string]$vmName) +(Get-VMNetworkAdapter -VMName $vmName).SwitchName +` + + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(script, vmName) + if err != nil { + return "", err + } + + return strings.TrimSpace(cmdOut), nil +} + +func ConnectVirtualMachineNetworkAdapterToSwitch(vmName string, switchName string) error { + + var script = ` +param([string]$vmName,[string]$switchName) +Get-VMNetworkAdapter -VMName $vmName | Connect-VMNetworkAdapter -SwitchName $switchName +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, switchName) + return err +} + +func UntagVirtualMachineNetworkAdapterVlan(vmName string, switchName string) error { + + var script = ` +param([string]$vmName,[string]$switchName) +Set-VMNetworkAdapterVlan -VMName $vmName -Untagged +Set-VMNetworkAdapterVlan -ManagementOS -VMNetworkAdapterName $switchName -Untagged +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, switchName) + return err +} + +func IsRunning(vmName string) (bool, error) { + + var script = ` +param([string]$vmName) +$vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue +$vm.State -eq [Microsoft.HyperV.PowerShell.VMState]::Running +` + + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(script, vmName) + + if err != nil { + return false, err + } + + var isRunning = strings.TrimSpace(cmdOut) == "True" + return isRunning, err +} + +func IsOff(vmName string) (bool, error) { + + var script = ` +param([string]$vmName) +$vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue +$vm.State -eq [Microsoft.HyperV.PowerShell.VMState]::Off +` + + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(script, vmName) + + if err != nil { + return false, err + } + + var isRunning = strings.TrimSpace(cmdOut) == "True" + return isRunning, err +} + +func Uptime(vmName string) (uint64, error) { + + var script = ` +param([string]$vmName) +$vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue +$vm.Uptime.TotalSeconds +` + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(script, vmName) + + if err != nil { + return 0, err + } + + uptime, err := strconv.ParseUint(strings.TrimSpace(string(cmdOut)), 10, 64) + + return uptime, err +} + +func Mac(vmName string) (string, error) { + var script = ` +param([string]$vmName, [int]$adapterIndex) +try { + $adapter = Get-VMNetworkAdapter -VMName $vmName -ErrorAction SilentlyContinue + $mac = $adapter[$adapterIndex].MacAddress + if($mac -eq $null) { + return "" + } +} catch { + return "" +} +$mac +` + + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(script, vmName, "0") + + return cmdOut, err +} + +func IpAddress(mac string) (string, error) { + var script = ` +param([string]$mac, [int]$addressIndex) +try { + $ip = Get-Vm | %{$_.NetworkAdapters} | ?{$_.MacAddress -eq $mac} | %{$_.IpAddresses[$addressIndex]} + + if($ip -eq $null) { + return "" + } +} catch { + return "" +} +$ip +` + + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(script, mac, "0") + + return cmdOut, err +} + +func TurnOff(vmName string) error { + + var script = ` +param([string]$vmName) +$vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue +if ($vm.State -eq [Microsoft.HyperV.PowerShell.VMState]::Running) { + Stop-VM -Name $vmName -TurnOff -Force -Confirm:$false +} +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName) + return err +} + +func ShutDown(vmName string) error { + + var script = ` +param([string]$vmName) +$vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue +if ($vm.State -eq [Microsoft.HyperV.PowerShell.VMState]::Running) { + Stop-VM -Name $vmName -Force -Confirm:$false +} +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName) + return err +} + +func TypeScanCodes(vmName string, scanCodes string) error { + if len(scanCodes) == 0 { + return nil + } + var script = ` +param([string]$vmName, [string]$scanCodes) + #Requires -Version 3 + #Requires -RunAsAdministrator + + function Get-VMConsole + { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string] $VMName + ) + + $ErrorActionPreference = "Stop" + + $vm = Get-CimInstance -Namespace "root\virtualization\v2" -ClassName Msvm_ComputerSystem -ErrorAction Ignore -Verbose:$false | where ElementName -eq $VMName | select -first 1 + if ($vm -eq $null){ + Write-Error ("VirtualMachine({0}) is not found!" -f $VMName) + } + + $vmKeyboard = $vm | Get-CimAssociatedInstance -ResultClassName "Msvm_Keyboard" -ErrorAction Ignore -Verbose:$false + + if ($vmKeyboard -eq $null) { + $vmKeyboard = Get-CimInstance -Namespace "root\virtualization\v2" -ClassName Msvm_Keyboard -ErrorAction Ignore -Verbose:$false | where SystemName -eq $vm.Name | select -first 1 + } + + if ($vmKeyboard -eq $null) { + $vmKeyboard = Get-CimInstance -Namespace "root\virtualization" -ClassName Msvm_Keyboard -ErrorAction Ignore -Verbose:$false | where SystemName -eq $vm.Name | select -first 1 + } + + if ($vmKeyboard -eq $null){ + Write-Error ("VirtualMachine({0}) keyboard class is not found!" -f $VMName) + } + + #TODO: It may be better using New-Module -AsCustomObject to return console object? + + #Console object to return + $console = [pscustomobject] @{ + Msvm_ComputerSystem = $vm + Msvm_Keyboard = $vmKeyboard + } + + #Need to import assembly to use System.Windows.Input.Key + Add-Type -AssemblyName WindowsBase + + #region Add Console Members + $console | Add-Member -MemberType ScriptMethod -Name TypeText -Value { + [OutputType([bool])] + param ( + [ValidateNotNullOrEmpty()] + [Parameter(Mandatory)] + [string] $AsciiText + ) + $result = $this.Msvm_Keyboard | Invoke-CimMethod -MethodName "TypeText" -Arguments @{ asciiText = $AsciiText } + return (0 -eq $result.ReturnValue) + } + + #Define method:TypeCtrlAltDel + $console | Add-Member -MemberType ScriptMethod -Name TypeCtrlAltDel -Value { + $result = $this.Msvm_Keyboard | Invoke-CimMethod -MethodName "TypeCtrlAltDel" + return (0 -eq $result.ReturnValue) + } + + #Define method:TypeKey + $console | Add-Member -MemberType ScriptMethod -Name TypeKey -Value { + [OutputType([bool])] + param ( + [Parameter(Mandatory)] + [Windows.Input.Key] $Key, + [Windows.Input.ModifierKeys] $ModifierKey = [Windows.Input.ModifierKeys]::None + ) + + $keyCode = [Windows.Input.KeyInterop]::VirtualKeyFromKey($Key) + + switch ($ModifierKey) + { + ([Windows.Input.ModifierKeys]::Control){ $modifierKeyCode = [Windows.Input.KeyInterop]::VirtualKeyFromKey([Windows.Input.Key]::LeftCtrl)} + ([Windows.Input.ModifierKeys]::Alt){ $modifierKeyCode = [Windows.Input.KeyInterop]::VirtualKeyFromKey([Windows.Input.Key]::LeftAlt)} + ([Windows.Input.ModifierKeys]::Shift){ $modifierKeyCode = [Windows.Input.KeyInterop]::VirtualKeyFromKey([Windows.Input.Key]::LeftShift)} + ([Windows.Input.ModifierKeys]::Windows){ $modifierKeyCode = [Windows.Input.KeyInterop]::VirtualKeyFromKey([Windows.Input.Key]::LWin)} + } + + if ($ModifierKey -eq [Windows.Input.ModifierKeys]::None) + { + $result = $this.Msvm_Keyboard | Invoke-CimMethod -MethodName "TypeKey" -Arguments @{ keyCode = $keyCode } + } + else + { + $this.Msvm_Keyboard | Invoke-CimMethod -MethodName "PressKey" -Arguments @{ keyCode = $modifierKeyCode } + $result = $this.Msvm_Keyboard | Invoke-CimMethod -MethodName "TypeKey" -Arguments @{ keyCode = $keyCode } + $this.Msvm_Keyboard | Invoke-CimMethod -MethodName "ReleaseKey" -Arguments @{ keyCode = $modifierKeyCode } + } + $result = return (0 -eq $result.ReturnValue) + } + + #Define method:Scancodes + $console | Add-Member -MemberType ScriptMethod -Name TypeScancodes -Value { + [OutputType([bool])] + param ( + [Parameter(Mandatory)] + [byte[]] $ScanCodes + ) + $result = $this.Msvm_Keyboard | Invoke-CimMethod -MethodName "TypeScancodes" -Arguments @{ ScanCodes = $ScanCodes } + return (0 -eq $result.ReturnValue) + } + + #Define method:ExecCommand + $console | Add-Member -MemberType ScriptMethod -Name ExecCommand -Value { + param ( + [Parameter(Mandatory)] + [string] $Command + ) + if ([String]::IsNullOrEmpty($Command)){ + return + } + + $console.TypeText($Command) > $null + $console.TypeKey([Windows.Input.Key]::Enter) > $null + #sleep -Milliseconds 100 + } + + #Define method:Dispose + $console | Add-Member -MemberType ScriptMethod -Name Dispose -Value { + $this.Msvm_ComputerSystem.Dispose() + $this.Msvm_Keyboard.Dispose() + } + + + #endregion + + return $console + } + + $vmConsole = Get-VMConsole -VMName $vmName + $scanCodesToSend = '' + $scanCodes.Split(' ') | %{ + $scanCode = $_ + + if ($scanCode.StartsWith('wait')){ + $timeToWait = $scanCode.Substring(4) + if (!$timeToWait){ + $timeToWait = "1" + } + + if ($scanCodesToSend){ + $scanCodesToSendByteArray = [byte[]]@($scanCodesToSend.Split(' ') | %{"0x$_"}) + + $scanCodesToSendByteArray | %{ + $vmConsole.TypeScancodes($_) + } + } + + write-host "Special code found, will sleep $timeToWait second(s) at this point." + Start-Sleep -s $timeToWait + + $scanCodesToSend = '' + } else { + if ($scanCodesToSend){ + write-host "Sending special code '$scanCodesToSend' '$scanCode'" + $scanCodesToSend = "$scanCodesToSend $scanCode" + } else { + write-host "Sending char '$scanCode'" + $scanCodesToSend = "$scanCode" + } + } + } + if ($scanCodesToSend){ + $scanCodesToSendByteArray = [byte[]]@($scanCodesToSend.Split(' ') | %{"0x$_"}) + + $scanCodesToSendByteArray | %{ + $vmConsole.TypeScancodes($_) + } + } +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, scanCodes) + return err +} + +func SetVirtualMachineIPNetworkConfiguration(vmName string, IPAddress string, Gateway string, DNSServer string, Subnet string) error { + + var script = ` +[CmdletBinding()] + Param ( + + [string]$vmName, + [String[]]$IPAddress = @(), + [String[]]$Gateway = @(), + [String[]]$DNSServers = @(), + [String[]]$Subnet = @() + ) + $NetworkAdapter = Get-VMNetworkAdapter -VMName $vmName | Select -first 1 + $vm = Get-WmiObject -Namespace 'root\virtualization\v2' -Class 'Msvm_ComputerSystem' | Where-Object { + $_.ElementName -eq $NetworkAdapter.VMName + } + $VMSettings = $vm.GetRelated('Msvm_VirtualSystemSettingData') | Where-Object { + $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' + } + $VMNetAdapters = $VMSettings.GetRelated('Msvm_SyntheticEthernetPortSettingData') + + $NetworkSettings = @() + foreach ($NetAdapter in $VMNetAdapters) + { + if ($NetAdapter.Address -eq $NetworkAdapter.MacAddress) + { + $NetworkSettings = $NetworkSettings + $NetAdapter.GetRelated('Msvm_GuestNetworkAdapterConfiguration') + } + } + + $NetworkSettings[0].IPAddresses = $IPAddress + $NetworkSettings[0].DefaultGateways = $Gateway + $NetworkSettings[0].DNSServers = $DNSServers + $NetworkSettings[0].Subnets = $Subnet + $NetworkSettings[0].ProtocolIFType = 4096 + $NetworkSettings[0].DHCPEnabled = $false + + + $Service = Get-WmiObject -Class 'Msvm_VirtualSystemManagementService' -Namespace 'root\virtualization\v2' + $setIP = $Service.SetGuestNetworkAdapterConfiguration($vm, $NetworkSettings[0].GetText(1)) + + if ($setIP.ReturnValue -eq 4096) + { + $job = [WMI]$setIP.job + + while ($job.JobState -eq 3 -or $job.JobState -eq 4) + { + Start-Sleep 1 + $job = [WMI]$setIP.job + } + + if ($job.JobState -ne 7) + { + $job.GetError() + } + } + (Get-VM -Id $NetworkAdapter.VmId).NetworkAdapter | Select-Object Name, IpAddress +` + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, IPAddress, Gateway, DNSServer, Subnet) + return err +} diff --git a/powershell/powershell.go b/powershell/powershell.go new file mode 100644 index 00000000000..93ba91150bc --- /dev/null +++ b/powershell/powershell.go @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +package powershell + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "strconv" + "strings" +) + +const ( + powerShellFalse = "False" + powerShellTrue = "True" +) + +type PowerShellCmd struct { + Stdout io.Writer + Stderr io.Writer +} + +func (ps *PowerShellCmd) Run(fileContents string, params ...string) error { + _, err := ps.Output(fileContents, params...) + return err +} + +// Output runs the PowerShell command and returns its standard output. +func (ps *PowerShellCmd) Output(fileContents string, params ...string) (string, error) { + path, err := ps.getPowerShellPath() + if err != nil { + return "", err + } + + filename, err := saveScript(fileContents) + if err != nil { + return "", err + } + + debug := os.Getenv("PACKER_POWERSHELL_DEBUG") != "" + verbose := debug || os.Getenv("PACKER_POWERSHELL_VERBOSE") != "" + + if !debug { + defer os.Remove(filename) + } + + args := createArgs(filename, params...) + + if verbose { + log.Printf("Run: %s %s", path, args) + } + + var stdout, stderr bytes.Buffer + command := exec.Command(path, args...) + command.Stdout = &stdout + command.Stderr = &stderr + + err = command.Run() + + if ps.Stdout != nil { + stdout.WriteTo(ps.Stdout) + } + + if ps.Stderr != nil { + stderr.WriteTo(ps.Stderr) + } + + stderrString := strings.TrimSpace(stderr.String()) + + if _, ok := err.(*exec.ExitError); ok { + err = fmt.Errorf("PowerShell error: %s", stderrString) + } + + if len(stderrString) > 0 { + err = fmt.Errorf("PowerShell error: %s", stderrString) + } + + stdoutString := strings.TrimSpace(stdout.String()) + + if verbose && stdoutString != "" { + log.Printf("stdout: %s", stdoutString) + } + + // only write the stderr string if verbose because + // the error string will already be in the err return value. + if verbose && stderrString != "" { + log.Printf("stderr: %s", stderrString) + } + + return stdoutString, err +} + +func IsPowershellAvailable() (bool, string, error) { + path, err := exec.LookPath("powershell") + if err != nil { + return false, "", err + } else { + return true, path, err + } +} + +func (ps *PowerShellCmd) getPowerShellPath() (string, error) { + powershellAvailable, path, err := IsPowershellAvailable() + + if !powershellAvailable { + log.Fatal("Cannot find PowerShell in the path") + return "", err + } + + return path, nil +} + +func saveScript(fileContents string) (string, error) { + file, err := ioutil.TempFile(os.TempDir(), "ps") + if err != nil { + return "", err + } + + _, err = file.Write([]byte(fileContents)) + if err != nil { + return "", err + } + + err = file.Close() + if err != nil { + return "", err + } + + newFilename := file.Name() + ".ps1" + err = os.Rename(file.Name(), newFilename) + if err != nil { + return "", err + } + + return newFilename, nil +} + +func createArgs(filename string, params ...string) []string { + args := make([]string, len(params)+4) + args[0] = "-ExecutionPolicy" + args[1] = "Bypass" + + args[2] = "-File" + args[3] = filename + + for key, value := range params { + args[key+4] = value + } + + return args +} + +func GetHostAvailableMemory() float64 { + + var script = "(Get-WmiObject Win32_OperatingSystem).FreePhysicalMemory / 1024" + + var ps PowerShellCmd + output, _ := ps.Output(script) + + freeMB, _ := strconv.ParseFloat(output, 64) + + return freeMB +} + +func GetHostName(ip string) (string, error) { + + var script = ` +param([string]$ip) +try { + $HostName = [System.Net.Dns]::GetHostEntry($ip).HostName + if ($HostName -ne $null) { + $HostName = $HostName.Split('.')[0] + } + $HostName +} catch { } +` + + // + var ps PowerShellCmd + cmdOut, err := ps.Output(script, ip) + if err != nil { + return "", err + } + + return cmdOut, nil +} + +func IsCurrentUserAnAdministrator() (bool, error) { + var script = ` +$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() +$principal = new-object System.Security.Principal.WindowsPrincipal($identity) +$administratorRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator +return $principal.IsInRole($administratorRole) +` + + var ps PowerShellCmd + cmdOut, err := ps.Output(script) + if err != nil { + return false, err + } + + res := strings.TrimSpace(cmdOut) + return res == powerShellTrue, nil +} + +func ModuleExists(moduleName string) (bool, error) { + + var script = ` +param([string]$moduleName) +(Get-Module -Name $moduleName) -ne $null +` + var ps PowerShellCmd + cmdOut, err := ps.Output(script) + if err != nil { + return false, err + } + + res := strings.TrimSpace(string(cmdOut)) + + if res == powerShellFalse { + err := fmt.Errorf("PowerShell %s module is not loaded. Make sure %s feature is on.", moduleName, moduleName) + return false, err + } + + return true, nil +} + +func HasVirtualMachineVirtualizationExtensions() (bool, error) { + + var script = ` +(GET-Command Set-VMProcessor).parameters.keys -contains "ExposeVirtualizationExtensions" +` + + var ps PowerShellCmd + cmdOut, err := ps.Output(script) + + if err != nil { + return false, err + } + + var hasVirtualMachineVirtualizationExtensions = strings.TrimSpace(cmdOut) == "True" + return hasVirtualMachineVirtualizationExtensions, err +} + +func SetUnattendedProductKey(path string, productKey string) error { + + var script = ` +param([string]$path,[string]$productKey) + +$unattend = [xml](Get-Content -Path $path) +$ns = @{ un = 'urn:schemas-microsoft-com:unattend' } + +$setupNode = $unattend | + Select-Xml -XPath '//un:settings[@pass = "specialize"]/un:component[@name = "Microsoft-Windows-Shell-Setup"]' -Namespace $ns | + Select-Object -ExpandProperty Node + +$productKeyNode = $setupNode | + Select-Xml -XPath '//un:ProductKey' -Namespace $ns | + Select-Object -ExpandProperty Node + +if ($productKeyNode -eq $null) { + $productKeyNode = $unattend.CreateElement('ProductKey', $ns.un) + [Void]$setupNode.AppendChild($productKeyNode) +} + +$productKeyNode.InnerText = $productKey + +$unattend.Save($path) +` + + var ps PowerShellCmd + err := ps.Run(script, path, productKey) + return err +} diff --git a/powershell/powershell_test.go b/powershell/powershell_test.go new file mode 100644 index 00000000000..91c9a83f83c --- /dev/null +++ b/powershell/powershell_test.go @@ -0,0 +1,69 @@ +package powershell + +import ( + "bytes" + "testing" +) + +func TestOutput(t *testing.T) { + + var ps PowerShellCmd + + powershellAvailable, _, _ := IsPowershellAvailable() + + if !powershellAvailable { + t.Skipf("powershell not installed") + return + } + + cmdOut, err := ps.Output("") + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if cmdOut != "" { + t.Fatalf("output '%v' is not ''", cmdOut) + } + + trueOutput, err := ps.Output("$True") + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if trueOutput != "True" { + t.Fatalf("output '%v' is not 'True'", trueOutput) + } + + falseOutput, err := ps.Output("$False") + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if falseOutput != "False" { + t.Fatalf("output '%v' is not 'False'", falseOutput) + } +} + +func TestRunFile(t *testing.T) { + var ps PowerShellCmd + + powershellAvailable, _, _ := IsPowershellAvailable() + + if !powershellAvailable { + t.Skipf("powershell not installed") + return + } + + var blockBuffer bytes.Buffer + blockBuffer.WriteString(`param([string]$a, [string]$b, [int]$x, [int]$y) if (Test-Path variable:global:ProgressPreference){$ProgressPreference="SilentlyContinue"}; $n = $x + $y; Write-Output "$a $b $n";`) + + cmdOut, err := ps.Output(blockBuffer.String(), "a", "b", "5", "10") + + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if cmdOut != "a b 15" { + t.Fatalf("output '%v' is not 'a b 15'", cmdOut) + } +} diff --git a/powershell/scriptbuilder.go b/powershell/scriptbuilder.go new file mode 100644 index 00000000000..d51156aa2a0 --- /dev/null +++ b/powershell/scriptbuilder.go @@ -0,0 +1,30 @@ +package powershell + +import ( + "bytes" +) + +type ScriptBuilder struct { + buffer bytes.Buffer +} + +func (b *ScriptBuilder) WriteLine(s string) (n int, err error) { + n, err = b.buffer.WriteString(s); + b.buffer.WriteString("\n") + + return n+1, err +} + +func (b *ScriptBuilder) WriteString(s string) (n int, err error) { + n, err = b.buffer.WriteString(s); + return n, err +} + +func (b *ScriptBuilder) String() string { + return b.buffer.String() +} + +func (b *ScriptBuilder) Reset() { + b.buffer.Reset() +} + diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go index 97623dda7b6..fe27e572779 100644 --- a/provisioner/ansible/provisioner.go +++ b/provisioner/ansible/provisioner.go @@ -17,6 +17,7 @@ import ( "os/exec" "path/filepath" "regexp" + "runtime" "strconv" "strings" "sync" @@ -131,6 +132,9 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { if p.config.User == "" { p.config.User = os.Getenv("USER") } + if p.config.User == "" { + p.config.User = os.Getenv("USERNAME") + } if p.config.User == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("user: could not determine current user from environment.")) } @@ -142,16 +146,26 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } func (p *Provisioner) getVersion() error { - out, err := exec.Command(p.config.Command, "--version").Output() + var output []byte + var err error + + if runtime.GOOS == "windows" && strings.HasSuffix(p.config.Command, ".sh") { + output, err = exec.Command("sh", p.config.Command, "--version").Output() + } else { + output, err = exec.Command(p.config.Command, "--version").Output() + } + if err != nil { return err } + out := string(output) + versionRe := regexp.MustCompile(`\w (\d+\.\d+[.\d+]*)`) - matches := versionRe.FindStringSubmatch(string(out)) + matches := versionRe.FindStringSubmatch(out) if matches == nil { return fmt.Errorf( - "Could not find %s version in output:\n%s", p.config.Command, string(out)) + "Could not find %s version in output:\n%s", p.config.Command, out) } version := matches[1] diff --git a/provisioner/powershell/elevated.go b/provisioner/powershell/elevated.go index 00bc72e4adf..80bdc005a6d 100644 --- a/provisioner/powershell/elevated.go +++ b/provisioner/powershell/elevated.go @@ -58,6 +58,7 @@ $t.XmlText = @' '@ +if (Test-Path variable:global:ProgressPreference){$ProgressPreference="SilentlyContinue"} $f = $s.GetFolder("\") $f.RegisterTaskDefinition($name, $t, 6, "{{.User}}", "{{.Password}}", 1, $null) | Out-Null $t = $f.GetTask("\$name") @@ -68,19 +69,16 @@ while ((!($t.state -eq 4)) -and ($sec -lt $timeout)) { Start-Sleep -s 1 $sec++ } -function SlurpOutput($l) { - if (Test-Path $log) { - Get-Content $log | select -skip $l | ForEach { - $l += 1 - Write-Host "$_" - } - } - return $l -} + $line = 0 do { Start-Sleep -m 100 - $line = SlurpOutput $line + if (Test-Path $log) { + Get-Content $log | select -skip $line | ForEach { + $line += 1 + Write-Output "$_" + } + } } while (!($t.state -eq 3)) $result = $t.LastTaskResult [System.Runtime.Interopservices.Marshal]::ReleaseComObject($s) | Out-Null diff --git a/provisioner/powershell/powershell.go b/provisioner/powershell/powershell.go index 1f5a7ffad64..086e3e5542e 100644 --- a/provisioner/powershell/powershell.go +++ b/provisioner/powershell/powershell.go @@ -2,16 +2,53 @@ package powershell import ( "encoding/base64" + "encoding/binary" + "unicode/utf16" + "unicode/utf8" + + "golang.org/x/text/encoding/unicode" ) -func powershellEncode(buffer []byte) string { - // 2 byte chars to make PowerShell happy - wideCmd := "" - for _, b := range buffer { - wideCmd += string(b) + "\x00" +func convertUtf8ToUtf16LE(message string) (string, error) { + utf16le := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) + utfEncoder := utf16le.NewEncoder() + ut16LeEncodedMessage, err := utfEncoder.String(message) + + return ut16LeEncodedMessage, err +} + +// UTF16BytesToString converts UTF-16 encoded bytes, in big or little endian byte order, +// to a UTF-8 encoded string. +func UTF16BytesToString(b []byte, o binary.ByteOrder) string { + utf := make([]uint16, (len(b)+(2-1))/2) + for i := 0; i+(2-1) < len(b); i += 2 { + utf[i/2] = o.Uint16(b[i:]) + } + if len(b)/2 < len(utf) { + utf[len(utf)-1] = utf8.RuneError + } + return string(utf16.Decode(utf)) +} + +func powershellEncode(message string) (string, error) { + utf16LEEncodedMessage, err := convertUtf8ToUtf16LE(message) + if err != nil { + return "", err } // Base64 encode the command - input := []uint8(wideCmd) - return base64.StdEncoding.EncodeToString(input) + input := []uint8(utf16LEEncodedMessage) + return base64.StdEncoding.EncodeToString(input), nil +} + +func powershellDecode(messageBase64 string) (retour string, err error) { + messageUtf16LeByteArray, err := base64.StdEncoding.DecodeString(messageBase64) + + if err != nil { + return "", err + } + + message := UTF16BytesToString(messageUtf16LeByteArray, binary.LittleEndian) + + return message, nil } diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go index a862ef9b3d5..ba0e2def3fd 100644 --- a/provisioner/powershell/provisioner.go +++ b/provisioner/powershell/provisioner.go @@ -107,12 +107,13 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { }, }, }, raws...) + if err != nil { return err } if p.config.EnvVarFormat == "" { - p.config.EnvVarFormat = `$env:%s=\"%s\"; ` + p.config.EnvVarFormat = `$env:%s="%s"; ` } if p.config.ElevatedEnvVarFormat == "" { @@ -120,11 +121,11 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } if p.config.ExecuteCommand == "" { - p.config.ExecuteCommand = `powershell "& { {{.Vars}}{{.Path}}; exit $LastExitCode}"` + p.config.ExecuteCommand = `if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};{{.Vars}}&'{{.Path}}';exit $LastExitCode` } if p.config.ElevatedExecuteCommand == "" { - p.config.ElevatedExecuteCommand = `{{.Vars}}{{.Path}}` + p.config.ElevatedExecuteCommand = `if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};{{.Vars}}&'{{.Path}}';exit $LastExitCode` } if p.config.Inline != nil && len(p.config.Inline) == 0 { @@ -347,8 +348,9 @@ func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string, e // Split vars into key/value components for _, envVar := range p.config.Vars { keyValue := strings.Split(envVar, "=") - if len(keyValue) != 2 { - err = errors.New("Shell provisioner environment variables must be in key=value format") + + if len(keyValue) != 2 || keyValue[0] == "" { + err = errors.New(fmt.Sprintf("Shell provisioner environment variables must be in key=value format. Currently it is '%s'", envVar)) return } envVars[keyValue[0]] = keyValue[1] @@ -373,28 +375,57 @@ func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string, e } func (p *Provisioner) createCommandText() (command string, err error) { + // Return the interpolated command + if p.config.ElevatedUser == "" { + return p.createCommandTextNonPrivileged() + } else { + return p.createCommandTextPrivileged() + } +} + +func (p *Provisioner) createCommandTextNonPrivileged() (command string, err error) { // Create environment variables to set before executing the command flattenedEnvVars, err := p.createFlattenedEnvVars(false) if err != nil { return "", err } - p.config.ctx.Data = &ExecuteCommandTemplate{ Vars: flattenedEnvVars, Path: p.config.RemotePath, } command, err = interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) + if err != nil { return "", fmt.Errorf("Error processing command: %s", err) } - // Return the interpolated command - if p.config.ElevatedUser == "" { - return command, nil + commandText, err := p.generateCommandLineRunner(command) + if err != nil { + return "", fmt.Errorf("Error generating command line runner: %s", err) } + return commandText, err +} + +func (p *Provisioner) generateCommandLineRunner(command string) (commandText string, err error) { + log.Printf("Building command line for: %s", command) + + base64EncodedCommand, err := powershellEncode(command) + if err != nil { + return "", fmt.Errorf("Error encoding command: %s", err) + } + + commandText = "powershell -executionpolicy bypass -encodedCommand " + base64EncodedCommand + + return commandText, nil +} + +func (p *Provisioner) createCommandTextPrivileged() (command string, err error) { // Can't double escape the env vars, lets create shiny new ones - flattenedEnvVars, err = p.createFlattenedEnvVars(true) + flattenedEnvVars, err := p.createFlattenedEnvVars(true) + if err != nil { + return "", err + } p.config.ctx.Data = &ExecuteCommandTemplate{ Vars: flattenedEnvVars, Path: p.config.RemotePath, @@ -407,11 +438,14 @@ func (p *Provisioner) createCommandText() (command string, err error) { // OK so we need an elevated shell runner to wrap our command, this is going to have its own path // generate the script and update the command runner in the process path, err := p.generateElevatedRunner(command) + if err != nil { + return "", fmt.Errorf("Error generating elevated runner: %s", err) + } // Return the path to the elevated shell wrapper command = fmt.Sprintf("powershell -executionpolicy bypass -file \"%s\"", path) - return + return command, err } func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath string, err error) { @@ -419,12 +453,18 @@ func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath strin // generate command var buffer bytes.Buffer + + base64EncodedCommand, err := powershellEncode(command) + if err != nil { + return "", fmt.Errorf("Error encoding command: %s", err) + } + err = elevatedTemplate.Execute(&buffer, elevatedOptions{ User: p.config.ElevatedUser, Password: p.config.ElevatedPassword, TaskDescription: "Packer elevated task", TaskName: fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()), - EncodedCommand: powershellEncode([]byte(command + "; exit $LASTEXITCODE")), + EncodedCommand: base64EncodedCommand, }) if err != nil { diff --git a/provisioner/powershell/provisioner_test.go b/provisioner/powershell/provisioner_test.go index 78484c18415..473fa1d8efa 100644 --- a/provisioner/powershell/provisioner_test.go +++ b/provisioner/powershell/provisioner_test.go @@ -75,12 +75,12 @@ func TestProvisionerPrepare_Defaults(t *testing.T) { t.Error("expected elevated_password to be empty") } - if p.config.ExecuteCommand != "powershell \"& { {{.Vars}}{{.Path}}; exit $LastExitCode}\"" { - t.Fatalf("Default command should be powershell \"& { {{.Vars}}{{.Path}}; exit $LastExitCode}\", but got %s", p.config.ExecuteCommand) + if p.config.ExecuteCommand != `if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};{{.Vars}}&'{{.Path}}';exit $LastExitCode` { + t.Fatalf(`Default command should be "if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};{{.Vars}}&'{{.Path}}';exit $LastExitCode", but got %s`, p.config.ExecuteCommand) } - if p.config.ElevatedExecuteCommand != "{{.Vars}}{{.Path}}" { - t.Fatalf("Default command should be powershell {{.Vars}}{{.Path}}, but got %s", p.config.ElevatedExecuteCommand) + if p.config.ElevatedExecuteCommand != `if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};{{.Vars}}&'{{.Path}}';exit $LastExitCode` { + t.Fatalf(`Default command should be "if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};{{.Vars}}&'{{.Path}}';exit $LastExitCode", but got %s`, p.config.ElevatedExecuteCommand) } if p.config.ValidExitCodes == nil { @@ -96,7 +96,7 @@ func TestProvisionerPrepare_Defaults(t *testing.T) { } if p.config.ElevatedEnvVarFormat != `$env:%s="%s"; ` { - t.Fatalf("Default command should be powershell \"{{.Vars}}{{.Path}}\", but got %s", p.config.ElevatedEnvVarFormat) + t.Fatalf(`Default command should be powershell '$env:%%s="%%s"; ', but got %s`, p.config.ElevatedEnvVarFormat) } } @@ -328,7 +328,7 @@ func TestProvisionerProvision_ValidExitCodes(t *testing.T) { delete(config, "inline") // Defaults provided by Packer - config["remote_path"] = "c:/Windows/Temp/inlineScript.bat" + config["remote_path"] = "c:/Windows/Temp/inlineScript.ps1" config["inline"] = []string{"whoami"} ui := testUi() p := new(Provisioner) @@ -351,7 +351,7 @@ func TestProvisionerProvision_InvalidExitCodes(t *testing.T) { delete(config, "inline") // Defaults provided by Packer - config["remote_path"] = "c:/Windows/Temp/inlineScript.bat" + config["remote_path"] = "c:/Windows/Temp/inlineScript.ps1" config["inline"] = []string{"whoami"} ui := testUi() p := new(Provisioner) @@ -374,7 +374,7 @@ func TestProvisionerProvision_Inline(t *testing.T) { delete(config, "inline") // Defaults provided by Packer - config["remote_path"] = "c:/Windows/Temp/inlineScript.bat" + config["remote_path"] = "c:/Windows/Temp/inlineScript.ps1" config["inline"] = []string{"whoami"} ui := testUi() p := new(Provisioner) @@ -389,18 +389,30 @@ func TestProvisionerProvision_Inline(t *testing.T) { t.Fatal("should not have error") } - expectedCommand := `powershell "& { $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; c:/Windows/Temp/inlineScript.bat; exit $LastExitCode}"` + expectedCommand := `if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};$env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; &'c:/Windows/Temp/inlineScript.ps1';exit $LastExitCode` + expectedCommandBase64Encoded := `aQBmACAAKABUAGUAcwB0AC0AUABhAHQAaAAgAHYAYQByAGkAYQBiAGwAZQA6AGcAbABvAGIAYQBsADoAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAKQB7ACQAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAPQAnAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAJwB9ADsAJABlAG4AdgA6AFAAQQBDAEsARQBSAF8AQgBVAEkATABEAEUAUgBfAFQAWQBQAEUAPQAiAGkAcwBvACIAOwAgACQAZQBuAHYAOgBQAEEAQwBLAEUAUgBfAEIAVQBJAEwARABfAE4AQQBNAEUAPQAiAHYAbQB3AGEAcgBlACIAOwAgACYAJwBjADoALwBXAGkAbgBkAG8AdwBzAC8AVABlAG0AcAAvAGkAbgBsAGkAbgBlAFMAYwByAGkAcAB0AC4AcABzADEAJwA7AGUAeABpAHQAIAAkAEwAYQBzAHQARQB4AGkAdABDAG8AZABlAA==` + expectedCommandPrefix := `powershell -executionpolicy bypass -encodedCommand ` + expectedCommandEncoded := expectedCommandPrefix + expectedCommandBase64Encoded - // Should run the command without alteration - if comm.StartCmd.Command != expectedCommand { - t.Fatalf("Expect command to be: %s, got %s", expectedCommand, comm.StartCmd.Command) + actualCommandWithoutPrefix := strings.Replace(comm.StartCmd.Command, expectedCommandPrefix, "", -1) + actualCommandDecoded, err := powershellDecode(actualCommandWithoutPrefix) + if err != nil { + t.Fatal("should not have error when base64 decoding") + } + + if actualCommandDecoded != expectedCommand { + t.Fatalf("Expected decoded: %s, got %s", expectedCommand, actualCommandDecoded) + } + + if comm.StartCmd.Command != expectedCommandEncoded { + t.Fatalf("Expect command to be: %s, got %s", expectedCommandEncoded, comm.StartCmd.Command) } envVars := make([]string, 2) envVars[0] = "FOO=BAR" envVars[1] = "BAR=BAZ" config["environment_vars"] = envVars - config["remote_path"] = "c:/Windows/Temp/inlineScript.bat" + config["remote_path"] = "c:/Windows/Temp/inlineScript.ps1" p.Prepare(config) err = p.Provision(ui, comm) @@ -408,11 +420,23 @@ func TestProvisionerProvision_Inline(t *testing.T) { t.Fatal("should not have error") } - expectedCommand = `powershell "& { $env:BAR=\"BAZ\"; $env:FOO=\"BAR\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; c:/Windows/Temp/inlineScript.bat; exit $LastExitCode}"` + expectedCommand = `if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};$env:BAR="BAZ"; $env:FOO="BAR"; $env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; &'c:/Windows/Temp/inlineScript.ps1';exit $LastExitCode` + expectedCommandBase64Encoded = `aQBmACAAKABUAGUAcwB0AC0AUABhAHQAaAAgAHYAYQByAGkAYQBiAGwAZQA6AGcAbABvAGIAYQBsADoAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAKQB7ACQAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAPQAnAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAJwB9ADsAJABlAG4AdgA6AEIAQQBSAD0AIgBCAEEAWgAiADsAIAAkAGUAbgB2ADoARgBPAE8APQAiAEIAQQBSACIAOwAgACQAZQBuAHYAOgBQAEEAQwBLAEUAUgBfAEIAVQBJAEwARABFAFIAXwBUAFkAUABFAD0AIgBpAHMAbwAiADsAIAAkAGUAbgB2ADoAUABBAEMASwBFAFIAXwBCAFUASQBMAEQAXwBOAEEATQBFAD0AIgB2AG0AdwBhAHIAZQAiADsAIAAmACcAYwA6AC8AVwBpAG4AZABvAHcAcwAvAFQAZQBtAHAALwBpAG4AbABpAG4AZQBTAGMAcgBpAHAAdAAuAHAAcwAxACcAOwBlAHgAaQB0ACAAJABMAGEAcwB0AEUAeABpAHQAQwBvAGQAZQA=` + expectedCommandPrefix = `powershell -executionpolicy bypass -encodedCommand ` + expectedCommandEncoded = expectedCommandPrefix + expectedCommandBase64Encoded + + actualCommandWithoutPrefix = strings.Replace(comm.StartCmd.Command, expectedCommandPrefix, "", -1) + actualCommandDecoded, err = powershellDecode(actualCommandWithoutPrefix) + if err != nil { + t.Fatal("should not have error when base64 decoding") + } + + if actualCommandDecoded != expectedCommand { + t.Fatalf("Expected decoded: %s, got %s", expectedCommand, actualCommandDecoded) + } - // Should run the command without alteration - if comm.StartCmd.Command != expectedCommand { - t.Fatalf("Expect command to be: %s, got: %s", expectedCommand, comm.StartCmd.Command) + if comm.StartCmd.Command != expectedCommandEncoded { + t.Fatalf("Expect command to be: %s, got %s", expectedCommandEncoded, comm.StartCmd.Command) } } @@ -434,12 +458,23 @@ func TestProvisionerProvision_Scripts(t *testing.T) { t.Fatal("should not have error") } - //powershell -Command "$env:PACKER_BUILDER_TYPE=''"; powershell -Command "$env:PACKER_BUILD_NAME='foobuild'"; powershell -Command c:/Windows/Temp/script.ps1 - expectedCommand := `powershell "& { $env:PACKER_BUILDER_TYPE=\"footype\"; $env:PACKER_BUILD_NAME=\"foobuild\"; c:/Windows/Temp/script.ps1; exit $LastExitCode}"` + expectedCommand := `if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};$env:PACKER_BUILDER_TYPE="footype"; $env:PACKER_BUILD_NAME="foobuild"; &'c:/Windows/Temp/script.ps1';exit $LastExitCode` + expectedCommandBase64Encoded := `aQBmACAAKABUAGUAcwB0AC0AUABhAHQAaAAgAHYAYQByAGkAYQBiAGwAZQA6AGcAbABvAGIAYQBsADoAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAKQB7ACQAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAPQAnAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAJwB9ADsAJABlAG4AdgA6AFAAQQBDAEsARQBSAF8AQgBVAEkATABEAEUAUgBfAFQAWQBQAEUAPQAiAGYAbwBvAHQAeQBwAGUAIgA7ACAAJABlAG4AdgA6AFAAQQBDAEsARQBSAF8AQgBVAEkATABEAF8ATgBBAE0ARQA9ACIAZgBvAG8AYgB1AGkAbABkACIAOwAgACYAJwBjADoALwBXAGkAbgBkAG8AdwBzAC8AVABlAG0AcAAvAHMAYwByAGkAcAB0AC4AcABzADEAJwA7AGUAeABpAHQAIAAkAEwAYQBzAHQARQB4AGkAdABDAG8AZABlAA==` + expectedCommandPrefix := `powershell -executionpolicy bypass -encodedCommand ` + expectedCommandEncoded := expectedCommandPrefix + expectedCommandBase64Encoded + + actualCommandWithoutPrefix := strings.Replace(comm.StartCmd.Command, expectedCommandPrefix, "", -1) + actualCommandDecoded, err := powershellDecode(actualCommandWithoutPrefix) + if err != nil { + t.Fatal("should not have error when base64 decoding") + } + + if actualCommandDecoded != expectedCommand { + t.Fatalf("Expected decoded: %s, got %s", expectedCommand, actualCommandDecoded) + } - // Should run the command without alteration - if comm.StartCmd.Command != expectedCommand { - t.Fatalf("Expect command to be %s NOT %s", expectedCommand, comm.StartCmd.Command) + if comm.StartCmd.Command != expectedCommandEncoded { + t.Fatalf("Expect command to be: %s, got %s", expectedCommandEncoded, comm.StartCmd.Command) } } @@ -468,11 +503,23 @@ func TestProvisionerProvision_ScriptsWithEnvVars(t *testing.T) { t.Fatal("should not have error") } - expectedCommand := `powershell "& { $env:BAR=\"BAZ\"; $env:FOO=\"BAR\"; $env:PACKER_BUILDER_TYPE=\"footype\"; $env:PACKER_BUILD_NAME=\"foobuild\"; c:/Windows/Temp/script.ps1; exit $LastExitCode}"` + expectedCommand := `if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};$env:BAR="BAZ"; $env:FOO="BAR"; $env:PACKER_BUILDER_TYPE="footype"; $env:PACKER_BUILD_NAME="foobuild"; &'c:/Windows/Temp/script.ps1';exit $LastExitCode` + expectedCommandBase64Encoded := `aQBmACAAKABUAGUAcwB0AC0AUABhAHQAaAAgAHYAYQByAGkAYQBiAGwAZQA6AGcAbABvAGIAYQBsADoAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAKQB7ACQAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAPQAnAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAJwB9ADsAJABlAG4AdgA6AEIAQQBSAD0AIgBCAEEAWgAiADsAIAAkAGUAbgB2ADoARgBPAE8APQAiAEIAQQBSACIAOwAgACQAZQBuAHYAOgBQAEEAQwBLAEUAUgBfAEIAVQBJAEwARABFAFIAXwBUAFkAUABFAD0AIgBmAG8AbwB0AHkAcABlACIAOwAgACQAZQBuAHYAOgBQAEEAQwBLAEUAUgBfAEIAVQBJAEwARABfAE4AQQBNAEUAPQAiAGYAbwBvAGIAdQBpAGwAZAAiADsAIAAmACcAYwA6AC8AVwBpAG4AZABvAHcAcwAvAFQAZQBtAHAALwBzAGMAcgBpAHAAdAAuAHAAcwAxACcAOwBlAHgAaQB0ACAAJABMAGEAcwB0AEUAeABpAHQAQwBvAGQAZQA=` + expectedCommandPrefix := `powershell -executionpolicy bypass -encodedCommand ` + expectedCommandEncoded := expectedCommandPrefix + expectedCommandBase64Encoded + + actualCommandWithoutPrefix := strings.Replace(comm.StartCmd.Command, expectedCommandPrefix, "", -1) + actualCommandDecoded, err := powershellDecode(actualCommandWithoutPrefix) + if err != nil { + t.Fatal("should not have error when base64 decoding") + } - // Should run the command without alteration - if comm.StartCmd.Command != expectedCommand { - t.Fatalf("Expect command to be %s NOT %s", expectedCommand, comm.StartCmd.Command) + if actualCommandDecoded != expectedCommand { + t.Fatalf("Expected decoded: %s, got %s", expectedCommand, actualCommandDecoded) + } + + if comm.StartCmd.Command != expectedCommandEncoded { + t.Fatalf("Expect command to be: %s, got %s", expectedCommandEncoded, comm.StartCmd.Command) } } @@ -500,7 +547,7 @@ func TestProvisioner_createFlattenedElevatedEnvVars_windows(t *testing.T) { if err != nil { t.Fatalf("should not have error creating flattened env vars: %s", err) } - if flattenedEnvVars != "$env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; " { + if flattenedEnvVars != `$env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; ` { t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars) } @@ -511,7 +558,7 @@ func TestProvisioner_createFlattenedElevatedEnvVars_windows(t *testing.T) { if err != nil { t.Fatalf("should not have error creating flattened env vars: %s", err) } - if flattenedEnvVars != "$env:FOO=\"bar\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; " { + if flattenedEnvVars != `$env:FOO="bar"; $env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; ` { t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars) } @@ -522,7 +569,7 @@ func TestProvisioner_createFlattenedElevatedEnvVars_windows(t *testing.T) { if err != nil { t.Fatalf("should not have error creating flattened env vars: %s", err) } - if flattenedEnvVars != "$env:BAZ=\"qux\"; $env:FOO=\"bar\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; " { + if flattenedEnvVars != `$env:BAZ="qux"; $env:FOO="bar"; $env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; ` { t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars) } } @@ -545,7 +592,7 @@ func TestProvisioner_createFlattenedEnvVars_windows(t *testing.T) { if err != nil { t.Fatalf("should not have error creating flattened env vars: %s", err) } - if flattenedEnvVars != "$env:PACKER_BUILDER_TYPE=\\\"iso\\\"; $env:PACKER_BUILD_NAME=\\\"vmware\\\"; " { + if flattenedEnvVars != `$env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; ` { t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars) } @@ -556,7 +603,7 @@ func TestProvisioner_createFlattenedEnvVars_windows(t *testing.T) { if err != nil { t.Fatalf("should not have error creating flattened env vars: %s", err) } - if flattenedEnvVars != "$env:FOO=\\\"bar\\\"; $env:PACKER_BUILDER_TYPE=\\\"iso\\\"; $env:PACKER_BUILD_NAME=\\\"vmware\\\"; " { + if flattenedEnvVars != `$env:FOO="bar"; $env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; ` { t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars) } @@ -567,7 +614,7 @@ func TestProvisioner_createFlattenedEnvVars_windows(t *testing.T) { if err != nil { t.Fatalf("should not have error creating flattened env vars: %s", err) } - if flattenedEnvVars != "$env:BAZ=\\\"qux\\\"; $env:FOO=\\\"bar\\\"; $env:PACKER_BUILDER_TYPE=\\\"iso\\\"; $env:PACKER_BUILD_NAME=\\\"vmware\\\"; " { + if flattenedEnvVars != `$env:BAZ="qux"; $env:FOO="bar"; $env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; ` { t.Fatalf("unexpected flattened env vars: %s", flattenedEnvVars) } } @@ -582,8 +629,25 @@ func TestProvision_createCommandText(t *testing.T) { // Non-elevated cmd, _ := p.createCommandText() - if cmd != "powershell \"& { $env:PACKER_BUILDER_TYPE=\\\"\\\"; $env:PACKER_BUILD_NAME=\\\"\\\"; c:/Windows/Temp/script.ps1; exit $LastExitCode}\"" { - t.Fatalf("Got unexpected non-elevated command: %s", cmd) + + expectedCommand := `if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};$env:PACKER_BUILDER_TYPE=""; $env:PACKER_BUILD_NAME=""; &'c:/Windows/Temp/script.ps1';exit $LastExitCode` + expectedCommandBase64Encoded := `aQBmACAAKABUAGUAcwB0AC0AUABhAHQAaAAgAHYAYQByAGkAYQBiAGwAZQA6AGcAbABvAGIAYQBsADoAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAKQB7ACQAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAPQAnAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAJwB9ADsAJABlAG4AdgA6AFAAQQBDAEsARQBSAF8AQgBVAEkATABEAEUAUgBfAFQAWQBQAEUAPQAiACIAOwAgACQAZQBuAHYAOgBQAEEAQwBLAEUAUgBfAEIAVQBJAEwARABfAE4AQQBNAEUAPQAiACIAOwAgACYAJwBjADoALwBXAGkAbgBkAG8AdwBzAC8AVABlAG0AcAAvAHMAYwByAGkAcAB0AC4AcABzADEAJwA7AGUAeABpAHQAIAAkAEwAYQBzAHQARQB4AGkAdABDAG8AZABlAA==` + expectedCommandPrefix := `powershell -executionpolicy bypass -encodedCommand ` + expectedCommandEncoded := expectedCommandPrefix + expectedCommandBase64Encoded + + actualCommandWithoutPrefix := strings.Replace(cmd, expectedCommandPrefix, "", -1) + + actualCommandDecoded, err := powershellDecode(actualCommandWithoutPrefix) + if err != nil { + t.Fatal("should not have error when base64 decoding") + } + + if actualCommandDecoded != expectedCommand { + t.Fatalf("Expected decoded: %s, got %s", expectedCommand, actualCommandDecoded) + } + + if cmd != expectedCommandEncoded { + t.Fatalf("Expect command to be: %s, got %s", expectedCommandEncoded, cmd) } // Elevated diff --git a/provisioner/windows-shell/provisioner.go b/provisioner/windows-shell/provisioner.go index 6e65c8c2046..17abcd2d571 100644 --- a/provisioner/windows-shell/provisioner.go +++ b/provisioner/windows-shell/provisioner.go @@ -305,10 +305,10 @@ func (p *Provisioner) createFlattenedEnvVars() (flattened string, err error) { // Split vars into key/value components for _, envVar := range p.config.Vars { keyValue := strings.Split(envVar, "=") - if len(keyValue) != 2 { - err = errors.New("Shell provisioner environment variables must be in key=value format") + if len(keyValue) != 2 || keyValue[0] == "" { + err = errors.New(fmt.Sprintf("Shell provisioner environment variables must be in key=value format. Currently it is '%s'", envVar)) return - } + } envVars[keyValue[0]] = keyValue[1] } // Create a list of env var keys in sorted order diff --git a/website/source/docs/builders/hyperv-iso.html.markdown b/website/source/docs/builders/hyperv-iso.html.markdown new file mode 100644 index 00000000000..621ee74952a --- /dev/null +++ b/website/source/docs/builders/hyperv-iso.html.markdown @@ -0,0 +1,1004 @@ +--- +description: |- + The HyperV Packer builder is able to create HyperV virtual machines and export them. +layout: "docs" +page_title: "HyperV Builder (from an ISO)" +--- + +# HyperV Builder (from an ISO) + +Type: `hyperv-iso` + +The HyperV Packer builder is able to create [HyperV](https://www.microsoft.com/en-us/server-cloud/solutions/virtualization.aspx) +virtual machines and export them, starting from an ISO image. + +The builder builds a virtual machine by creating a new virtual machine +from scratch, booting it, installing an OS, provisioning software within +the OS, then shutting it down. The result of the HyperV builder is a directory +containing all the files necessary to run the virtual machine portably. + +## Basic Example + +Here is a basic example. This example is not functional. It will start the +OS installer but then fail because we don't provide the preseed file for +Ubuntu to self-install. Still, the example serves to show the basic configuration: + +```javascript +{ + "type": "hyperv-iso", + "guest_os_type": "Ubuntu_64", + "iso_url": "http://releases.ubuntu.com/12.04/ubuntu-12.04.5-server-amd64.iso", + "iso_checksum": "769474248a3897f4865817446f9a4a53", + "iso_checksum_type": "md5", + "ssh_username": "packer", + "ssh_password": "packer", + "shutdown_command": "echo 'packer' | sudo -S shutdown -P now" +} +``` + +It is important to add a `shutdown_command`. By default Packer halts the +virtual machine and the file system may not be sync'd. Thus, changes made in a +provisioner might not be saved. + +## Configuration Reference + +There are many configuration options available for the HyperV builder. +They are organized below into two categories: required and optional. Within +each category, the available options are alphabetized and described. + +In addition to the options listed here, a +[communicator](/docs/templates/communicator.html) +can be configured for this builder. + + + + + + + + +### Required: + +- `iso_checksum` (string) - The checksum for the OS ISO file. Because ISO + files are so large, this is required and Packer will verify it prior + to booting a virtual machine with the ISO attached. The type of the + checksum is specified with `iso_checksum_type`, documented below. + +- `iso_checksum_type` (string) - The type of the checksum specified in + `iso_checksum`. Valid values are "none", "md5", "sha1", "sha256", or + "sha512" currently. While "none" will skip checksumming, this is not + recommended since ISO files are generally large and corruption does happen + from time to time. + +- `iso_url` (string) - A URL to the ISO containing the installation image. + This URL can be either an HTTP URL or a file URL (or path to a file). + If this is an HTTP URL, Packer will download it and cache it between + runs. + +### Optional: + +- `boot_command` (array of strings) - This is an array of commands to type + when the virtual machine is first booted. The goal of these commands should + be to type just enough to initialize the operating system installer. Special + keys can be typed as well, and are covered in the section below on the boot + command. If this is not specified, it is assumed the installer will start + itself. + +- `boot_wait` (string) - The time to wait after booting the initial virtual + machine before typing the `boot_command`. The value of this should be + a duration. Examples are "5s" and "1m30s" which will cause Packer to wait + five seconds and one minute 30 seconds, respectively. If this isn't specified, + the default is 10 seconds. + +- `cpu` (int) - The number of cpus the virtual machine should use. If this isn't specified, + the default is 1 cpu. + +- `disk_size` (integer) - The size, in megabytes, of the hard disk to create + for the VM. By default, this is 40000 (about 40 GB). + +- `enable_mac_spoofing` (bool) - If true enable mac spoofing for virtual machine. + This defaults to false. + +- `enable_dynamic_memory` (bool) - If true enable dynamic memory for virtual machine. + This defaults to false. + +- `enable_secure_boot` (bool) - If true enable secure boot for virtual machine. + This defaults to false. + +- `enable_virtualization_extensions` (bool) - If true enable virtualization extensions for virtual machine. + This defaults to false. For nested virtualization you need to enable mac spoofing, disable dynamic memory + and have at least 4GB of RAM for virtual machine. + +- `floppy_files` (array of strings) - A list of files to place onto a floppy + disk that is attached when the VM is booted. This is most useful + for unattended Windows installs, which look for an `Autounattend.xml` file + on removable media. By default, no floppy will be attached. All files + listed in this setting get placed into the root directory of the floppy + and the floppy is attached as the first floppy device. Currently, no + support exists for creating sub-directories on the floppy. Wildcard + characters (*, ?, and []) are allowed. Directory names are also allowed, + which will add all the files found in the directory to the floppy. + +- `generation` (int) - The HyperV generation for the virtual machine. By + default, this is 1. Generation 2 HyperV virtual machines do not support + floppy drives. In this scenario use secondary_iso_images instead. Hard + drives and dvd drives will also be scsi and not ide. + +- `http_directory` (string) - Path to a directory to serve using an HTTP + server. The files in this directory will be available over HTTP that will + be requestable from the virtual machine. This is useful for hosting + kickstart files and so on. By default this is "", which means no HTTP + server will be started. The address and port of the HTTP server will be + available as variables in `boot_command`. This is covered in more detail + below. + +- `http_port_min` and `http_port_max` (integer) - These are the minimum and + maximum port to use for the HTTP server started to serve the `http_directory`. + Because Packer often runs in parallel, Packer will choose a randomly available + port in this range to run the HTTP server. If you want to force the HTTP + server to be on one port, make this minimum and maximum port the same. + By default the values are 8000 and 9000, respectively. + +- `ip_address_timeout` (string) - The time to wait after creating the initial virtual + machine and waiting for an ip address before assuming there is an error in the process. + The value of this should be a duration. Examples are "5s" and "1m30s" which will cause Packer to wait + five seconds and one minute 30 seconds, respectively. If this isn't specified, + the default is 10 seconds. + +- `ip_address` (string) - Static IP address to be set to the VM. This is particulary useful when you + don't have a DHCP server available, like when you are using a NAT switch in HyperV. + +- `subnet_mask` (string) - Subnet mask of the IP address set for the virtual machine. + +- `gateway` (string) - Default Gateway of the virtual machine + +- `dns_server` (string) - DNS server IP address for name resolution by the VM, need to be + accessible from the virtual network. + +- `iso_urls` (array of strings) - Multiple URLs for the ISO to download. + Packer will try these in order. If anything goes wrong attempting to download + or while downloading a single URL, it will move on to the next. All URLs + must point to the same file (same checksum). By default this is empty + and `iso_url` is used. Only one of `iso_url` or `iso_urls` can be specified. + +- `output_directory` (string) - This is the path to the directory where the + resulting virtual machine will be created. This may be relative or absolute. + If relative, the path is relative to the working directory when `packer` + is executed. This directory must not exist or be empty prior to running the builder. + By default this is "output-BUILDNAME" where "BUILDNAME" is the name + of the build. + +* `secondary_iso_images` (array of strings) - A list of files to place onto a floppy + disk that is attached when the VM is booted. This is most useful + for unattended Windows installs, which look for an `Autounattend.xml` file + on removable media. By default, no floppy will be attached. All files + listed in this setting get placed into the root directory of the floppy + and the floppy is attached as the first floppy device. Currently, no + support exists for creating sub-directories on the floppy. Wildcard + characters (*, ?, and []) are allowed. Directory names are also allowed, + which will add all the files found in the directory to the floppy. + +- `shutdown_command` (string) - The command to use to gracefully shut down the machine once all + the provisioning is done. By default this is an empty string, which tells Packer to just + forcefully shut down the machine unless a shutdown command takes place inside script so this may + safely be omitted. If one or more scripts require a reboot it is suggested to leave this blank + since reboots may fail and specify the final shutdown command in your last script. + +- `shutdown_timeout` (string) - The amount of time to wait after executing + the `shutdown_command` for the virtual machine to actually shut down. + If it doesn't shut down in this time, it is an error. By default, the timeout + is "5m", or five minutes. + +- `skip_compaction` (bool) - If true skip compacting the hard disk for virtual machine when + exporting. This defaults to false. + +- `switch_name` (string) - The name of the switch to connect the virtual machine to. Be defaulting + this to an empty string, Packer will try to determine the switch to use by looking for + external switch that is up and running. + +- `switch_vlan_id` (string) - This is the vlan of the virtual switch's network card. + By default none is set. If none is set then a vlan is not set on the switch's network card. + If this value is set it should match the vlan specified in by `vlan_id`. + +- `vlan_id` (string) - This is the vlan of the virtual machine's network card for the new virtual + machine. By default none is set. If none is set then vlans are not set on the virtual machine's + network card. + +- `vm_name` (string) - This is the name of the virtual machine for the new virtual + machine, without the file extension. By default this is "packer-BUILDNAME", + where "BUILDNAME" is the name of the build. + +## Boot Command + +The `boot_command` configuration is very important: it specifies the keys +to type when the virtual machine is first booted in order to start the +OS installer. This command is typed after `boot_wait`, which gives the +virtual machine some time to actually load the ISO. + +As documented above, the `boot_command` is an array of strings. The +strings are all typed in sequence. It is an array only to improve readability +within the template. + +The boot command is "typed" character for character over the virtual keyboard +to the machine, simulating a human actually typing the keyboard. There are +a set of special keys available. If these are in your boot command, they +will be replaced by the proper key: + +- `` - Backspace + +- `` - Delete + +- `` and `` - Simulates an actual "enter" or "return" keypress. + +- `` - Simulates pressing the escape key. + +- `` - Simulates pressing the tab key. + +- `` - `` - Simulates pressing a function key. + +- `` `` `` `` - Simulates pressing an arrow key. + +- `` - Simulates pressing the spacebar. + +- `` - Simulates pressing the insert key. + +- `` `` - Simulates pressing the home and end keys. + +- `` `` - Simulates pressing the page up and page down keys. + +- `` `` - Simulates pressing the alt key. + +- `` `` - Simulates pressing the ctrl key. + +- `` `` - Simulates pressing the shift key. + +- `` `` - Simulates pressing and holding the alt key. + +- `` `` - Simulates pressing and holding the ctrl key. + +- `` `` - Simulates pressing and holding the shift key. + +- `` `` - Simulates releasing a held alt key. + +- `` `` - Simulates releasing a held ctrl key. + +- `` `` - Simulates releasing a held shift key. + +- `` `` `` - Adds a 1, 5 or 10 second pause before + sending any additional keys. This is useful if you have to generally wait + for the UI to update before typing more. + +When using modifier keys `ctrl`, `alt`, `shift` ensure that you release them, otherwise they will be held down until the machine reboots. Use lowercase characters as well inside modifiers. For example: to simulate ctrl+c use `c`. + +In addition to the special keys, each command to type is treated as a +[configuration template](/docs/templates/configuration-templates.html). +The available variables are: + +* `HTTPIP` and `HTTPPort` - The IP and port, respectively of an HTTP server + that is started serving the directory specified by the `http_directory` + configuration parameter. If `http_directory` isn't specified, these will + be blank! + +Example boot command. This is actually a working boot command used to start +an Ubuntu 12.04 installer: + +```text +[ + "", + "/install/vmlinuz noapic ", + "preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg ", + "debian-installer=en_US auto locale=en_US kbd-chooser/method=us ", + "hostname={{ .Name }} ", + "fb=false debconf/frontend=noninteractive ", + "keyboard-configuration/modelcode=SKIP keyboard-configuration/layout=USA ", + "keyboard-configuration/variant=USA console-setup/ask_detect=false ", + "initrd=/install/initrd.gz -- " +] +``` + +## Integration Services + +Packer will automatically attach the integration services iso as a dvd drive +for the version of HyperV that is running. + +## Generation 1 vs Generation 2 + +Floppy drives are no longer supported by generation 2 machines. This requires you to +take another approach when dealing with preseed or answer files. Two possible options +are using virtua dvd drives or using the built in web server. + +When dealing with Windows you need to enable UEFI drives for generation 2 virtual machines. + +## Creating iso from directory + +Programs like mkisofs can be used to create an iso from a directory. +There is a [windows version of mkisofs](http://opensourcepack.blogspot.co.uk/p/cdrtools.html). + +Example powershell script. This is an actually working powershell script used to create a Windows answer iso: + +```text +$isoFolder = "answer-iso" +if (test-path $isoFolder){ + remove-item $isoFolder -Force -Recurse +} + +if (test-path windows\windows-2012R2-serverdatacenter-amd64\answer.iso){ + remove-item windows\windows-2012R2-serverdatacenter-amd64\answer.iso -Force +} + +mkdir $isoFolder + +copy windows\windows-2012R2-serverdatacenter-amd64\Autounattend.xml $isoFolder\ +copy windows\windows-2012R2-serverdatacenter-amd64\sysprep-unattend.xml $isoFolder\ +copy windows\common\set-power-config.ps1 $isoFolder\ +copy windows\common\microsoft-updates.ps1 $isoFolder\ +copy windows\common\win-updates.ps1 $isoFolder\ +copy windows\common\run-sysprep.ps1 $isoFolder\ +copy windows\common\run-sysprep.cmd $isoFolder\ + +$textFile = "$isoFolder\Autounattend.xml" + +$c = Get-Content -Encoding UTF8 $textFile + +# Enable UEFI and disable Non EUFI +$c | % { $_ -replace '','','Finish Non UEFI -->' } | % { $_ -replace '' } | % { $_ -replace 'Finish UEFI compatible -->','' } | sc -Path $textFile + +& .\mkisofs.exe -r -iso-level 4 -UDF -o windows\windows-2012R2-serverdatacenter-amd64\answer.iso $isoFolder + +if (test-path $isoFolder){ + remove-item $isoFolder -Force -Recurse +} +``` + + +## Example For Windows Server 2012 R2 Generation 2 + +Packer config: + +```text +{ + "builders": [ + { + "vm_name":"windows2012r2", + "type": "hyperv-iso", + "disk_size": 61440, + "floppy_files": [], + "secondary_iso_images": [ + "./windows/windows-2012R2-serverdatacenter-amd64/answer.iso" + ], + "http_directory": "./windows/common/http/", + "boot_wait": "0s", + "boot_command": [ + "aaa" + ], + "headless": false, + "iso_url": "http://download.microsoft.com/download/6/2/A/62A76ABB-9990-4EFC-A4FE-C7D698DAEB96/9600.16384.WINBLUE_RTM.130821-1623_X64FRE_SERVER_EVAL_EN-US-IRM_SSS_X64FREE_EN-US_DV5.ISO", + "iso_checksum_type": "md5", + "iso_checksum": "458ff91f8abc21b75cb544744bf92e6a", + "communicator":"winrm", + "winrm_username": "vagrant", + "winrm_password": "vagrant", + "winrm_timeout" : "4h", + "shutdown_command": "f:\\run-sysprep.cmd", + "ram_size_mb": 4096, + "cpu": 4, + "generation": 2, + "switch_name":"LAN", + "enable_secure_boot":true + }], + "provisioners": [{ + "type": "powershell", + "elevated_user":"vagrant", + "elevated_password":"vagrant", + "scripts": [ + "./windows/common/install-7zip.ps1", + "./windows/common/install-chef.ps1", + "./windows/common/compile-dotnet-assemblies.ps1", + "./windows/common/cleanup.ps1", + "./windows/common/ultradefrag.ps1", + "./windows/common/sdelete.ps1" + ] + }], + "post-processors": [ + { + "type": "vagrant", + "keep_input_artifact": false, + "output": "{{.Provider}}_windows-2012r2_chef.box" + } + ] +} +``` + +autounattend.xml: + +```text + + + + + + en-US + + en-US + en-US + en-US + en-US + en-US + + + + + + + + Primary + 1 + 350 + + + 2 + Primary + true + + + + + true + NTFS + + 1 + 1 + + + NTFS + + C + 2 + 2 + + + 0 + true + + + + + + + /IMAGE/NAME + Windows Server 2012 R2 SERVERSTANDARD + + + + 0 + 2 + + + + + + + + + + + + OnError + + true + Vagrant + Vagrant + + + + + + + false + + vagrant-2012r2 + Coordinated Universal Time + + + + true + + + false + false + + + true + + + true + + + + + + + + vagrant + true</PlainText> + </Password> + <Enabled>true</Enabled> + <Username>vagrant</Username> + </AutoLogon> + <FirstLogonCommands> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force"</CommandLine> + <Description>Set Execution Policy 64 Bit</Description> + <Order>1</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>C:\Windows\SysWOW64\cmd.exe /c powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force"</CommandLine> + <Description>Set Execution Policy 32 Bit</Description> + <Order>2</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c winrm quickconfig -q</CommandLine> + <Description>winrm quickconfig -q</Description> + <Order>3</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c winrm quickconfig -transport:http</CommandLine> + <Description>winrm quickconfig -transport:http</Description> + <Order>4</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c winrm set winrm/config @{MaxTimeoutms="1800000"}</CommandLine> + <Description>Win RM MaxTimoutms</Description> + <Order>5</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c winrm set winrm/config/winrs @{MaxMemoryPerShellMB="300"}</CommandLine> + <Description>Win RM MaxMemoryPerShellMB</Description> + <Order>6</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c winrm set winrm/config/service @{AllowUnencrypted="true"}</CommandLine> + <Description>Win RM AllowUnencrypted</Description> + <Order>7</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c winrm set winrm/config/service/auth @{Basic="true"}</CommandLine> + <Description>Win RM auth Basic</Description> + <Order>8</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c winrm set winrm/config/client/auth @{Basic="true"}</CommandLine> + <Description>Win RM client auth Basic</Description> + <Order>9</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c winrm set winrm/config/listener?Address=*+Transport=HTTP @{Port="5985"} </CommandLine> + <Description>Win RM listener Address/Port</Description> + <Order>10</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c netsh advfirewall firewall set rule group="remote administration" new enable=yes </CommandLine> + <Description>Win RM adv firewall enable</Description> + <Order>11</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c netsh advfirewall firewall add rule name="WinRM 5985" protocol=TCP dir=in localport=5985 action=allow</CommandLine> + <Description>Win RM port open</Description> + <Order>12</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c netsh advfirewall firewall add rule name="WinRM 5986" protocol=TCP dir=in localport=5986 action=allow</CommandLine> + <Description>Win RM port open</Description> + <Order>13</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c net stop winrm </CommandLine> + <Description>Stop Win RM Service </Description> + <Order>14</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c sc config winrm start= disabled</CommandLine> + <Description>Win RM Autostart</Description> + <Order>15</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ /v HideFileExt /t REG_DWORD /d 0 /f</CommandLine> + <Order>16</Order> + <Description>Show file extensions in Explorer</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD HKCU\Console /v QuickEdit /t REG_DWORD /d 1 /f</CommandLine> + <Order>17</Order> + <Description>Enable QuickEdit mode</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ /v Start_ShowRun /t REG_DWORD /d 1 /f</CommandLine> + <Order>18</Order> + <Description>Show Run command in Start Menu</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\ /v StartMenuAdminTools /t REG_DWORD /d 1 /f</CommandLine> + <Order>19</Order> + <Description>Show Administrative Tools in Start Menu</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD HKLM\SYSTEM\CurrentControlSet\Control\Power\ /v HibernateFileSizePercent /t REG_DWORD /d 0 /f</CommandLine> + <Order>20</Order> + <Description>Zero Hibernation File</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD HKLM\SYSTEM\CurrentControlSet\Control\Power\ /v HibernateEnabled /t REG_DWORD /d 0 /f</CommandLine> + <Order>21</Order> + <Description>Disable Hibernation Mode</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c wmic useraccount where "name='vagrant'" set PasswordExpires=FALSE</CommandLine> + <Order>22</Order> + <Description>Disable password expiration for vagrant user</Description> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c winrm set winrm/config/winrs @{MaxShellsPerUser="30"}</CommandLine> + <Description>Win RM MaxShellsPerUser</Description> + <Order>23</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c winrm set winrm/config/winrs @{MaxProcessesPerShell="25"}</CommandLine> + <Description>Win RM MaxProcessesPerShell</Description> + <Order>24</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>%SystemRoot%\System32\reg.exe ADD "HKLM\System\CurrentControlSet\Services\Netlogon\Parameters" /v DisablePasswordChange /t REG_DWORD /d 1 /f</CommandLine> + <Description>Turn off computer password</Description> + <Order>25</Order> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c netsh advfirewall firewall add rule name="ICMP Allow incoming V4 echo request" protocol=icmpv4:8,any dir=in action=allow</CommandLine> + <Description>ICMP open for ping</Description> + <Order>26</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <!-- WITH WINDOWS UPDATES --> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c IF EXIST a:\set-power-config.ps1 (C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\set-power-config.ps1) ELSE (C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File f:\set-power-config.ps1)</CommandLine> + <Order>97</Order> + <Description>Turn off all power saving and timeouts</Description> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c IF EXIST a:\microsoft-updates.ps1 (C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\microsoft-updates.ps1) ELSE (C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File f:\microsoft-updates.ps1)</CommandLine> + <Order>98</Order> + <Description>Enable Microsoft Updates</Description> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>cmd.exe /c IF EXIST a:\win-updates.ps1 (C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File a:\win-updates.ps1) ELSE (C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File f:\win-updates.ps1)</CommandLine> + <Description>Install Windows Updates</Description> + <Order>100</Order> + <RequiresUserInput>true</RequiresUserInput> + </SynchronousCommand> + <!-- END WITH WINDOWS UPDATES --> + </FirstLogonCommands> + <OOBE> + <HideEULAPage>true</HideEULAPage> + <HideLocalAccountScreen>true</HideLocalAccountScreen> + <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <NetworkLocation>Work</NetworkLocation> + <ProtectYourPC>1</ProtectYourPC> + </OOBE> + <UserAccounts> + <AdministratorPassword> + <Value>vagrant</Value> + <PlainText>true</PlainText> + </AdministratorPassword> + <LocalAccounts> + <LocalAccount wcm:action="add"> + <Password> + <Value>vagrant</Value> + <PlainText>true</PlainText> + </Password> + <Group>administrators</Group> + <DisplayName>Vagrant</DisplayName> + <Name>vagrant</Name> + <Description>Vagrant User</Description> + </LocalAccount> + </LocalAccounts> + </UserAccounts> + <RegisteredOwner /> + <TimeZone>Coordinated Universal Time</TimeZone> + </component> + </settings> + <settings pass="offlineServicing"> + <component name="Microsoft-Windows-LUA-Settings" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <EnableLUA>false</EnableLUA> + </component> + </settings> + <cpi:offlineImage cpi:source="wim:c:/projects/baseboxes/9600.16384.winblue_rtm.130821-1623_x64fre_server_eval_en-us-irm_sss_x64free_en-us_dv5_slipstream/sources/install.wim#Windows Server 2012 R2 SERVERDATACENTER" xmlns:cpi="urn:schemas-microsoft-com:cpi" /> +</unattend> + +``` + +sysprep-unattend.xml: + +```text +<?xml version="1.0" encoding="utf-8"?> +<unattend xmlns="urn:schemas-microsoft-com:unattend"> + <settings pass="generalize"> + <component language="neutral" name="Microsoft-Windows-Security-SPP" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <SkipRearm>1</SkipRearm> + </component> + </settings> + <settings pass="oobeSystem"> +<!-- Setup proxy after sysprep + <component name="Microsoft-Windows-IE-ClientNetworkProtocolImplementation" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <POLICYProxySettingsPerUser>1</POLICYProxySettingsPerUser> + <HKLMProxyEnable>false</HKLMProxyEnable> + <HKLMProxyServer>cache-proxy:3142</HKLMProxyServer> + </component> +Finish proxy after sysprep --> + <component language="neutral" name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <InputLocale>0809:00000809</InputLocale> + <SystemLocale>en-GB</SystemLocale> + <UILanguage>en-US</UILanguage> + <UILanguageFallback>en-US</UILanguageFallback> + <UserLocale>en-GB</UserLocale> + </component> + <component language="neutral" name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <OOBE> + <HideEULAPage>true</HideEULAPage> + <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <NetworkLocation>Work</NetworkLocation> + <ProtectYourPC>1</ProtectYourPC> + <SkipUserOOBE>true</SkipUserOOBE> + <SkipMachineOOBE>true</SkipMachineOOBE> + </OOBE> + <UserAccounts> + <AdministratorPassword> + <Value>vagrant</Value> + <PlainText>true</PlainText> + </AdministratorPassword> + <LocalAccounts> + <LocalAccount wcm:action="add"> + <Password> + <Value>vagrant</Value> + <PlainText>true</PlainText> + </Password> + <Group>administrators</Group> + <DisplayName>Vagrant</DisplayName> + <Name>vagrant</Name> + <Description>Vagrant User</Description> + </LocalAccount> + </LocalAccounts> + </UserAccounts> + <DisableAutoDaylightTimeSet>true</DisableAutoDaylightTimeSet> + <TimeZone>Coordinated Universal Time</TimeZone> + <VisualEffects> + <SystemDefaultBackgroundColor>2</SystemDefaultBackgroundColor> + </VisualEffects> + </component> + </settings> +</unattend> +``` + +## Example For Ubuntu Vivid Generation 2 + +Packer config: + +```text +{ + "builders": [ + { + "vm_name":"ubuntu-vivid", + "type": "hyperv-iso", + "disk_size": 61440, + "headless": false, + "iso_url": "http://releases.ubuntu.com/15.04/ubuntu-15.04-server-amd64.iso", + "iso_checksum_type": "sha1", + "iso_checksum": "D10248965C2C749DF6BCCE9F2F90F16A2E75E843", + "communicator":"ssh", + "ssh_username": "vagrant", + "ssh_password": "vagrant", + "ssh_timeout" : "4h", + "http_directory": "./linux/ubuntu/http/", + "boot_wait": "5s", + "boot_command": [ + "<esc><esc><enter><wait>", + "/install/vmlinuz ", + "preseed/url=http://{{.HTTPIP}}:{{.HTTPPort}}/preseed.cfg ", + "debian-installer=en_US auto locale=en_US kbd-chooser/method=us ", + "hostname={{.Name}} ", + "fb=false debconf/frontend=noninteractive ", + "keyboard-configuration/modelcode=SKIP keyboard-configuration/layout=USA ", + "keyboard-configuration/variant=USA console-setup/ask_detect=false ", + "initrd=/install/initrd.gz -- <enter>" + ], + "shutdown_command": "echo 'vagrant' | sudo -S -E shutdown -P now", + "ram_size_mb": 4096, + "cpu": 4, + "generation": 1, + "switch_name":"LAN" + }], + "provisioners": [{ + "type": "shell", + "execute_command": "echo 'vagrant' | sudo -S -E sh {{.Path}}", + "scripts": [ + "./linux/ubuntu/update.sh", + "./linux/ubuntu/network.sh", + "./linux/common/vagrant.sh", + "./linux/common/chef.sh", + "./linux/common/motd.sh", + "./linux/ubuntu/cleanup.sh" + ] + }], + "post-processors": [ + { + "type": "vagrant", + "keep_input_artifact": true, + "output": "{{.Provider}}_ubuntu-15.04_chef.box" + } + ] +} +``` + +preseed.cfg: + +```text +## Options to set on the command line +d-i debian-installer/locale string en_US.utf8 +d-i console-setup/ask_detect boolean false +d-i console-setup/layout string us + +d-i netcfg/get_hostname string unassigned-hostname +d-i netcfg/get_domain string unassigned-domain + +d-i time/zone string UTC +d-i clock-setup/utc-auto boolean true +d-i clock-setup/utc boolean true + +d-i kbd-chooser/method select American English + +d-i netcfg/wireless_wep string + +d-i base-installer/kernel/override-image string linux-server + +d-i debconf debconf/frontend select Noninteractive + +d-i pkgsel/install-language-support boolean false +tasksel tasksel/first multiselect standard, ubuntu-server + +d-i partman-auto/method string lvm + +d-i partman-lvm/confirm boolean true +d-i partman-lvm/device_remove_lvm boolean true +d-i partman-auto/choose_recipe select atomic + +d-i partman/confirm_write_new_label boolean true +d-i partman/confirm_nooverwrite boolean true +d-i partman/choose_partition select finish +d-i partman/confirm boolean true + +# Write the changes to disks and configure LVM? +d-i partman-lvm/confirm boolean true +d-i partman-lvm/confirm_nooverwrite boolean true +d-i partman-auto-lvm/guided_size string max + +# Default user +d-i passwd/user-fullname string vagrant +d-i passwd/username string vagrant +d-i passwd/user-password password vagrant +d-i passwd/user-password-again password vagrant +d-i user-setup/encrypt-home boolean false +d-i user-setup/allow-password-weak boolean true + +# Minimum packages (see postinstall.sh) +d-i pkgsel/include string openssh-server ntp + +# Upgrade packages after debootstrap? (none, safe-upgrade, full-upgrade) +# (note: set to none for speed) +d-i pkgsel/upgrade select none + +d-i grub-installer/only_debian boolean true +d-i grub-installer/with_other_os boolean true +d-i finish-install/reboot_in_progress note + +d-i pkgsel/update-policy select none + +choose-mirror-bin mirror/http/proxy string + +#d-i mirror/http/proxy string http://apt-cacher:3142/ +``` \ No newline at end of file diff --git a/website/source/docs/builders/hyperv.html.markdown b/website/source/docs/builders/hyperv.html.markdown new file mode 100644 index 00000000000..79c6fbff1b5 --- /dev/null +++ b/website/source/docs/builders/hyperv.html.markdown @@ -0,0 +1,18 @@ +--- +layout: "docs" +page_title: "HyperV Builder" +description: |- + The HyperV Packer builder is able to create HyperV virtual machines and export them. +--- + +# HyperV Builder + +The HyperV Packer builder is able to create [HyperV](https://www.microsoft.com/en-us/server-cloud/solutions/virtualization.aspx) +virtual machines and export them. + +Packer currently only support building HyperV machines with an iso: + +* [hyperv-iso](/docs/builders/hyperv-iso.html) - Starts from + an ISO file, creates a brand new HyperV VM, installs an OS, + provisions software within the OS, then exports that machine to create + an image. This is best for people who want to start from scratch. \ No newline at end of file