diff --git a/provisioner/guest_commands.go b/provisioner/guest_commands.go index 715d6cb31..3a361a208 100644 --- a/provisioner/guest_commands.go +++ b/provisioner/guest_commands.go @@ -13,6 +13,8 @@ type guestOSTypeCommand struct { chmod string mkdir string removeDir string + statPath string + mv string } var guestOSTypeCommands = map[string]guestOSTypeCommand{ @@ -20,11 +22,15 @@ var guestOSTypeCommands = map[string]guestOSTypeCommand{ chmod: "chmod %s '%s'", mkdir: "mkdir -p '%s'", removeDir: "rm -rf '%s'", + statPath: "stat '%s'", + mv: "mv '%s' '%s'", }, WindowsOSType: { 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\"", + statPath: "powershell.exe -Command { if (test-path %s) { exit 0 } else { exit 1 } }", + mv: "powershell.exe -Command \"mv %s %s\"", }, } @@ -64,6 +70,14 @@ func (g *GuestCommands) escapePath(path string) string { return path } +func (g *GuestCommands) StatPath(path string) string { + return g.sudo(fmt.Sprintf(g.commands().statPath, g.escapePath(path))) +} + +func (g *GuestCommands) MovePath(srcPath string, dstPath string) string { + return g.sudo(fmt.Sprintf(g.commands().mv, g.escapePath(srcPath), g.escapePath(dstPath))) +} + func (g *GuestCommands) sudo(cmd string) string { if g.GuestOSType == UnixOSType && g.Sudo { return "sudo " + cmd diff --git a/provisioner/guest_commands_test.go b/provisioner/guest_commands_test.go index 50d15870a..08baca058 100644 --- a/provisioner/guest_commands_test.go +++ b/provisioner/guest_commands_test.go @@ -118,3 +118,78 @@ func TestRemoveDir(t *testing.T) { t.Fatalf("Unexpected Windows remove dir cmd: %s", cmd) } } + +func TestStatPath(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.StatPath("/tmp/somedir") + if cmd != "stat '/tmp/somedir'" { + t.Fatalf("Unexpected Unix stat cmd: %s", cmd) + } + + guestCmd, err = NewGuestCommands(UnixOSType, true) + if err != nil { + t.Fatalf("Failed to create new GuestCommands for OS: %s", UnixOSType) + } + cmd = guestCmd.StatPath("/tmp/somedir") + if cmd != "sudo stat '/tmp/somedir'" { + t.Fatalf("Unexpected Unix stat 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.StatPath("C:\\Temp\\SomeDir") + if cmd != "powershell.exe -Command { if (test-path C:\\Temp\\SomeDir) { exit 0 } else { exit 1 } }" { + t.Fatalf("Unexpected Windows stat cmd: %s", cmd) + } + + // Windows OS w/ space in path + cmd = guestCmd.StatPath("C:\\Temp\\Some Dir") + if cmd != "powershell.exe -Command { if (test-path C:\\Temp\\Some` Dir) { exit 0 } else { exit 1 } }" { + t.Fatalf("Unexpected Windows stat cmd: %s", cmd) + } +} + +func TestMovePath(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.MovePath("/tmp/somedir", "/tmp/newdir") + if cmd != "mv '/tmp/somedir' '/tmp/newdir'" { + t.Fatalf("Unexpected Unix move 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.MovePath("/tmp/somedir", "/tmp/newdir") + if cmd != "sudo mv '/tmp/somedir' '/tmp/newdir'" { + t.Fatalf("Unexpected Unix sudo mv 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.MovePath("C:\\Temp\\SomeDir", "C:\\Temp\\NewDir") + if cmd != "powershell.exe -Command \"mv C:\\Temp\\SomeDir C:\\Temp\\NewDir\"" { + t.Fatalf("Unexpected Windows remove dir cmd: %s", cmd) + } + + // Windows OS w/ space in path + cmd = guestCmd.MovePath("C:\\Temp\\Some Dir", "C:\\Temp\\New Dir") + if cmd != "powershell.exe -Command \"mv C:\\Temp\\Some` Dir C:\\Temp\\New` Dir\"" { + t.Fatalf("Unexpected Windows remove dir cmd: %s", cmd) + } +} diff --git a/provisioner/salt-masterless/provisioner.go b/provisioner/salt-masterless/provisioner.go index dad5f7109..f1ae1fe84 100644 --- a/provisioner/salt-masterless/provisioner.go +++ b/provisioner/salt-masterless/provisioner.go @@ -8,17 +8,15 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/provisioner" "github.com/hashicorp/packer/template/interpolate" ) -const DefaultTempConfigDir = "/tmp/salt" -const DefaultStateTreeDir = "/srv/salt" -const DefaultPillarRootDir = "/srv/pillar" - type Config struct { common.PackerConfig `mapstructure:",squash"` @@ -67,11 +65,44 @@ type Config struct { // Command line args passed onto salt-call CmdArgs string "" + // The Guest OS Type (unix or windows) + GuestOSType string `mapstructure:"guest_os_type"` + ctx interpolate.Context } type Provisioner struct { - config Config + config Config + guestOSTypeConfig guestOSTypeConfig + guestCommands *provisioner.GuestCommands +} + +type guestOSTypeConfig struct { + tempDir string + stateRoot string + pillarRoot string + configDir string + bootstrapFetchCmd string + bootstrapRunCmd string +} + +var guestOSTypeConfigs = map[string]guestOSTypeConfig{ + provisioner.UnixOSType: { + configDir: "/etc/salt", + tempDir: "/tmp/salt/", + stateRoot: "/srv/salt/", + pillarRoot: "/srv/pillar/", + bootstrapFetchCmd: "curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com", + bootstrapRunCmd: "sh /tmp/install_salt.sh", + }, + provisioner.WindowsOSType: { + configDir: "C:/salt/conf", + tempDir: "C:/Windows/Temp/salt/", + stateRoot: "C:/salt/state", + pillarRoot: "C:/salt/pillar/", + bootstrapFetchCmd: "powershell Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/saltstack/salt-bootstrap/stable/bootstrap-salt.ps1' -OutFile 'C:/Windows/Temp/bootstrap-salt.ps1'", + bootstrapRunCmd: "Powershell C:/Windows/Temp/bootstrap-salt.ps1", + }, } func (p *Provisioner) Prepare(raws ...interface{}) error { @@ -86,8 +117,25 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { return err } + if p.config.GuestOSType == "" { + p.config.GuestOSType = provisioner.DefaultOSType + } else { + 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.DisableSudo) + if err != nil { + return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) + } + if p.config.TempConfigDir == "" { - p.config.TempConfigDir = DefaultTempConfigDir + p.config.TempConfigDir = p.guestOSTypeConfig.tempDir } var errs *packer.MultiError @@ -135,14 +183,14 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { cmd_args.WriteString(p.config.RemoteStateTree) } else { cmd_args.WriteString(" --file-root=") - cmd_args.WriteString(DefaultStateTreeDir) + cmd_args.WriteString(p.guestOSTypeConfig.stateRoot) } if p.config.RemotePillarRoots != "" { cmd_args.WriteString(" --pillar-root=") cmd_args.WriteString(p.config.RemotePillarRoots) } else { cmd_args.WriteString(" --pillar-root=") - cmd_args.WriteString(DefaultPillarRootDir) + cmd_args.WriteString(p.guestOSTypeConfig.pillarRoot) } } @@ -179,14 +227,14 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { if !p.config.SkipBootstrap { cmd := &packer.RemoteCmd{ // Fallback on wget if curl failed for any reason (such as not being installed) - Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com"), + Command: fmt.Sprintf(p.guestOSTypeConfig.bootstrapFetchCmd), } ui.Message(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh")) if err = cmd.StartWithUi(comm, ui); err != nil { return fmt.Errorf("Unable to download Salt: %s", err) } cmd = &packer.RemoteCmd{ - Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.config.BootstrapArgs), + Command: fmt.Sprintf("%s %s", p.sudo(p.guestOSTypeConfig.bootstrapRunCmd), p.config.BootstrapArgs), } ui.Message(fmt.Sprintf("Installing Salt with command %s", cmd.Command)) if err = cmd.StartWithUi(comm, ui); err != nil { @@ -208,14 +256,14 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { } // move minion config into /etc/salt - ui.Message(fmt.Sprintf("Make sure directory %s exists", "/etc/salt")) - if err := p.createDir(ui, comm, "/etc/salt"); err != nil { + ui.Message(fmt.Sprintf("Make sure directory %s exists", p.guestOSTypeConfig.configDir)) + if err := p.createDir(ui, comm, p.guestOSTypeConfig.configDir); err != nil { return fmt.Errorf("Error creating remote salt configuration directory: %s", err) } src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion")) - dst = "/etc/salt/minion" + dst = filepath.ToSlash(filepath.Join(p.guestOSTypeConfig.configDir, "minion")) if err = p.moveFile(ui, comm, dst, src); err != nil { - return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.config.TempConfigDir, err) + return fmt.Errorf("Unable to move %s/minion to %s/minion: %s", p.config.TempConfigDir, p.guestOSTypeConfig.configDir, err) } } @@ -228,14 +276,14 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { } // move grains file into /etc/salt - ui.Message(fmt.Sprintf("Make sure directory %s exists", "/etc/salt")) - if err := p.createDir(ui, comm, "/etc/salt"); err != nil { + ui.Message(fmt.Sprintf("Make sure directory %s exists", p.guestOSTypeConfig.configDir)) + if err := p.createDir(ui, comm, p.guestOSTypeConfig.configDir); err != nil { return fmt.Errorf("Error creating remote salt configuration directory: %s", err) } src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "grains")) - dst = "/etc/salt/grains" + dst = filepath.ToSlash(filepath.Join(p.guestOSTypeConfig.configDir, "grains")) if err = p.moveFile(ui, comm, dst, src); err != nil { - return fmt.Errorf("Unable to move %s/grains to /etc/salt/grains: %s", p.config.TempConfigDir, err) + return fmt.Errorf("Unable to move %s/grains to %s/grains: %s", p.config.TempConfigDir, p.guestOSTypeConfig.configDir, err) } } @@ -251,11 +299,15 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { if p.config.RemoteStateTree != "" { dst = p.config.RemoteStateTree } else { - dst = DefaultStateTreeDir + dst = p.guestOSTypeConfig.stateRoot } - if err = p.removeDir(ui, comm, dst); err != nil { - return fmt.Errorf("Unable to clear salt tree: %s", err) + + if err = p.statPath(ui, comm, dst); err != nil { + if err = p.removeDir(ui, comm, dst); err != nil { + return fmt.Errorf("Unable to clear salt tree: %s", err) + } } + if err = p.moveFile(ui, comm, dst, src); err != nil { return fmt.Errorf("Unable to move %s/states to %s: %s", p.config.TempConfigDir, dst, err) } @@ -273,11 +325,15 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { if p.config.RemotePillarRoots != "" { dst = p.config.RemotePillarRoots } else { - dst = DefaultPillarRootDir + dst = p.guestOSTypeConfig.pillarRoot } - if err = p.removeDir(ui, comm, dst); err != nil { - return fmt.Errorf("Unable to clear pillar root: %s", err) + + if err = p.statPath(ui, comm, dst); err != nil { + if err = p.removeDir(ui, comm, dst); err != nil { + return fmt.Errorf("Unable to clear pillar root: %s", err) + } } + if err = p.moveFile(ui, comm, dst, src); err != nil { return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.config.TempConfigDir, dst, err) } @@ -304,7 +360,7 @@ func (p *Provisioner) Cancel() { // Prepends sudo to supplied command if config says to func (p *Provisioner) sudo(cmd string) string { - if p.config.DisableSudo { + if p.config.DisableSudo || (p.config.GuestOSType == provisioner.WindowsOSType) { return cmd } @@ -354,9 +410,11 @@ func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst, sr return nil } -func (p *Provisioner) moveFile(ui packer.Ui, comm packer.Communicator, dst, src string) error { +func (p *Provisioner) moveFile(ui packer.Ui, comm packer.Communicator, dst string, src string) error { ui.Message(fmt.Sprintf("Moving %s to %s", src, dst)) - cmd := &packer.RemoteCmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)} + cmd := &packer.RemoteCmd{ + Command: p.guestCommands.MovePath(src, dst), + } if err := cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 { if err == nil { err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) @@ -370,7 +428,21 @@ func (p *Provisioner) moveFile(ui packer.Ui, comm packer.Communicator, dst, src 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), + 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 nil +} + +func (p *Provisioner) statPath(ui packer.Ui, comm packer.Communicator, path string) error { + ui.Message(fmt.Sprintf("Verifying Path: %s", path)) + cmd := &packer.RemoteCmd{ + Command: p.guestCommands.StatPath(path), } if err := cmd.StartWithUi(comm, ui); err != nil { return err @@ -384,7 +456,7 @@ func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir stri func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { ui.Message(fmt.Sprintf("Removing directory: %s", dir)) cmd := &packer.RemoteCmd{ - Command: fmt.Sprintf(p.sudo("rm -rf '%s'"), dir), + Command: p.guestCommands.RemoveDir(dir), } if err := cmd.StartWithUi(comm, ui); err != nil { return err diff --git a/provisioner/salt-masterless/provisioner_test.go b/provisioner/salt-masterless/provisioner_test.go index bad83b3f6..a4bd64890 100644 --- a/provisioner/salt-masterless/provisioner_test.go +++ b/provisioner/salt-masterless/provisioner_test.go @@ -32,7 +32,7 @@ func TestProvisionerPrepare_Defaults(t *testing.T) { t.Fatalf("err: %s", err) } - if p.config.TempConfigDir != DefaultTempConfigDir { + if p.config.TempConfigDir != p.guestOSTypeConfig.tempDir { t.Errorf("unexpected temp config dir: %s", p.config.TempConfigDir) } } @@ -309,3 +309,19 @@ func TestProvisionerPrepare_LogLevel(t *testing.T) { t.Fatal("-l debug should be set in CmdArgs") } } + +func TestProvisionerPrepare_GuestOSType(t *testing.T) { + var p Provisioner + config := testConfig() + + config["guest_os_type"] = "Windows" + + err := p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if p.config.GuestOSType != "windows" { + t.Fatalf("GuestOSType should be 'windows'") + } +} diff --git a/website/source/docs/provisioners/salt-masterless.html.md b/website/source/docs/provisioners/salt-masterless.html.md index 443f37561..ad6920614 100644 --- a/website/source/docs/provisioners/salt-masterless.html.md +++ b/website/source/docs/provisioners/salt-masterless.html.md @@ -90,3 +90,5 @@ Optional: - `salt_bin_dir` (string) - Path to the `salt-call` executable. Useful if it is not on the PATH. + +- `guest_os_type` (string) - The target guest OS type, either "unix" or "windows".