Commit ccf20568 authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

Merge pull request #2240 from mitchellh/f-windows

Windows AWS instances
parents b8d5bd37 b2e9277d
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"time"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator" "github.com/mitchellh/packer/helper/communicator"
...@@ -27,6 +28,7 @@ type RunConfig struct { ...@@ -27,6 +28,7 @@ type RunConfig struct {
TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"` TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"`
UserData string `mapstructure:"user_data"` UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"` UserDataFile string `mapstructure:"user_data_file"`
WindowsPasswordTimeout time.Duration `mapstructure:"windows_password_timeout"`
VpcId string `mapstructure:"vpc_id"` VpcId string `mapstructure:"vpc_id"`
// Communicator settings // Communicator settings
...@@ -40,6 +42,10 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { ...@@ -40,6 +42,10 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
"packer %s", uuid.TimeOrderedUUID()) "packer %s", uuid.TimeOrderedUUID())
} }
if c.WindowsPasswordTimeout == 0 {
c.WindowsPasswordTimeout = 10 * time.Minute
}
// Validation // Validation
errs := c.Comm.Prepare(ctx) errs := c.Comm.Prepare(ctx)
if c.SourceAmi == "" { if c.SourceAmi == "" {
......
package common
import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"log"
"time"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer"
)
// StepGetPassword reads the password from a Windows server and sets it
// on the WinRM config.
type StepGetPassword struct {
Comm *communicator.Config
Timeout time.Duration
}
func (s *StepGetPassword) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
image := state.Get("source_image").(*ec2.Image)
// Skip if we're not Windows...
if image.Platform == nil || *image.Platform != "windows" {
log.Printf("[INFO] Not Windows, skipping get password...")
return multistep.ActionContinue
}
// If we already have a password, skip it
if s.Comm.WinRMPassword != "" {
ui.Say("Skipping waiting for password since WinRM password set...")
return multistep.ActionContinue
}
// Get the password
var password string
var err error
cancel := make(chan struct{})
waitDone := make(chan bool, 1)
go func() {
ui.Say("Waiting for auto-generated password for instance...")
ui.Message(
"It is normal for this process to take up to 15 minutes,\n" +
"but it usually takes around 5. Please wait.")
password, err = s.waitForPassword(state, cancel)
waitDone <- true
}()
timeout := time.After(s.Timeout)
WaitLoop:
for {
// Wait for either SSH to become available, a timeout to occur,
// or an interrupt to come through.
select {
case <-waitDone:
if err != nil {
ui.Error(fmt.Sprintf("Error waiting for password: %s", err))
state.Put("error", err)
return multistep.ActionHalt
}
ui.Message(fmt.Sprintf(" \nPassword retrieved!"))
s.Comm.WinRMPassword = password
break WaitLoop
case <-timeout:
err := fmt.Errorf("Timeout waiting for password.")
state.Put("error", err)
ui.Error(err.Error())
close(cancel)
return multistep.ActionHalt
case <-time.After(1 * time.Second):
if _, ok := state.GetOk(multistep.StateCancelled); ok {
// The step sequence was cancelled, so cancel waiting for password
// and just start the halting process.
close(cancel)
log.Println("[WARN] Interrupt detected, quitting waiting for password.")
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *StepGetPassword) Cleanup(multistep.StateBag) {}
func (s *StepGetPassword) waitForPassword(state multistep.StateBag, cancel <-chan struct{}) (string, error) {
ec2conn := state.Get("ec2").(*ec2.EC2)
instance := state.Get("instance").(*ec2.Instance)
privateKey := state.Get("privateKey").(string)
for {
select {
case <-cancel:
log.Println("[INFO] Retrieve password wait cancelled. Exiting loop.")
return "", errors.New("Retrieve password wait cancelled")
case <-time.After(5 * time.Second):
}
resp, err := ec2conn.GetPasswordData(&ec2.GetPasswordDataInput{
InstanceID: instance.InstanceID,
})
if err != nil {
err := fmt.Errorf("Error retrieving auto-generated instance password: %s", err)
return "", err
}
if resp.PasswordData != nil && *resp.PasswordData != "" {
decryptedPassword, err := decryptPasswordDataWithPrivateKey(
*resp.PasswordData, []byte(privateKey))
if err != nil {
err := fmt.Errorf("Error decrypting auto-generated instance password: %s", err)
return "", err
}
return decryptedPassword, nil
}
log.Printf("[DEBUG] Password is blank, will retry...")
}
}
func decryptPasswordDataWithPrivateKey(passwordData string, pemBytes []byte) (string, error) {
encryptedPasswd, err := base64.StdEncoding.DecodeString(passwordData)
if err != nil {
return "", err
}
block, _ := pem.Decode(pemBytes)
var asn1Bytes []byte
if _, ok := block.Headers["DEK-Info"]; ok {
return "", errors.New("encrypted private key isn't yet supported")
/*
asn1Bytes, err = x509.DecryptPEMBlock(block, password)
if err != nil {
return "", err
}
*/
} else {
asn1Bytes = block.Bytes
}
key, err := x509.ParsePKCS1PrivateKey(asn1Bytes)
if err != nil {
return "", err
}
out, err := rsa.DecryptPKCS1v15(nil, key, encryptedPasswd)
if err != nil {
return "", err
}
return string(out), nil
}
package common package common
import ( import (
"encoding/base64"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
...@@ -53,7 +54,14 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -53,7 +54,14 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
return multistep.ActionHalt return multistep.ActionHalt
} }
// Test if it is encoded already, and if not, encode it
if _, err := base64.StdEncoding.DecodeString(string(contents)); err != nil {
log.Printf("[DEBUG] base64 encoding user data...")
contents = []byte(base64.StdEncoding.EncodeToString(contents))
}
userData = string(contents) userData = string(contents)
} }
ui.Say("Launching a source AWS instance...") ui.Say("Launching a source AWS instance...")
......
...@@ -9,12 +9,13 @@ import ( ...@@ -9,12 +9,13 @@ import (
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/communicator"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type StepSecurityGroup struct { type StepSecurityGroup struct {
CommConfig *communicator.Config
SecurityGroupIds []string SecurityGroupIds []string
SSHPort int
VpcId string VpcId string
createdGroupId string createdGroupId string
...@@ -30,8 +31,9 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction { ...@@ -30,8 +31,9 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue return multistep.ActionContinue
} }
if s.SSHPort == 0 { port := s.CommConfig.Port()
panic("SSHPort must be set to a non-zero value.") if port == 0 {
panic("port must be set to a non-zero value.")
} }
// Create the group // Create the group
...@@ -57,15 +59,17 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction { ...@@ -57,15 +59,17 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction {
req := &ec2.AuthorizeSecurityGroupIngressInput{ req := &ec2.AuthorizeSecurityGroupIngressInput{
GroupID: groupResp.GroupID, GroupID: groupResp.GroupID,
IPProtocol: aws.String("tcp"), IPProtocol: aws.String("tcp"),
FromPort: aws.Long(int64(s.SSHPort)), FromPort: aws.Long(int64(port)),
ToPort: aws.Long(int64(s.SSHPort)), ToPort: aws.Long(int64(port)),
CIDRIP: aws.String("0.0.0.0/0"), CIDRIP: aws.String("0.0.0.0/0"),
} }
// We loop and retry this a few times because sometimes the security // We loop and retry this a few times because sometimes the security
// group isn't available immediately because AWS resources are eventaully // group isn't available immediately because AWS resources are eventaully
// consistent. // consistent.
ui.Say("Authorizing SSH access on the temporary security group...") ui.Say(fmt.Sprintf(
"Authorizing access to port %d the temporary security group...",
port))
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
_, err = ec2conn.AuthorizeSecurityGroupIngress(req) _, err = ec2conn.AuthorizeSecurityGroupIngress(req)
if err == nil { if err == nil {
......
...@@ -94,7 +94,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -94,7 +94,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}, },
&awscommon.StepSecurityGroup{ &awscommon.StepSecurityGroup{
SecurityGroupIds: b.config.SecurityGroupIds, SecurityGroupIds: b.config.SecurityGroupIds,
SSHPort: b.config.RunConfig.Comm.SSHPort, CommConfig: &b.config.RunConfig.Comm,
VpcId: b.config.VpcId, VpcId: b.config.VpcId,
}, },
&awscommon.StepRunSourceInstance{ &awscommon.StepRunSourceInstance{
...@@ -113,6 +113,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -113,6 +113,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
BlockDevices: b.config.BlockDevices, BlockDevices: b.config.BlockDevices,
Tags: b.config.RunTags, Tags: b.config.RunTags,
}, },
&awscommon.StepGetPassword{
Comm: &b.config.RunConfig.Comm,
Timeout: b.config.WindowsPasswordTimeout,
},
&communicator.StepConnect{ &communicator.StepConnect{
Config: &b.config.RunConfig.Comm, Config: &b.config.RunConfig.Comm,
Host: awscommon.SSHHost( Host: awscommon.SSHHost(
......
...@@ -179,8 +179,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -179,8 +179,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey, PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
}, },
&awscommon.StepSecurityGroup{ &awscommon.StepSecurityGroup{
CommConfig: &b.config.RunConfig.Comm,
SecurityGroupIds: b.config.SecurityGroupIds, SecurityGroupIds: b.config.SecurityGroupIds,
SSHPort: b.config.RunConfig.Comm.SSHPort,
VpcId: b.config.VpcId, VpcId: b.config.VpcId,
}, },
&awscommon.StepRunSourceInstance{ &awscommon.StepRunSourceInstance{
...@@ -198,6 +198,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -198,6 +198,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
BlockDevices: b.config.BlockDevices, BlockDevices: b.config.BlockDevices,
Tags: b.config.RunTags, Tags: b.config.RunTags,
}, },
&awscommon.StepGetPassword{
Comm: &b.config.RunConfig.Comm,
Timeout: b.config.WindowsPasswordTimeout,
},
&communicator.StepConnect{ &communicator.StepConnect{
Config: &b.config.RunConfig.Comm, Config: &b.config.RunConfig.Comm,
Host: awscommon.SSHHost( Host: awscommon.SSHHost(
......
...@@ -31,6 +31,18 @@ type Config struct { ...@@ -31,6 +31,18 @@ type Config struct {
WinRMTimeout time.Duration `mapstructure:"winrm_timeout"` WinRMTimeout time.Duration `mapstructure:"winrm_timeout"`
} }
// Port returns the port that will be used for access based on config.
func (c *Config) Port() int {
switch c.Type {
case "ssh":
return c.SSHPort
case "winrm":
return c.WinRMPort
default:
return 0
}
}
func (c *Config) Prepare(ctx *interpolate.Context) []error { func (c *Config) Prepare(ctx *interpolate.Context) []error {
if c.Type == "" { if c.Type == "" {
c.Type = "ssh" c.Type = "ssh"
......
...@@ -42,6 +42,7 @@ func Decode(target interface{}, config *DecodeOpts, raws ...interface{}) error { ...@@ -42,6 +42,7 @@ func Decode(target interface{}, config *DecodeOpts, raws ...interface{}) error {
if config.InterpolateContext == nil { if config.InterpolateContext == nil {
config.InterpolateContext = ctx config.InterpolateContext = ctx
} else { } else {
config.InterpolateContext.TemplatePath = ctx.TemplatePath
config.InterpolateContext.UserVariables = ctx.UserVariables config.InterpolateContext.UserVariables = ctx.UserVariables
} }
ctx = config.InterpolateContext ctx = config.InterpolateContext
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"sort" "sort"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
...@@ -317,6 +318,13 @@ func ParseFile(path string) (*Template, error) { ...@@ -317,6 +318,13 @@ func ParseFile(path string) (*Template, error) {
return nil, err return nil, err
} }
if !filepath.IsAbs(path) {
path, err = filepath.Abs(path)
if err != nil {
return nil, err
}
}
tpl.Path = path tpl.Path = path
return tpl, nil return tpl, nil
} }
package template package template
import ( import (
"path/filepath"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
...@@ -306,7 +307,7 @@ func TestParse(t *testing.T) { ...@@ -306,7 +307,7 @@ func TestParse(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
path := fixtureDir(tc.File) path, _ := filepath.Abs(fixtureDir(tc.File))
tpl, err := ParseFile(fixtureDir(tc.File)) tpl, err := ParseFile(fixtureDir(tc.File))
if (err != nil) != tc.Err { if (err != nil) != tc.Err {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
......
...@@ -168,6 +168,10 @@ each category, the available configuration keys are alphabetized. ...@@ -168,6 +168,10 @@ each category, the available configuration keys are alphabetized.
* `vpc_id` (string) - If launching into a VPC subnet, Packer needs the * `vpc_id` (string) - If launching into a VPC subnet, Packer needs the
VPC ID in order to create a temporary security group within the VPC. VPC ID in order to create a temporary security group within the VPC.
* `windows_password_timeout` (string) - The timeout for waiting for
a Windows password for Windows instances. Defaults to 20 minutes.
Example value: "10m"
## Basic Example ## Basic Example
Here is a basic example. It is completely valid except for the access keys: Here is a basic example. It is completely valid except for the access keys:
......
...@@ -209,6 +209,10 @@ each category, the available configuration keys are alphabetized. ...@@ -209,6 +209,10 @@ each category, the available configuration keys are alphabetized.
it is perfectly okay to create this directory as part of the provisioning it is perfectly okay to create this directory as part of the provisioning
process. process.
* `windows_password_timeout` (string) - The timeout for waiting for
a Windows password for Windows instances. Defaults to 20 minutes.
Example value: "10m"
## Basic Example ## Basic Example
Here is a basic example. It is completely valid except for the access keys: Here is a basic example. It is completely valid except for the access keys:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment