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