github.phpd.cn/hashicorp/packer@v1.3.2/provisioner/salt-masterless/provisioner.go (about) 1 // This package implements a provisioner for Packer that executes a 2 // saltstack state within the remote machine 3 package saltmasterless 4 5 import ( 6 "bytes" 7 "errors" 8 "fmt" 9 "os" 10 "path/filepath" 11 "strings" 12 13 "github.com/hashicorp/packer/common" 14 "github.com/hashicorp/packer/helper/config" 15 "github.com/hashicorp/packer/packer" 16 "github.com/hashicorp/packer/provisioner" 17 "github.com/hashicorp/packer/template/interpolate" 18 ) 19 20 type Config struct { 21 common.PackerConfig `mapstructure:",squash"` 22 23 // If true, run the salt-bootstrap script 24 SkipBootstrap bool `mapstructure:"skip_bootstrap"` 25 BootstrapArgs string `mapstructure:"bootstrap_args"` 26 27 DisableSudo bool `mapstructure:"disable_sudo"` 28 29 // Custom state to run instead of highstate 30 CustomState string `mapstructure:"custom_state"` 31 32 // Local path to the minion config 33 MinionConfig string `mapstructure:"minion_config"` 34 35 // Local path to the minion grains 36 GrainsFile string `mapstructure:"grains_file"` 37 38 // Local path to the salt state tree 39 LocalStateTree string `mapstructure:"local_state_tree"` 40 41 // Local path to the salt pillar roots 42 LocalPillarRoots string `mapstructure:"local_pillar_roots"` 43 44 // Remote path to the salt state tree 45 RemoteStateTree string `mapstructure:"remote_state_tree"` 46 47 // Remote path to the salt pillar roots 48 RemotePillarRoots string `mapstructure:"remote_pillar_roots"` 49 50 // Where files will be copied before moving to the /srv/salt directory 51 TempConfigDir string `mapstructure:"temp_config_dir"` 52 53 // Don't exit packer if salt-call returns an error code 54 NoExitOnFailure bool `mapstructure:"no_exit_on_failure"` 55 56 // Set the logging level for the salt-call run 57 LogLevel string `mapstructure:"log_level"` 58 59 // Arguments to pass to salt-call 60 SaltCallArgs string `mapstructure:"salt_call_args"` 61 62 // Directory containing salt-call 63 SaltBinDir string `mapstructure:"salt_bin_dir"` 64 65 // Command line args passed onto salt-call 66 CmdArgs string "" 67 68 // The Guest OS Type (unix or windows) 69 GuestOSType string `mapstructure:"guest_os_type"` 70 71 ctx interpolate.Context 72 } 73 74 type Provisioner struct { 75 config Config 76 guestOSTypeConfig guestOSTypeConfig 77 guestCommands *provisioner.GuestCommands 78 } 79 80 type guestOSTypeConfig struct { 81 tempDir string 82 stateRoot string 83 pillarRoot string 84 configDir string 85 bootstrapFetchCmd string 86 bootstrapRunCmd string 87 } 88 89 var guestOSTypeConfigs = map[string]guestOSTypeConfig{ 90 provisioner.UnixOSType: { 91 configDir: "/etc/salt", 92 tempDir: "/tmp/salt", 93 stateRoot: "/srv/salt", 94 pillarRoot: "/srv/pillar", 95 bootstrapFetchCmd: "curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com", 96 bootstrapRunCmd: "sh /tmp/install_salt.sh", 97 }, 98 provisioner.WindowsOSType: { 99 configDir: "C:/salt/conf", 100 tempDir: "C:/Windows/Temp/salt/", 101 stateRoot: "C:/salt/state", 102 pillarRoot: "C:/salt/pillar/", 103 bootstrapFetchCmd: "powershell Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/saltstack/salt-bootstrap/stable/bootstrap-salt.ps1' -OutFile 'C:/Windows/Temp/bootstrap-salt.ps1'", 104 bootstrapRunCmd: "Powershell C:/Windows/Temp/bootstrap-salt.ps1", 105 }, 106 } 107 108 func (p *Provisioner) Prepare(raws ...interface{}) error { 109 err := config.Decode(&p.config, &config.DecodeOpts{ 110 Interpolate: true, 111 InterpolateContext: &p.config.ctx, 112 InterpolateFilter: &interpolate.RenderFilter{ 113 Exclude: []string{}, 114 }, 115 }, raws...) 116 if err != nil { 117 return err 118 } 119 120 if p.config.GuestOSType == "" { 121 p.config.GuestOSType = provisioner.DefaultOSType 122 } else { 123 p.config.GuestOSType = strings.ToLower(p.config.GuestOSType) 124 } 125 126 var ok bool 127 p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType] 128 if !ok { 129 return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) 130 } 131 132 p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, false) 133 if err != nil { 134 return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) 135 } 136 137 if p.config.TempConfigDir == "" { 138 p.config.TempConfigDir = p.guestOSTypeConfig.tempDir 139 } 140 141 var errs *packer.MultiError 142 143 // require a salt state tree 144 err = validateDirConfig(p.config.LocalStateTree, "local_state_tree", true) 145 if err != nil { 146 errs = packer.MultiErrorAppend(errs, err) 147 } 148 149 err = validateDirConfig(p.config.LocalPillarRoots, "local_pillar_roots", false) 150 if err != nil { 151 errs = packer.MultiErrorAppend(errs, err) 152 } 153 154 err = validateFileConfig(p.config.MinionConfig, "minion_config", false) 155 if err != nil { 156 errs = packer.MultiErrorAppend(errs, err) 157 } 158 159 if p.config.MinionConfig != "" && (p.config.RemoteStateTree != "" || p.config.RemotePillarRoots != "") { 160 errs = packer.MultiErrorAppend(errs, 161 errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config is not used")) 162 } 163 164 err = validateFileConfig(p.config.GrainsFile, "grains_file", false) 165 if err != nil { 166 errs = packer.MultiErrorAppend(errs, err) 167 } 168 169 // build the command line args to pass onto salt 170 var cmd_args bytes.Buffer 171 172 if p.config.CustomState == "" { 173 cmd_args.WriteString(" state.highstate") 174 } else { 175 cmd_args.WriteString(" state.sls ") 176 cmd_args.WriteString(p.config.CustomState) 177 } 178 179 if p.config.MinionConfig == "" { 180 // pass --file-root and --pillar-root if no minion_config is supplied 181 if p.config.RemoteStateTree != "" { 182 cmd_args.WriteString(" --file-root=") 183 cmd_args.WriteString(p.config.RemoteStateTree) 184 } else { 185 cmd_args.WriteString(" --file-root=") 186 cmd_args.WriteString(p.guestOSTypeConfig.stateRoot) 187 } 188 if p.config.RemotePillarRoots != "" { 189 cmd_args.WriteString(" --pillar-root=") 190 cmd_args.WriteString(p.config.RemotePillarRoots) 191 } else { 192 cmd_args.WriteString(" --pillar-root=") 193 cmd_args.WriteString(p.guestOSTypeConfig.pillarRoot) 194 } 195 } 196 197 if !p.config.NoExitOnFailure { 198 cmd_args.WriteString(" --retcode-passthrough") 199 } 200 201 if p.config.LogLevel == "" { 202 cmd_args.WriteString(" -l info") 203 } else { 204 cmd_args.WriteString(" -l ") 205 cmd_args.WriteString(p.config.LogLevel) 206 } 207 208 if p.config.SaltCallArgs != "" { 209 cmd_args.WriteString(" ") 210 cmd_args.WriteString(p.config.SaltCallArgs) 211 } 212 213 p.config.CmdArgs = cmd_args.String() 214 215 if errs != nil && len(errs.Errors) > 0 { 216 return errs 217 } 218 219 return nil 220 } 221 222 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 223 var err error 224 var src, dst string 225 226 ui.Say("Provisioning with Salt...") 227 if !p.config.SkipBootstrap { 228 cmd := &packer.RemoteCmd{ 229 // Fallback on wget if curl failed for any reason (such as not being installed) 230 Command: fmt.Sprintf(p.guestOSTypeConfig.bootstrapFetchCmd), 231 } 232 ui.Message(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh")) 233 if err = cmd.StartWithUi(comm, ui); err != nil { 234 return fmt.Errorf("Unable to download Salt: %s", err) 235 } 236 cmd = &packer.RemoteCmd{ 237 Command: fmt.Sprintf("%s %s", p.sudo(p.guestOSTypeConfig.bootstrapRunCmd), p.config.BootstrapArgs), 238 } 239 ui.Message(fmt.Sprintf("Installing Salt with command %s", cmd.Command)) 240 if err = cmd.StartWithUi(comm, ui); err != nil { 241 return fmt.Errorf("Unable to install Salt: %s", err) 242 } 243 } 244 245 ui.Message(fmt.Sprintf("Creating remote temporary directory: %s", p.config.TempConfigDir)) 246 if err := p.createDir(ui, comm, p.config.TempConfigDir); err != nil { 247 return fmt.Errorf("Error creating remote temporary directory: %s", err) 248 } 249 250 if p.config.MinionConfig != "" { 251 ui.Message(fmt.Sprintf("Uploading minion config: %s", p.config.MinionConfig)) 252 src = p.config.MinionConfig 253 dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion")) 254 if err = p.uploadFile(ui, comm, dst, src); err != nil { 255 return fmt.Errorf("Error uploading local minion config file to remote: %s", err) 256 } 257 258 // move minion config into /etc/salt 259 ui.Message(fmt.Sprintf("Make sure directory %s exists", p.guestOSTypeConfig.configDir)) 260 if err := p.createDir(ui, comm, p.guestOSTypeConfig.configDir); err != nil { 261 return fmt.Errorf("Error creating remote salt configuration directory: %s", err) 262 } 263 src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion")) 264 dst = filepath.ToSlash(filepath.Join(p.guestOSTypeConfig.configDir, "minion")) 265 if err = p.moveFile(ui, comm, dst, src); err != nil { 266 return fmt.Errorf("Unable to move %s/minion to %s/minion: %s", p.config.TempConfigDir, p.guestOSTypeConfig.configDir, err) 267 } 268 } 269 270 if p.config.GrainsFile != "" { 271 ui.Message(fmt.Sprintf("Uploading grains file: %s", p.config.GrainsFile)) 272 src = p.config.GrainsFile 273 dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "grains")) 274 if err = p.uploadFile(ui, comm, dst, src); err != nil { 275 return fmt.Errorf("Error uploading local grains file to remote: %s", err) 276 } 277 278 // move grains file into /etc/salt 279 ui.Message(fmt.Sprintf("Make sure directory %s exists", p.guestOSTypeConfig.configDir)) 280 if err := p.createDir(ui, comm, p.guestOSTypeConfig.configDir); err != nil { 281 return fmt.Errorf("Error creating remote salt configuration directory: %s", err) 282 } 283 src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "grains")) 284 dst = filepath.ToSlash(filepath.Join(p.guestOSTypeConfig.configDir, "grains")) 285 if err = p.moveFile(ui, comm, dst, src); err != nil { 286 return fmt.Errorf("Unable to move %s/grains to %s/grains: %s", p.config.TempConfigDir, p.guestOSTypeConfig.configDir, err) 287 } 288 } 289 290 ui.Message(fmt.Sprintf("Uploading local state tree: %s", p.config.LocalStateTree)) 291 src = p.config.LocalStateTree 292 dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "states")) 293 if err = p.uploadDir(ui, comm, dst, src, []string{".git"}); err != nil { 294 return fmt.Errorf("Error uploading local state tree to remote: %s", err) 295 } 296 297 // move state tree from temporary directory 298 src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "states")) 299 if p.config.RemoteStateTree != "" { 300 dst = p.config.RemoteStateTree 301 } else { 302 dst = p.guestOSTypeConfig.stateRoot 303 } 304 305 if err = p.statPath(ui, comm, dst); err != nil { 306 if err = p.removeDir(ui, comm, dst); err != nil { 307 return fmt.Errorf("Unable to clear salt tree: %s", err) 308 } 309 } 310 311 if err = p.moveFile(ui, comm, dst, src); err != nil { 312 return fmt.Errorf("Unable to move %s/states to %s: %s", p.config.TempConfigDir, dst, err) 313 } 314 315 if p.config.LocalPillarRoots != "" { 316 ui.Message(fmt.Sprintf("Uploading local pillar roots: %s", p.config.LocalPillarRoots)) 317 src = p.config.LocalPillarRoots 318 dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "pillar")) 319 if err = p.uploadDir(ui, comm, dst, src, []string{".git"}); err != nil { 320 return fmt.Errorf("Error uploading local pillar roots to remote: %s", err) 321 } 322 323 // move pillar root from temporary directory 324 src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "pillar")) 325 if p.config.RemotePillarRoots != "" { 326 dst = p.config.RemotePillarRoots 327 } else { 328 dst = p.guestOSTypeConfig.pillarRoot 329 } 330 331 if err = p.statPath(ui, comm, dst); err != nil { 332 if err = p.removeDir(ui, comm, dst); err != nil { 333 return fmt.Errorf("Unable to clear pillar root: %s", err) 334 } 335 } 336 337 if err = p.moveFile(ui, comm, dst, src); err != nil { 338 return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.config.TempConfigDir, dst, err) 339 } 340 } 341 342 ui.Message(fmt.Sprintf("Running: salt-call --local %s", p.config.CmdArgs)) 343 cmd := &packer.RemoteCmd{Command: p.sudo(fmt.Sprintf("%s --local %s", filepath.Join(p.config.SaltBinDir, "salt-call"), p.config.CmdArgs))} 344 if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 { 345 if err == nil { 346 err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) 347 } 348 349 return fmt.Errorf("Error executing salt-call: %s", err) 350 } 351 352 return nil 353 } 354 355 func (p *Provisioner) Cancel() { 356 // Just hard quit. It isn't a big deal if what we're doing keeps 357 // running on the other side. 358 os.Exit(0) 359 } 360 361 // Prepends sudo to supplied command if config says to 362 func (p *Provisioner) sudo(cmd string) string { 363 if p.config.DisableSudo || (p.config.GuestOSType == provisioner.WindowsOSType) { 364 return cmd 365 } 366 367 return "sudo " + cmd 368 } 369 370 func validateDirConfig(path string, name string, required bool) error { 371 if required && path == "" { 372 return fmt.Errorf("%s cannot be empty", name) 373 } else if required == false && path == "" { 374 return nil 375 } 376 info, err := os.Stat(path) 377 if err != nil { 378 return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err) 379 } else if !info.IsDir() { 380 return fmt.Errorf("%s: path '%s' must point to a directory", name, path) 381 } 382 return nil 383 } 384 385 func validateFileConfig(path string, name string, required bool) error { 386 if required == true && path == "" { 387 return fmt.Errorf("%s cannot be empty", name) 388 } else if required == false && path == "" { 389 return nil 390 } 391 info, err := os.Stat(path) 392 if err != nil { 393 return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err) 394 } else if info.IsDir() { 395 return fmt.Errorf("%s: path '%s' must point to a file", name, path) 396 } 397 return nil 398 } 399 400 func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst, src string) error { 401 f, err := os.Open(src) 402 if err != nil { 403 return fmt.Errorf("Error opening: %s", err) 404 } 405 defer f.Close() 406 407 if err = comm.Upload(dst, f, nil); err != nil { 408 return fmt.Errorf("Error uploading %s: %s", src, err) 409 } 410 return nil 411 } 412 413 func (p *Provisioner) moveFile(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 414 ui.Message(fmt.Sprintf("Moving %s to %s", src, dst)) 415 cmd := &packer.RemoteCmd{ 416 Command: p.sudo(p.guestCommands.MovePath(src, dst)), 417 } 418 if err := cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 { 419 if err == nil { 420 err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) 421 } 422 423 return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err) 424 } 425 return nil 426 } 427 428 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 429 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 430 cmd := &packer.RemoteCmd{ 431 Command: p.guestCommands.CreateDir(dir), 432 } 433 if err := cmd.StartWithUi(comm, ui); err != nil { 434 return err 435 } 436 if cmd.ExitStatus != 0 { 437 return fmt.Errorf("Non-zero exit status.") 438 } 439 return nil 440 } 441 442 func (p *Provisioner) statPath(ui packer.Ui, comm packer.Communicator, path string) error { 443 ui.Message(fmt.Sprintf("Verifying Path: %s", path)) 444 cmd := &packer.RemoteCmd{ 445 Command: p.guestCommands.StatPath(path), 446 } 447 if err := cmd.StartWithUi(comm, ui); err != nil { 448 return err 449 } 450 if cmd.ExitStatus != 0 { 451 return fmt.Errorf("Non-zero exit status.") 452 } 453 return nil 454 } 455 456 func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { 457 ui.Message(fmt.Sprintf("Removing directory: %s", dir)) 458 cmd := &packer.RemoteCmd{ 459 Command: p.guestCommands.RemoveDir(dir), 460 } 461 if err := cmd.StartWithUi(comm, ui); err != nil { 462 return err 463 } 464 if cmd.ExitStatus != 0 { 465 return fmt.Errorf("Non-zero exit status.") 466 } 467 return nil 468 } 469 470 func (p *Provisioner) uploadDir(ui packer.Ui, comm packer.Communicator, dst, src string, ignore []string) error { 471 if err := p.createDir(ui, comm, dst); err != nil { 472 return err 473 } 474 475 // Make sure there is a trailing "/" so that the directory isn't 476 // created on the other side. 477 if src[len(src)-1] != '/' { 478 src = src + "/" 479 } 480 return comm.UploadDir(dst, src, ignore) 481 }