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