github.com/hooklift/terraform@v0.11.0-beta1.0.20171117000744-6786c1361ffe/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_file": &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 connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState) 120 121 p, err := decodeConfig(d) 122 if err != nil { 123 return err 124 } 125 126 // Get a new communicator 127 comm, err := communicator.New(connState) 128 if err != nil { 129 return err 130 } 131 132 var src, dst string 133 134 o.Output("Provisioning with Salt...") 135 if !p.SkipBootstrap { 136 cmd := &remote.Cmd{ 137 // Fallback on wget if curl failed for any reason (such as not being installed) 138 Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com"), 139 } 140 o.Output(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh")) 141 if err = comm.Start(cmd); err != nil { 142 return fmt.Errorf("Unable to download Salt: %s", err) 143 } 144 cmd = &remote.Cmd{ 145 Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.BootstrapArgs), 146 } 147 o.Output(fmt.Sprintf("Installing Salt with command %s", cmd.Command)) 148 if err = comm.Start(cmd); err != nil { 149 return fmt.Errorf("Unable to install Salt: %s", err) 150 } 151 } 152 153 o.Output(fmt.Sprintf("Creating remote temporary directory: %s", p.TempConfigDir)) 154 if err := p.createDir(o, comm, p.TempConfigDir); err != nil { 155 return fmt.Errorf("Error creating remote temporary directory: %s", err) 156 } 157 158 if p.MinionConfig != "" { 159 o.Output(fmt.Sprintf("Uploading minion config: %s", p.MinionConfig)) 160 src = p.MinionConfig 161 dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion")) 162 if err = p.uploadFile(o, comm, dst, src); err != nil { 163 return fmt.Errorf("Error uploading local minion config file to remote: %s", err) 164 } 165 166 // move minion config into /etc/salt 167 o.Output(fmt.Sprintf("Make sure directory %s exists", "/etc/salt")) 168 if err := p.createDir(o, comm, "/etc/salt"); err != nil { 169 return fmt.Errorf("Error creating remote salt configuration directory: %s", err) 170 } 171 src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion")) 172 dst = "/etc/salt/minion" 173 if err = p.moveFile(o, comm, dst, src); err != nil { 174 return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.TempConfigDir, err) 175 } 176 } 177 178 o.Output(fmt.Sprintf("Uploading local state tree: %s", p.LocalStateTree)) 179 src = p.LocalStateTree 180 dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states")) 181 if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil { 182 return fmt.Errorf("Error uploading local state tree to remote: %s", err) 183 } 184 185 // move state tree from temporary directory 186 src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states")) 187 dst = p.RemoteStateTree 188 if err = p.removeDir(o, comm, dst); err != nil { 189 return fmt.Errorf("Unable to clear salt tree: %s", err) 190 } 191 if err = p.moveFile(o, comm, dst, src); err != nil { 192 return fmt.Errorf("Unable to move %s/states to %s: %s", p.TempConfigDir, dst, err) 193 } 194 195 if p.LocalPillarRoots != "" { 196 o.Output(fmt.Sprintf("Uploading local pillar roots: %s", p.LocalPillarRoots)) 197 src = p.LocalPillarRoots 198 dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar")) 199 if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil { 200 return fmt.Errorf("Error uploading local pillar roots to remote: %s", err) 201 } 202 203 // move pillar root from temporary directory 204 src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar")) 205 dst = p.RemotePillarRoots 206 207 if err = p.removeDir(o, comm, dst); err != nil { 208 return fmt.Errorf("Unable to clear pillar root: %s", err) 209 } 210 if err = p.moveFile(o, comm, dst, src); err != nil { 211 return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.TempConfigDir, dst, err) 212 } 213 } 214 215 o.Output(fmt.Sprintf("Running: salt-call --local %s", p.CmdArgs)) 216 cmd := &remote.Cmd{Command: p.sudo(fmt.Sprintf("salt-call --local %s", p.CmdArgs))} 217 if err = comm.Start(cmd); err != nil || cmd.ExitStatus != 0 { 218 if err == nil { 219 err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) 220 } 221 222 return fmt.Errorf("Error executing salt-call: %s", err) 223 } 224 225 return nil 226 } 227 228 // Prepends sudo to supplied command if config says to 229 func (p *provisioner) sudo(cmd string) string { 230 if p.DisableSudo { 231 return cmd 232 } 233 234 return "sudo " + cmd 235 } 236 237 func validateDirConfig(path string, name string, required bool) error { 238 if required == true && path == "" { 239 return fmt.Errorf("%s cannot be empty", name) 240 } else if required == false && path == "" { 241 return nil 242 } 243 info, err := os.Stat(path) 244 if err != nil { 245 return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err) 246 } else if !info.IsDir() { 247 return fmt.Errorf("%s: path '%s' must point to a directory", name, path) 248 } 249 return nil 250 } 251 252 func validateFileConfig(path string, name string, required bool) error { 253 if required == true && path == "" { 254 return fmt.Errorf("%s cannot be empty", name) 255 } else if required == false && path == "" { 256 return nil 257 } 258 info, err := os.Stat(path) 259 if err != nil { 260 return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err) 261 } else if info.IsDir() { 262 return fmt.Errorf("%s: path '%s' must point to a file", name, path) 263 } 264 return nil 265 } 266 267 func (p *provisioner) uploadFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error { 268 f, err := os.Open(src) 269 if err != nil { 270 return fmt.Errorf("Error opening: %s", err) 271 } 272 defer f.Close() 273 274 if err = comm.Upload(dst, f); err != nil { 275 return fmt.Errorf("Error uploading %s: %s", src, err) 276 } 277 return nil 278 } 279 280 func (p *provisioner) moveFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error { 281 o.Output(fmt.Sprintf("Moving %s to %s", src, dst)) 282 cmd := &remote.Cmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)} 283 if err := comm.Start(cmd); err != nil || cmd.ExitStatus != 0 { 284 if err == nil { 285 err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) 286 } 287 288 return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err) 289 } 290 return nil 291 } 292 293 func (p *provisioner) createDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error { 294 o.Output(fmt.Sprintf("Creating directory: %s", dir)) 295 cmd := &remote.Cmd{ 296 Command: fmt.Sprintf("mkdir -p '%s'", dir), 297 } 298 if err := comm.Start(cmd); err != nil { 299 return err 300 } 301 if cmd.ExitStatus != 0 { 302 return fmt.Errorf("Non-zero exit status.") 303 } 304 return nil 305 } 306 307 func (p *provisioner) removeDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error { 308 o.Output(fmt.Sprintf("Removing directory: %s", dir)) 309 cmd := &remote.Cmd{ 310 Command: fmt.Sprintf("rm -rf '%s'", dir), 311 } 312 if err := comm.Start(cmd); err != nil { 313 return err 314 } 315 if cmd.ExitStatus != 0 { 316 return fmt.Errorf("Non-zero exit status.") 317 } 318 return nil 319 } 320 321 func (p *provisioner) uploadDir(o terraform.UIOutput, comm communicator.Communicator, dst, src string, ignore []string) error { 322 if err := p.createDir(o, comm, dst); err != nil { 323 return err 324 } 325 326 // Make sure there is a trailing "/" so that the directory isn't 327 // created on the other side. 328 if src[len(src)-1] != '/' { 329 src = src + "/" 330 } 331 return comm.UploadDir(dst, src) 332 } 333 334 // Validate checks if the required arguments are configured 335 func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) { 336 // require a salt state tree 337 localStateTreeTmp, ok := c.Get("local_state_tree") 338 var localStateTree string 339 if !ok { 340 es = append(es, 341 errors.New("Required local_state_tree is not set")) 342 } else { 343 localStateTree = localStateTreeTmp.(string) 344 } 345 err := validateDirConfig(localStateTree, "local_state_tree", true) 346 if err != nil { 347 es = append(es, err) 348 } 349 350 var localPillarRoots string 351 localPillarRootsTmp, ok := c.Get("local_pillar_roots") 352 if !ok { 353 localPillarRoots = "" 354 } else { 355 localPillarRoots = localPillarRootsTmp.(string) 356 } 357 358 err = validateDirConfig(localPillarRoots, "local_pillar_roots", false) 359 if err != nil { 360 es = append(es, err) 361 } 362 363 var minionConfig string 364 minionConfigTmp, ok := c.Get("minion_config_file") 365 if !ok { 366 minionConfig = "" 367 } else { 368 minionConfig = minionConfigTmp.(string) 369 } 370 err = validateFileConfig(minionConfig, "minion_config_file", false) 371 if err != nil { 372 es = append(es, err) 373 } 374 375 var remoteStateTree string 376 remoteStateTreeTmp, ok := c.Get("remote_state_tree") 377 if !ok { 378 remoteStateTree = "" 379 } else { 380 remoteStateTree = remoteStateTreeTmp.(string) 381 } 382 383 var remotePillarRoots string 384 remotePillarRootsTmp, ok := c.Get("remote_pillar_roots") 385 if !ok { 386 remotePillarRoots = "" 387 } else { 388 remotePillarRoots = remotePillarRootsTmp.(string) 389 } 390 391 if minionConfig != "" && (remoteStateTree != "" || remotePillarRoots != "") { 392 es = append(es, 393 errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config_file is not used")) 394 } 395 396 if len(es) > 0 { 397 return ws, es 398 } 399 400 return ws, es 401 } 402 403 func decodeConfig(d *schema.ResourceData) (*provisioner, error) { 404 p := &provisioner{ 405 LocalStateTree: d.Get("local_state_tree").(string), 406 LogLevel: d.Get("log_level").(string), 407 SaltCallArgs: d.Get("salt_call_args").(string), 408 CmdArgs: d.Get("cmd_args").(string), 409 MinionConfig: d.Get("minion_config_file").(string), 410 CustomState: d.Get("custom_state").(string), 411 DisableSudo: d.Get("disable_sudo").(bool), 412 BootstrapArgs: d.Get("bootstrap_args").(string), 413 NoExitOnFailure: d.Get("no_exit_on_failure").(bool), 414 SkipBootstrap: d.Get("skip_bootstrap").(bool), 415 TempConfigDir: d.Get("temp_config_dir").(string), 416 RemotePillarRoots: d.Get("remote_pillar_roots").(string), 417 RemoteStateTree: d.Get("remote_state_tree").(string), 418 LocalPillarRoots: d.Get("local_pillar_roots").(string), 419 } 420 421 // build the command line args to pass onto salt 422 var cmdArgs bytes.Buffer 423 424 if p.CustomState == "" { 425 cmdArgs.WriteString(" state.highstate") 426 } else { 427 cmdArgs.WriteString(" state.sls ") 428 cmdArgs.WriteString(p.CustomState) 429 } 430 431 if p.MinionConfig == "" { 432 // pass --file-root and --pillar-root if no minion_config_file is supplied 433 if p.RemoteStateTree != "" { 434 cmdArgs.WriteString(" --file-root=") 435 cmdArgs.WriteString(p.RemoteStateTree) 436 } else { 437 cmdArgs.WriteString(" --file-root=") 438 cmdArgs.WriteString(DefaultStateTreeDir) 439 } 440 if p.RemotePillarRoots != "" { 441 cmdArgs.WriteString(" --pillar-root=") 442 cmdArgs.WriteString(p.RemotePillarRoots) 443 } else { 444 cmdArgs.WriteString(" --pillar-root=") 445 cmdArgs.WriteString(DefaultPillarRootDir) 446 } 447 } 448 449 if !p.NoExitOnFailure { 450 cmdArgs.WriteString(" --retcode-passthrough") 451 } 452 453 if p.LogLevel == "" { 454 cmdArgs.WriteString(" -l info") 455 } else { 456 cmdArgs.WriteString(" -l ") 457 cmdArgs.WriteString(p.LogLevel) 458 } 459 460 if p.SaltCallArgs != "" { 461 cmdArgs.WriteString(" ") 462 cmdArgs.WriteString(p.SaltCallArgs) 463 } 464 465 p.CmdArgs = cmdArgs.String() 466 467 return p, nil 468 }