From 3efe5af068d10657721d80474bb94e0cbd8c1a61 Mon Sep 17 00:00:00 2001 From: Pieter Lazzaro Date: Sun, 25 Sep 2016 22:46:52 -0400 Subject: [PATCH] Create a password for windows instances when using WinRM communicator and no password is provided. --- builder/googlecompute/builder.go | 11 +- builder/googlecompute/driver.go | 29 ++++ builder/googlecompute/driver_gce.go | 113 ++++++++++++ builder/googlecompute/driver_mock.go | 28 +++ builder/googlecompute/step_create_instance.go | 4 + .../step_create_instance_test.go | 95 ++++++++++ .../step_create_windows_password.go | 114 ++++++++++++ .../step_create_windows_password_test.go | 162 ++++++++++++++++++ builder/googlecompute/winrm.go | 17 ++ 9 files changed, 570 insertions(+), 3 deletions(-) create mode 100644 builder/googlecompute/step_create_windows_password.go create mode 100644 builder/googlecompute/step_create_windows_password_test.go create mode 100644 builder/googlecompute/winrm.go diff --git a/builder/googlecompute/builder.go b/builder/googlecompute/builder.go index 014c40a38..7be284b50 100644 --- a/builder/googlecompute/builder.go +++ b/builder/googlecompute/builder.go @@ -58,13 +58,18 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepCreateInstance{ Debug: b.config.PackerDebug, }, + &StepCreateWindowsPassword{ + Debug: b.config.PackerDebug, + DebugKeyPath: fmt.Sprintf("gce_windows_%s.pem", b.config.PackerBuildName), + }, &StepInstanceInfo{ Debug: b.config.PackerDebug, }, &communicator.StepConnect{ - Config: &b.config.Comm, - Host: commHost, - SSHConfig: sshConfig, + Config: &b.config.Comm, + Host: commHost, + SSHConfig: sshConfig, + WinRMConfig: winrmConfig, }, new(common.StepProvision), new(StepWaitInstanceStartup), diff --git a/builder/googlecompute/driver.go b/builder/googlecompute/driver.go index 183ef294d..b235a33dd 100644 --- a/builder/googlecompute/driver.go +++ b/builder/googlecompute/driver.go @@ -1,5 +1,10 @@ package googlecompute +import ( + "crypto/rsa" + "time" +) + // Driver is the interface that has to be implemented to communicate // with GCE. The Driver interface exists mostly to allow a mock implementation // to be used to test the steps. @@ -44,6 +49,9 @@ type Driver interface { // WaitForInstance waits for an instance to reach the given state. WaitForInstance(state, zone, name string) <-chan error + + // CreateOrResetWindowsPassword creates or resets the password for a user on an Windows instance. + CreateOrResetWindowsPassword(zone, name string, config *WindowsPasswordConfig) (<-chan error, error) } type InstanceConfig struct { @@ -64,3 +72,24 @@ type InstanceConfig struct { Tags []string Zone string } + +// WindowsPasswordConfig is the data structue that GCE needs to encrypt the created +// windows password. +type WindowsPasswordConfig struct { + key *rsa.PrivateKey + password string + UserName string `json:"userName"` + Modulus string `json:"modulus"` + Exponent string `json:"exponent"` + Email string `json:"email"` + ExpireOn time.Time `json:"expireOn"` +} + +type windowsPasswordResponse struct { + UserName string `json:"userName"` + PasswordFound bool `json:"passwordFound"` + EncryptedPassword string `json:"encryptedPassword"` + Modulus string `json:"modulus"` + Exponent string `json:"exponent"` + ErrorMessage string `json:"errorMessage"` +} diff --git a/builder/googlecompute/driver_gce.go b/builder/googlecompute/driver_gce.go index d5d081793..cc871a3af 100644 --- a/builder/googlecompute/driver_gce.go +++ b/builder/googlecompute/driver_gce.go @@ -1,11 +1,18 @@ package googlecompute import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "encoding/base64" + "encoding/json" + "errors" "fmt" "log" "net/http" "runtime" "strings" + "time" "github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/version" @@ -393,6 +400,112 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) { return errCh, nil } +func (d *driverGCE) CreateOrResetWindowsPassword(instance, zone string, c *WindowsPasswordConfig) (<-chan error, error) { + + errCh := make(chan error, 1) + go d.createWindowsPassword(errCh, instance, zone, c) + + return errCh, nil +} + +func (d *driverGCE) createWindowsPassword(errCh chan<- error, name, zone string, c *WindowsPasswordConfig) { + + data, err := json.Marshal(c) + + if err != nil { + errCh <- err + return + } + dCopy := string(data) + + instance, err := d.service.Instances.Get(d.projectId, zone, name).Do() + instance.Metadata.Items = append(instance.Metadata.Items, &compute.MetadataItems{Key: "windows-keys", Value: &dCopy}) + + op, err := d.service.Instances.SetMetadata(d.projectId, zone, name, &compute.Metadata{ + Fingerprint: instance.Metadata.Fingerprint, + Items: instance.Metadata.Items, + }).Do() + + if err != nil { + errCh <- err + return + } + + newErrCh := make(chan error, 1) + go waitForState(newErrCh, "DONE", d.refreshZoneOp(zone, op)) + + select { + case err = <-newErrCh: + case <-time.After(time.Second * 30): + err = errors.New("time out while waiting for instance to create") + } + + if err != nil { + errCh <- err + return + } + + timeout := time.Now().Add(time.Minute * 3) + hash := sha1.New() + random := rand.Reader + + for time.Now().Before(timeout) { + if passwordResponses, err := d.getPasswordResponses(zone, name); err == nil { + for _, response := range passwordResponses { + if response.Modulus == c.Modulus { + + decodedPassword, err := base64.StdEncoding.DecodeString(response.EncryptedPassword) + + if err != nil { + errCh <- err + return + } + password, err := rsa.DecryptOAEP(hash, random, c.key, decodedPassword, nil) + + if err != nil { + errCh <- err + return + } + + c.password = string(password) + errCh <- nil + return + } + } + } + + time.Sleep(2 * time.Second) + } + err = errors.New("Could not retrieve password. Timed out.") + + errCh <- err + return + +} + +func (d *driverGCE) getPasswordResponses(zone, instance string) ([]windowsPasswordResponse, error) { + output, err := d.service.Instances.GetSerialPortOutput(d.projectId, zone, instance).Port(4).Do() + + if err != nil { + return nil, err + } + + responses := strings.Split(output.Contents, "\n") + + passwordResponses := make([]windowsPasswordResponse, 0, len(responses)) + + for _, response := range responses { + var passwordResponse windowsPasswordResponse + if err := json.Unmarshal([]byte(response), &passwordResponse); err != nil { + continue + } + + passwordResponses = append(passwordResponses, passwordResponse) + } + + return passwordResponses, nil +} + func (d *driverGCE) WaitForInstance(state, zone, name string) <-chan error { errCh := make(chan error, 1) go waitForState(errCh, state, d.refreshInstanceState(zone, name)) diff --git a/builder/googlecompute/driver_mock.go b/builder/googlecompute/driver_mock.go index b911aa6d6..f6f0831c1 100644 --- a/builder/googlecompute/driver_mock.go +++ b/builder/googlecompute/driver_mock.go @@ -67,6 +67,12 @@ type DriverMock struct { RunInstanceErrCh <-chan error RunInstanceErr error + CreateOrResetWindowsPasswordZone string + CreateOrResetWindowsPasswordInstance string + CreateOrResetWindowsPasswordConfig *WindowsPasswordConfig + CreateOrResetWindowsPasswordErr error + CreateOrResetWindowsPasswordErrCh <-chan error + WaitForInstanceState string WaitForInstanceZone string WaitForInstanceName string @@ -224,3 +230,25 @@ func (d *DriverMock) WaitForInstance(state, zone, name string) <-chan error { return resultCh } + +func (d *DriverMock) GetWindowsPassword() (string, error) { + return "", nil +} + +func (d *DriverMock) CreateOrResetWindowsPassword(instance, zone string, c *WindowsPasswordConfig) (<-chan error, error) { + + d.CreateOrResetWindowsPasswordInstance = instance + d.CreateOrResetWindowsPasswordZone = zone + d.CreateOrResetWindowsPasswordConfig = c + + c.password = "MOCK_PASSWORD" + + resultCh := d.CreateOrResetWindowsPasswordErrCh + if resultCh == nil { + ch := make(chan error) + close(ch) + resultCh = ch + } + + return resultCh, d.CreateOrResetWindowsPasswordErr +} diff --git a/builder/googlecompute/step_create_instance.go b/builder/googlecompute/step_create_instance.go index f3844ef67..2ab928fab 100644 --- a/builder/googlecompute/step_create_instance.go +++ b/builder/googlecompute/step_create_instance.go @@ -76,6 +76,10 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction return multistep.ActionHalt } + if sourceImage.IsWindows() && c.Comm.Type == "winrm" && c.Comm.WinRMPassword == "" { + state.Put("create_windows_password", true) + } + ui.Say("Creating instance...") name := c.InstanceName diff --git a/builder/googlecompute/step_create_instance_test.go b/builder/googlecompute/step_create_instance_test.go index 689eb3a35..8c567479b 100644 --- a/builder/googlecompute/step_create_instance_test.go +++ b/builder/googlecompute/step_create_instance_test.go @@ -41,6 +41,101 @@ func TestStepCreateInstance(t *testing.T) { assert.Equal(t, d.DeleteDiskZone, c.Zone, "Incorrect disk zone passed to driver.") } +func TestStepCreateInstance_windowsNeedsPassword(t *testing.T) { + + state := testState(t) + step := new(StepCreateInstance) + defer step.Cleanup(state) + + state.Put("ssh_public_key", "key") + c := state.Get("config").(*Config) + d := state.Get("driver").(*DriverMock) + d.GetImageResult = StubImage("test-image", "test-project", []string{"windows"}, 100) + c.Comm.Type = "winrm" + // run the step + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + // Verify state + nameRaw, ok := state.GetOk("instance_name") + if !ok { + t.Fatal("should have instance name") + } + + createPassword, ok := state.GetOk("create_windows_password") + + if !ok || !createPassword.(bool) { + t.Fatal("should need to create a windows password") + } + + // cleanup + step.Cleanup(state) + + if d.DeleteInstanceName != nameRaw.(string) { + t.Fatal("should've deleted instance") + } + if d.DeleteInstanceZone != c.Zone { + t.Fatalf("bad instance zone: %#v", d.DeleteInstanceZone) + } + + if d.DeleteDiskName != c.InstanceName { + t.Fatal("should've deleted disk") + } + if d.DeleteDiskZone != c.Zone { + t.Fatalf("bad disk zone: %#v", d.DeleteDiskZone) + } +} + +func TestStepCreateInstance_windowsPasswordSet(t *testing.T) { + + state := testState(t) + step := new(StepCreateInstance) + defer step.Cleanup(state) + + state.Put("ssh_public_key", "key") + + config := state.Get("config").(*Config) + driver := state.Get("driver").(*DriverMock) + driver.GetImageResult = StubImage("test-image", "test-project", []string{"windows"}, 100) + config.Comm.Type = "winrm" + config.Comm.WinRMPassword = "password" + + // run the step + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + // Verify state + nameRaw, ok := state.GetOk("instance_name") + if !ok { + t.Fatal("should have instance name") + } + + _, ok = state.GetOk("create_windows_password") + + if ok { + t.Fatal("should not need to create windows password") + } + + // cleanup + step.Cleanup(state) + + if driver.DeleteInstanceName != nameRaw.(string) { + t.Fatal("should've deleted instance") + } + if driver.DeleteInstanceZone != config.Zone { + t.Fatalf("bad instance zone: %#v", driver.DeleteInstanceZone) + } + + if driver.DeleteDiskName != config.InstanceName { + t.Fatal("should've deleted disk") + } + if driver.DeleteDiskZone != config.Zone { + t.Fatalf("bad disk zone: %#v", driver.DeleteDiskZone) + } +} + func TestStepCreateInstance_error(t *testing.T) { state := testState(t) step := new(StepCreateInstance) diff --git a/builder/googlecompute/step_create_windows_password.go b/builder/googlecompute/step_create_windows_password.go new file mode 100644 index 000000000..e131aecff --- /dev/null +++ b/builder/googlecompute/step_create_windows_password.go @@ -0,0 +1,114 @@ +package googlecompute + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/binary" + "encoding/pem" + "errors" + "fmt" + "os" + "time" + + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// StepCreateWindowsPassword represents a Packer build step that sets the windows password on a Windows GCE instance. +type StepCreateWindowsPassword struct { + Debug bool + DebugKeyPath string +} + +// Run executes the Packer build step that sets the windows password on a Windows GCE instance. +func (s *StepCreateWindowsPassword) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + d := state.Get("driver").(Driver) + c := state.Get("config").(*Config) + name := state.Get("instance_name").(string) + + if c.Comm.WinRMPassword != "" { + state.Put("winrm_password", c.Comm.WinRMPassword) + return multistep.ActionContinue + } + + create, ok := state.GetOk("create_windows_password") + + if !ok || !create.(bool) { + return multistep.ActionContinue + + } + ui.Say("Creating windows user for instance...") + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + err := fmt.Errorf("Error creating temporary key: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + buf := make([]byte, 4) + binary.BigEndian.PutUint32(buf, uint32(priv.E)) + + data := WindowsPasswordConfig{ + key: priv, + UserName: c.Comm.WinRMUser, + Modulus: base64.StdEncoding.EncodeToString(priv.N.Bytes()), + Exponent: base64.StdEncoding.EncodeToString(buf[1:]), + Email: c.Account.ClientEmail, + ExpireOn: time.Now().Add(time.Minute * 5), + } + + if s.Debug { + + priv_blk := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: x509.MarshalPKCS1PrivateKey(priv), + } + + ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath)) + f, err := os.Create(s.DebugKeyPath) + if err != nil { + state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) + return multistep.ActionHalt + } + + // Write out the key + err = pem.Encode(f, &priv_blk) + f.Close() + if err != nil { + state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) + return multistep.ActionHalt + } + } + + errCh, err := d.CreateOrResetWindowsPassword(name, c.Zone, &data) + + if err == nil { + ui.Message("Waiting for windows password to complete...") + select { + case err = <-errCh: + case <-time.After(c.stateTimeout): + err = errors.New("time out while waiting for the password to be created") + } + } + + if err != nil { + err := fmt.Errorf("Error creating windows password: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Message("Created password.") + + state.Put("winrm_password", data.password) + + return multistep.ActionContinue +} + +// Nothing to clean up. The windows password is only created on the single instance. +func (s *StepCreateWindowsPassword) Cleanup(state multistep.StateBag) {} diff --git a/builder/googlecompute/step_create_windows_password_test.go b/builder/googlecompute/step_create_windows_password_test.go new file mode 100644 index 000000000..58fc8ee83 --- /dev/null +++ b/builder/googlecompute/step_create_windows_password_test.go @@ -0,0 +1,162 @@ +package googlecompute + +import ( + "errors" + "io/ioutil" + "os" + + "github.com/mitchellh/multistep" + + "testing" +) + +func TestStepCreateOrResetWindowsPassword(t *testing.T) { + state := testState(t) + + // Step is run after the instance is created so we will have an instance name set + state.Put("instance_name", "mock_instance") + state.Put("create_windows_password", true) + + step := new(StepCreateWindowsPassword) + defer step.Cleanup(state) + + // run the step + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + if password, ok := state.GetOk("winrm_password"); !ok || password.(string) != "MOCK_PASSWORD" { + t.Fatal("should have a password", password, ok) + } +} + +func TestStepCreateOrResetWindowsPassword_passwordSet(t *testing.T) { + state := testState(t) + + // Step is run after the instance is created so we will have an instance name set + state.Put("instance_name", "mock_instance") + + c := state.Get("config").(*Config) + + c.Comm.WinRMPassword = "password" + + step := new(StepCreateWindowsPassword) + defer step.Cleanup(state) + + // run the step + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + if password, ok := state.GetOk("winrm_password"); !ok || password.(string) != "password" { + t.Fatal("should have used existing password", password, ok) + } +} + +func TestStepCreateOrResetWindowsPassword_dontNeedPassword(t *testing.T) { + state := testState(t) + + // Step is run after the instance is created so we will have an instance name set + state.Put("instance_name", "mock_instance") + + step := new(StepCreateWindowsPassword) + defer step.Cleanup(state) + + // run the step + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + +} + +func TestStepCreateOrResetWindowsPassword_debug(t *testing.T) { + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + tf.Close() + + state := testState(t) + // Step is run after the instance is created so we will have an instance name set + state.Put("instance_name", "mock_instance") + state.Put("create_windows_password", true) + + step := new(StepCreateWindowsPassword) + + step.Debug = true + step.DebugKeyPath = tf.Name() + + defer step.Cleanup(state) + + // run the step + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + if password, ok := state.GetOk("winrm_password"); !ok || password.(string) != "MOCK_PASSWORD" { + t.Fatal("should have a password", password, ok) + } + + if _, err := os.Stat(tf.Name()); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestStepCreateOrResetWindowsPassword_error(t *testing.T) { + state := testState(t) + + // Step is run after the instance is created so we will have an instance name set + state.Put("instance_name", "mock_instance") + state.Put("create_windows_password", true) + + step := new(StepCreateWindowsPassword) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + driver.CreateOrResetWindowsPasswordErr = errors.New("error") + + // run the step + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + + // Verify state + if _, ok := state.GetOk("error"); !ok { + t.Fatal("should have error") + } + + if _, ok := state.GetOk("winrm_password"); ok { + t.Fatal("should NOT have instance name") + } +} + +func TestStepCreateOrResetWindowsPassword_errorOnChannel(t *testing.T) { + state := testState(t) + + // Step is run after the instance is created so we will have an instance name set + state.Put("instance_name", "mock_instance") + state.Put("create_windows_password", true) + + step := new(StepCreateWindowsPassword) + defer step.Cleanup(state) + + driver := state.Get("driver").(*DriverMock) + + errCh := make(chan error, 1) + errCh <- errors.New("error") + + driver.CreateOrResetWindowsPasswordErrCh = errCh + + // run the step + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + + // Verify state + if _, ok := state.GetOk("error"); !ok { + t.Fatal("should have error") + } + if _, ok := state.GetOk("winrm_password"); ok { + t.Fatal("should NOT have instance name") + } +} diff --git a/builder/googlecompute/winrm.go b/builder/googlecompute/winrm.go new file mode 100644 index 000000000..8f744f9aa --- /dev/null +++ b/builder/googlecompute/winrm.go @@ -0,0 +1,17 @@ +package googlecompute + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/helper/communicator" +) + +// winrmConfig returns the WinRM configuration. +func winrmConfig(state multistep.StateBag) (*communicator.WinRMConfig, error) { + config := state.Get("config").(*Config) + password := state.Get("winrm_password").(string) + + return &communicator.WinRMConfig{ + Username: config.Comm.WinRMUser, + Password: password, + }, nil +}