From 8014dac742544578380d9442737ca7a7acc47534 Mon Sep 17 00:00:00 2001 From: Shawn Neal Date: Thu, 20 Aug 2015 13:27:51 -0700 Subject: [PATCH] Add Windows support to Chef provisioners - Add guest os type to change the default Chef-Solo and Chef-Client provisioner behavior. Paths, commands etc. - Change Chef installation download location to chef.io domain - Add encrypted data bag secret configuration --- provisioner/chef-client/provisioner.go | 113 ++++++++++++++------ provisioner/chef-client/provisioner_test.go | 82 +++++++------- provisioner/chef-solo/provisioner.go | 54 ++++++++-- 3 files changed, 169 insertions(+), 80 deletions(-) diff --git a/provisioner/chef-client/provisioner.go b/provisioner/chef-client/provisioner.go index ddb55714b..e630a4cbb 100644 --- a/provisioner/chef-client/provisioner.go +++ b/provisioner/chef-client/provisioner.go @@ -16,9 +16,29 @@ import ( "github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/provisioner" "github.com/mitchellh/packer/template/interpolate" ) +type guestOSTypeConfig struct { + executeCommand string + installCommand string + stagingDir string +} + +var guestOSTypeConfigs = map[string]guestOSTypeConfig{ + provisioner.UnixOSType: guestOSTypeConfig{ + executeCommand: "{{if .Sudo}}sudo {{end}}chef-client --no-color -c {{.ConfigPath}} -j {{.JsonPath}}", + installCommand: "curl -L https://www.chef.io/chef/install.sh | {{if .Sudo}}sudo {{end}}bash", + stagingDir: "/tmp/packer-chef-client", + }, + provisioner.WindowsOSType: guestOSTypeConfig{ + executeCommand: "c:/opscode/chef/bin/chef-client.bat --no-color -c {{.ConfigPath}} -j {{.JsonPath}}", + installCommand: "powershell.exe -Command \"(New-Object System.Net.WebClient).DownloadFile('http://chef.io/chef/install.msi', 'C:\\Windows\\Temp\\chef.msi');Start-Process 'msiexec' -ArgumentList '/qb /i C:\\Windows\\Temp\\chef.msi' -NoNewWindow -Wait\"", + stagingDir: "C:/Windows/Temp/packer-chef-client", + }, +} + type Config struct { common.PackerConfig `mapstructure:",squash"` @@ -39,12 +59,16 @@ type Config struct { ClientKey string `mapstructure:"client_key"` ValidationKeyPath string `mapstructure:"validation_key_path"` ValidationClientName string `mapstructure:"validation_client_name"` + GuestOSType string `mapstructure:"guest_os_type"` + EncryptedDataBagSecretPath string `mapstructure:"encrypted_data_bag_secret_path"` ctx interpolate.Context } type Provisioner struct { - config Config + config Config + guestOSTypeConfig guestOSTypeConfig + guestCommands *provisioner.GuestCommands } type ConfigTemplate struct { @@ -55,6 +79,7 @@ type ConfigTemplate struct { ValidationClientName string ChefEnvironment string SslVerifyMode string + EncryptedDataBagSecretPath string } type ExecuteTemplate struct { @@ -82,15 +107,28 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { return err } + if p.config.GuestOSType == "" { + p.config.GuestOSType = provisioner.DefaultOSType + } + p.config.GuestOSType = strings.ToLower(p.config.GuestOSType) + + var ok bool + p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType] + if !ok { + return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) + } + + p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo) + if err != nil { + return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) + } + if p.config.ExecuteCommand == "" { - p.config.ExecuteCommand = "{{if .Sudo}}sudo {{end}}chef-client " + - "--no-color -c {{.ConfigPath}} -j {{.JsonPath}}" + p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand } if p.config.InstallCommand == "" { - p.config.InstallCommand = "curl -L " + - "https://www.chef.io/chef/install.sh | " + - "{{if .Sudo}}sudo {{end}}bash" + p.config.InstallCommand = p.guestOSTypeConfig.installCommand } if p.config.RunList == nil { @@ -98,7 +136,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } if p.config.StagingDir == "" { - p.config.StagingDir = "/tmp/packer-chef-client" + p.config.StagingDir = p.guestOSTypeConfig.stagingDir } var errs *packer.MultiError @@ -113,6 +151,15 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } } + if p.config.EncryptedDataBagSecretPath != "" { + pFileInfo, err := os.Stat(p.config.EncryptedDataBagSecretPath) + + if err != nil || pFileInfo.IsDir() { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Bad encrypted data bag secret '%s': %s", p.config.EncryptedDataBagSecretPath, err)) + } + } + if p.config.ServerUrl == "" { errs = packer.MultiErrorAppend( errs, fmt.Errorf("server_url must be set")) @@ -168,15 +215,23 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { p.config.ClientKey = fmt.Sprintf("%s/client.pem", p.config.StagingDir) } + encryptedDataBagSecretPath := "" + if p.config.EncryptedDataBagSecretPath != "" { + encryptedDataBagSecretPath = fmt.Sprintf("%s/encrypted_data_bag_secret", p.config.StagingDir) + if err := p.uploadFile(ui, comm, encryptedDataBagSecretPath, p.config.EncryptedDataBagSecretPath); err != nil { + return fmt.Errorf("Error uploading encrypted data bag secret: %s", err) + } + } + if p.config.ValidationKeyPath != "" { remoteValidationKeyPath = fmt.Sprintf("%s/validation.pem", p.config.StagingDir) - if err := p.copyValidationKey(ui, comm, remoteValidationKeyPath); err != nil { + if err := p.uploadFile(ui, comm, remoteValidationKeyPath, p.config.ValidationKeyPath); err != nil { return fmt.Errorf("Error copying validation key: %s", err) } } configPath, err := p.createConfig( - ui, comm, nodeName, serverUrl, p.config.ClientKey, remoteValidationKeyPath, p.config.ValidationClientName, p.config.ChefEnvironment, p.config.SslVerifyMode) + ui, comm, nodeName, serverUrl, p.config.ClientKey, encryptedDataBagSecretPath, remoteValidationKeyPath, p.config.ValidationClientName, p.config.ChefEnvironment, p.config.SslVerifyMode) if err != nil { return fmt.Errorf("Error creating Chef config file: %s", err) } @@ -210,7 +265,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { } if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil { - return fmt.Errorf("Error removing /etc/chef directory: %s", err) + return fmt.Errorf("Error removing %s: %s", p.config.StagingDir, err) } return nil @@ -236,7 +291,7 @@ func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, ds return comm.UploadDir(dst, src, nil) } -func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, nodeName string, serverUrl string, clientKey string, remoteKeyPath string, validationClientName string, chefEnvironment string, sslVerifyMode string) (string, error) { +func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, nodeName string, serverUrl string, clientKey string, encryptedDataBagSecretPath string, remoteKeyPath string, validationClientName string, chefEnvironment string, sslVerifyMode string) (string, error) { ui.Message("Creating configuration file 'client.rb'") // Read the template @@ -265,6 +320,7 @@ func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, nodeN ValidationClientName: validationClientName, ChefEnvironment: chefEnvironment, SslVerifyMode: sslVerifyMode, + EncryptedDataBagSecretPath: encryptedDataBagSecretPath, } configString, err := interpolate.Render(tpl, &ctx) if err != nil { @@ -337,12 +393,7 @@ func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { ui.Message(fmt.Sprintf("Creating directory: %s", dir)) - mkdirCmd := fmt.Sprintf("mkdir -p '%s'", dir) - if !p.config.PreventSudo { - mkdirCmd = "sudo " + mkdirCmd - } - - cmd := &packer.RemoteCmd{Command: mkdirCmd} + cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)} if err := cmd.StartWithUi(comm, ui); err != nil { return err } @@ -351,11 +402,7 @@ func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir stri } // Chmod the directory to 0777 just so that we can access it as our user - mkdirCmd = fmt.Sprintf("chmod 0777 '%s'", dir) - if !p.config.PreventSudo { - mkdirCmd = "sudo " + mkdirCmd - } - cmd = &packer.RemoteCmd{Command: mkdirCmd} + cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")} if err := cmd.StartWithUi(comm, ui); err != nil { return err } @@ -415,15 +462,7 @@ func (p *Provisioner) knifeExec(ui packer.Ui, comm packer.Communicator, node str func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { ui.Message(fmt.Sprintf("Removing directory: %s", dir)) - rmCmd := fmt.Sprintf("rm -rf '%s'", dir) - if !p.config.PreventSudo { - rmCmd = "sudo " + rmCmd - } - - cmd := &packer.RemoteCmd{ - Command: rmCmd, - } - + cmd := &packer.RemoteCmd{Command: p.guestCommands.RemoveDir(dir)} if err := cmd.StartWithUi(comm, ui); err != nil { return err } @@ -470,6 +509,8 @@ func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error return err } + ui.Message(command) + cmd := &packer.RemoteCmd{Command: command} if err := cmd.StartWithUi(comm, ui); err != nil { return err @@ -483,11 +524,10 @@ func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error return nil } -func (p *Provisioner) copyValidationKey(ui packer.Ui, comm packer.Communicator, remotePath string) error { - ui.Message("Uploading validation key...") +func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, remotePath string, localPath string) error { + ui.Message(fmt.Sprintf("Uploading %s...", localPath)) - // First upload the validation key to a writable location - f, err := os.Open(p.config.ValidationKeyPath) + f, err := os.Open(localPath) if err != nil { return err } @@ -587,6 +627,9 @@ log_level :info log_location STDOUT chef_server_url "{{.ServerUrl}}" client_key "{{.ClientKey}}" +{{if ne .EncryptedDataBagSecretPath ""}} +encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}" +{{end}} {{if ne .ValidationClientName ""}} validation_client_name "{{.ValidationClientName}}" {{else}} diff --git a/provisioner/chef-client/provisioner_test.go b/provisioner/chef-client/provisioner_test.go index 934403040..740c11c19 100644 --- a/provisioner/chef-client/provisioner_test.go +++ b/provisioner/chef-client/provisioner_test.go @@ -139,53 +139,63 @@ func TestProvisionerPrepare_serverUrl(t *testing.T) { } func TestProvisioner_createDir(t *testing.T) { - p1 := &Provisioner{config: Config{PreventSudo: true}} - p2 := &Provisioner{config: Config{PreventSudo: false}} - comm := &packer.MockCommunicator{} - ui := &packer.BasicUi{ - Reader: new(bytes.Buffer), - Writer: new(bytes.Buffer), - } + for _, sudo := range []bool{true, false} { + config := testConfig() + config["prevent_sudo"] = !sudo - if err := p1.createDir(ui, comm, "/tmp/foo"); err != nil { - t.Fatalf("err: %s", err) - } + p := &Provisioner{} + comm := &packer.MockCommunicator{} + ui := &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + } - if strings.HasPrefix(comm.StartCmd.Command, "sudo") { - t.Fatalf("createDir should not use sudo, got: \"%s\"", comm.StartCmd.Command) - } + err := p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } - if err := p2.createDir(ui, comm, "/tmp/foo"); err != nil { - t.Fatalf("err: %s", err) - } + if err := p.createDir(ui, comm, "/tmp/foo"); err != nil { + t.Fatalf("err: %s", err) + } - if !strings.HasPrefix(comm.StartCmd.Command, "sudo") { - t.Fatalf("createDir should use sudo, got: \"%s\"", comm.StartCmd.Command) + if !sudo && strings.HasPrefix(comm.StartCmd.Command, "sudo") { + t.Fatalf("createDir should not use sudo, got: \"%s\"", comm.StartCmd.Command) + } + + if sudo && !strings.HasPrefix(comm.StartCmd.Command, "sudo") { + t.Fatalf("createDir should use sudo, got: \"%s\"", comm.StartCmd.Command) + } } } func TestProvisioner_removeDir(t *testing.T) { - p1 := &Provisioner{config: Config{PreventSudo: true}} - p2 := &Provisioner{config: Config{PreventSudo: false}} - comm := &packer.MockCommunicator{} - ui := &packer.BasicUi{ - Reader: new(bytes.Buffer), - Writer: new(bytes.Buffer), - } + for _, sudo := range []bool{true, false} { + config := testConfig() + config["prevent_sudo"] = !sudo + + p := &Provisioner{} + comm := &packer.MockCommunicator{} + ui := &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + } - if err := p1.removeDir(ui, comm, "/tmp/foo"); err != nil { - t.Fatalf("err: %s", err) - } + err := p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } - if strings.HasPrefix(comm.StartCmd.Command, "sudo") { - t.Fatalf("removeDir should not use sudo, got: \"%s\"", comm.StartCmd.Command) - } + if err := p.removeDir(ui, comm, "/tmp/foo"); err != nil { + t.Fatalf("err: %s", err) + } - if err := p2.removeDir(ui, comm, "/tmp/foo"); err != nil { - t.Fatalf("err: %s", err) - } + if !sudo && strings.HasPrefix(comm.StartCmd.Command, "sudo") { + t.Fatalf("removeDir should not use sudo, got: \"%s\"", comm.StartCmd.Command) + } - if !strings.HasPrefix(comm.StartCmd.Command, "sudo") { - t.Fatalf("removeDir should use sudo, got: \"%s\"", comm.StartCmd.Command) + if sudo && !strings.HasPrefix(comm.StartCmd.Command, "sudo") { + t.Fatalf("removeDir should use sudo, got: \"%s\"", comm.StartCmd.Command) + } } } diff --git a/provisioner/chef-solo/provisioner.go b/provisioner/chef-solo/provisioner.go index 04ecadb1c..607b98645 100644 --- a/provisioner/chef-solo/provisioner.go +++ b/provisioner/chef-solo/provisioner.go @@ -15,9 +15,29 @@ import ( "github.com/mitchellh/packer/common" "github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/provisioner" "github.com/mitchellh/packer/template/interpolate" ) +type guestOSTypeConfig struct { + executeCommand string + installCommand string + stagingDir string +} + +var guestOSTypeConfigs = map[string]guestOSTypeConfig{ + provisioner.UnixOSType: guestOSTypeConfig{ + executeCommand: "{{if .Sudo}}sudo {{end}}chef-solo --no-color -c {{.ConfigPath}} -j {{.JsonPath}}", + installCommand: "curl -L https://www.chef.io/chef/install.sh | {{if .Sudo}}sudo {{end}}bash", + stagingDir: "/tmp/packer-chef-client", + }, + provisioner.WindowsOSType: guestOSTypeConfig{ + executeCommand: "c:/opscode/chef/bin/chef-solo.bat --no-color -c {{.ConfigPath}} -j {{.JsonPath}}", + installCommand: "powershell.exe -Command \"(New-Object System.Net.WebClient).DownloadFile('http://chef.io/chef/install.msi', 'C:\\Windows\\Temp\\chef.msi');Start-Process 'msiexec' -ArgumentList '/qb /i C:\\Windows\\Temp\\chef.msi' -NoNewWindow -Wait\"", + stagingDir: "C:/Windows/Temp/packer-chef-client", + }, +} + type Config struct { common.PackerConfig `mapstructure:",squash"` @@ -36,12 +56,15 @@ type Config struct { RunList []string `mapstructure:"run_list"` SkipInstall bool `mapstructure:"skip_install"` StagingDir string `mapstructure:"staging_directory"` + GuestOSType string `mapstructure:"guest_os_type"` ctx interpolate.Context } type Provisioner struct { - config Config + config Config + guestOSTypeConfig guestOSTypeConfig + guestCommands *provisioner.GuestCommands } type ConfigTemplate struct { @@ -86,12 +109,28 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { return err } + if p.config.GuestOSType == "" { + p.config.GuestOSType = provisioner.DefaultOSType + } + p.config.GuestOSType = strings.ToLower(p.config.GuestOSType) + + var ok bool + p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType] + if !ok { + return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) + } + + p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo) + if err != nil { + return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) + } + if p.config.ExecuteCommand == "" { - p.config.ExecuteCommand = "{{if .Sudo}}sudo {{end}}chef-solo --no-color -c {{.ConfigPath}} -j {{.JsonPath}}" + p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand } if p.config.InstallCommand == "" { - p.config.InstallCommand = "curl -L https://www.chef.io/chef/install.sh | {{if .Sudo}}sudo {{end}}bash" + p.config.InstallCommand = p.guestOSTypeConfig.installCommand } if p.config.RunList == nil { @@ -99,7 +138,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } if p.config.StagingDir == "" { - p.config.StagingDir = "/tmp/packer-chef-solo" + p.config.StagingDir = p.guestOSTypeConfig.stagingDir } var errs *packer.MultiError @@ -374,16 +413,13 @@ func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { ui.Message(fmt.Sprintf("Creating directory: %s", dir)) - cmd := &packer.RemoteCmd{ - Command: fmt.Sprintf("mkdir -p '%s'", dir), - } + cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)} if err := cmd.StartWithUi(comm, ui); err != nil { return err } - if cmd.ExitStatus != 0 { - return fmt.Errorf("Non-zero exit status.") + return fmt.Errorf("Non-zero exit status. See output above for more info.") } return nil