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 }