Merge pull request #2240 from mitchellh/f-windows

Windows AWS instances
This commit is contained in:
Mitchell Hashimoto 2015-06-14 10:52:10 -07:00
commit ccf20568a7
12 changed files with 225 additions and 9 deletions

View File

@ -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 {
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 {
"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 == "" {

View File

@ -0,0 +1,160 @@
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
}

View File

@ -1,6 +1,7 @@
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
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...")

View File

@ -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 {
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 {
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 {

View File

@ -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
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(

View File

@ -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
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(

View File

@ -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"

View File

@ -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

View File

@ -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) {
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
} }

View File

@ -1,6 +1,7 @@
package template package template
import ( import (
"path/filepath"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@ -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)

View File

@ -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:

View File

@ -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: