Create a password for windows instances when using WinRM communicator and no password is provided.

This commit is contained in:
Pieter Lazzaro 2016-09-25 22:46:52 -04:00
parent 74ccdddd13
commit 3efe5af068
9 changed files with 570 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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