From 67f6d6cdb53894967b1c5bcdb359c8f0434ca3a1 Mon Sep 17 00:00:00 2001 From: Shawn Neal Date: Wed, 19 Aug 2015 10:38:31 -0700 Subject: [PATCH 1/5] Adds provisioner guest commands abstraction Provisioners often needs to perform command line operations on guests that may have different syntax and shells. The GuestCommands type abstracts these away so provisioners can avoid littering branching logic all over the place. --- provisioner/guest_commands.go | 72 +++++++++++++++++ provisioner/guest_commands_test.go | 120 +++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 provisioner/guest_commands.go create mode 100644 provisioner/guest_commands_test.go diff --git a/provisioner/guest_commands.go b/provisioner/guest_commands.go new file mode 100644 index 000000000..4a65312f9 --- /dev/null +++ b/provisioner/guest_commands.go @@ -0,0 +1,72 @@ +package provisioner + +import ( + "fmt" + "strings" +) + +const UnixOSType = "unix" +const WindowsOSType = "windows" +const DefaultOSType = UnixOSType + +type guestOSTypeCommand struct { + chmod string + mkdir string + removeDir string +} + +var guestOSTypeCommands = map[string]guestOSTypeCommand{ + UnixOSType: guestOSTypeCommand{ + chmod: "chmod %s '%s'", + mkdir: "mkdir -p '%s'", + removeDir: "rm -rf '%s'", + }, + WindowsOSType: guestOSTypeCommand{ + chmod: "echo 'skipping chmod %s %s'", // no-op + mkdir: "New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path %s", + removeDir: "rm %s -recurse -force", + }, +} + +type GuestCommands struct { + GuestOSType string + Sudo bool +} + +func NewGuestCommands(osType string, sudo bool) (*GuestCommands, error) { + _, ok := guestOSTypeCommands[osType] + if !ok { + return nil, fmt.Errorf("Invalid osType: \"%s\"", osType) + } + return &GuestCommands{GuestOSType: osType, Sudo: sudo}, nil +} + +func (g *GuestCommands) Chmod(path string, mode string) string { + return g.sudo(fmt.Sprintf(g.commands().chmod, mode, g.escapePath(path))) +} + +func (g *GuestCommands) CreateDir(path string) string { + return g.sudo(fmt.Sprintf(g.commands().mkdir, g.escapePath(path))) +} + +func (g *GuestCommands) RemoveDir(path string) string { + return g.sudo(fmt.Sprintf(g.commands().removeDir, g.escapePath(path))) +} + +func (g *GuestCommands) commands() guestOSTypeCommand { + return guestOSTypeCommands[g.GuestOSType] +} + +func (g *GuestCommands) escapePath(path string) string { + if g.GuestOSType == WindowsOSType { + return strings.Replace(path, " ", "` ", -1) + } + return path +} + +func (g *GuestCommands) sudo(cmd string) string { + if g.GuestOSType == UnixOSType && g.Sudo { + return "sudo " + cmd + } + return cmd +} \ No newline at end of file diff --git a/provisioner/guest_commands_test.go b/provisioner/guest_commands_test.go new file mode 100644 index 000000000..d07c4bd44 --- /dev/null +++ b/provisioner/guest_commands_test.go @@ -0,0 +1,120 @@ +package provisioner + +import ( + "testing" +) + +func TestNewGuestCommands(t *testing.T) { + _, err := NewGuestCommands("Amiga", true) + if err == nil { + t.Fatalf("Should have returned an err for unsupported OS type") + } +} + +func TestCreateDir(t *testing.T) { + // *nix OS + guestCmd, err := NewGuestCommands(UnixOSType, false) + if err != nil { + t.Fatalf("Failed to create new GuestCommands for OS: %s", UnixOSType) + } + cmd := guestCmd.CreateDir("/tmp/tempdir") + if cmd != "mkdir -p '/tmp/tempdir'" { + t.Fatalf("Unexpected Unix create dir cmd: %s", cmd) + } + + // *nix OS w/sudo + guestCmd, err = NewGuestCommands(UnixOSType, true) + if err != nil { + t.Fatalf("Failed to create new sudo GuestCommands for OS: %s", UnixOSType) + } + cmd = guestCmd.CreateDir("/tmp/tempdir") + if cmd != "sudo mkdir -p '/tmp/tempdir'" { + t.Fatalf("Unexpected Unix sudo create dir cmd: %s", cmd) + } + + // Windows OS + guestCmd, err = NewGuestCommands(WindowsOSType, false) + if err != nil { + t.Fatalf("Failed to create new GuestCommands for OS: %s", WindowsOSType) + } + cmd = guestCmd.CreateDir("C:\\Windows\\Temp\\tempdir") + if cmd != "New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path C:\\Windows\\Temp\\tempdir" { + t.Fatalf("Unexpected Windows create dir cmd: %s", cmd) + } + + // Windows OS w/ space in path + cmd = guestCmd.CreateDir("C:\\Windows\\Temp\\temp dir") + if cmd != "New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path C:\\Windows\\Temp\\temp` dir" { + t.Fatalf("Unexpected Windows create dir cmd: %s", cmd) + } +} + +func TestChmod(t *testing.T) { + // *nix + guestCmd, err := NewGuestCommands(UnixOSType, false) + if err != nil { + t.Fatalf("Failed to create new GuestCommands for OS: %s", UnixOSType) + } + cmd := guestCmd.Chmod("/usr/local/bin/script.sh", "0666") + if cmd != "chmod 0666 '/usr/local/bin/script.sh'" { + t.Fatalf("Unexpected Unix chmod 0666 cmd: %s", cmd) + } + + // sudo *nix + guestCmd, err = NewGuestCommands(UnixOSType, true) + if err != nil { + t.Fatalf("Failed to create new sudo GuestCommands for OS: %s", UnixOSType) + } + cmd = guestCmd.Chmod("/usr/local/bin/script.sh", "+x") + if cmd != "sudo chmod +x '/usr/local/bin/script.sh'" { + t.Fatalf("Unexpected Unix chmod +x cmd: %s", cmd) + } + + // Windows + guestCmd, err = NewGuestCommands(WindowsOSType, false) + if err != nil { + t.Fatalf("Failed to create new GuestCommands for OS: %s", WindowsOSType) + } + cmd = guestCmd.Chmod("C:\\Program Files\\SomeApp\\someapp.exe", "+x") + if cmd != "echo 'skipping chmod +x C:\\Program` Files\\SomeApp\\someapp.exe'" { + t.Fatalf("Unexpected Windows chmod +x cmd: %s", cmd) + } +} + +func TestRemoveDir(t *testing.T) { + // *nix + guestCmd, err := NewGuestCommands(UnixOSType, false) + if err != nil { + t.Fatalf("Failed to create new GuestCommands for OS: %s", UnixOSType) + } + cmd := guestCmd.RemoveDir("/tmp/somedir") + if cmd != "rm -rf '/tmp/somedir'" { + t.Fatalf("Unexpected Unix remove dir cmd: %s", cmd) + } + + // sudo *nix + guestCmd, err = NewGuestCommands(UnixOSType, true) + if err != nil { + t.Fatalf("Failed to create new sudo GuestCommands for OS: %s", UnixOSType) + } + cmd = guestCmd.RemoveDir("/tmp/somedir") + if cmd != "sudo rm -rf '/tmp/somedir'" { + t.Fatalf("Unexpected Unix sudo remove dir cmd: %s", cmd) + } + + // Windows OS + guestCmd, err = NewGuestCommands(WindowsOSType, false) + if err != nil { + t.Fatalf("Failed to create new GuestCommands for OS: %s", WindowsOSType) + } + cmd = guestCmd.RemoveDir("C:\\Temp\\SomeDir") + if cmd != "rm C:\\Temp\\SomeDir -recurse -force" { + t.Fatalf("Unexpected Windows remove dir cmd: %s", cmd) + } + + // Windows OS w/ space in path + cmd = guestCmd.RemoveDir("C:\\Temp\\Some Dir") + if cmd != "rm C:\\Temp\\Some` Dir -recurse -force" { + t.Fatalf("Unexpected Windows remove dir cmd: %s", cmd) + } +} \ No newline at end of file From affebcda869f55c9c0a1c0f337350abc17b467bc Mon Sep 17 00:00:00 2001 From: Shawn Neal Date: Thu, 20 Aug 2015 13:25:27 -0700 Subject: [PATCH 2/5] Windows Powershell commands need to specify shell The WinRM communicator defaults to the regular Windows cmd shell so we need to tell Packer to use Powershell when running these guest commands. --- provisioner/guest_commands.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/provisioner/guest_commands.go b/provisioner/guest_commands.go index 4a65312f9..85d45a05b 100644 --- a/provisioner/guest_commands.go +++ b/provisioner/guest_commands.go @@ -23,8 +23,8 @@ var guestOSTypeCommands = map[string]guestOSTypeCommand{ }, WindowsOSType: guestOSTypeCommand{ chmod: "echo 'skipping chmod %s %s'", // no-op - mkdir: "New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path %s", - removeDir: "rm %s -recurse -force", + mkdir: "powershell.exe -Command \"New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path %s\"", + removeDir: "powershell.exe -Command \"rm %s -recurse -force\"", }, } From 8014dac742544578380d9442737ca7a7acc47534 Mon Sep 17 00:00:00 2001 From: Shawn Neal Date: Thu, 20 Aug 2015 13:27:51 -0700 Subject: [PATCH 3/5] 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 From 7195b2f35b1ee26e83de5107eac6ddbb38045b60 Mon Sep 17 00:00:00 2001 From: Shawn Neal Date: Thu, 20 Aug 2015 13:56:58 -0700 Subject: [PATCH 4/5] Add Windows to Chef provisioner documentation --- .../provisioners/chef-client.html.markdown | 55 ++++++++++++++++--- .../docs/provisioners/chef-solo.html.markdown | 37 +++++++++++-- 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/website/source/docs/provisioners/chef-client.html.markdown b/website/source/docs/provisioners/chef-client.html.markdown index e655a4622..e9638b3b7 100644 --- a/website/source/docs/provisioners/chef-client.html.markdown +++ b/website/source/docs/provisioners/chef-client.html.markdown @@ -50,11 +50,19 @@ configuration is actually required. should use a custom configuration template. See the dedicated "Chef Configuration" section below for more details. +- `encrypted_data_bag_secret_path` (string) - The path to the file containing + the secret for encrypted data bags. By default, this is empty, so no + secret will be available. + - `execute_command` (string) - The command used to execute Chef. This has various [configuration template variables](/docs/templates/configuration-templates.html) available. See below for more information. +- `guest_os_type` (string) - The target guest OS type, either "unix" or + "windows". Setting this to "windows" will cause the provisioner to use + Windows friendly paths and commands. By default, this is "unix". + - `install_command` (string) - The command used to install Chef. This has various [configuration template variables](/docs/templates/configuration-templates.html) available. See @@ -68,7 +76,8 @@ configuration is actually required. - `prevent_sudo` (boolean) - By default, the configured commands that are executed to install and run Chef are executed with `sudo`. If this is true, - then the sudo will be omitted. + then the sudo will be omitted. This has no effect when guest_os_type is + windows. - `run_list` (array of strings) - The [run list](http://docs.chef.io/essentials_node_object_run_lists.html) @@ -87,11 +96,12 @@ configuration is actually required. on the machine using the Chef omnibus installers. - `staging_directory` (string) - This is the directory where all the - configuration of Chef by Packer will be placed. By default this - is "/tmp/packer-chef-client". This directory doesn't need to exist but must - have proper permissions so that the SSH user that Packer uses is able to - create directories and write into this folder. If the permissions are not - correct, use a shell provisioner prior to this to configure it properly. + configuration of Chef by Packer will be placed. By default this is + "/tmp/packer-chef-client" when guest_os_type unix and + "$env:TEMP/packer-chef-client" when windows. This directory doesn't need to + exist but must have proper permissions so that the user that Packer uses is + able to create directories and write into this folder. By default the + provisioner will create and chmod 0777 this directory. - `client_key` (string) - Path to client key. If not set, this defaults to a file named client.pem in `staging_directory`. @@ -119,6 +129,10 @@ The default value for the configuration template is: 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}} @@ -127,8 +141,12 @@ validation_client_name "chef-validator" {{if ne .ValidationKeyPath ""}} validation_key "{{.ValidationKeyPath}}" {{end}} -{{if ne .NodeName ""}} node_name "{{.NodeName}}" +{{if ne .ChefEnvironment ""}} +environment "{{.ChefEnvironment}}" +{{end}} +{{if ne .SslVerifyMode ""}} +ssl_verify_mode :{{.SslVerifyMode}} {{end}} ``` @@ -136,8 +154,13 @@ This template is a [configuration template](/docs/templates/configuration-templates.html) and has a set of variables available to use: +- `ChefEnvironment` - The Chef environment name. +- `EncryptedDataBagSecretPath` - The path to the secret key file to decrypt + encrypted data bags. - `NodeName` - The node name set in the configuration. - `ServerUrl` - The URL of the Chef Server set in the configuration. +- `SslVerifyMode` - Whether Chef SSL verify mode is on or off. +- `ValidationClientName` - The name of the client used for validation. - `ValidationKeyPath` - Path to the validation key, if it is set. ## Execute Command @@ -152,6 +175,17 @@ readability) to execute Chef: -j {{.JsonPath}} ``` +When guest_os_type is set to "windows", Packer uses the following command to +execute Chef. The full path to Chef is required because the PATH environment +variable changes don't immediately propogate to running processes. + +``` {.liquid} +c:/opscode/chef/bin/chef-client.bat \ + --no-color \ + -c {{.ConfigPath}} \ + -j {{.JsonPath}} +``` + This command can be customized using the `execute_command` configuration. As you can see from the default value above, the value of this configuration can contain various template variables, defined below: @@ -172,6 +206,13 @@ curl -L https://www.chef.io/chef/install.sh | \ {{if .Sudo}}sudo{{end}} bash ``` +When guest_os_type is set to "windows", Packer uses the following command to +install the latest version of Chef: + +``` {.text} +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" +``` + This command can be customized using the `install_command` configuration. ## Folder Permissions diff --git a/website/source/docs/provisioners/chef-solo.html.markdown b/website/source/docs/provisioners/chef-solo.html.markdown index 9534c32f1..ca97a31be 100644 --- a/website/source/docs/provisioners/chef-solo.html.markdown +++ b/website/source/docs/provisioners/chef-solo.html.markdown @@ -68,6 +68,10 @@ configuration is actually required, but at least `run_list` is recommended. variables](/docs/templates/configuration-templates.html) available. See below for more information. +- `guest_os_type` (string) - The target guest OS type, either "unix" or + "windows". Setting this to "windows" will cause the provisioner to use + Windows friendly paths and commands. By default, this is "unix". + - `install_command` (string) - The command used to install Chef. This has various [configuration template variables](/docs/templates/configuration-templates.html) available. See @@ -78,7 +82,8 @@ configuration is actually required, but at least `run_list` is recommended. - `prevent_sudo` (boolean) - By default, the configured commands that are executed to install and run Chef are executed with `sudo`. If this is true, - then the sudo will be omitted. + then the sudo will be omitted. This has no effect when guest_os_type is + windows. - `remote_cookbook_paths` (array of strings) - A list of paths on the remote machine where cookbooks will already exist. These may exist from a previous @@ -97,11 +102,13 @@ configuration is actually required, but at least `run_list` is recommended. on the machine using the Chef omnibus installers. - `staging_directory` (string) - This is the directory where all the - configuration of Chef by Packer will be placed. By default this - is "/tmp/packer-chef-solo". This directory doesn't need to exist but must - have proper permissions so that the SSH user that Packer uses is able to - create directories and write into this folder. If the permissions are not - correct, use a shell provisioner prior to this to configure it properly. + configuration of Chef by Packer will be placed. By default this is + "/tmp/packer-chef-solo" when guest_os_type unix and + "$env:TEMP/packer-chef-solo" when windows. This directory doesn't need to + exist but must have proper permissions so that the user that Packer uses is + able to create directories and write into this folder. If the permissions + are not correct, use a shell provisioner prior to this to configure it + properly. ## Chef Configuration @@ -141,6 +148,17 @@ readability) to execute Chef: -j {{.JsonPath}} ``` +When guest_os_type is set to "windows", Packer uses the following command to +execute Chef. The full path to Chef is required because the PATH environment +variable changes don't immediately propogate to running processes. + +``` {.liquid} +c:/opscode/chef/bin/chef-solo.bat \ + --no-color \ + -c {{.ConfigPath}} \ + -j {{.JsonPath}} +``` + This command can be customized using the `execute_command` configuration. As you can see from the default value above, the value of this configuration can contain various template variables, defined below: @@ -161,4 +179,11 @@ curl -L https://www.chef.io/chef/install.sh | \ {{if .Sudo}}sudo{{end}} bash ``` +When guest_os_type is set to "windows", Packer uses the following command to +install the latest version of Chef: + +``` {.text} +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" +``` + This command can be customized using the `install_command` configuration. From 357e494ee39744e68fc153341284ccd15ad0b2ff Mon Sep 17 00:00:00 2001 From: Shawn Neal Date: Thu, 20 Aug 2015 15:50:58 -0700 Subject: [PATCH 5/5] Fix failing Windows guest command tests --- provisioner/guest_commands_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/provisioner/guest_commands_test.go b/provisioner/guest_commands_test.go index d07c4bd44..dcf2212ea 100644 --- a/provisioner/guest_commands_test.go +++ b/provisioner/guest_commands_test.go @@ -38,13 +38,13 @@ func TestCreateDir(t *testing.T) { t.Fatalf("Failed to create new GuestCommands for OS: %s", WindowsOSType) } cmd = guestCmd.CreateDir("C:\\Windows\\Temp\\tempdir") - if cmd != "New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path C:\\Windows\\Temp\\tempdir" { + if cmd != "powershell.exe -Command \"New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path C:\\Windows\\Temp\\tempdir\"" { t.Fatalf("Unexpected Windows create dir cmd: %s", cmd) } // Windows OS w/ space in path cmd = guestCmd.CreateDir("C:\\Windows\\Temp\\temp dir") - if cmd != "New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path C:\\Windows\\Temp\\temp` dir" { + if cmd != "powershell.exe -Command \"New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path C:\\Windows\\Temp\\temp` dir\"" { t.Fatalf("Unexpected Windows create dir cmd: %s", cmd) } } @@ -108,13 +108,13 @@ func TestRemoveDir(t *testing.T) { t.Fatalf("Failed to create new GuestCommands for OS: %s", WindowsOSType) } cmd = guestCmd.RemoveDir("C:\\Temp\\SomeDir") - if cmd != "rm C:\\Temp\\SomeDir -recurse -force" { + if cmd != "powershell.exe -Command \"rm C:\\Temp\\SomeDir -recurse -force\"" { t.Fatalf("Unexpected Windows remove dir cmd: %s", cmd) } // Windows OS w/ space in path cmd = guestCmd.RemoveDir("C:\\Temp\\Some Dir") - if cmd != "rm C:\\Temp\\Some` Dir -recurse -force" { + if cmd != "powershell.exe -Command \"rm C:\\Temp\\Some` Dir -recurse -force\"" { t.Fatalf("Unexpected Windows remove dir cmd: %s", cmd) } } \ No newline at end of file