github.com/askholme/packer@v0.7.2-0.20140924152349-70d9566a6852/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  	"log"
    10  	"os"
    11  	"time"
    12  
    13  	"github.com/mitchellh/multistep"
    14  	"github.com/mitchellh/packer/common"
    15  	"github.com/mitchellh/packer/common/uuid"
    16  	"github.com/mitchellh/packer/packer"
    17  )
    18  
    19  // see https://api.digitalocean.com/images/?client_id=[client_id]&api_key=[api_key]
    20  // name="Ubuntu 12.04.4 x64", id=3101045,
    21  const DefaultImage = "ubuntu-12-04-x64"
    22  
    23  // see https://api.digitalocean.com/regions/?client_id=[client_id]&api_key=[api_key]
    24  // name="New York", id=1
    25  const DefaultRegion = "nyc1"
    26  
    27  // see https://api.digitalocean.com/sizes/?client_id=[client_id]&api_key=[api_key]
    28  // name="512MB", id=66 (the smallest droplet size)
    29  const DefaultSize = "512mb"
    30  
    31  // The unique id for the builder
    32  const BuilderId = "pearkes.digitalocean"
    33  
    34  // Configuration tells the builder the credentials
    35  // to use while communicating with DO and describes the image
    36  // you are creating
    37  type config struct {
    38  	common.PackerConfig `mapstructure:",squash"`
    39  
    40  	ClientID string `mapstructure:"client_id"`
    41  	APIKey   string `mapstructure:"api_key"`
    42  	APIURL   string `mapstructure:"api_url"`
    43  	RegionID uint   `mapstructure:"region_id"`
    44  	SizeID   uint   `mapstructure:"size_id"`
    45  	ImageID  uint   `mapstructure:"image_id"`
    46  
    47  	Region string `mapstructure:"region"`
    48  	Size   string `mapstructure:"size"`
    49  	Image  string `mapstructure:"image"`
    50  
    51  	PrivateNetworking bool   `mapstructure:"private_networking"`
    52  	SnapshotName      string `mapstructure:"snapshot_name"`
    53  	DropletName       string `mapstructure:"droplet_name"`
    54  	SSHUsername       string `mapstructure:"ssh_username"`
    55  	SSHPort           uint   `mapstructure:"ssh_port"`
    56  
    57  	RawSSHTimeout   string `mapstructure:"ssh_timeout"`
    58  	RawStateTimeout string `mapstructure:"state_timeout"`
    59  
    60  	// These are unexported since they're set by other fields
    61  	// being set.
    62  	sshTimeout   time.Duration
    63  	stateTimeout time.Duration
    64  
    65  	tpl *packer.ConfigTemplate
    66  }
    67  
    68  type Builder struct {
    69  	config config
    70  	runner multistep.Runner
    71  }
    72  
    73  func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
    74  	md, err := common.DecodeConfig(&b.config, raws...)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	b.config.tpl, err = packer.NewConfigTemplate()
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	b.config.tpl.UserVars = b.config.PackerUserVars
    84  
    85  	// Accumulate any errors
    86  	errs := common.CheckUnusedConfig(md)
    87  
    88  	// Optional configuration with defaults
    89  	if b.config.APIKey == "" {
    90  		// Default to environment variable for api_key, if it exists
    91  		b.config.APIKey = os.Getenv("DIGITALOCEAN_API_KEY")
    92  	}
    93  
    94  	if b.config.ClientID == "" {
    95  		// Default to environment variable for client_id, if it exists
    96  		b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID")
    97  	}
    98  
    99  	if b.config.APIURL == "" {
   100  		// Default to environment variable for api_url, if it exists
   101  		b.config.APIURL = os.Getenv("DIGITALOCEAN_API_URL")
   102  	}
   103  
   104  	if b.config.Region == "" {
   105  		if b.config.RegionID != 0 {
   106  			b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
   107  		} else {
   108  			b.config.Region = DefaultRegion
   109  		}
   110  	}
   111  
   112  	if b.config.Size == "" {
   113  		if b.config.SizeID != 0 {
   114  			b.config.Size = fmt.Sprintf("%v", b.config.SizeID)
   115  		} else {
   116  			b.config.Size = DefaultSize
   117  		}
   118  	}
   119  
   120  	if b.config.Image == "" {
   121  		if b.config.ImageID != 0 {
   122  			b.config.Image = fmt.Sprintf("%v", b.config.ImageID)
   123  		} else {
   124  			b.config.Image = DefaultImage
   125  		}
   126  	}
   127  
   128  	if b.config.SnapshotName == "" {
   129  		// Default to packer-{{ unix timestamp (utc) }}
   130  		b.config.SnapshotName = "packer-{{timestamp}}"
   131  	}
   132  
   133  	if b.config.DropletName == "" {
   134  		// Default to packer-[time-ordered-uuid]
   135  		b.config.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
   136  	}
   137  
   138  	if b.config.SSHUsername == "" {
   139  		// Default to "root". You can override this if your
   140  		// SourceImage has a different user account then the DO default
   141  		b.config.SSHUsername = "root"
   142  	}
   143  
   144  	if b.config.SSHPort == 0 {
   145  		// Default to port 22 per DO default
   146  		b.config.SSHPort = 22
   147  	}
   148  
   149  	if b.config.RawSSHTimeout == "" {
   150  		// Default to 1 minute timeouts
   151  		b.config.RawSSHTimeout = "1m"
   152  	}
   153  
   154  	if b.config.RawStateTimeout == "" {
   155  		// Default to 6 minute timeouts waiting for
   156  		// desired state. i.e waiting for droplet to become active
   157  		b.config.RawStateTimeout = "6m"
   158  	}
   159  
   160  	templates := map[string]*string{
   161  		"region":        &b.config.Region,
   162  		"size":          &b.config.Size,
   163  		"image":         &b.config.Image,
   164  		"client_id":     &b.config.ClientID,
   165  		"api_key":       &b.config.APIKey,
   166  		"api_url":       &b.config.APIURL,
   167  		"snapshot_name": &b.config.SnapshotName,
   168  		"droplet_name":  &b.config.DropletName,
   169  		"ssh_username":  &b.config.SSHUsername,
   170  		"ssh_timeout":   &b.config.RawSSHTimeout,
   171  		"state_timeout": &b.config.RawStateTimeout,
   172  	}
   173  
   174  	for n, ptr := range templates {
   175  		var err error
   176  		*ptr, err = b.config.tpl.Process(*ptr, nil)
   177  		if err != nil {
   178  			errs = packer.MultiErrorAppend(
   179  				errs, fmt.Errorf("Error processing %s: %s", n, err))
   180  		}
   181  	}
   182  
   183  	// Required configurations that will display errors if not set
   184  	if b.config.ClientID == "" {
   185  		errs = packer.MultiErrorAppend(
   186  			errs, errors.New("a client_id must be specified"))
   187  	}
   188  
   189  	if b.config.APIURL == "" {
   190  		b.config.APIURL = "https://api.digitalocean.com"
   191  	}
   192  
   193  	if b.config.APIKey == "" {
   194  		errs = packer.MultiErrorAppend(
   195  			errs, errors.New("an api_key must be specified"))
   196  	}
   197  
   198  	sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
   199  	if err != nil {
   200  		errs = packer.MultiErrorAppend(
   201  			errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
   202  	}
   203  	b.config.sshTimeout = sshTimeout
   204  
   205  	stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
   206  	if err != nil {
   207  		errs = packer.MultiErrorAppend(
   208  			errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
   209  	}
   210  	b.config.stateTimeout = stateTimeout
   211  
   212  	if errs != nil && len(errs.Errors) > 0 {
   213  		return nil, errs
   214  	}
   215  
   216  	common.ScrubConfig(b.config, b.config.ClientID, b.config.APIKey)
   217  	return nil, nil
   218  }
   219  
   220  func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
   221  	// Initialize the DO API client
   222  	client := DigitalOceanClient{}.New(b.config.ClientID, b.config.APIKey, b.config.APIURL)
   223  
   224  	// Set up the state
   225  	state := new(multistep.BasicStateBag)
   226  	state.Put("config", b.config)
   227  	state.Put("client", client)
   228  	state.Put("hook", hook)
   229  	state.Put("ui", ui)
   230  
   231  	// Build the steps
   232  	steps := []multistep.Step{
   233  		new(stepCreateSSHKey),
   234  		new(stepCreateDroplet),
   235  		new(stepDropletInfo),
   236  		&common.StepConnectSSH{
   237  			SSHAddress:     sshAddress,
   238  			SSHConfig:      sshConfig,
   239  			SSHWaitTimeout: 5 * time.Minute,
   240  		},
   241  		new(common.StepProvision),
   242  		new(stepShutdown),
   243  		new(stepPowerOff),
   244  		new(stepSnapshot),
   245  	}
   246  
   247  	// Run the steps
   248  	if b.config.PackerDebug {
   249  		b.runner = &multistep.DebugRunner{
   250  			Steps:   steps,
   251  			PauseFn: common.MultistepDebugFn(ui),
   252  		}
   253  	} else {
   254  		b.runner = &multistep.BasicRunner{Steps: steps}
   255  	}
   256  
   257  	b.runner.Run(state)
   258  
   259  	// If there was an error, return that
   260  	if rawErr, ok := state.GetOk("error"); ok {
   261  		return nil, rawErr.(error)
   262  	}
   263  
   264  	if _, ok := state.GetOk("snapshot_name"); !ok {
   265  		log.Println("Failed to find snapshot_name in state. Bug?")
   266  		return nil, nil
   267  	}
   268  
   269  	sregion := state.Get("region")
   270  
   271  	var region string
   272  
   273  	if sregion != nil {
   274  		region = sregion.(string)
   275  	} else {
   276  		region = fmt.Sprintf("%v", state.Get("region_id").(uint))
   277  	}
   278  
   279  	found_region, err := client.Region(region)
   280  
   281  	if err != nil {
   282  		return nil, err
   283  	}
   284  
   285  	artifact := &Artifact{
   286  		snapshotName: state.Get("snapshot_name").(string),
   287  		snapshotId:   state.Get("snapshot_image_id").(uint),
   288  		regionName:   found_region.Name,
   289  		client:       client,
   290  	}
   291  
   292  	return artifact, nil
   293  }
   294  
   295  func (b *Builder) Cancel() {
   296  	if b.runner != nil {
   297  		log.Println("Cancelling the step runner...")
   298  		b.runner.Cancel()
   299  	}
   300  }