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