github.com/rahart/packer@v0.12.2-0.20161229105310-282bb6ad370f/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 // Fallback on wget if curl failed for any reason (such as not being installed) 171 Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com"), 172 } 173 ui.Message(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh")) 174 if err = cmd.StartWithUi(comm, ui); err != nil { 175 return fmt.Errorf("Unable to download Salt: %s", err) 176 } 177 cmd = &packer.RemoteCmd{ 178 Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.config.BootstrapArgs), 179 } 180 ui.Message(fmt.Sprintf("Installing Salt with command %s", cmd.Command)) 181 if err = cmd.StartWithUi(comm, ui); err != nil { 182 return fmt.Errorf("Unable to install Salt: %s", err) 183 } 184 } 185 186 ui.Message(fmt.Sprintf("Creating remote temporary directory: %s", p.config.TempConfigDir)) 187 if err := p.createDir(ui, comm, p.config.TempConfigDir); err != nil { 188 return fmt.Errorf("Error creating remote temporary directory: %s", err) 189 } 190 191 if p.config.MinionConfig != "" { 192 ui.Message(fmt.Sprintf("Uploading minion config: %s", p.config.MinionConfig)) 193 src = p.config.MinionConfig 194 dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion")) 195 if err = p.uploadFile(ui, comm, dst, src); err != nil { 196 return fmt.Errorf("Error uploading local minion config file to remote: %s", err) 197 } 198 199 // move minion config into /etc/salt 200 ui.Message(fmt.Sprintf("Make sure directory %s exists", "/etc/salt")) 201 if err := p.createDir(ui, comm, "/etc/salt"); err != nil { 202 return fmt.Errorf("Error creating remote salt configuration directory: %s", err) 203 } 204 src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion")) 205 dst = "/etc/salt/minion" 206 if err = p.moveFile(ui, comm, dst, src); err != nil { 207 return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.config.TempConfigDir, err) 208 } 209 } 210 211 ui.Message(fmt.Sprintf("Uploading local state tree: %s", p.config.LocalStateTree)) 212 src = p.config.LocalStateTree 213 dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "states")) 214 if err = p.uploadDir(ui, comm, dst, src, []string{".git"}); err != nil { 215 return fmt.Errorf("Error uploading local state tree to remote: %s", err) 216 } 217 218 // move state tree from temporary directory 219 src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "states")) 220 if p.config.RemoteStateTree != "" { 221 dst = p.config.RemoteStateTree 222 } else { 223 dst = DefaultStateTreeDir 224 } 225 if err = p.removeDir(ui, comm, dst); err != nil { 226 return fmt.Errorf("Unable to clear salt tree: %s", err) 227 } 228 if err = p.moveFile(ui, comm, dst, src); err != nil { 229 return fmt.Errorf("Unable to move %s/states to %s: %s", p.config.TempConfigDir, dst, err) 230 } 231 232 if p.config.LocalPillarRoots != "" { 233 ui.Message(fmt.Sprintf("Uploading local pillar roots: %s", p.config.LocalPillarRoots)) 234 src = p.config.LocalPillarRoots 235 dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "pillar")) 236 if err = p.uploadDir(ui, comm, dst, src, []string{".git"}); err != nil { 237 return fmt.Errorf("Error uploading local pillar roots to remote: %s", err) 238 } 239 240 // move pillar root from temporary directory 241 src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "pillar")) 242 if p.config.RemotePillarRoots != "" { 243 dst = p.config.RemotePillarRoots 244 } else { 245 dst = DefaultPillarRootDir 246 } 247 if err = p.removeDir(ui, comm, dst); err != nil { 248 return fmt.Errorf("Unable to clear pillar root: %s", err) 249 } 250 if err = p.moveFile(ui, comm, dst, src); err != nil { 251 return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.config.TempConfigDir, dst, err) 252 } 253 } 254 255 ui.Message(fmt.Sprintf("Running: salt-call --local %s", p.config.CmdArgs)) 256 cmd := &packer.RemoteCmd{Command: p.sudo(fmt.Sprintf("salt-call --local %s", p.config.CmdArgs))} 257 if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 { 258 if err == nil { 259 err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) 260 } 261 262 return fmt.Errorf("Error executing salt-call: %s", err) 263 } 264 265 return nil 266 } 267 268 func (p *Provisioner) Cancel() { 269 // Just hard quit. It isn't a big deal if what we're doing keeps 270 // running on the other side. 271 os.Exit(0) 272 } 273 274 // Prepends sudo to supplied command if config says to 275 func (p *Provisioner) sudo(cmd string) string { 276 if p.config.DisableSudo { 277 return cmd 278 } 279 280 return "sudo " + cmd 281 } 282 283 func validateDirConfig(path string, name string, required bool) error { 284 if required == true && path == "" { 285 return fmt.Errorf("%s cannot be empty", name) 286 } else if required == false && path == "" { 287 return nil 288 } 289 info, err := os.Stat(path) 290 if err != nil { 291 return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err) 292 } else if !info.IsDir() { 293 return fmt.Errorf("%s: path '%s' must point to a directory", name, path) 294 } 295 return nil 296 } 297 298 func validateFileConfig(path string, name string, required bool) error { 299 if required == true && path == "" { 300 return fmt.Errorf("%s cannot be empty", name) 301 } else if required == false && path == "" { 302 return nil 303 } 304 info, err := os.Stat(path) 305 if err != nil { 306 return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err) 307 } else if info.IsDir() { 308 return fmt.Errorf("%s: path '%s' must point to a file", name, path) 309 } 310 return nil 311 } 312 313 func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst, src string) error { 314 f, err := os.Open(src) 315 if err != nil { 316 return fmt.Errorf("Error opening: %s", err) 317 } 318 defer f.Close() 319 320 if err = comm.Upload(dst, f, nil); err != nil { 321 return fmt.Errorf("Error uploading %s: %s", src, err) 322 } 323 return nil 324 } 325 326 func (p *Provisioner) moveFile(ui packer.Ui, comm packer.Communicator, dst, src string) error { 327 ui.Message(fmt.Sprintf("Moving %s to %s", src, dst)) 328 cmd := &packer.RemoteCmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)} 329 if err := cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 { 330 if err == nil { 331 err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) 332 } 333 334 return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err) 335 } 336 return nil 337 } 338 339 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 340 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 341 cmd := &packer.RemoteCmd{ 342 Command: fmt.Sprintf("mkdir -p '%s'", dir), 343 } 344 if err := cmd.StartWithUi(comm, ui); err != nil { 345 return err 346 } 347 if cmd.ExitStatus != 0 { 348 return fmt.Errorf("Non-zero exit status.") 349 } 350 return nil 351 } 352 353 func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { 354 ui.Message(fmt.Sprintf("Removing directory: %s", dir)) 355 cmd := &packer.RemoteCmd{ 356 Command: fmt.Sprintf("rm -rf '%s'", dir), 357 } 358 if err := cmd.StartWithUi(comm, ui); err != nil { 359 return err 360 } 361 if cmd.ExitStatus != 0 { 362 return fmt.Errorf("Non-zero exit status.") 363 } 364 return nil 365 } 366 367 func (p *Provisioner) uploadDir(ui packer.Ui, comm packer.Communicator, dst, src string, ignore []string) error { 368 if err := p.createDir(ui, comm, dst); err != nil { 369 return err 370 } 371 372 // Make sure there is a trailing "/" so that the directory isn't 373 // created on the other side. 374 if src[len(src)-1] != '/' { 375 src = src + "/" 376 } 377 return comm.UploadDir(dst, src, ignore) 378 }