diff --git a/provisioner/chef-client/provisioner.go b/provisioner/chef-client/provisioner.go index edba46e7f..5d9fb751a 100644 --- a/provisioner/chef-client/provisioner.go +++ b/provisioner/chef-client/provisioner.go @@ -16,19 +16,41 @@ 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"` - ChefEnvironment string `mapstructure:"chef_environment"` - EncryptedDataBagSecretPath string `mapstructure:"encrypted_data_bag_secret_path"` - SslVerifyMode string `mapstructure:"ssl_verify_mode"` - ConfigTemplate string `mapstructure:"config_template"` - ExecuteCommand string `mapstructure:"execute_command"` - InstallCommand string `mapstructure:"install_command"` - Json map[string]interface{} + Json map[string]interface{} + + ChefEnvironment string `mapstructure:"chef_environment"` + ClientKey string `mapstructure:"client_key"` + ConfigTemplate string `mapstructure:"config_template"` + EncryptedDataBagSecretPath string `mapstructure:"encrypted_data_bag_secret_path"` + ExecuteCommand string `mapstructure:"execute_command"` + GuestOSType string `mapstructure:"guest_os_type"` + InstallCommand string `mapstructure:"install_command"` NodeName string `mapstructure:"node_name"` PreventSudo bool `mapstructure:"prevent_sudo"` RunList []string `mapstructure:"run_list"` @@ -36,28 +58,29 @@ type Config struct { SkipCleanClient bool `mapstructure:"skip_clean_client"` SkipCleanNode bool `mapstructure:"skip_clean_node"` SkipInstall bool `mapstructure:"skip_install"` + SslVerifyMode string `mapstructure:"ssl_verify_mode"` StagingDir string `mapstructure:"staging_directory"` - ClientKey string `mapstructure:"client_key"` - ValidationKeyPath string `mapstructure:"validation_key_path"` ValidationClientName string `mapstructure:"validation_client_name"` + ValidationKeyPath string `mapstructure:"validation_key_path"` ctx interpolate.Context } type Provisioner struct { - config Config + config Config + guestOSTypeConfig guestOSTypeConfig + guestCommands *provisioner.GuestCommands } type ConfigTemplate struct { - NodeName string - ServerUrl string - ClientKey string - ValidationKeyPath string - ValidationClientName string - EncryptedDataBagSecretPath string - ChefEnvironment string - SslVerifyMode string - HasEncryptedDataBagSecretPath bool + ChefEnvironment string + ClientKey string + EncryptedDataBagSecretPath string + NodeName string + ServerUrl string + SslVerifyMode string + ValidationClientName string + ValidationKeyPath string } type ExecuteTemplate struct { @@ -85,15 +108,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 { @@ -101,7 +137,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 @@ -116,6 +152,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")) @@ -180,27 +225,38 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { p.config.ClientKey = fmt.Sprintf("%s/client.pem", p.config.StagingDir) } - if p.config.ValidationKeyPath != "" { - remoteValidationKeyPath = fmt.Sprintf("%s/validation.pem", p.config.StagingDir) - if err := p.copyValidationKey(ui, comm, remoteValidationKeyPath); err != nil { - return fmt.Errorf("Error copying validation key: %s", err) - } - } - 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 { + if err := p.uploadFile(ui, + comm, + encryptedDataBagSecretPath, + p.config.EncryptedDataBagSecretPath); err != nil { return fmt.Errorf("Error uploading encrypted data bag secret: %s", err) } } - configPath, err := p.createConfig( - ui, comm, nodeName, serverUrl, p.config.ClientKey, remoteValidationKeyPath, p.config.ValidationClientName, encryptedDataBagSecretPath, 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) } + if p.config.ValidationKeyPath != "" { + remoteValidationKeyPath = fmt.Sprintf("%s/validation.pem", p.config.StagingDir) + if err := p.uploadFile(ui, comm, remoteValidationKeyPath, p.config.ValidationKeyPath); err != nil { + return fmt.Errorf("Error copying validation key: %s", err) + } + } + jsonPath, err := p.createJson(ui, comm) if err != nil { return fmt.Errorf("Error creating JSON attributes: %s", err) @@ -230,7 +286,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 @@ -256,17 +312,30 @@ func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, ds return comm.UploadDir(dst, src, nil) } -func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst string, src string) error { - f, err := os.Open(src) +func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, remotePath string, localPath string) error { + ui.Message(fmt.Sprintf("Uploading %s...", localPath)) + + f, err := os.Open(localPath) if err != nil { return err } defer f.Close() - return comm.Upload(dst, f, nil) + return comm.Upload(remotePath, f, nil) } -func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, nodeName string, serverUrl string, clientKey string, remoteKeyPath string, validationClientName string, encryptedDataBagSecretPath string, chefEnvironment string, sslVerifyMode string) (string, error) { +func (p *Provisioner) createConfig( + ui packer.Ui, + comm packer.Communicator, + nodeName string, + serverUrl string, + clientKey string, + encryptedDataBagSecretPath, + remoteKeyPath string, + validationClientName string, + chefEnvironment string, + sslVerifyMode string) (string, error) { + ui.Message("Creating configuration file 'client.rb'") // Read the template @@ -288,15 +357,14 @@ func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, nodeN ctx := p.config.ctx ctx.Data = &ConfigTemplate{ - NodeName: nodeName, - ServerUrl: serverUrl, - ClientKey: clientKey, - ValidationKeyPath: remoteKeyPath, - ValidationClientName: validationClientName, - ChefEnvironment: chefEnvironment, - SslVerifyMode: sslVerifyMode, - EncryptedDataBagSecretPath: encryptedDataBagSecretPath, - HasEncryptedDataBagSecretPath: encryptedDataBagSecretPath != "", + NodeName: nodeName, + ServerUrl: serverUrl, + ClientKey: clientKey, + ValidationKeyPath: remoteKeyPath, + ValidationClientName: validationClientName, + ChefEnvironment: chefEnvironment, + SslVerifyMode: sslVerifyMode, + EncryptedDataBagSecretPath: encryptedDataBagSecretPath, } configString, err := interpolate.Render(tpl, &ctx) if err != nil { @@ -369,12 +437,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 } @@ -383,11 +446,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 } @@ -447,15 +506,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 } @@ -502,6 +553,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 @@ -515,23 +568,6 @@ 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...") - - // First upload the validation key to a writable location - f, err := os.Open(p.config.ValidationKeyPath) - if err != nil { - return err - } - defer f.Close() - - if err := comm.Upload(remotePath, f, nil); err != nil { - return err - } - - return nil -} - func (p *Provisioner) deepJsonFix(key string, current interface{}) (interface{}, error) { if current == nil { return nil, nil @@ -619,7 +655,7 @@ log_level :info log_location STDOUT chef_server_url "{{.ServerUrl}}" client_key "{{.ClientKey}}" -{{if .HasEncryptedDataBagSecretPath}} +{{if ne .EncryptedDataBagSecretPath ""}} encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}" {{end}} {{if ne .ValidationClientName ""}} diff --git a/provisioner/chef-client/provisioner_test.go b/provisioner/chef-client/provisioner_test.go index 10477aa39..7cc1bbe94 100644 --- a/provisioner/chef-client/provisioner_test.go +++ b/provisioner/chef-client/provisioner_test.go @@ -182,53 +182,63 @@ func TestProvisionerPrepare_encryptedDataBagSecretPath(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 diff --git a/provisioner/guest_commands.go b/provisioner/guest_commands.go new file mode 100644 index 000000000..85d45a05b --- /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: "powershell.exe -Command \"New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path %s\"", + removeDir: "powershell.exe -Command \"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..dcf2212ea --- /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 != "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 != "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) + } +} + +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 != "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 != "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 diff --git a/website/source/docs/provisioners/chef-client.html.markdown b/website/source/docs/provisioners/chef-client.html.markdown index f3765c429..65d073093 100644 --- a/website/source/docs/provisioners/chef-client.html.markdown +++ b/website/source/docs/provisioners/chef-client.html.markdown @@ -51,14 +51,18 @@ configuration is actually required. 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. + 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 @@ -72,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) for Chef. @@ -91,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`. @@ -123,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}} @@ -131,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}} ``` @@ -140,9 +154,13 @@ This template is a [configuration template](/docs/templates/configuration-templates.html) and has a set of variables available to use: -- `EncryptedDataBagSecretPath` - The path to the encrypted data bag secret +- `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 @@ -157,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: @@ -177,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.