github.com/aspring/packer@v0.8.1-0.20150629211158-9db281ac0f89/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 "errors" 7 "fmt" 8 "os" 9 "path/filepath" 10 11 "github.com/mitchellh/packer/common" 12 "github.com/mitchellh/packer/helper/config" 13 "github.com/mitchellh/packer/packer" 14 "github.com/mitchellh/packer/template/interpolate" 15 ) 16 17 const DefaultTempConfigDir = "/tmp/salt" 18 19 type Config struct { 20 common.PackerConfig `mapstructure:",squash"` 21 22 // If true, run the salt-bootstrap script 23 SkipBootstrap bool `mapstructure:"skip_bootstrap"` 24 BootstrapArgs string `mapstructure:"bootstrap_args"` 25 26 DisableSudo bool `mapstructure:"disable_sudo"` 27 28 // Local path to the minion config 29 MinionConfig string `mapstructure:"minion_config"` 30 31 // Local path to the salt state tree 32 LocalStateTree string `mapstructure:"local_state_tree"` 33 34 // Local path to the salt pillar roots 35 LocalPillarRoots string `mapstructure:"local_pillar_roots"` 36 37 // Where files will be copied before moving to the /srv/salt directory 38 TempConfigDir string `mapstructure:"temp_config_dir"` 39 40 ctx interpolate.Context 41 } 42 43 type Provisioner struct { 44 config Config 45 } 46 47 func (p *Provisioner) Prepare(raws ...interface{}) error { 48 err := config.Decode(&p.config, &config.DecodeOpts{ 49 Interpolate: true, 50 InterpolateContext: &p.config.ctx, 51 InterpolateFilter: &interpolate.RenderFilter{ 52 Exclude: []string{}, 53 }, 54 }, raws...) 55 if err != nil { 56 return err 57 } 58 59 if p.config.TempConfigDir == "" { 60 p.config.TempConfigDir = DefaultTempConfigDir 61 } 62 63 var errs *packer.MultiError 64 65 // require a salt state tree 66 if p.config.LocalStateTree == "" { 67 errs = packer.MultiErrorAppend(errs, 68 errors.New("local_state_tree must be supplied")) 69 } else { 70 if _, err := os.Stat(p.config.LocalStateTree); err != nil { 71 errs = packer.MultiErrorAppend(errs, 72 errors.New("local_state_tree must exist and be accessible")) 73 } 74 } 75 76 if p.config.LocalPillarRoots != "" { 77 if _, err := os.Stat(p.config.LocalPillarRoots); err != nil { 78 errs = packer.MultiErrorAppend(errs, 79 errors.New("local_pillar_roots must exist and be accessible")) 80 } 81 } 82 83 if p.config.MinionConfig != "" { 84 if _, err := os.Stat(p.config.MinionConfig); err != nil { 85 errs = packer.MultiErrorAppend(errs, 86 errors.New("minion_config must exist and be accessible")) 87 } 88 } 89 90 if errs != nil && len(errs.Errors) > 0 { 91 return errs 92 } 93 94 return nil 95 } 96 97 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 98 var err error 99 var src, dst string 100 101 ui.Say("Provisioning with Salt...") 102 if !p.config.SkipBootstrap { 103 cmd := &packer.RemoteCmd{ 104 Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh"), 105 } 106 ui.Message(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh")) 107 if err = cmd.StartWithUi(comm, ui); err != nil { 108 return fmt.Errorf("Unable to download Salt: %s", err) 109 } 110 cmd = &packer.RemoteCmd{ 111 Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.config.BootstrapArgs), 112 } 113 ui.Message(fmt.Sprintf("Installing Salt with command %s", cmd.Command)) 114 if err = cmd.StartWithUi(comm, ui); err != nil { 115 return fmt.Errorf("Unable to install Salt: %s", err) 116 } 117 } 118 119 ui.Message(fmt.Sprintf("Creating remote directory: %s", p.config.TempConfigDir)) 120 if err := p.createDir(ui, comm, p.config.TempConfigDir); err != nil { 121 return fmt.Errorf("Error creating remote salt state directory: %s", err) 122 } 123 124 if p.config.MinionConfig != "" { 125 ui.Message(fmt.Sprintf("Uploading minion config: %s", p.config.MinionConfig)) 126 src = p.config.MinionConfig 127 dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion")) 128 if err = p.uploadFile(ui, comm, dst, src); err != nil { 129 return fmt.Errorf("Error uploading local minion config file to remote: %s", err) 130 } 131 132 // move minion config into /etc/salt 133 src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "minion")) 134 dst = "/etc/salt/minion" 135 if err = p.moveFile(ui, comm, dst, src); err != nil { 136 return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.config.TempConfigDir, err) 137 } 138 } 139 140 ui.Message(fmt.Sprintf("Uploading local state tree: %s", p.config.LocalStateTree)) 141 src = p.config.LocalStateTree 142 dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "states")) 143 if err = p.uploadDir(ui, comm, dst, src, []string{".git"}); err != nil { 144 return fmt.Errorf("Error uploading local state tree to remote: %s", err) 145 } 146 147 // move state tree into /srv/salt 148 src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "states")) 149 dst = "/srv/salt" 150 if err = p.moveFile(ui, comm, dst, src); err != nil { 151 return fmt.Errorf("Unable to move %s/states to /srv/salt: %s", p.config.TempConfigDir, err) 152 } 153 154 if p.config.LocalPillarRoots != "" { 155 ui.Message(fmt.Sprintf("Uploading local pillar roots: %s", p.config.LocalPillarRoots)) 156 src = p.config.LocalPillarRoots 157 dst = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "pillar")) 158 if err = p.uploadDir(ui, comm, dst, src, []string{".git"}); err != nil { 159 return fmt.Errorf("Error uploading local pillar roots to remote: %s", err) 160 } 161 162 // move pillar tree into /srv/pillar 163 src = filepath.ToSlash(filepath.Join(p.config.TempConfigDir, "pillar")) 164 dst = "/srv/pillar" 165 if err = p.moveFile(ui, comm, dst, src); err != nil { 166 return fmt.Errorf("Unable to move %s/pillar to /srv/pillar: %s", p.config.TempConfigDir, err) 167 } 168 } 169 170 ui.Message("Running highstate") 171 cmd := &packer.RemoteCmd{Command: p.sudo("salt-call --local state.highstate -l info --retcode-passthrough")} 172 if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 { 173 if err == nil { 174 err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) 175 } 176 177 return fmt.Errorf("Error executing highstate: %s", err) 178 } 179 180 return nil 181 } 182 183 func (p *Provisioner) Cancel() { 184 // Just hard quit. It isn't a big deal if what we're doing keeps 185 // running on the other side. 186 os.Exit(0) 187 } 188 189 // Prepends sudo to supplied command if config says to 190 func (p *Provisioner) sudo(cmd string) string { 191 if p.config.DisableSudo { 192 return cmd 193 } 194 195 return "sudo " + cmd 196 } 197 198 func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, dst, src string) error { 199 f, err := os.Open(src) 200 if err != nil { 201 return fmt.Errorf("Error opening: %s", err) 202 } 203 defer f.Close() 204 205 if err = comm.Upload(dst, f, nil); err != nil { 206 return fmt.Errorf("Error uploading %s: %s", src, err) 207 } 208 return nil 209 } 210 211 func (p *Provisioner) moveFile(ui packer.Ui, comm packer.Communicator, dst, src string) error { 212 ui.Message(fmt.Sprintf("Moving %s to %s", src, dst)) 213 cmd := &packer.RemoteCmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)} 214 if err := cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 { 215 if err == nil { 216 err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) 217 } 218 219 return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.config.TempConfigDir, err) 220 } 221 return nil 222 } 223 224 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 225 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 226 cmd := &packer.RemoteCmd{ 227 Command: fmt.Sprintf("mkdir -p '%s'", dir), 228 } 229 if err := cmd.StartWithUi(comm, ui); err != nil { 230 return err 231 } 232 if cmd.ExitStatus != 0 { 233 return fmt.Errorf("Non-zero exit status.") 234 } 235 return nil 236 } 237 238 func (p *Provisioner) uploadDir(ui packer.Ui, comm packer.Communicator, dst, src string, ignore []string) error { 239 if err := p.createDir(ui, comm, dst); err != nil { 240 return err 241 } 242 243 // Make sure there is a trailing "/" so that the directory isn't 244 // created on the other side. 245 if src[len(src)-1] != '/' { 246 src = src + "/" 247 } 248 return comm.UploadDir(dst, src, ignore) 249 }