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