From fd4e0e9da47f461e820ba4f87b780a66fbebac5a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jun 2015 22:35:45 -0700 Subject: [PATCH 1/9] builder/amazon: StepGetPassword --- builder/amazon/common/run_config.go | 6 + builder/amazon/common/step_get_password.go | 155 +++++++++++++++++++++ builder/amazon/ebs/builder.go | 4 + 3 files changed, 165 insertions(+) create mode 100644 builder/amazon/common/step_get_password.go diff --git a/builder/amazon/common/run_config.go b/builder/amazon/common/run_config.go index 6dec07b39..5589a5578 100644 --- a/builder/amazon/common/run_config.go +++ b/builder/amazon/common/run_config.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "time" "github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/helper/communicator" @@ -27,6 +28,7 @@ type RunConfig struct { TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"` UserData string `mapstructure:"user_data"` UserDataFile string `mapstructure:"user_data_file"` + WindowsPasswordTimeout time.Duration `mapstructure:"windows_password_timeout"` VpcId string `mapstructure:"vpc_id"` // Communicator settings @@ -40,6 +42,10 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { "packer %s", uuid.TimeOrderedUUID()) } + if c.WindowsPasswordTimeout == 0 { + c.WindowsPasswordTimeout = 10 * time.Minute + } + // Validation errs := c.Comm.Prepare(ctx) if c.SourceAmi == "" { diff --git a/builder/amazon/common/step_get_password.go b/builder/amazon/common/step_get_password.go new file mode 100644 index 000000000..9d982b10b --- /dev/null +++ b/builder/amazon/common/step_get_password.go @@ -0,0 +1,155 @@ +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 != "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...") + 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("Password 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 + } + } +} + +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 +} diff --git a/builder/amazon/ebs/builder.go b/builder/amazon/ebs/builder.go index cd3cd8f05..f61b258f4 100644 --- a/builder/amazon/ebs/builder.go +++ b/builder/amazon/ebs/builder.go @@ -113,6 +113,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe BlockDevices: b.config.BlockDevices, Tags: b.config.RunTags, }, + &awscommon.StepGetPassword{ + Comm: &b.config.RunConfig.Comm, + Timeout: b.config.WindowsPasswordTimeout, + }, &communicator.StepConnect{ Config: &b.config.RunConfig.Comm, Host: awscommon.SSHHost( From d23f254b7675dd1e49b5a393b27e38e0a6214c32 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jun 2015 22:39:19 -0700 Subject: [PATCH 2/9] builder/amazon: don't get password if platform not set on image --- builder/amazon/common/step_get_password.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/amazon/common/step_get_password.go b/builder/amazon/common/step_get_password.go index 9d982b10b..a2ef04952 100644 --- a/builder/amazon/common/step_get_password.go +++ b/builder/amazon/common/step_get_password.go @@ -28,7 +28,7 @@ func (s *StepGetPassword) Run(state multistep.StateBag) multistep.StepAction { image := state.Get("source_image").(*ec2.Image) // Skip if we're not Windows... - if *image.Platform != "windows" { + if image.Platform == nil || *image.Platform != "windows" { log.Printf("[INFO] Not Windows, skipping get password...") return multistep.ActionContinue } From 022a115d190a4c6eef332bf3358af75991d26c3e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jun 2015 22:42:10 -0700 Subject: [PATCH 3/9] builder/amazon: improve messaging --- builder/amazon/common/step_get_password.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/builder/amazon/common/step_get_password.go b/builder/amazon/common/step_get_password.go index a2ef04952..0fdd467eb 100644 --- a/builder/amazon/common/step_get_password.go +++ b/builder/amazon/common/step_get_password.go @@ -46,6 +46,9 @@ func (s *StepGetPassword) Run(state multistep.StateBag) multistep.StepAction { 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 }() From 1d94e0f8e38ec69c31b7a28549f06c26687e7f23 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jun 2015 22:50:02 -0700 Subject: [PATCH 4/9] template: abslute path for template path --- template/parse.go | 8 ++++++++ template/parse_test.go | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/template/parse.go b/template/parse.go index dbb29569d..4a7069dea 100644 --- a/template/parse.go +++ b/template/parse.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "path/filepath" "sort" "github.com/hashicorp/go-multierror" @@ -317,6 +318,13 @@ func ParseFile(path string) (*Template, error) { return nil, err } + if !filepath.IsAbs(path) { + path, err = filepath.Abs(path) + if err != nil { + return nil, err + } + } + tpl.Path = path return tpl, nil } diff --git a/template/parse_test.go b/template/parse_test.go index 9abca2f77..fa5477a4f 100644 --- a/template/parse_test.go +++ b/template/parse_test.go @@ -1,6 +1,7 @@ package template import ( + "path/filepath" "reflect" "strings" "testing" @@ -306,7 +307,7 @@ func TestParse(t *testing.T) { } for _, tc := range cases { - path := fixtureDir(tc.File) + path, _ := filepath.Abs(fixtureDir(tc.File)) tpl, err := ParseFile(fixtureDir(tc.File)) if (err != nil) != tc.Err { t.Fatalf("err: %s", err) From dc8c94890a8f926c621f062fc693c20ee93a21be Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jun 2015 22:56:36 -0700 Subject: [PATCH 5/9] helper/config: copy template path properly --- helper/config/decode.go | 1 + 1 file changed, 1 insertion(+) diff --git a/helper/config/decode.go b/helper/config/decode.go index 20554da61..1088fd19b 100644 --- a/helper/config/decode.go +++ b/helper/config/decode.go @@ -42,6 +42,7 @@ func Decode(target interface{}, config *DecodeOpts, raws ...interface{}) error { if config.InterpolateContext == nil { config.InterpolateContext = ctx } else { + config.InterpolateContext.TemplatePath = ctx.TemplatePath config.InterpolateContext.UserVariables = ctx.UserVariables } ctx = config.InterpolateContext From 8f6ecfd9e31c8d57b228ff656da7144ce304dab3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jun 2015 23:12:59 -0700 Subject: [PATCH 6/9] builder/amazon: various fixes (minor) to get things going --- builder/amazon/common/step_get_password.go | 8 ++++++-- .../amazon/common/step_run_source_instance.go | 8 ++++++++ builder/amazon/common/step_security_group.go | 16 ++++++++++------ builder/amazon/ebs/builder.go | 2 +- builder/amazon/instance/builder.go | 2 +- helper/communicator/config.go | 12 ++++++++++++ 6 files changed, 38 insertions(+), 10 deletions(-) diff --git a/builder/amazon/common/step_get_password.go b/builder/amazon/common/step_get_password.go index 0fdd467eb..37cfe3af6 100644 --- a/builder/amazon/common/step_get_password.go +++ b/builder/amazon/common/step_get_password.go @@ -48,7 +48,9 @@ func (s *StepGetPassword) Run(state multistep.StateBag) multistep.StepAction { 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.") + "but it usually takes around 5. Please wait. After the\n" + + "password is read, it will printed out below. Since it should\n" + + "be a temporary password, this should be a minimal security risk.") password, err = s.waitForPassword(state, cancel) waitDone <- true }() @@ -66,7 +68,7 @@ WaitLoop: return multistep.ActionHalt } - ui.Message("Password retrieved!") + ui.Message(fmt.Sprintf(" \nPassword retrieved: %s", password)) s.Comm.WinRMPassword = password break WaitLoop case <-timeout: @@ -121,6 +123,8 @@ func (s *StepGetPassword) waitForPassword(state multistep.StateBag, cancel <-cha return decryptedPassword, nil } + + log.Printf("[DEBUG] Password is blank, will retry...") } } diff --git a/builder/amazon/common/step_run_source_instance.go b/builder/amazon/common/step_run_source_instance.go index 92dafa564..021432e77 100644 --- a/builder/amazon/common/step_run_source_instance.go +++ b/builder/amazon/common/step_run_source_instance.go @@ -1,6 +1,7 @@ package common import ( + "encoding/base64" "fmt" "io/ioutil" "log" @@ -53,7 +54,14 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi 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) + } ui.Say("Launching a source AWS instance...") diff --git a/builder/amazon/common/step_security_group.go b/builder/amazon/common/step_security_group.go index d870fd1c3..b65ebb408 100644 --- a/builder/amazon/common/step_security_group.go +++ b/builder/amazon/common/step_security_group.go @@ -9,12 +9,13 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/common/uuid" + "github.com/mitchellh/packer/helper/communicator" "github.com/mitchellh/packer/packer" ) type StepSecurityGroup struct { + CommConfig *communicator.Config SecurityGroupIds []string - SSHPort int VpcId string createdGroupId string @@ -30,8 +31,9 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionContinue } - if s.SSHPort == 0 { - panic("SSHPort must be set to a non-zero value.") + port := s.CommConfig.Port() + if port == 0 { + panic("port must be set to a non-zero value.") } // Create the group @@ -57,15 +59,17 @@ func (s *StepSecurityGroup) Run(state multistep.StateBag) multistep.StepAction { req := &ec2.AuthorizeSecurityGroupIngressInput{ GroupID: groupResp.GroupID, IPProtocol: aws.String("tcp"), - FromPort: aws.Long(int64(s.SSHPort)), - ToPort: aws.Long(int64(s.SSHPort)), + FromPort: aws.Long(int64(port)), + ToPort: aws.Long(int64(port)), CIDRIP: aws.String("0.0.0.0/0"), } // We loop and retry this a few times because sometimes the security // group isn't available immediately because AWS resources are eventaully // 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++ { _, err = ec2conn.AuthorizeSecurityGroupIngress(req) if err == nil { diff --git a/builder/amazon/ebs/builder.go b/builder/amazon/ebs/builder.go index f61b258f4..162c06e28 100644 --- a/builder/amazon/ebs/builder.go +++ b/builder/amazon/ebs/builder.go @@ -94,7 +94,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe }, &awscommon.StepSecurityGroup{ SecurityGroupIds: b.config.SecurityGroupIds, - SSHPort: b.config.RunConfig.Comm.SSHPort, + CommConfig: &b.config.RunConfig.Comm, VpcId: b.config.VpcId, }, &awscommon.StepRunSourceInstance{ diff --git a/builder/amazon/instance/builder.go b/builder/amazon/instance/builder.go index d26cc63e3..ffe1c2da6 100644 --- a/builder/amazon/instance/builder.go +++ b/builder/amazon/instance/builder.go @@ -179,8 +179,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey, }, &awscommon.StepSecurityGroup{ + CommConfig: &b.config.RunConfig.Comm, SecurityGroupIds: b.config.SecurityGroupIds, - SSHPort: b.config.RunConfig.Comm.SSHPort, VpcId: b.config.VpcId, }, &awscommon.StepRunSourceInstance{ diff --git a/helper/communicator/config.go b/helper/communicator/config.go index f0cb78df7..72dc69b7e 100644 --- a/helper/communicator/config.go +++ b/helper/communicator/config.go @@ -31,6 +31,18 @@ type Config struct { 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 { if c.Type == "" { c.Type = "ssh" From e9d916a7bcd3a99ee3161efc1ac69f1c95255634 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jun 2015 23:14:48 -0700 Subject: [PATCH 7/9] builder/amazon: don't print windows password --- builder/amazon/common/step_get_password.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/builder/amazon/common/step_get_password.go b/builder/amazon/common/step_get_password.go index 37cfe3af6..ab51f4394 100644 --- a/builder/amazon/common/step_get_password.go +++ b/builder/amazon/common/step_get_password.go @@ -48,9 +48,7 @@ func (s *StepGetPassword) Run(state multistep.StateBag) multistep.StepAction { 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. After the\n" + - "password is read, it will printed out below. Since it should\n" + - "be a temporary password, this should be a minimal security risk.") + "but it usually takes around 5. Please wait.") password, err = s.waitForPassword(state, cancel) waitDone <- true }() @@ -68,7 +66,7 @@ WaitLoop: return multistep.ActionHalt } - ui.Message(fmt.Sprintf(" \nPassword retrieved: %s", password)) + ui.Message(fmt.Sprintf(" \nPassword retrieved!")) s.Comm.WinRMPassword = password break WaitLoop case <-timeout: From 101e5986dcef11c688b5ecfb23511abc90a5d5bd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jun 2015 10:50:18 -0700 Subject: [PATCH 8/9] builder/amazon: enable windows for instance type too --- builder/amazon/instance/builder.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/builder/amazon/instance/builder.go b/builder/amazon/instance/builder.go index ffe1c2da6..ec62394ee 100644 --- a/builder/amazon/instance/builder.go +++ b/builder/amazon/instance/builder.go @@ -198,6 +198,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe BlockDevices: b.config.BlockDevices, Tags: b.config.RunTags, }, + &awscommon.StepGetPassword{ + Comm: &b.config.RunConfig.Comm, + Timeout: b.config.WindowsPasswordTimeout, + }, &communicator.StepConnect{ Config: &b.config.RunConfig.Comm, Host: awscommon.SSHHost( From b2e9277d3b8cec19e6e935629d8888927bd4c95c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jun 2015 10:51:34 -0700 Subject: [PATCH 9/9] website: update for Windows AWS instances --- website/source/docs/builders/amazon-ebs.html.markdown | 4 ++++ website/source/docs/builders/amazon-instance.html.markdown | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/website/source/docs/builders/amazon-ebs.html.markdown b/website/source/docs/builders/amazon-ebs.html.markdown index 0ff9522df..088e6e974 100644 --- a/website/source/docs/builders/amazon-ebs.html.markdown +++ b/website/source/docs/builders/amazon-ebs.html.markdown @@ -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 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 Here is a basic example. It is completely valid except for the access keys: diff --git a/website/source/docs/builders/amazon-instance.html.markdown b/website/source/docs/builders/amazon-instance.html.markdown index ae5fbff27..249313160 100644 --- a/website/source/docs/builders/amazon-instance.html.markdown +++ b/website/source/docs/builders/amazon-instance.html.markdown @@ -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 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 Here is a basic example. It is completely valid except for the access keys: