Merge branch 'chef-provisioner-windows' of https://github.com/sneal/packer into f-chef-provisioner-windows

This commit is contained in:
Chris Bednarski 2016-01-28 14:55:17 -08:00
commit 87532b1b00
7 changed files with 483 additions and 148 deletions

View File

@ -16,19 +16,41 @@ import (
"github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/provisioner"
"github.com/mitchellh/packer/template/interpolate" "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 { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
ChefEnvironment string `mapstructure:"chef_environment"` Json map[string]interface{}
EncryptedDataBagSecretPath string `mapstructure:"encrypted_data_bag_secret_path"`
SslVerifyMode string `mapstructure:"ssl_verify_mode"` ChefEnvironment string `mapstructure:"chef_environment"`
ConfigTemplate string `mapstructure:"config_template"` ClientKey string `mapstructure:"client_key"`
ExecuteCommand string `mapstructure:"execute_command"` ConfigTemplate string `mapstructure:"config_template"`
InstallCommand string `mapstructure:"install_command"` EncryptedDataBagSecretPath string `mapstructure:"encrypted_data_bag_secret_path"`
Json map[string]interface{} ExecuteCommand string `mapstructure:"execute_command"`
GuestOSType string `mapstructure:"guest_os_type"`
InstallCommand string `mapstructure:"install_command"`
NodeName string `mapstructure:"node_name"` NodeName string `mapstructure:"node_name"`
PreventSudo bool `mapstructure:"prevent_sudo"` PreventSudo bool `mapstructure:"prevent_sudo"`
RunList []string `mapstructure:"run_list"` RunList []string `mapstructure:"run_list"`
@ -36,28 +58,29 @@ type Config struct {
SkipCleanClient bool `mapstructure:"skip_clean_client"` SkipCleanClient bool `mapstructure:"skip_clean_client"`
SkipCleanNode bool `mapstructure:"skip_clean_node"` SkipCleanNode bool `mapstructure:"skip_clean_node"`
SkipInstall bool `mapstructure:"skip_install"` SkipInstall bool `mapstructure:"skip_install"`
SslVerifyMode string `mapstructure:"ssl_verify_mode"`
StagingDir string `mapstructure:"staging_directory"` StagingDir string `mapstructure:"staging_directory"`
ClientKey string `mapstructure:"client_key"`
ValidationKeyPath string `mapstructure:"validation_key_path"`
ValidationClientName string `mapstructure:"validation_client_name"` ValidationClientName string `mapstructure:"validation_client_name"`
ValidationKeyPath string `mapstructure:"validation_key_path"`
ctx interpolate.Context ctx interpolate.Context
} }
type Provisioner struct { type Provisioner struct {
config Config config Config
guestOSTypeConfig guestOSTypeConfig
guestCommands *provisioner.GuestCommands
} }
type ConfigTemplate struct { type ConfigTemplate struct {
NodeName string ChefEnvironment string
ServerUrl string ClientKey string
ClientKey string EncryptedDataBagSecretPath string
ValidationKeyPath string NodeName string
ValidationClientName string ServerUrl string
EncryptedDataBagSecretPath string SslVerifyMode string
ChefEnvironment string ValidationClientName string
SslVerifyMode string ValidationKeyPath string
HasEncryptedDataBagSecretPath bool
} }
type ExecuteTemplate struct { type ExecuteTemplate struct {
@ -85,15 +108,28 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
return err 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 == "" { if p.config.ExecuteCommand == "" {
p.config.ExecuteCommand = "{{if .Sudo}}sudo {{end}}chef-client " + p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand
"--no-color -c {{.ConfigPath}} -j {{.JsonPath}}"
} }
if p.config.InstallCommand == "" { if p.config.InstallCommand == "" {
p.config.InstallCommand = "curl -L " + p.config.InstallCommand = p.guestOSTypeConfig.installCommand
"https://www.chef.io/chef/install.sh | " +
"{{if .Sudo}}sudo {{end}}bash"
} }
if p.config.RunList == nil { if p.config.RunList == nil {
@ -101,7 +137,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
} }
if p.config.StagingDir == "" { if p.config.StagingDir == "" {
p.config.StagingDir = "/tmp/packer-chef-client" p.config.StagingDir = p.guestOSTypeConfig.stagingDir
} }
var errs *packer.MultiError 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 == "" { if p.config.ServerUrl == "" {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, fmt.Errorf("server_url must be set")) 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) 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 := "" encryptedDataBagSecretPath := ""
if p.config.EncryptedDataBagSecretPath != "" { if p.config.EncryptedDataBagSecretPath != "" {
encryptedDataBagSecretPath = fmt.Sprintf("%s/encrypted_data_bag_secret", p.config.StagingDir) 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) return fmt.Errorf("Error uploading encrypted data bag secret: %s", err)
} }
} }
configPath, err := p.createConfig( 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 { if err != nil {
return fmt.Errorf("Error creating Chef config file: %s", err) 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) jsonPath, err := p.createJson(ui, comm)
if err != nil { if err != nil {
return fmt.Errorf("Error creating JSON attributes: %s", err) 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 { 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 return nil
@ -256,17 +312,30 @@ func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, ds
return comm.UploadDir(dst, src, nil) return comm.UploadDir(dst, src, nil)
} }
func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst string, src string) error { func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, remotePath string, localPath string) error {
f, err := os.Open(src) ui.Message(fmt.Sprintf("Uploading %s...", localPath))
f, err := os.Open(localPath)
if err != nil { if err != nil {
return err return err
} }
defer f.Close() 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'") ui.Message("Creating configuration file 'client.rb'")
// Read the template // Read the template
@ -288,15 +357,14 @@ func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, nodeN
ctx := p.config.ctx ctx := p.config.ctx
ctx.Data = &ConfigTemplate{ ctx.Data = &ConfigTemplate{
NodeName: nodeName, NodeName: nodeName,
ServerUrl: serverUrl, ServerUrl: serverUrl,
ClientKey: clientKey, ClientKey: clientKey,
ValidationKeyPath: remoteKeyPath, ValidationKeyPath: remoteKeyPath,
ValidationClientName: validationClientName, ValidationClientName: validationClientName,
ChefEnvironment: chefEnvironment, ChefEnvironment: chefEnvironment,
SslVerifyMode: sslVerifyMode, SslVerifyMode: sslVerifyMode,
EncryptedDataBagSecretPath: encryptedDataBagSecretPath, EncryptedDataBagSecretPath: encryptedDataBagSecretPath,
HasEncryptedDataBagSecretPath: encryptedDataBagSecretPath != "",
} }
configString, err := interpolate.Render(tpl, &ctx) configString, err := interpolate.Render(tpl, &ctx)
if err != nil { 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 { func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
ui.Message(fmt.Sprintf("Creating directory: %s", dir)) ui.Message(fmt.Sprintf("Creating directory: %s", dir))
mkdirCmd := fmt.Sprintf("mkdir -p '%s'", dir) cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)}
if !p.config.PreventSudo {
mkdirCmd = "sudo " + mkdirCmd
}
cmd := &packer.RemoteCmd{Command: mkdirCmd}
if err := cmd.StartWithUi(comm, ui); err != nil { if err := cmd.StartWithUi(comm, ui); err != nil {
return err 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 // Chmod the directory to 0777 just so that we can access it as our user
mkdirCmd = fmt.Sprintf("chmod 0777 '%s'", dir) cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")}
if !p.config.PreventSudo {
mkdirCmd = "sudo " + mkdirCmd
}
cmd = &packer.RemoteCmd{Command: mkdirCmd}
if err := cmd.StartWithUi(comm, ui); err != nil { if err := cmd.StartWithUi(comm, ui); err != nil {
return err 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 { func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error {
ui.Message(fmt.Sprintf("Removing directory: %s", dir)) ui.Message(fmt.Sprintf("Removing directory: %s", dir))
rmCmd := fmt.Sprintf("rm -rf '%s'", dir) cmd := &packer.RemoteCmd{Command: p.guestCommands.RemoveDir(dir)}
if !p.config.PreventSudo {
rmCmd = "sudo " + rmCmd
}
cmd := &packer.RemoteCmd{
Command: rmCmd,
}
if err := cmd.StartWithUi(comm, ui); err != nil { if err := cmd.StartWithUi(comm, ui); err != nil {
return err return err
} }
@ -502,6 +553,8 @@ func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error
return err return err
} }
ui.Message(command)
cmd := &packer.RemoteCmd{Command: command} cmd := &packer.RemoteCmd{Command: command}
if err := cmd.StartWithUi(comm, ui); err != nil { if err := cmd.StartWithUi(comm, ui); err != nil {
return err return err
@ -515,23 +568,6 @@ func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error
return nil 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) { func (p *Provisioner) deepJsonFix(key string, current interface{}) (interface{}, error) {
if current == nil { if current == nil {
return nil, nil return nil, nil
@ -619,7 +655,7 @@ log_level :info
log_location STDOUT log_location STDOUT
chef_server_url "{{.ServerUrl}}" chef_server_url "{{.ServerUrl}}"
client_key "{{.ClientKey}}" client_key "{{.ClientKey}}"
{{if .HasEncryptedDataBagSecretPath}} {{if ne .EncryptedDataBagSecretPath ""}}
encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}" encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}"
{{end}} {{end}}
{{if ne .ValidationClientName ""}} {{if ne .ValidationClientName ""}}

View File

@ -182,53 +182,63 @@ func TestProvisionerPrepare_encryptedDataBagSecretPath(t *testing.T) {
} }
func TestProvisioner_createDir(t *testing.T) { func TestProvisioner_createDir(t *testing.T) {
p1 := &Provisioner{config: Config{PreventSudo: true}} for _, sudo := range []bool{true, false} {
p2 := &Provisioner{config: Config{PreventSudo: false}} config := testConfig()
comm := &packer.MockCommunicator{} config["prevent_sudo"] = !sudo
ui := &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
if err := p1.createDir(ui, comm, "/tmp/foo"); err != nil { p := &Provisioner{}
t.Fatalf("err: %s", err) comm := &packer.MockCommunicator{}
} ui := &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
if strings.HasPrefix(comm.StartCmd.Command, "sudo") { err := p.Prepare(config)
t.Fatalf("createDir should not use sudo, got: \"%s\"", comm.StartCmd.Command) if err != nil {
} t.Fatalf("err: %s", err)
}
if err := p2.createDir(ui, comm, "/tmp/foo"); err != nil { if err := p.createDir(ui, comm, "/tmp/foo"); err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
if !strings.HasPrefix(comm.StartCmd.Command, "sudo") { if !sudo && strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatalf("createDir should use sudo, got: \"%s\"", comm.StartCmd.Command) 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) { func TestProvisioner_removeDir(t *testing.T) {
p1 := &Provisioner{config: Config{PreventSudo: true}} for _, sudo := range []bool{true, false} {
p2 := &Provisioner{config: Config{PreventSudo: false}} config := testConfig()
comm := &packer.MockCommunicator{} config["prevent_sudo"] = !sudo
ui := &packer.BasicUi{
Reader: new(bytes.Buffer), p := &Provisioner{}
Writer: new(bytes.Buffer), comm := &packer.MockCommunicator{}
} ui := &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
}
if err := p1.removeDir(ui, comm, "/tmp/foo"); err != nil { err := p.Prepare(config)
t.Fatalf("err: %s", err) if err != nil {
} t.Fatalf("err: %s", err)
}
if strings.HasPrefix(comm.StartCmd.Command, "sudo") { if err := p.removeDir(ui, comm, "/tmp/foo"); err != nil {
t.Fatalf("removeDir should not use sudo, got: \"%s\"", comm.StartCmd.Command) t.Fatalf("err: %s", err)
} }
if err := p2.removeDir(ui, comm, "/tmp/foo"); err != nil { if !sudo && strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatalf("err: %s", err) t.Fatalf("removeDir should not use sudo, got: \"%s\"", comm.StartCmd.Command)
} }
if !strings.HasPrefix(comm.StartCmd.Command, "sudo") { if sudo && !strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatalf("removeDir should use sudo, got: \"%s\"", comm.StartCmd.Command) t.Fatalf("removeDir should use sudo, got: \"%s\"", comm.StartCmd.Command)
}
} }
} }

View File

@ -15,9 +15,29 @@ import (
"github.com/mitchellh/packer/common" "github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/provisioner"
"github.com/mitchellh/packer/template/interpolate" "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 { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
@ -36,12 +56,15 @@ type Config struct {
RunList []string `mapstructure:"run_list"` RunList []string `mapstructure:"run_list"`
SkipInstall bool `mapstructure:"skip_install"` SkipInstall bool `mapstructure:"skip_install"`
StagingDir string `mapstructure:"staging_directory"` StagingDir string `mapstructure:"staging_directory"`
GuestOSType string `mapstructure:"guest_os_type"`
ctx interpolate.Context ctx interpolate.Context
} }
type Provisioner struct { type Provisioner struct {
config Config config Config
guestOSTypeConfig guestOSTypeConfig
guestCommands *provisioner.GuestCommands
} }
type ConfigTemplate struct { type ConfigTemplate struct {
@ -86,12 +109,28 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
return err 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 == "" { 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 == "" { 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 { if p.config.RunList == nil {
@ -99,7 +138,7 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
} }
if p.config.StagingDir == "" { if p.config.StagingDir == "" {
p.config.StagingDir = "/tmp/packer-chef-solo" p.config.StagingDir = p.guestOSTypeConfig.stagingDir
} }
var errs *packer.MultiError 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 { func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error {
ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 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 { if err := cmd.StartWithUi(comm, ui); err != nil {
return err return err
} }
if cmd.ExitStatus != 0 { 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 return nil

View File

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

View File

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

View File

@ -51,14 +51,18 @@ configuration is actually required.
Configuration" section below for more details. Configuration" section below for more details.
- `encrypted_data_bag_secret_path` (string) - The path to the file containing - `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 the secret for encrypted data bags. By default, this is empty, so no
will be available. secret will be available.
- `execute_command` (string) - The command used to execute Chef. This has - `execute_command` (string) - The command used to execute Chef. This has
various [configuration template various [configuration template
variables](/docs/templates/configuration-templates.html) available. See variables](/docs/templates/configuration-templates.html) available. See
below for more information. 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 - `install_command` (string) - The command used to install Chef. This has
various [configuration template various [configuration template
variables](/docs/templates/configuration-templates.html) available. See 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 - `prevent_sudo` (boolean) - By default, the configured commands that are
executed to install and run Chef are executed with `sudo`. If this is true, 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 - `run_list` (array of strings) - The [run
list](http://docs.chef.io/essentials_node_object_run_lists.html) for Chef. 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. on the machine using the Chef omnibus installers.
- `staging_directory` (string) - This is the directory where all the - `staging_directory` (string) - This is the directory where all the
configuration of Chef by Packer will be placed. By default this configuration of Chef by Packer will be placed. By default this is
is "/tmp/packer-chef-client". This directory doesn't need to exist but must "/tmp/packer-chef-client" when guest_os_type unix and
have proper permissions so that the SSH user that Packer uses is able to "$env:TEMP/packer-chef-client" when windows. This directory doesn't need to
create directories and write into this folder. If the permissions are not exist but must have proper permissions so that the user that Packer uses is
correct, use a shell provisioner prior to this to configure it properly. 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 - `client_key` (string) - Path to client key. If not set, this defaults to a
file named client.pem in `staging_directory`. file named client.pem in `staging_directory`.
@ -123,6 +129,10 @@ The default value for the configuration template is:
log_level :info log_level :info
log_location STDOUT log_location STDOUT
chef_server_url "{{.ServerUrl}}" chef_server_url "{{.ServerUrl}}"
client_key "{{.ClientKey}}"
{{if ne .EncryptedDataBagSecretPath ""}}
encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}"
{{end}}
{{if ne .ValidationClientName ""}} {{if ne .ValidationClientName ""}}
validation_client_name "{{.ValidationClientName}}" validation_client_name "{{.ValidationClientName}}"
{{else}} {{else}}
@ -131,8 +141,12 @@ validation_client_name "chef-validator"
{{if ne .ValidationKeyPath ""}} {{if ne .ValidationKeyPath ""}}
validation_key "{{.ValidationKeyPath}}" validation_key "{{.ValidationKeyPath}}"
{{end}} {{end}}
{{if ne .NodeName ""}}
node_name "{{.NodeName}}" node_name "{{.NodeName}}"
{{if ne .ChefEnvironment ""}}
environment "{{.ChefEnvironment}}"
{{end}}
{{if ne .SslVerifyMode ""}}
ssl_verify_mode :{{.SslVerifyMode}}
{{end}} {{end}}
``` ```
@ -140,9 +154,13 @@ This template is a [configuration
template](/docs/templates/configuration-templates.html) and has a set of template](/docs/templates/configuration-templates.html) and has a set of
variables available to use: 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. - `NodeName` - The node name set in the configuration.
- `ServerUrl` - The URL of the Chef Server 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. - `ValidationKeyPath` - Path to the validation key, if it is set.
## Execute Command ## Execute Command
@ -157,6 +175,17 @@ readability) to execute Chef:
-j {{.JsonPath}} -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 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 can see from the default value above, the value of this configuration can
contain various template variables, defined below: contain various template variables, defined below:
@ -177,6 +206,13 @@ curl -L https://www.chef.io/chef/install.sh | \
{{if .Sudo}}sudo{{end}} bash {{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. This command can be customized using the `install_command` configuration.
## Folder Permissions ## Folder Permissions

View File

@ -68,6 +68,10 @@ configuration is actually required, but at least `run_list` is recommended.
variables](/docs/templates/configuration-templates.html) available. See variables](/docs/templates/configuration-templates.html) available. See
below for more information. 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 - `install_command` (string) - The command used to install Chef. This has
various [configuration template various [configuration template
variables](/docs/templates/configuration-templates.html) available. See 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 - `prevent_sudo` (boolean) - By default, the configured commands that are
executed to install and run Chef are executed with `sudo`. If this is true, 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 - `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 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. on the machine using the Chef omnibus installers.
- `staging_directory` (string) - This is the directory where all the - `staging_directory` (string) - This is the directory where all the
configuration of Chef by Packer will be placed. By default this configuration of Chef by Packer will be placed. By default this is
is "/tmp/packer-chef-solo". This directory doesn't need to exist but must "/tmp/packer-chef-solo" when guest_os_type unix and
have proper permissions so that the SSH user that Packer uses is able to "$env:TEMP/packer-chef-solo" when windows. This directory doesn't need to
create directories and write into this folder. If the permissions are not exist but must have proper permissions so that the user that Packer uses is
correct, use a shell provisioner prior to this to configure it properly. 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 ## Chef Configuration
@ -141,6 +148,17 @@ readability) to execute Chef:
-j {{.JsonPath}} -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 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 can see from the default value above, the value of this configuration can
contain various template variables, defined below: contain various template variables, defined below:
@ -161,4 +179,11 @@ curl -L https://www.chef.io/chef/install.sh | \
{{if .Sudo}}sudo{{end}} bash {{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. This command can be customized using the `install_command` configuration.