github.com/jerryclinesmith/packer@v0.3.7/builder/digitalocean/builder.go (about)

     1  // The digitalocean package contains a packer.Builder implementation
     2  // that builds DigitalOcean images (snapshots).
     3  
     4  package digitalocean
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"github.com/mitchellh/multistep"
    10  	"github.com/mitchellh/packer/common"
    11  	"github.com/mitchellh/packer/packer"
    12  	"log"
    13  	"os"
    14  	"time"
    15  )
    16  
    17  // The unique id for the builder
    18  const BuilderId = "pearkes.digitalocean"
    19  
    20  // Configuration tells the builder the credentials
    21  // to use while communicating with DO and describes the image
    22  // you are creating
    23  type config struct {
    24  	common.PackerConfig `mapstructure:",squash"`
    25  
    26  	ClientID string `mapstructure:"client_id"`
    27  	APIKey   string `mapstructure:"api_key"`
    28  	RegionID uint   `mapstructure:"region_id"`
    29  	SizeID   uint   `mapstructure:"size_id"`
    30  	ImageID  uint   `mapstructure:"image_id"`
    31  
    32  	SnapshotName string `mapstructure:"snapshot_name"`
    33  	SSHUsername  string `mapstructure:"ssh_username"`
    34  	SSHPort      uint   `mapstructure:"ssh_port"`
    35  
    36  	RawSSHTimeout   string `mapstructure:"ssh_timeout"`
    37  	RawStateTimeout string `mapstructure:"state_timeout"`
    38  
    39  	// These are unexported since they're set by other fields
    40  	// being set.
    41  	sshTimeout   time.Duration
    42  	stateTimeout time.Duration
    43  
    44  	tpl *packer.ConfigTemplate
    45  }
    46  
    47  type Builder struct {
    48  	config config
    49  	runner multistep.Runner
    50  }
    51  
    52  func (b *Builder) Prepare(raws ...interface{}) error {
    53  	md, err := common.DecodeConfig(&b.config, raws...)
    54  	if err != nil {
    55  		return err
    56  	}
    57  
    58  	b.config.tpl, err = packer.NewConfigTemplate()
    59  	if err != nil {
    60  		return err
    61  	}
    62  	b.config.tpl.UserVars = b.config.PackerUserVars
    63  
    64  	// Accumulate any errors
    65  	errs := common.CheckUnusedConfig(md)
    66  
    67  	// Optional configuration with defaults
    68  	if b.config.APIKey == "" {
    69  		// Default to environment variable for api_key, if it exists
    70  		b.config.APIKey = os.Getenv("DIGITALOCEAN_API_KEY")
    71  	}
    72  
    73  	if b.config.ClientID == "" {
    74  		// Default to environment variable for client_id, if it exists
    75  		b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID")
    76  	}
    77  
    78  	if b.config.RegionID == 0 {
    79  		// Default to Region "New York"
    80  		b.config.RegionID = 1
    81  	}
    82  
    83  	if b.config.SizeID == 0 {
    84  		// Default to 512mb, the smallest droplet size
    85  		b.config.SizeID = 66
    86  	}
    87  
    88  	if b.config.ImageID == 0 {
    89  		// Default to base image "Ubuntu 12.04 x64 Server (id: 284203)"
    90  		b.config.ImageID = 284203
    91  	}
    92  
    93  	if b.config.SnapshotName == "" {
    94  		// Default to packer-{{ unix timestamp (utc) }}
    95  		b.config.SnapshotName = "packer-{{timestamp}}"
    96  	}
    97  
    98  	if b.config.SSHUsername == "" {
    99  		// Default to "root". You can override this if your
   100  		// SourceImage has a different user account then the DO default
   101  		b.config.SSHUsername = "root"
   102  	}
   103  
   104  	if b.config.SSHPort == 0 {
   105  		// Default to port 22 per DO default
   106  		b.config.SSHPort = 22
   107  	}
   108  
   109  	if b.config.RawSSHTimeout == "" {
   110  		// Default to 1 minute timeouts
   111  		b.config.RawSSHTimeout = "1m"
   112  	}
   113  
   114  	if b.config.RawStateTimeout == "" {
   115  		// Default to 6 minute timeouts waiting for
   116  		// desired state. i.e waiting for droplet to become active
   117  		b.config.RawStateTimeout = "6m"
   118  	}
   119  
   120  	templates := map[string]*string{
   121  		"client_id":     &b.config.ClientID,
   122  		"api_key":       &b.config.APIKey,
   123  		"snapshot_name": &b.config.SnapshotName,
   124  		"ssh_username":  &b.config.SSHUsername,
   125  		"ssh_timeout":   &b.config.RawSSHTimeout,
   126  		"state_timeout": &b.config.RawStateTimeout,
   127  	}
   128  
   129  	for n, ptr := range templates {
   130  		var err error
   131  		*ptr, err = b.config.tpl.Process(*ptr, nil)
   132  		if err != nil {
   133  			errs = packer.MultiErrorAppend(
   134  				errs, fmt.Errorf("Error processing %s: %s", n, err))
   135  		}
   136  	}
   137  
   138  	// Required configurations that will display errors if not set
   139  	if b.config.ClientID == "" {
   140  		errs = packer.MultiErrorAppend(
   141  			errs, errors.New("a client_id must be specified"))
   142  	}
   143  
   144  	if b.config.APIKey == "" {
   145  		errs = packer.MultiErrorAppend(
   146  			errs, errors.New("an api_key must be specified"))
   147  	}
   148  
   149  	sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
   150  	if err != nil {
   151  		errs = packer.MultiErrorAppend(
   152  			errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
   153  	}
   154  	b.config.sshTimeout = sshTimeout
   155  
   156  	stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
   157  	if err != nil {
   158  		errs = packer.MultiErrorAppend(
   159  			errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
   160  	}
   161  	b.config.stateTimeout = stateTimeout
   162  
   163  	if errs != nil && len(errs.Errors) > 0 {
   164  		return errs
   165  	}
   166  
   167  	log.Printf("Config: %+v", b.config)
   168  	return nil
   169  }
   170  
   171  func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
   172  	// Initialize the DO API client
   173  	client := DigitalOceanClient{}.New(b.config.ClientID, b.config.APIKey)
   174  
   175  	// Set up the state
   176  	state := new(multistep.BasicStateBag)
   177  	state.Put("config", b.config)
   178  	state.Put("client", client)
   179  	state.Put("hook", hook)
   180  	state.Put("ui", ui)
   181  
   182  	// Build the steps
   183  	steps := []multistep.Step{
   184  		new(stepCreateSSHKey),
   185  		new(stepCreateDroplet),
   186  		new(stepDropletInfo),
   187  		&common.StepConnectSSH{
   188  			SSHAddress:     sshAddress,
   189  			SSHConfig:      sshConfig,
   190  			SSHWaitTimeout: 5 * time.Minute,
   191  		},
   192  		new(common.StepProvision),
   193  		new(stepShutdown),
   194  		new(stepPowerOff),
   195  		new(stepSnapshot),
   196  	}
   197  
   198  	// Run the steps
   199  	if b.config.PackerDebug {
   200  		b.runner = &multistep.DebugRunner{
   201  			Steps:   steps,
   202  			PauseFn: common.MultistepDebugFn(ui),
   203  		}
   204  	} else {
   205  		b.runner = &multistep.BasicRunner{Steps: steps}
   206  	}
   207  
   208  	b.runner.Run(state)
   209  
   210  	// If there was an error, return that
   211  	if rawErr, ok := state.GetOk("error"); ok {
   212  		return nil, rawErr.(error)
   213  	}
   214  
   215  	if _, ok := state.GetOk("snapshot_name"); !ok {
   216  		log.Println("Failed to find snapshot_name in state. Bug?")
   217  		return nil, nil
   218  	}
   219  
   220  	artifact := &Artifact{
   221  		snapshotName: state.Get("snapshot_name").(string),
   222  		snapshotId:   state.Get("snapshot_image_id").(uint),
   223  		client:       client,
   224  	}
   225  
   226  	return artifact, nil
   227  }
   228  
   229  func (b *Builder) Cancel() {
   230  	if b.runner != nil {
   231  		log.Println("Cancelling the step runner...")
   232  		b.runner.Cancel()
   233  	}
   234  }