github.com/pgray/terraform@v0.5.4-0.20170822184730-b6a464c5214d/builtin/provisioners/salt-masterless/resource_provisioner.go (about) 1 // This package implements a provisioner for Terraform that executes a 2 // saltstack state within the remote machine 3 // 4 // Adapted from gitub.com/hashicorp/packer/provisioner/salt-masterless 5 6 package saltmasterless 7 8 import ( 9 "bytes" 10 "context" 11 "errors" 12 "fmt" 13 "os" 14 "path/filepath" 15 16 "github.com/hashicorp/terraform/communicator" 17 "github.com/hashicorp/terraform/communicator/remote" 18 "github.com/hashicorp/terraform/helper/schema" 19 "github.com/hashicorp/terraform/terraform" 20 ) 21 22 type provisionFn func(terraform.UIOutput, communicator.Communicator) error 23 24 type provisioner struct { 25 SkipBootstrap bool 26 BootstrapArgs string 27 LocalStateTree string 28 DisableSudo bool 29 CustomState string 30 MinionConfig string 31 LocalPillarRoots string 32 RemoteStateTree string 33 RemotePillarRoots string 34 TempConfigDir string 35 NoExitOnFailure bool 36 LogLevel string 37 SaltCallArgs string 38 CmdArgs string 39 } 40 41 const DefaultStateTreeDir = "/srv/salt" 42 const DefaultPillarRootDir = "/srv/pillar" 43 44 // Provisioner returns a salt-masterless provisioner 45 func Provisioner() terraform.ResourceProvisioner { 46 return &schema.Provisioner{ 47 Schema: map[string]*schema.Schema{ 48 "local_state_tree": &schema.Schema{ 49 Type: schema.TypeString, 50 Required: true, 51 }, 52 "local_pillar_roots": &schema.Schema{ 53 Type: schema.TypeString, 54 Optional: true, 55 }, 56 "remote_state_tree": &schema.Schema{ 57 Type: schema.TypeString, 58 Optional: true, 59 }, 60 "remote_pillar_roots": &schema.Schema{ 61 Type: schema.TypeString, 62 Optional: true, 63 }, 64 "temp_config_dir": &schema.Schema{ 65 Type: schema.TypeString, 66 Optional: true, 67 Default: "/tmp/salt", 68 }, 69 "skip_bootstrap": &schema.Schema{ 70 Type: schema.TypeBool, 71 Optional: true, 72 }, 73 "no_exit_on_failure": &schema.Schema{ 74 Type: schema.TypeBool, 75 Optional: true, 76 }, 77 "bootstrap_args": &schema.Schema{ 78 Type: schema.TypeString, 79 Optional: true, 80 }, 81 "disable_sudo": &schema.Schema{ 82 Type: schema.TypeBool, 83 Optional: true, 84 }, 85 "custom_state": &schema.Schema{ 86 Type: schema.TypeString, 87 Optional: true, 88 }, 89 "minion_config": &schema.Schema{ 90 Type: schema.TypeString, 91 Optional: true, 92 }, 93 "cmd_args": &schema.Schema{ 94 Type: schema.TypeString, 95 Optional: true, 96 }, 97 "salt_call_args": &schema.Schema{ 98 Type: schema.TypeString, 99 Optional: true, 100 }, 101 "log_level": &schema.Schema{ 102 Type: schema.TypeString, 103 Optional: true, 104 }, 105 }, 106 107 ApplyFunc: applyFn, 108 ValidateFunc: validateFn, 109 } 110 } 111 112 // Apply executes the file provisioner 113 func applyFn(ctx context.Context) error { 114 // Decode the raw config for this provisioner 115 var err error 116 117 o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput) 118 d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData) 119 120 p, err := decodeConfig(d) 121 if err != nil { 122 return err 123 } 124 125 // Get a new communicator 126 comm, err := communicator.New(d.State()) 127 if err != nil { 128 return err 129 } 130 131 var src, dst string 132 133 o.Output("Provisioning with Salt...") 134 if !p.SkipBootstrap { 135 cmd := &remote.Cmd{ 136 // Fallback on wget if curl failed for any reason (such as not being installed) 137 Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com"), 138 } 139 o.Output(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh")) 140 if err = comm.Start(cmd); err != nil { 141 return fmt.Errorf("Unable to download Salt: %s", err) 142 } 143 cmd = &remote.Cmd{ 144 Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.BootstrapArgs), 145 } 146 o.Output(fmt.Sprintf("Installing Salt with command %s", cmd.Command)) 147 if err = comm.Start(cmd); err != nil { 148 return fmt.Errorf("Unable to install Salt: %s", err) 149 } 150 } 151 152 o.Output(fmt.Sprintf("Creating remote temporary directory: %s", p.TempConfigDir)) 153 if err := p.createDir(o, comm, p.TempConfigDir); err != nil { 154 return fmt.Errorf("Error creating remote temporary directory: %s", err) 155 } 156 157 if p.MinionConfig != "" { 158 o.Output(fmt.Sprintf("Uploading minion config: %s", p.MinionConfig)) 159 src = p.MinionConfig 160 dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion")) 161 if err = p.uploadFile(o, comm, dst, src); err != nil { 162 return fmt.Errorf("Error uploading local minion config file to remote: %s", err) 163 } 164 165 // move minion config into /etc/salt 166 o.Output(fmt.Sprintf("Make sure directory %s exists", "/etc/salt")) 167 if err := p.createDir(o, comm, "/etc/salt"); err != nil { 168 return fmt.Errorf("Error creating remote salt configuration directory: %s", err) 169 } 170 src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion")) 171 dst = "/etc/salt/minion" 172 if err = p.moveFile(o, comm, dst, src); err != nil { 173 return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.TempConfigDir, err) 174 } 175 } 176 177 o.Output(fmt.Sprintf("Uploading local state tree: %s", p.LocalStateTree)) 178 src = p.LocalStateTree 179 dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states")) 180 if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil { 181 return fmt.Errorf("Error uploading local state tree to remote: %s", err) 182 } 183 184 // move state tree from temporary directory 185 src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states")) 186 dst = p.RemoteStateTree 187 if err = p.removeDir(o, comm, dst); err != nil { 188 return fmt.Errorf("Unable to clear salt tree: %s", err) 189 } 190 if err = p.moveFile(o, comm, dst, src); err != nil { 191 return fmt.Errorf("Unable to move %s/states to %s: %s", p.TempConfigDir, dst, err) 192 } 193 194 if p.LocalPillarRoots != "" { 195 o.Output(fmt.Sprintf("Uploading local pillar roots: %s", p.LocalPillarRoots)) 196 src = p.LocalPillarRoots 197 dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar")) 198 if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil { 199 return fmt.Errorf("Error uploading local pillar roots to remote: %s", err) 200 } 201 202 // move pillar root from temporary directory 203 src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar")) 204 dst = p.RemotePillarRoots 205 206 if err = p.removeDir(o, comm, dst); err != nil { 207 return fmt.Errorf("Unable to clear pillar root: %s", err) 208 } 209 if err = p.moveFile(o, comm, dst, src); err != nil { 210 return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.TempConfigDir, dst, err) 211 } 212 } 213 214 o.Output(fmt.Sprintf("Running: salt-call --local %s", p.CmdArgs)) 215 cmd := &remote.Cmd{Command: p.sudo(fmt.Sprintf("salt-call --local %s", p.CmdArgs))} 216 if err = comm.Start(cmd); err != nil || cmd.ExitStatus != 0 { 217 if err == nil { 218 err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) 219 } 220 221 return fmt.Errorf("Error executing salt-call: %s", err) 222 } 223 224 return nil 225 } 226 227 // Prepends sudo to supplied command if config says to 228 func (p *provisioner) sudo(cmd string) string { 229 if p.DisableSudo { 230 return cmd 231 } 232 233 return "sudo " + cmd 234 } 235 236 func validateDirConfig(path string, name string, required bool) error { 237 if required == true && path == "" { 238 return fmt.Errorf("%s cannot be empty", name) 239 } else if required == false && path == "" { 240 return nil 241 } 242 info, err := os.Stat(path) 243 if err != nil { 244 return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err) 245 } else if !info.IsDir() { 246 return fmt.Errorf("%s: path '%s' must point to a directory", name, path) 247 } 248 return nil 249 } 250 251 func validateFileConfig(path string, name string, required bool) error { 252 if required == true && path == "" { 253 return fmt.Errorf("%s cannot be empty", name) 254 } else if required == false && path == "" { 255 return nil 256 } 257 info, err := os.Stat(path) 258 if err != nil { 259 return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err) 260 } else if info.IsDir() { 261 return fmt.Errorf("%s: path '%s' must point to a file", name, path) 262 } 263 return nil 264 } 265 266 func (p *provisioner) uploadFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error { 267 f, err := os.Open(src) 268 if err != nil { 269 return fmt.Errorf("Error opening: %s", err) 270 } 271 defer f.Close() 272 273 if err = comm.Upload(dst, f); err != nil { 274 return fmt.Errorf("Error uploading %s: %s", src, err) 275 } 276 return nil 277 } 278 279 func (p *provisioner) moveFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error { 280 o.Output(fmt.Sprintf("Moving %s to %s", src, dst)) 281 cmd := &remote.Cmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)} 282 if err := comm.Start(cmd); err != nil || cmd.ExitStatus != 0 { 283 if err == nil { 284 err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) 285 } 286 287 return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err) 288 } 289 return nil 290 } 291 292 func (p *provisioner) createDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error { 293 o.Output(fmt.Sprintf("Creating directory: %s", dir)) 294 cmd := &remote.Cmd{ 295 Command: fmt.Sprintf("mkdir -p '%s'", dir), 296 } 297 if err := comm.Start(cmd); err != nil { 298 return err 299 } 300 if cmd.ExitStatus != 0 { 301 return fmt.Errorf("Non-zero exit status.") 302 } 303 return nil 304 } 305 306 func (p *provisioner) removeDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error { 307 o.Output(fmt.Sprintf("Removing directory: %s", dir)) 308 cmd := &remote.Cmd{ 309 Command: fmt.Sprintf("rm -rf '%s'", dir), 310 } 311 if err := comm.Start(cmd); err != nil { 312 return err 313 } 314 if cmd.ExitStatus != 0 { 315 return fmt.Errorf("Non-zero exit status.") 316 } 317 return nil 318 } 319 320 func (p *provisioner) uploadDir(o terraform.UIOutput, comm communicator.Communicator, dst, src string, ignore []string) error { 321 if err := p.createDir(o, comm, dst); err != nil { 322 return err 323 } 324 325 // Make sure there is a trailing "/" so that the directory isn't 326 // created on the other side. 327 if src[len(src)-1] != '/' { 328 src = src + "/" 329 } 330 return comm.UploadDir(dst, src) 331 } 332 333 // Validate checks if the required arguments are configured 334 func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) { 335 // require a salt state tree 336 localStateTreeTmp, ok := c.Get("local_state_tree") 337 var localStateTree string 338 if !ok { 339 es = append(es, 340 errors.New("Required local_state_tree is not set")) 341 } else { 342 localStateTree = localStateTreeTmp.(string) 343 } 344 err := validateDirConfig(localStateTree, "local_state_tree", true) 345 if err != nil { 346 es = append(es, err) 347 } 348 349 var localPillarRoots string 350 localPillarRootsTmp, ok := c.Get("local_pillar_roots") 351 if !ok { 352 localPillarRoots = "" 353 } else { 354 localPillarRoots = localPillarRootsTmp.(string) 355 } 356 357 err = validateDirConfig(localPillarRoots, "local_pillar_roots", false) 358 if err != nil { 359 es = append(es, err) 360 } 361 362 var minionConfig string 363 minionConfigTmp, ok := c.Get("minion_config") 364 if !ok { 365 minionConfig = "" 366 } else { 367 minionConfig = minionConfigTmp.(string) 368 } 369 err = validateFileConfig(minionConfig, "minion_config", false) 370 if err != nil { 371 es = append(es, err) 372 } 373 374 var remoteStateTree string 375 remoteStateTreeTmp, ok := c.Get("remote_state_tree") 376 if !ok { 377 remoteStateTree = "" 378 } else { 379 remoteStateTree = remoteStateTreeTmp.(string) 380 } 381 382 var remotePillarRoots string 383 remotePillarRootsTmp, ok := c.Get("remote_pillar_roots") 384 if !ok { 385 remotePillarRoots = "" 386 } else { 387 remotePillarRoots = remotePillarRootsTmp.(string) 388 } 389 390 if minionConfig != "" && (remoteStateTree != "" || remotePillarRoots != "") { 391 es = append(es, 392 errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config is not used")) 393 } 394 395 if len(es) > 0 { 396 return ws, es 397 } 398 399 return ws, es 400 } 401 402 func decodeConfig(d *schema.ResourceData) (*provisioner, error) { 403 p := &provisioner{ 404 LocalStateTree: d.Get("local_state_tree").(string), 405 LogLevel: d.Get("log_level").(string), 406 SaltCallArgs: d.Get("salt_call_args").(string), 407 CmdArgs: d.Get("cmd_args").(string), 408 MinionConfig: d.Get("minion_config").(string), 409 CustomState: d.Get("custom_state").(string), 410 DisableSudo: d.Get("disable_sudo").(bool), 411 BootstrapArgs: d.Get("bootstrap_args").(string), 412 NoExitOnFailure: d.Get("no_exit_on_failure").(bool), 413 SkipBootstrap: d.Get("skip_bootstrap").(bool), 414 TempConfigDir: d.Get("temp_config_dir").(string), 415 RemotePillarRoots: d.Get("remote_pillar_roots").(string), 416 RemoteStateTree: d.Get("remote_state_tree").(string), 417 LocalPillarRoots: d.Get("local_pillar_roots").(string), 418 } 419 420 // build the command line args to pass onto salt 421 var cmdArgs bytes.Buffer 422 423 if p.CustomState == "" { 424 cmdArgs.WriteString(" state.highstate") 425 } else { 426 cmdArgs.WriteString(" state.sls ") 427 cmdArgs.WriteString(p.CustomState) 428 } 429 430 if p.MinionConfig == "" { 431 // pass --file-root and --pillar-root if no minion_config is supplied 432 if p.RemoteStateTree != "" { 433 cmdArgs.WriteString(" --file-root=") 434 cmdArgs.WriteString(p.RemoteStateTree) 435 } else { 436 cmdArgs.WriteString(" --file-root=") 437 cmdArgs.WriteString(DefaultStateTreeDir) 438 } 439 if p.RemotePillarRoots != "" { 440 cmdArgs.WriteString(" --pillar-root=") 441 cmdArgs.WriteString(p.RemotePillarRoots) 442 } else { 443 cmdArgs.WriteString(" --pillar-root=") 444 cmdArgs.WriteString(DefaultPillarRootDir) 445 } 446 } 447 448 if !p.NoExitOnFailure { 449 cmdArgs.WriteString(" --retcode-passthrough") 450 } 451 452 if p.LogLevel == "" { 453 cmdArgs.WriteString(" -l info") 454 } else { 455 cmdArgs.WriteString(" -l ") 456 cmdArgs.WriteString(p.LogLevel) 457 } 458 459 if p.SaltCallArgs != "" { 460 cmdArgs.WriteString(" ") 461 cmdArgs.WriteString(p.SaltCallArgs) 462 } 463 464 p.CmdArgs = cmdArgs.String() 465 466 return p, nil 467 }