diff --git a/builder/hyperv/common/artifact.go b/builder/hyperv/common/artifact.go new file mode 100644 index 000000000..de7f94ca7 --- /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 = "mitchellh.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 000000000..f9ddc5dbf --- /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 000000000..eeeda864a --- /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 000000000..2c9f570b4 --- /dev/null +++ b/builder/hyperv/common/driver.go @@ -0,0 +1,34 @@ +// 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) + + // 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) +} diff --git a/builder/hyperv/common/driver_ps_4.go b/builder/hyperv/common/driver_ps_4.go new file mode 100644 index 000000000..a5c2abd6f --- /dev/null +++ b/builder/hyperv/common/driver_ps_4.go @@ -0,0 +1,142 @@ +// 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/packer/powershell" + "github.com/mitchellh/packer/powershell/hyperv" + "log" + "runtime" + "strconv" + "strings" +) + +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) +} + +// Start starts a VM specified by the name given. +func (d *HypervPS4Driver) Start(vmName string) error { + return hyperv.Start(vmName) +} + +// Stop stops a VM specified by the name given. +func (d *HypervPS4Driver) Stop(vmName string) error { + return hyperv.TurnOff(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) { + return hyperv.Mac(vmName) +} + +// Get ip address for mac address. +func (d *HypervPS4Driver) IpAddress(mac string) (string, error) { + return hyperv.IpAddress(mac) +} + +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/output_config.go b/builder/hyperv/common/output_config.go new file mode 100644 index 000000000..40b10e6e5 --- /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 000000000..a4d8e7999 --- /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 not have errors") + } +} diff --git a/builder/hyperv/common/ssh.go b/builder/hyperv/common/ssh.go new file mode 100644 index 000000000..f61047dd4 --- /dev/null +++ b/builder/hyperv/common/ssh.go @@ -0,0 +1,51 @@ +package common + +import ( + "github.com/mitchellh/multistep" + commonssh "github.com/mitchellh/packer/common/ssh" + packerssh "github.com/mitchellh/packer/communicator/ssh" + "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) (*ssh.ClientConfig, error) { + return func(state multistep.StateBag) (*ssh.ClientConfig, error) { + auth := []ssh.AuthMethod{ + ssh.Password(config.Comm.SSHPassword), + ssh.KeyboardInteractive( + packerssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)), + } + + if config.SSHKeyPath != "" { + signer, err := commonssh.FileSigner(config.Comm.SSHPrivateKey) + if err != nil { + return nil, err + } + + auth = append(auth, ssh.PublicKeys(signer)) + } + + return &ssh.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 000000000..bea164b06 --- /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 000000000..ea35818e9 --- /dev/null +++ b/builder/hyperv/common/step_configure_ip.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 ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "strings" + "time" + "log" + powershell "github.com/mitchellh/packer/powershell" + "github.com/mitchellh/packer/powershell/hyperv" +) + + +type StepConfigureIp struct { +} + +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...") + + count := 60 + var duration time.Duration = 1 + sleepTime := time.Minute * duration + var ip string + + for count != 0 { + cmdOut, err := hyperv.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 := powershell.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 000000000..508b43e98 --- /dev/null +++ b/builder/hyperv/common/step_configure_vlan.go @@ -0,0 +1,54 @@ +// 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" + "github.com/mitchellh/packer/powershell/hyperv" +) + + +const( + vlanId = "1724" +) + +type StepConfigureVlan struct { +} + +func (s *StepConfigureVlan) Run(state multistep.StateBag) multistep.StepAction { + //config := state.Get("config").(*config) + //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) + + ui.Say("Configuring vlan...") + + err := hyperv.SetNetworkAdapterVlanId(switchName, vlanId) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + err = hyperv.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 000000000..1685d069e --- /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/packer" + "code.google.com/p/go-uuid/uuid" + "github.com/mitchellh/packer/powershell/hyperv" +) + +// 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.New() + + err = hyperv.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 := hyperv.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 = hyperv.ConnectVirtualMachineNetworkAdapterToSwitch(vmName, s.oldSwitchName) + if err != nil { + ui.Error(fmt.Sprintf(errMsg, err)) + return + } + + state.Put("SwitchName", s.oldSwitchName) + + err = hyperv.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 000000000..5620e3f93 --- /dev/null +++ b/builder/hyperv/common/step_create_switch.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 ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/powershell/hyperv" +) + +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 := hyperv.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 := hyperv.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 000000000..c4682a852 --- /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 000000000..6a172ada8 --- /dev/null +++ b/builder/hyperv/common/step_create_vm.go @@ -0,0 +1,67 @@ +// 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" + "strconv" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/powershell/hyperv" +) + +// 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 +} + +func (s *StepCreateVM) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + ui.Say("Creating virtual machine...") + + path := state.Get("packerTempDir").(string) + + // convert the MB to bytes + ramBytes := int64(s.RamSizeMB * 1024 * 1024) + diskSizeBytes := int64(s.DiskSize * 1024 * 1024) + + ram := strconv.FormatInt(ramBytes, 10) + diskSize := strconv.FormatInt(diskSizeBytes, 10) + switchName := s.SwitchName + + err := hyperv.CreateVirtualMachine(s.VMName, path, ram, diskSize, switchName) + if err != nil { + err := fmt.Errorf("Error creating virtual machine: %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 := hyperv.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 000000000..264affbdd --- /dev/null +++ b/builder/hyperv/common/step_disable_vlan.go @@ -0,0 +1,41 @@ +// 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" + "github.com/mitchellh/packer/powershell/hyperv" +) + +type StepDisableVlan struct { +} + +func (s *StepDisableVlan) Run(state multistep.StateBag) multistep.StepAction { + //config := state.Get("config").(*config) + //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 := hyperv.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 000000000..89259d4d8 --- /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" + "github.com/mitchellh/packer/powershell/hyperv" +) + +type StepEnableIntegrationService struct { + name string +} + +func (s *StepEnableIntegrationService) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + ui.Say("Enabling Integration Service...") + + vmName := state.Get("vmName").(string) + s.name = "Guest Service Interface" + + err := hyperv.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_execute_online_activation.go b/builder/hyperv/common/step_execute_online_activation.go new file mode 100644 index 000000000..b369934e1 --- /dev/null +++ b/builder/hyperv/common/step_execute_online_activation.go @@ -0,0 +1,62 @@ +// 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" + "bytes" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "strings" + "log" +) + +type StepExecuteOnlineActivation struct { +} + +func (s *StepExecuteOnlineActivation) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + comm := state.Get("communicator").(packer.Communicator) + + errorMsg := "Error Executing Online Activation: %s" + + var remoteCmd packer.RemoteCmd + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + var err error + + ui.Say("Executing Online Activation...") + + var blockBuffer bytes.Buffer + blockBuffer.WriteString("{ cscript \"$env:SystemRoot/system32/slmgr.vbs\" -ato //nologo }") + + remoteCmd.Command = "-ScriptBlock " + blockBuffer.String() + + remoteCmd.Stdout = stdout + remoteCmd.Stderr = stderr + + err = comm.Start(&remoteCmd) + + stderrString := strings.TrimSpace(stderr.String()) + stdoutString := strings.TrimSpace(stdout.String()) + + log.Printf("stdout: %s", stdoutString) + log.Printf("stderr: %s", stderrString) + + if len(stderrString) > 0 { + err = fmt.Errorf(errorMsg, stderrString) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say(stdoutString) + + return multistep.ActionContinue +} + +func (s *StepExecuteOnlineActivation) Cleanup(state multistep.StateBag) { + // do nothing +} diff --git a/builder/hyperv/common/step_execute_online_activation_full.go b/builder/hyperv/common/step_execute_online_activation_full.go new file mode 100644 index 000000000..6eb91936c --- /dev/null +++ b/builder/hyperv/common/step_execute_online_activation_full.go @@ -0,0 +1,90 @@ +// 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" + "bytes" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "strings" + "log" +) + +type StepExecuteOnlineActivationFull struct { + Pk string +} + +func (s *StepExecuteOnlineActivationFull) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + comm := state.Get("communicator").(packer.Communicator) + + errorMsg := "Error Executing Online Activation: %s" + + var remoteCmd packer.RemoteCmd + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + var err error + var stderrString string + var stdoutString string + + ui.Say("Executing Online Activation Full version...") + + var blockBuffer bytes.Buffer + blockBuffer.WriteString("{ cscript \"$env:SystemRoot/system32/slmgr.vbs\" /ipk "+ s.Pk +" //nologo }") + + log.Printf("cmd: %s", blockBuffer.String()) + remoteCmd.Command = "-ScriptBlock " + blockBuffer.String() + + remoteCmd.Stdout = stdout + remoteCmd.Stderr = stderr + + err = comm.Start(&remoteCmd) + + stderrString = strings.TrimSpace(stderr.String()) + stdoutString = strings.TrimSpace(stdout.String()) + + log.Printf("stdout: %s", stdoutString) + log.Printf("stderr: %s", stderrString) + + if len(stderrString) > 0 { + err = fmt.Errorf(errorMsg, stderrString) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + +// ui.Say(stdoutString) + +/* + blockBuffer.Reset() + blockBuffer.WriteString("{ cscript \"$env:SystemRoot/system32/slmgr.vbs\" -ato //nologo }") + + log.Printf("cmd: %s", blockBuffer.String()) + remoteCmd.Command = "-ScriptBlock " + blockBuffer.String() + + err = comm.Start(&remoteCmd) + + stderrString = strings.TrimSpace(stderr.String()) + stdoutString = strings.TrimSpace(stdout.String()) + + log.Printf("stdout: %s", stdoutString) + log.Printf("stderr: %s", stderrString) + + if len(stderrString) > 0 { + err = fmt.Errorf(errorMsg, stderrString) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say(stdoutString) +*/ + return multistep.ActionContinue +} + +func (s *StepExecuteOnlineActivationFull) 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 000000000..95076fd14 --- /dev/null +++ b/builder/hyperv/common/step_export_vm.go @@ -0,0 +1,74 @@ +// 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" + "path/filepath" + "io/ioutil" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/powershell/hyperv" +) + +const( + vhdDir string = "Virtual Hard Disks" + vmDir string = "Virtual Machines" +) + +type StepExportVm struct { + OutputDir string +} + +func (s *StepExportVm) Run(state multistep.StateBag) multistep.StepAction { + 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 = hyperv.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) + + ui.Say("Coping to output dir...") + err = hyperv.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 000000000..d5e9b4ef3 --- /dev/null +++ b/builder/hyperv/common/step_mount_dvddrive.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" + "github.com/mitchellh/packer/powershell/hyperv" +) + + +type StepMountDvdDrive struct { + RawSingleISOUrl string + path string +} + +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 := s.RawSingleISOUrl + + ui.Say("Mounting dvd drive...") + + err := hyperv.MountDvdDrive(vmName, isoPath) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.path = isoPath + + return multistep.ActionContinue +} + +func (s *StepMountDvdDrive) Cleanup(state multistep.StateBag) { + if s.path == "" { + return + } + + errorMsg := "Error unmounting dvd drive: %s" + + vmName := state.Get("vmName").(string) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Unmounting dvd drive...") + + err := hyperv.UnmountDvdDrive(vmName) + if err != nil { + ui.Error(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 000000000..c30054141 --- /dev/null +++ b/builder/hyperv/common/step_mount_floppydrive.go @@ -0,0 +1,189 @@ +// 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" + "os" + "strings" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/powershell" + "github.com/mitchellh/packer/powershell/hyperv" + "log" + "io" + "io/ioutil" + "path/filepath" +) + + +const( + FloppyFileName = "assets.vfd" +) + + + + +type StepSetUnattendedProductKey struct { + Files []string + ProductKey string +} + +func (s *StepSetUnattendedProductKey) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + if s.ProductKey == "" { + ui.Say("No product key specified...") + return multistep.ActionContinue + } + + index := -1 + for i, value := range s.Files { + if s.caseInsensitiveContains(value, "Autounattend.xml") { + index = i + break + } + } + + ui.Say("Setting product key in Autounattend.xml...") + copyOfAutounattend, err := s.copyAutounattend(s.Files[index]) + if err != nil { + state.Put("error", fmt.Errorf("Error copying Autounattend.xml: %s", err)) + return multistep.ActionHalt + } + + powershell.SetUnattendedProductKey(copyOfAutounattend, s.ProductKey) + s.Files[index] = copyOfAutounattend + return multistep.ActionContinue +} + + +func (s *StepSetUnattendedProductKey) caseInsensitiveContains(str, substr string) bool { + str, substr = strings.ToUpper(str), strings.ToUpper(substr) + return strings.Contains(str, substr) +} + +func (s *StepSetUnattendedProductKey) copyAutounattend(path string) (string, error) { + tempdir, err := ioutil.TempDir("", "packer") + if err != nil { + return "", err + } + + autounattend := filepath.Join(tempdir, "Autounattend.xml") + f, err := os.Create(autounattend) + if err != nil { + return "", err + } + defer f.Close() + + sourceF, err := os.Open(path) + if err != nil { + return "", err + } + defer sourceF.Close() + + log.Printf("Copying %s to temp location: %s", path, autounattend) + if _, err := io.Copy(f, sourceF); err != nil { + return "", err + } + + return autounattend, nil +} + + +func (s *StepSetUnattendedProductKey) Cleanup(state multistep.StateBag) { +} + + + +type StepMountFloppydrive struct { + floppyPath string +} + +func (s *StepMountFloppydrive) Run(state multistep.StateBag) multistep.StepAction { + // 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 = hyperv.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.floppyPath == "" { + return + } + + errorMsg := "Error unmounting floppy drive: %s" + + vmName := state.Get("vmName").(string) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Unmounting floppy drive (cleanup)...") + + err := hyperv.UnmountFloppyDrive(vmName) + if err != nil { + ui.Error(fmt.Sprintf(errorMsg, err)) + } + + err = os.Remove(s.floppyPath) + + if err != nil { + ui.Error(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_integration_services.go b/builder/hyperv/common/step_mount_integration_services.go new file mode 100644 index 000000000..05eaf6dc8 --- /dev/null +++ b/builder/hyperv/common/step_mount_integration_services.go @@ -0,0 +1,142 @@ +// 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" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + powershell "github.com/mitchellh/packer/powershell" +) + +type StepMountSecondaryDvdImages struct { + Files [] string + dvdProperties []DvdControllerProperties +} + +type DvdControllerProperties struct { + ControllerNumber string + ControllerLocation string +} + +func (s *StepMountSecondaryDvdImages) Run(state multistep.StateBag) multistep.StepAction { + 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) + dvdProperties, err := s.mountFiles(vmName); + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + log.Println(fmt.Sprintf("Saving DVD properties %s DVDs", len(dvdProperties))) + + state.Put("secondary.dvd.properties", dvdProperties) + + return multistep.ActionContinue +} + +func (s *StepMountSecondaryDvdImages) Cleanup(state multistep.StateBag) { + +} + + +func (s *StepMountSecondaryDvdImages) mountFiles(vmName string) ([]DvdControllerProperties, error) { + + var dvdProperties []DvdControllerProperties + + properties, err := s.addAndMountIntegrationServicesSetupDisk(vmName) + if err != nil { + return dvdProperties, err + } + + dvdProperties = append(dvdProperties, properties) + + for _, value := range s.Files { + properties, err := s.addAndMountDvdDisk(vmName, value) + if err != nil { + return dvdProperties, err + } + + dvdProperties = append(dvdProperties, properties) + } + + return dvdProperties, nil +} + + +func (s *StepMountSecondaryDvdImages) addAndMountIntegrationServicesSetupDisk(vmName string) (DvdControllerProperties, error) { + + isoPath := os.Getenv("WINDIR") + "\\system32\\vmguest.iso" + properties, err := s.addAndMountDvdDisk(vmName, isoPath) + if err != nil { + return properties, err + } + + return properties, nil +} + + + + +func (s *StepMountSecondaryDvdImages) addAndMountDvdDisk(vmName string, isoPath string) (DvdControllerProperties, error) { + + var properties DvdControllerProperties + var script powershell.ScriptBuilder + powershell := new(powershell.PowerShellCmd) + + // get the controller number that the OS install disk is mounted on + script.Reset() + script.WriteLine("param([string]$vmName)") + script.WriteLine("(Get-VMDvdDrive -VMName $vmName).ControllerNumber") + controllerNumber, err := powershell.Output(script.String(), vmName) + if err != nil { + return properties, err + } + + script.Reset() + script.WriteLine("param([string]$vmName,[int]$controllerNumber)") + script.WriteLine("Add-VMDvdDrive -VMName $vmName -ControllerNumber $controllerNumber") + err = powershell.Run(script.String(), vmName, controllerNumber) + if err != nil { + return properties, err + } + + // we could try to get the controller location and number in one call, but this way we do not + // need to parse the output + script.Reset() + script.WriteLine("param([string]$vmName)") + script.WriteLine("(Get-VMDvdDrive -VMName $vmName | Where-Object {$_.Path -eq $null}).ControllerLocation") + controllerLocation, err := powershell.Output(script.String(), vmName) + if err != nil { + return properties, err + } + + script.Reset() + script.WriteLine("param([string]$vmName,[string]$path,[string]$controllerNumber,[string]$controllerLocation)") + script.WriteLine("Set-VMDvdDrive -VMName $vmName -Path $path -ControllerNumber $controllerNumber -ControllerLocation $controllerLocation") + + err = powershell.Run(script.String(), vmName, isoPath, controllerNumber, controllerLocation) + if err != nil { + return properties, err + } + + log.Println(fmt.Sprintf("ISO %s mounted on DVD controller %v, location %v",isoPath, controllerNumber, controllerLocation)) + + properties.ControllerNumber = controllerNumber + properties.ControllerLocation = controllerLocation + + return properties, nil +} diff --git a/builder/hyperv/common/step_output_dir.go b/builder/hyperv/common/step_output_dir.go new file mode 100644 index 000000000..602ce2654 --- /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 000000000..c462f3da9 --- /dev/null +++ b/builder/hyperv/common/step_polling_installation.go @@ -0,0 +1,114 @@ +// 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" +// "net" + "log" + "os/exec" + "strings" + "bytes" +) + +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...") +/* + count := 30 + var minutes time.Duration = 1 + sleepMin := time.Minute * minutes + host := vmIp + ":" + port + + timeoutSec := time.Second * 15 + + for count > 0 { + ui.Say(fmt.Sprintf("Connecting vm (%s)...", host )) + conn, err := net.DialTimeout("tcp", host, timeoutSec) + if err == nil { + ui.Say("Done!") + conn.Close() + break; + } + + log.Println(err) + ui.Say(fmt.Sprintf("Waiting more %v minutes...", uint(minutes))) + time.Sleep(sleepMin) + 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 + } +*/ + 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 000000000..3053a0855 --- /dev/null +++ b/builder/hyperv/common/step_reboot_vm.go @@ -0,0 +1,45 @@ +// 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" + "github.com/mitchellh/packer/powershell/hyperv" +) + +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 := hyperv.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_shutdown.go b/builder/hyperv/common/step_shutdown.go new file mode 100644 index 000000000..db80e7c56 --- /dev/null +++ b/builder/hyperv/common/step_shutdown.go @@ -0,0 +1,141 @@ +package common + +import ( + "bytes" + "errors" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "time" +) + +type ShutdownConfig struct { + ShutdownCommand string `mapstructure:"shutdown_command"` + RawShutdownTimeout string `mapstructure:"shutdown_timeout"` + + ShutdownTimeout time.Duration `` +} + +func (c *ShutdownConfig) Prepare(t *packer.ConfigTemplate) []error { + if c.RawShutdownTimeout == "" { + c.RawShutdownTimeout = "5m" + } + + templates := map[string]*string{ + "shutdown_command": &c.ShutdownCommand, + "shutdown_timeout": &c.RawShutdownTimeout, + } + + errs := make([]error, 0) + for n, ptr := range templates { + var err error + *ptr, err = t.Process(*ptr, nil) + if err != nil { + errs = append(errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + 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 +} + +// 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() + + // If the command failed to run, notify the user in some way. + if cmd.ExitStatus != 0 { + state.Put("error", fmt.Errorf( + "Shutdown command has non-zero exit status.\n\nStdout: %s\n\nStderr: %s", + stdout.String(), stderr.String())) + return multistep.ActionHalt + } + + if stdout.Len() > 0 { + log.Printf("Shutdown stdout: %s", stdout.String()) + } + + if stderr.Len() > 0 { + log.Printf("Shutdown stderr: %s", stderr.String()) + } + + // 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 000000000..2d0f0053a --- /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_start_vm.go b/builder/hyperv/common/step_start_vm.go new file mode 100644 index 000000000..af1fcda5d --- /dev/null +++ b/builder/hyperv/common/step_start_vm.go @@ -0,0 +1,48 @@ +// 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" + "github.com/mitchellh/packer/powershell/hyperv" +) + +type StepStartVm struct { + Reason string + StartUpDelay int +} + +func (s *StepStartVm) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + errorMsg := "Error starting vm: %s" + vmName := state.Get("vmName").(string) + + ui.Say("Starting vm for " + s.Reason + "...") + + err := hyperv.StartVirtualMachine(vmName) + if err != nil { + err := fmt.Errorf(errorMsg, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if s.StartUpDelay != 0 { + //sleepTime := s.StartUpDelay * time.Second + sleepTime := 60 * time.Second + + ui.Say(fmt.Sprintf(" Waiting %v for vm to start...", sleepTime)) + time.Sleep(sleepTime); + } + + return multistep.ActionContinue +} + +func (s *StepStartVm) Cleanup(state multistep.StateBag) { +} diff --git a/builder/hyperv/common/step_unmount_dvddrive.go b/builder/hyperv/common/step_unmount_dvddrive.go new file mode 100644 index 000000000..57ffb422d --- /dev/null +++ b/builder/hyperv/common/step_unmount_dvddrive.go @@ -0,0 +1,38 @@ +// 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" + "github.com/mitchellh/packer/powershell/hyperv" +) + + +type StepUnmountDvdDrive struct { +} + +func (s *StepUnmountDvdDrive) Run(state multistep.StateBag) multistep.StepAction { + //driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + vmName := state.Get("vmName").(string) + + ui.Say("Unmounting dvd drive...") + + err := hyperv.UnmountDvdDrive(vmName) + if err != nil { + err := fmt.Errorf("Error unmounting dvd drive: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + 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 000000000..ae7813400 --- /dev/null +++ b/builder/hyperv/common/step_unmount_floppydrive.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" + "github.com/mitchellh/packer/powershell/hyperv" +) + + +type StepUnmountFloppyDrive struct { +} + +func (s *StepUnmountFloppyDrive) Run(state multistep.StateBag) multistep.StepAction { + //driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + + errorMsg := "Error Unmounting floppy drive: %s" + vmName := state.Get("vmName").(string) + + ui.Say("Unmounting floppy drive (Run)...") + + err := hyperv.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_integration_services.go b/builder/hyperv/common/step_unmount_integration_services.go new file mode 100644 index 000000000..7c40389fe --- /dev/null +++ b/builder/hyperv/common/step_unmount_integration_services.go @@ -0,0 +1,52 @@ +// 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" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + powershell "github.com/mitchellh/packer/powershell" +) + +type StepUnmountSecondaryDvdImages struct { +} + +func (s *StepUnmountSecondaryDvdImages) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + ui.Say("Unmounting Integration Services Setup Disk...") + + vmName := state.Get("vmName").(string) + + // todo: should this message say removing the dvd? + + dvdProperties := state.Get("secondary.dvd.properties").([]DvdControllerProperties) + + log.Println(fmt.Sprintf("Found DVD properties %s", len(dvdProperties))) + + for _, dvdProperty := range dvdProperties { + controllerNumber := dvdProperty.ControllerNumber + controllerLocation := dvdProperty.ControllerLocation + + var script powershell.ScriptBuilder + powershell := new(powershell.PowerShellCmd) + + script.WriteLine("param([string]$vmName,[int]$controllerNumber,[int]$controllerLocation)") + script.WriteLine("Remove-VMDvdDrive -VMName $vmName -ControllerNumber $controllerNumber -ControllerLocation $controllerLocation") + err := powershell.Run(script.String(), vmName, controllerNumber, controllerLocation) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + + return multistep.ActionContinue +} + +func (s *StepUnmountSecondaryDvdImages) Cleanup(state multistep.StateBag) { +} diff --git a/builder/hyperv/common/step_upgrade_integration_services.go b/builder/hyperv/common/step_upgrade_integration_services.go new file mode 100644 index 000000000..ab076e7ed --- /dev/null +++ b/builder/hyperv/common/step_upgrade_integration_services.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 + +import ( + //"fmt" + "os" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + powershell "github.com/mitchellh/packer/powershell" +) + +type StepUpdateIntegrationServices struct { + Username string + Password string + + newDvdDriveProperties dvdDriveProperties +} + +type dvdDriveProperties struct { + ControllerNumber string + ControllerLocation string +} + +func (s *StepUpdateIntegrationServices) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + ui.Say("Mounting Integration Services Setup Disk...") + + _, err := s.mountIntegrationServicesSetupDisk(vmName); + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // dvdDriveLetter, err := s.getDvdDriveLetter(vmName) + // if err != nil { + // state.Put("error", err) + // ui.Error(err.Error()) + // return multistep.ActionHalt + // } + + // setup := dvdDriveLetter + ":\\support\\"+osArchitecture+"\\setup.exe /quiet /norestart" + + // ui.Say("Run: " + setup) + + return multistep.ActionContinue +} + +func (s *StepUpdateIntegrationServices) Cleanup(state multistep.StateBag) { + vmName := state.Get("vmName").(string) + + var script powershell.ScriptBuilder + script.WriteLine("param([string]$vmName)") + script.WriteLine("Set-VMDvdDrive -VMName $vmName -Path $null") + + powershell := new(powershell.PowerShellCmd) + _ = powershell.Run(script.String(), vmName) +} + +func (s *StepUpdateIntegrationServices) mountIntegrationServicesSetupDisk(vmName string) (dvdDriveProperties, error) { + + var dvdProperties dvdDriveProperties + + var script powershell.ScriptBuilder + script.WriteLine("param([string]$vmName)") + script.WriteLine("Add-VMDvdDrive -VMName $vmName") + + powershell := new(powershell.PowerShellCmd) + err := powershell.Run(script.String(), vmName) + if err != nil { + return dvdProperties, err + } + + script.Reset() + script.WriteLine("param([string]$vmName)") + script.WriteLine("(Get-VMDvdDrive -VMName $vmName | Where-Object {$_.Path -eq $null}).ControllerLocation") + controllerLocation, err := powershell.Output(script.String(), vmName) + if err != nil { + return dvdProperties, err + } + + script.Reset() + script.WriteLine("param([string]$vmName)") + script.WriteLine("(Get-VMDvdDrive -VMName $vmName | Where-Object {$_.Path -eq $null}).ControllerNumber") + controllerNumber, err := powershell.Output(script.String(), vmName) + if err != nil { + return dvdProperties, err + } + + isoPath := os.Getenv("WINDIR") + "\\system32\\vmguest.iso" + + script.Reset() + script.WriteLine("param([string]$vmName,[string]$path,[string]$controllerNumber,[string]$controllerLocation)") + script.WriteLine("Set-VMDvdDrive -VMName $vmName -Path $path -ControllerNumber $controllerNumber -ControllerLocation $controllerLocation") + + err = powershell.Run(script.String(), vmName, isoPath, controllerNumber, controllerLocation) + if err != nil { + return dvdProperties, err + } + + dvdProperties.ControllerNumber = controllerNumber + dvdProperties.ControllerLocation = controllerLocation + + return dvdProperties, err +} 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 000000000..22da371c8 --- /dev/null +++ b/builder/hyperv/common/step_wait_for_install_to_complete.go @@ -0,0 +1,130 @@ +// 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" + "strings" + "strconv" + "time" + powershell "github.com/mitchellh/packer/powershell" +) + +const ( + SleepSeconds = 10 +) + +type StepWaitForPowerOff struct { +} + +func (s *StepWaitForPowerOff) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + ui.Say("Waiting for vm to be powered down...") + + // unless the person has a super fast disk, it should take at least 5 minutes + // for the install and post-install operations to take. Wait 5 minutes to + // avoid hammering on getting VM status via PowerShell + time.Sleep(time.Second * 300); + + var script powershell.ScriptBuilder + script.WriteLine("param([string]$vmName)") + script.WriteLine("(Get-VM -Name $vmName).State -eq [Microsoft.HyperV.PowerShell.VMState]::Off") + isOffScript := script.String() + + for { + powershell := new(powershell.PowerShellCmd) + cmdOut, err := powershell.Output(isOffScript, vmName); + if err != nil { + err := fmt.Errorf("Error checking VM's state: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if cmdOut == "True" { + 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 { + 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 + + var script powershell.ScriptBuilder + script.WriteLine("param([string]$vmName)") + script.WriteLine("(Get-VM -Name $vmName).Uptime.TotalSeconds") + + uptimeScript := script.String() + + for rebootCount < s.ExpectedRebootCount { + powershell := new(powershell.PowerShellCmd) + cmdOut, err := powershell.Output(uptimeScript, vmName); + if err != nil { + err := fmt.Errorf("Error checking uptime: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + uptime, _ := strconv.ParseUint(strings.TrimSpace(string(cmdOut)), 10, 64) + if uint64(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) { + +} + + +type StepWaitForWinRm struct { +} + +func (s *StepWaitForWinRm) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + //vmName := state.Get("vmName").(string) + + ui.Say("Waiting for WinRM to be ready...") + + return multistep.ActionContinue +} + +func (s *StepWaitForWinRm) Cleanup(state multistep.StateBag) { + +} diff --git a/builder/hyperv/iso/builder.go b/builder/hyperv/iso/builder.go new file mode 100644 index 000000000..60bd5f77f --- /dev/null +++ b/builder/hyperv/iso/builder.go @@ -0,0 +1,391 @@ +// 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 ( + "code.google.com/p/go-uuid/uuid" + "errors" + "fmt" + "github.com/mitchellh/multistep" + hypervcommon "github.com/mitchellh/packer/builder/hyperv/common" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/helper/communicator" + powershell "github.com/mitchellh/packer/powershell" + "github.com/mitchellh/packer/powershell/hyperv" + "log" + "os" + "regexp" + "strings" + "time" +) + +const ( + DefaultDiskSize = 127 * 1024 // 127GB + MinDiskSize = 10 * 1024 // 10GB + MaxDiskSize = 65536 * 1024 // 64TB + + DefaultRamSize = 1024 // 1GB + MinRamSize = 512 // 512MB + MaxRamSize = 32768 // 32GB + + LowRam = 512 // 512MB + + 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 { + // 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"` + // 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. + ISOChecksum string `mapstructure:"iso_checksum"` + // 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. + ISOChecksumType string `mapstructure:"iso_checksum_type"` + // 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. + RawSingleISOUrl string `mapstructure:"iso_url"` + // 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. + ISOUrls []string `mapstructure:"iso_urls"` + // 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"` + + common.PackerConfig `mapstructure:",squash"` + hypervcommon.OutputConfig `mapstructure:",squash"` + hypervcommon.SSHConfig `mapstructure:",squash"` + hypervcommon.ShutdownConfig `mapstructure:",squash"` + + SwitchName string `mapstructure:"switch_name"` + + 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 + + tpl *packer.ConfigTemplate +} + +// Prepare processes the build configuration parameters. +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + + md, err := common.DecodeConfig(&b.config, raws...) + if err != nil { + return nil, err + } + + b.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return nil, err + } + + log.Println(fmt.Sprintf("%s: %v", "PackerUserVars", b.config.PackerUserVars)) + + b.config.tpl.UserVars = b.config.PackerUserVars + + // Accumulate any errors and warnings + errs := common.CheckUnusedConfig(md) + errs = packer.MultiErrorAppend(errs, b.config.OutputConfig.Prepare(b.config.tpl, &b.config.PackerConfig)...) + errs = packer.MultiErrorAppend(errs, b.config.SSHConfig.Prepare(b.config.tpl)...) + errs = packer.MultiErrorAppend(errs, b.config.ShutdownConfig.Prepare(b.config.tpl)...) + + warnings := make([]string, 0) + + 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("pvm_%s", uuid.New()) + } + + if b.config.SwitchName == "" { + // no switch name, try to get one attached to a online network adapter + onlineSwitchName, err := hyperv.GetExternalOnlineVirtualSwitch() + if onlineSwitchName == "" || err != nil { + b.config.SwitchName = fmt.Sprintf("pis_%s", uuid.New()) + } else { + b.config.SwitchName = onlineSwitchName + } + } + + log.Println(fmt.Sprintf("Using switch %s", 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) + } + + // Errors + templates := map[string]*string{ + "iso_url": &b.config.RawSingleISOUrl + } + + for n, ptr := range templates { + var err error + *ptr, err = b.config.tpl.Process(*ptr, nil) + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + log.Println(fmt.Sprintf("%s: %v", "VMName", b.config.VMName)) + log.Println(fmt.Sprintf("%s: %v", "SwitchName", b.config.SwitchName)) + log.Println(fmt.Sprintf("%s: %v", "Communicator", b.config.Communicator)) + + if b.config.RawSingleISOUrl == "" { + errs = packer.MultiErrorAppend(errs, errors.New("iso_url: The option can't be missed and a path must be specified.")) + } else if _, err := os.Stat(b.config.RawSingleISOUrl); err != nil { + errs = packer.MultiErrorAppend(errs, errors.New("iso_url: Check the path is correct")) + } + + log.Println(fmt.Sprintf("%s: %v", "RawSingleISOUrl", b.config.RawSingleISOUrl)) + + b.config.SSHWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout) + + // Warnings + warning := b.checkHostAvailableMemory() + if warning != "" { + warnings = appendWarnings(warnings, warning) + } + + 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.") + } + + 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("config", &b.config) + 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.StepCreateFloppy{ + Files: b.config.FloppyFiles, + }, + &hypervcommon.StepCreateSwitch{ + SwitchName: b.config.SwitchName, + }, + &hypervcommon.StepCreateVM{ + VMName: b.config.VMName, + SwitchName: b.config.SwitchName, + RamSizeMB: b.config.RamSizeMB, + DiskSize: b.config.DiskSize, + }, + &hypervcommon.StepEnableIntegrationService{}, + + &hypervcommon.StepMountDvdDrive{ + RawSingleISOUrl: b.config.RawSingleISOUrl, + }, + &hypervcommon.StepMountFloppydrive{}, + + &hypervcommon.StepMountSecondaryDvdImages{}, + + + &hypervcommon.StepStartVm{ + Reason: "OS installation", + }, + + // wait for the vm to be powered off + &hypervcommon.StepWaitForPowerOff{}, + + // remove the integration services dvd drive + // after we power down + &hypervcommon.StepUnmountSecondaryDvdImages{}, + + // + &hypervcommon.StepStartVm{ + Reason: "provisioning", + StartUpDelay: 60, + }, + + // configure the communicator ssh, winrm + &communicator.StepConnect{ + Config: &b.config.SSHConfig.Comm, + Host: hypervcommon.CommHost, + SSHConfig: hypervcommon.SSHConfigFunc(b.config.SSHConfig), + SSHPort: hypervcommon.SSHPort, + }, + + // provision requires communicator to be setup + &common.StepProvision{}, + + &hypervcommon.StepUnmountFloppyDrive{}, + &hypervcommon.StepUnmountDvdDrive{}, + + &hypervcommon.StepShutdown{ + Command: b.config.ShutdownCommand, + Timeout: b.config.ShutdownTimeout, + }, + + &hypervcommon.StepExportVm{ + OutputDir: b.config.OutputDir, + }, + + // the clean up actions for each step will be executed reverse order + } + + // Run the steps. + if b.config.PackerDebug { + b.runner = &multistep.DebugRunner{ + Steps: steps, + PauseFn: common.MultistepDebugFn(ui), + } + } 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: Windows server 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: Windows server 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: Windows server requires memory size >= %v MB, but defined: %v", MinRamSize, b.config.RamSizeMB) + } else if b.config.RamSizeMB > MaxRamSize { + return fmt.Errorf("ram_size_mb: Windows server requires memory size <= %v MB, but defined: %v", MaxRamSize, b.config.RamSizeMB) + } + + return nil +} + +func (b *Builder) checkHostAvailableMemory() string { + 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 "" +} \ No newline at end of file diff --git a/plugin/packer-builder-hyperv-iso/build-and-deploy.sh b/plugin/packer-builder-hyperv-iso/build-and-deploy.sh new file mode 100644 index 000000000..8e0185a4e --- /dev/null +++ b/plugin/packer-builder-hyperv-iso/build-and-deploy.sh @@ -0,0 +1,3 @@ +go build +cp packer-builder-hyperv-iso.exe ../../../bin/ + diff --git a/plugin/packer-builder-hyperv-iso/main.go b/plugin/packer-builder-hyperv-iso/main.go new file mode 100644 index 000000000..c7be7a1ed --- /dev/null +++ b/plugin/packer-builder-hyperv-iso/main.go @@ -0,0 +1,19 @@ +// 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 main + +import ( + "github.com/mitchellh/packer/builder/hyperv/iso" + "github.com/mitchellh/packer/plugin" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterBuilder(new(iso.Builder)) + server.Serve() +} diff --git a/plugin/packer-builder-hyperv-iso/main_test.go b/plugin/packer-builder-hyperv-iso/main_test.go new file mode 100644 index 000000000..b07e18080 --- /dev/null +++ b/plugin/packer-builder-hyperv-iso/main_test.go @@ -0,0 +1,5 @@ +// 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 main diff --git a/powershell/hyperv/hyperv.go b/powershell/hyperv/hyperv.go new file mode 100644 index 000000000..afddc928b --- /dev/null +++ b/powershell/hyperv/hyperv.go @@ -0,0 +1,433 @@ +package hyperv + +import ( + "github.com/mitchellh/packer/powershell" + "strings" +) + +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 MountDvdDrive(vmName string, path string) error { + + var script = ` +param([string]$vmName,[string]$path) +Set-VMDvdDrive -VMName $vmName -Path $path +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, path) + return err +} + +func UnmountDvdDrive(vmName string) error { + + var script = ` +param([string]$vmName) +Set-VMDvdDrive -VMName $vmName -Path $null +` + + 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 string, diskSize string, switchName string) error { + + 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, ram, diskSize, switchName) + return err +} + +func DeleteVirtualMachine(vmName string) error { + + var script = ` +param([string]$vmName) +Remove-VM -Name $vmName -Force +` + + 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 +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, path) + return err +} + +func CopyExportedVirtualMachine(expPath string, outputPath string, vhdDir string, vmDir string) error { + + var script = ` +param([string]$srcPath, [string]$dstPath, [string]$vhdDirName, [string]$vmDir) +Copy-Item -Path $srcPath/$vhdDirName -Destination $dstPath -recurse +Copy-Item -Path $srcPath/$vmDir -Destination $dstPath +Copy-Item -Path $srcPath/$vmDir/*.xml -Destination $dstPath/$vmDir +` + + 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 +} +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, switchName) + return err +} + +func StartVirtualMachine(vmName string) error { + + var script = ` +param([string]$vmName) +Start-VM -Name $vmName +` + + 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 +` + + 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 +} +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName) + return err +} + +func EnableVirtualMachineIntegrationService(vmName string, integrationServiceName string) error { + + var script = ` +param([string]$vmName,[string]$integrationServiceName) +Enable-VMIntegrationService -VMName $vmName -Name $integrationServiceName +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName, integrationServiceName) + 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) + var isRunning = strings.TrimSpace(cmdOut) == "True" + return isRunning, err +} + +func Mac(vmName string) (string, error) { + var script = ` +param([string]$vmName, [int]$addressIndex) +try { + $adapter = Get-VMNetworkAdapter -VMName $vmName -ErrorAction SilentlyContinue + $mac = $adapter.MacAddress[$addressIndex] + if($mac -eq $null) { + return $false + } +} catch { + return $false +} +$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 $false + } +} catch { + return $false +} +$ip +` + + var ps powershell.PowerShellCmd + cmdOut, err := ps.Output(script, mac, "0") + + return cmdOut, err +} + +func Start(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 +} +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName) + return 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 +} +` + + 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 +} +` + + var ps powershell.PowerShellCmd + err := ps.Run(script, vmName) + return err +} diff --git a/powershell/powershell.go b/powershell/powershell.go new file mode 100644 index 000000000..28c3d908b --- /dev/null +++ b/powershell/powershell.go @@ -0,0 +1,255 @@ +// 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 ( + "fmt" + "log" + "io" + "os" + "os/exec" + "strings" + "bytes" + "io/ioutil" + "strconv" +) + +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 "", nil + } + + 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 (ps *PowerShellCmd) getPowerShellPath() (string, error) { + path, err := exec.LookPath("powershell") + if err != nil { + log.Fatal("Cannot find PowerShell in the path", err) + 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 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 000000000..9d60e8f60 --- /dev/null +++ b/powershell/powershell_test.go @@ -0,0 +1,72 @@ + + +package powershell + +import ( + "bytes" + "testing" +) + +func TestOutputScriptBlock(t *testing.T) { + + ps, err := powershell.Command() + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + trueOutput, err := powershell.OutputScriptBlock("$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 := powershell.OutputScriptBlock("$False") + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if falseOutput != "False" { + t.Fatalf("output '%v' is not 'False'", falseOutput) + } +} + +func TestRunScriptBlock(t *testing.T) { + powershell, err := powershell.Command() + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + err = powershell.RunScriptBlock("$True") +} + +func TestVersion(t *testing.T) { + powershell, err := powershell.Command() + version, err := powershell.Version(); + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if (version != 4) { + t.Fatalf("expected version 4") + } +} + +func TestRunFile(t *testing.T) { + powershell, err := powershell.Command() + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + var blockBuffer bytes.Buffer + blockBuffer.WriteString("param([string]$a, [string]$b, [int]$x, [int]$y) $n = $x + $y; Write-Host $a, $b, $n") + + err = powershell.Run(blockBuffer.String(), "a", "b", "5", "10") + + if err != nil { + t.Fatalf("should not have error: %s", err) + } + +} diff --git a/powershell/scriptbuilder.go b/powershell/scriptbuilder.go new file mode 100644 index 000000000..d51156aa2 --- /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() +} +