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