Add Linode Images builder

Packer Builder for [Linode Images](https://www.linode.com/docs/platform/disk-images/linode-images/)

Adds the following builder:

  * `linode`

Based on https://github.com/linode/packer-builder-linode (MPL/2)
(formerly maintained by @dradtke).  Includes website docs and tests.

Relates to #174, #3131
This commit is contained in:
Marques Johansson 2019-04-15 17:22:25 -04:00
parent 45af9f0cbc
commit 99987c2d56
19 changed files with 1114 additions and 0 deletions

View File

@ -0,0 +1,32 @@
package linode
import (
"context"
"fmt"
"log"
"github.com/linode/linodego"
)
type Artifact struct {
ImageID string
ImageLabel string
Driver *linodego.Client
}
func (a Artifact) BuilderId() string { return BuilderID }
func (a Artifact) Files() []string { return nil }
func (a Artifact) Id() string { return a.ImageID }
func (a Artifact) String() string {
return fmt.Sprintf("Linode image: %s (%s)", a.ImageLabel, a.ImageID)
}
func (a Artifact) State(name string) interface{} { return nil }
func (a Artifact) Destroy() error {
log.Printf("Destroying image: %s (%s)", a.ImageID, a.ImageLabel)
err := a.Driver.DeleteImage(context.TODO(), a.ImageID)
return err
}

View File

@ -0,0 +1,33 @@
package linode
import (
"testing"
"github.com/hashicorp/packer/packer"
)
func TestArtifact_Impl(t *testing.T) {
var raw interface{}
raw = &Artifact{}
if _, ok := raw.(packer.Artifact); !ok {
t.Fatalf("Artifact should be artifact")
}
}
func TestArtifactId(t *testing.T) {
a := &Artifact{"private/42", "packer-foobar", nil}
expected := "private/42"
if a.Id() != expected {
t.Fatalf("artifact ID should match: %v", expected)
}
}
func TestArtifactString(t *testing.T) {
a := &Artifact{"private/42", "packer-foobar", nil}
expected := "Linode image: packer-foobar (private/42)"
if a.String() != expected {
t.Fatalf("artifact string should match: %v", expected)
}
}

98
builder/linode/builder.go Normal file
View File

@ -0,0 +1,98 @@
// The linode package contains a packer.Builder implementation
// that builds Linode images.
package linode
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/packer/common"
"github.com/linode/linodego"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// The unique ID for this builder.
const BuilderID = "packer.linode"
// Builder represents a Packer Builder.
type Builder struct {
config *Config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
c, warnings, errs := NewConfig(raws...)
if errs != nil {
return warnings, errs
}
b.config = c
return nil, nil
}
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (ret packer.Artifact, err error) {
ui.Say("Running builder ...")
client := newLinodeClient(b.config.PersonalAccessToken)
if err != nil {
ui.Error(err.Error())
return nil, err
}
state := new(multistep.BasicStateBag)
state.Put("config", b.config)
state.Put("hook", hook)
state.Put("ui", ui)
steps := []multistep.Step{
&StepCreateSSHKey{
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("linode_%s.pem", b.config.PackerBuildName),
},
&stepCreateLinode{client},
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost,
SSHConfig: b.config.Comm.SSHConfigFunc(),
},
&common.StepProvision{},
&common.StepCleanupTempKeys{
Comm: &b.config.Comm,
},
&stepShutdownLinode{client},
&stepCreateImage{client},
}
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(ctx, state)
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
// If we were interrupted or cancelled, then just exit.
if _, ok := state.GetOk(multistep.StateCancelled); ok {
return nil, errors.New("Build was cancelled.")
}
if _, ok := state.GetOk(multistep.StateHalted); ok {
return nil, errors.New("Build was halted.")
}
if _, ok := state.GetOk("image"); !ok {
return nil, errors.New("Cannot find image in state.")
}
image := state.Get("image").(*linodego.Image)
artifact := Artifact{
ImageLabel: image.Label,
ImageID: image.ID,
Driver: &client,
}
return artifact, nil
}

View File

@ -0,0 +1,34 @@
package linode
import (
"os"
"testing"
builderT "github.com/hashicorp/packer/helper/builder/testing"
)
func TestBuilderAcc_basic(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccBasic,
})
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("LINODE_TOKEN"); v == "" {
t.Fatal("LINODE_TOKEN must be set for acceptance tests")
}
}
const testBuilderAccBasic = `
{
"builders": [{
"type": "test",
"region": "us-east",
"instance_type": "g6-nanode-1",
"image": "linode/alpine3.9",
"ssh_username": "root"
}]
}
`

View File

@ -0,0 +1,287 @@
package linode
import (
"strconv"
"testing"
"github.com/hashicorp/packer/packer"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"linode_token": "bar",
"region": "us-east",
"instance_type": "g6-nanode-1",
"ssh_username": "root",
"image": "linode/alpine3.9",
}
}
func TestBuilder_ImplementsBuilder(t *testing.T) {
var raw interface{}
raw = &Builder{}
if _, ok := raw.(packer.Builder); !ok {
t.Fatalf("Builder should be a builder")
}
}
func TestBuilder_Prepare_BadType(t *testing.T) {
b := &Builder{}
c := map[string]interface{}{
"linode_token": []string{},
}
warnings, err := b.Prepare(c)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatalf("prepare should fail")
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_Region(t *testing.T) {
var b Builder
config := testConfig()
// Test default
delete(config, "region")
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatalf("should error")
}
expected := "us-east"
// Test set
config["region"] = expected
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Region != expected {
t.Errorf("found %s, expected %s", b.config.Region, expected)
}
}
func TestBuilderPrepare_Size(t *testing.T) {
var b Builder
config := testConfig()
// Test default
delete(config, "instance_type")
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatalf("should error")
}
expected := "g6-nanode-1"
// Test set
config["instance_type"] = expected
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.InstanceType != expected {
t.Errorf("found %s, expected %s", b.config.InstanceType, expected)
}
}
func TestBuilderPrepare_Image(t *testing.T) {
var b Builder
config := testConfig()
// Test default
delete(config, "image")
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should error")
}
expected := "linode/alpine3.9"
// Test set
config["image"] = expected
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Image != expected {
t.Errorf("found %s, expected %s", b.config.Image, expected)
}
}
func TestBuilderPrepare_StateTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test set
config["state_timeout"] = "5m"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["state_timeout"] = "tubes"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_ImageLabel(t *testing.T) {
var b Builder
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ImageLabel == "" {
t.Errorf("invalid: %s", b.config.ImageLabel)
}
// Test set
config["image_label"] = "foobarbaz"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test set with template
config["image_label"] = "{{timestamp}}"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
_, err = strconv.ParseInt(b.config.ImageLabel, 0, 0)
if err != nil {
t.Fatalf("failed to parse int in template: %s", err)
}
}
func TestBuilderPrepare_Label(t *testing.T) {
var b Builder
config := testConfig()
// Test default
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Label == "" {
t.Errorf("invalid: %s", b.config.Label)
}
// Test normal set
config["instance_label"] = "foobar"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test with template
config["instance_label"] = "foobar-{{timestamp}}"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test with bad template
config["instance_label"] = "foobar-{{"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}

156
builder/linode/config.go Normal file
View File

@ -0,0 +1,156 @@
package linode
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"os"
"regexp"
"time"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
ctx interpolate.Context
Comm communicator.Config `mapstructure:",squash"`
PersonalAccessToken string `mapstructure:"linode_token"`
Region string `mapstructure:"region"`
InstanceType string `mapstructure:"instance_type"`
Label string `mapstructure:"instance_label"`
Tags []string `mapstructure:"instance_tags"`
Image string `mapstructure:"image"`
SwapSize int `mapstructure:"swap_size"`
RootPass string `mapstructure:"root_pass"`
RootSSHKey string `mapstructure:"root_ssh_key"`
ImageLabel string `mapstructure:"image_label"`
Description string `mapstructure:"image_description"`
RawStateTimeout string `mapstructure:"state_timeout"`
stateTimeout time.Duration
interCtx interpolate.Context
}
func createRandomRootPassword() (string, error) {
rawRootPass := make([]byte, 50)
_, err := rand.Read(rawRootPass)
if err != nil {
return "", fmt.Errorf("Failed to generate random password")
}
rootPass := base64.StdEncoding.EncodeToString(rawRootPass)
return rootPass, nil
}
func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config)
if err := config.Decode(c, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"run_command",
},
},
}, raws...); err != nil {
return nil, nil, err
}
var errs *packer.MultiError
// Defaults
if c.PersonalAccessToken == "" {
// Default to environment variable for linode_token, if it exists
c.PersonalAccessToken = os.Getenv("LINODE_TOKEN")
}
if c.ImageLabel == "" {
if def, err := interpolate.Render("packer-{{timestamp}}", nil); err == nil {
c.ImageLabel = def
} else {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Unable to render image name: %s", err))
}
}
if c.Label == "" {
// Default to packer-[time-ordered-uuid]
if def, err := interpolate.Render("packer-{{timestamp}}", nil); err == nil {
c.Label = def
} else {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Unable to render Linode label: %s", err))
}
}
if c.RootPass == "" {
var err error
c.RootPass, err = createRandomRootPassword()
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Unable to generate root_pass: %s", err))
}
}
if c.RawStateTimeout == "" {
c.stateTimeout = 5 * time.Minute
} else {
if stateTimeout, err := time.ParseDuration(c.RawStateTimeout); err == nil {
c.stateTimeout = stateTimeout
} else {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Unable to parse state timeout: %s", err))
}
}
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
c.Comm.SSHPassword = c.RootPass
if c.PersonalAccessToken == "" {
// Required configurations that will display errors if not set
errs = packer.MultiErrorAppend(
errs, errors.New("linode_token is required"))
}
if c.Region == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("region is required"))
}
if c.InstanceType == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("instance_type is required"))
}
if c.Image == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("image is required"))
}
if c.Tags == nil {
c.Tags = make([]string, 0)
}
tagRe := regexp.MustCompile("^[[:alnum:]:_-]{1,255}$")
for _, t := range c.Tags {
if !tagRe.MatchString(t) {
errs = packer.MultiErrorAppend(errs, errors.New(fmt.Sprintf("invalid tag: %s", t)))
}
}
if errs != nil && len(errs.Errors) > 0 {
return nil, nil, errs
}
packer.LogSecretFilter.Set(c.PersonalAccessToken)
return c, nil, nil
}

30
builder/linode/linode.go Normal file
View File

@ -0,0 +1,30 @@
package linode
import (
"fmt"
"net/http"
"github.com/hashicorp/packer/version"
"github.com/linode/linodego"
"golang.org/x/oauth2"
)
func newLinodeClient(pat string) linodego.Client {
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: pat})
oauthTransport := &oauth2.Transport{
Source: tokenSource,
}
oauth2Client := &http.Client{
Transport: oauthTransport,
}
client := linodego.NewClient(oauth2Client)
projectURL := "https://www.packer.io"
userAgent := fmt.Sprintf("Packer/%s (+%s) linodego/%s",
version.FormattedVersion(), projectURL, linodego.Version)
client.SetUserAgent(userAgent)
return client
}

27
builder/linode/ssh.go Normal file
View File

@ -0,0 +1,27 @@
package linode
import (
"fmt"
"github.com/hashicorp/packer/helper/multistep"
"github.com/linode/linodego"
"golang.org/x/crypto/ssh"
)
func commHost(state multistep.StateBag) (string, error) {
instance := state.Get("instance").(*linodego.Instance)
if len(instance.IPv4) == 0 {
return "", fmt.Errorf("Linode instance %d has no IPv4 addresses!", instance.ID)
}
return instance.IPv4[0].String(), nil
}
func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
return &ssh.ClientConfig{
User: "root",
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Auth: []ssh.AuthMethod{
ssh.Password(state.Get("root_pass").(string)),
},
}, nil
}

View File

@ -0,0 +1,48 @@
package linode
import (
"context"
"errors"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/linode/linodego"
)
type stepCreateImage struct {
client linodego.Client
}
func (s *stepCreateImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
c := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
disk := state.Get("disk").(*linodego.InstanceDisk)
instance := state.Get("instance").(*linodego.Instance)
ui.Say("Creating image...")
image, err := s.client.CreateImage(ctx, linodego.ImageCreateOptions{
DiskID: disk.ID,
Label: c.ImageLabel,
Description: c.Description,
})
if err == nil {
_, err = s.client.WaitForInstanceDiskStatus(ctx, instance.ID, disk.ID, linodego.DiskReady, 600)
}
if err == nil {
image, err = s.client.GetImage(ctx, image.ID)
}
if err != nil {
err = errors.New("Error creating image: " + err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("image", image)
return multistep.ActionContinue
}
func (s *stepCreateImage) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,94 @@
package linode
import (
"context"
"errors"
"time"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/linode/linodego"
)
type stepCreateLinode struct {
client linodego.Client
}
func (s *stepCreateLinode) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
c := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
ui.Say("Creating Linode...")
createOpts := linodego.InstanceCreateOptions{
RootPass: c.Comm.Password(),
AuthorizedKeys: []string{string(c.Comm.SSHPublicKey)},
Region: c.Region,
Type: c.InstanceType,
Label: c.Label,
Image: c.Image,
SwapSize: &c.SwapSize,
}
instance, err := s.client.CreateInstance(ctx, createOpts)
if err != nil {
err = errors.New("Error creating Linode: " + err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("instance", instance)
// wait until instance is running
for instance.Status != linodego.InstanceRunning {
time.Sleep(2 * time.Second)
if instance, err = s.client.GetInstance(ctx, instance.ID); err != nil {
err = errors.New("Error creating Linode: " + err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("instance", instance)
}
disk, err := s.findDisk(ctx, instance.ID)
if err != nil {
err = errors.New("Error creating Linode: " + err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
} else if disk == nil {
err := errors.New("Error creating Linode: no suitable disk was found")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("disk", disk)
return multistep.ActionContinue
}
func (s *stepCreateLinode) findDisk(ctx context.Context, instanceID int) (*linodego.InstanceDisk, error) {
disks, err := s.client.ListInstanceDisks(ctx, instanceID, nil)
if err != nil {
return nil, err
}
for _, disk := range disks {
if disk.Filesystem != linodego.FilesystemSwap {
return &disk, nil
}
}
return nil, nil
}
func (s *stepCreateLinode) Cleanup(state multistep.StateBag) {
instance, ok := state.GetOk("instance")
if !ok {
return
}
ui := state.Get("ui").(packer.Ui)
if err := s.client.DeleteInstance(context.Background(), instance.(*linodego.Instance).ID); err != nil {
ui.Error("Error cleaning up Linode: " + err.Error())
}
}

View File

@ -0,0 +1,95 @@
package linode
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"golang.org/x/crypto/ssh"
)
// StepCreateSSHKey represents a Packer build step that generates SSH key pairs.
type StepCreateSSHKey struct {
Debug bool
DebugKeyPath string
}
// Run executes the Packer build step that generates SSH key pairs.
// The key pairs are added to the ssh config
func (s *StepCreateSSHKey) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(*Config)
if config.Comm.SSHPrivateKeyFile != "" {
ui.Say("Using existing SSH private key")
privateKeyBytes, err := ioutil.ReadFile(config.Comm.SSHPrivateKeyFile)
if err != nil {
state.Put("error", fmt.Errorf(
"Error loading configured private key file: %s", err))
return multistep.ActionHalt
}
config.Comm.SSHPrivateKey = privateKeyBytes
config.Comm.SSHPublicKey = nil
return multistep.ActionContinue
}
ui.Say("Creating temporary SSH key for instance...")
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
err := fmt.Errorf("Error creating temporary ssh key: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
priv_blk := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: x509.MarshalPKCS1PrivateKey(priv),
}
pub, err := ssh.NewPublicKey(&priv.PublicKey)
if err != nil {
err := fmt.Errorf("Error creating temporary ssh key: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
config.Comm.SSHPrivateKey = pem.EncodeToMemory(&priv_blk)
config.Comm.SSHPublicKey = ssh.MarshalAuthorizedKey(pub)
// Linode has a serious issue with the newline that the ssh package appends to the end of the key.
if config.Comm.SSHPublicKey[len(config.Comm.SSHPublicKey)-1] == '\n' {
config.Comm.SSHPublicKey = config.Comm.SSHPublicKey[:len(config.Comm.SSHPublicKey)-1]
}
if s.Debug {
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
f, err := os.Create(s.DebugKeyPath)
if err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt
}
// Write out the key
err = pem.Encode(f, &priv_blk)
f.Close()
if err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
// Nothing to clean up. SSH keys are associated with a single Linode instance.
func (s *StepCreateSSHKey) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,39 @@
package linode
import (
"context"
"errors"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/linode/linodego"
)
type stepShutdownLinode struct {
client linodego.Client
}
func (s *stepShutdownLinode) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
instance := state.Get("instance").(*linodego.Instance)
ui.Say("Shutting down Linode...")
if err := s.client.ShutdownInstance(ctx, instance.ID); err != nil {
err = errors.New("Error shutting down Linode: " + err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
_, err := s.client.WaitForInstanceStatus(ctx, instance.ID, linodego.InstanceOffline, 120)
if err != nil {
err = errors.New("Error shutting down Linode: " + err.Error())
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepShutdownLinode) Cleanup(state multistep.StateBag) {}

View File

@ -29,6 +29,7 @@ import (
hyperonebuilder "github.com/hashicorp/packer/builder/hyperone"
hypervisobuilder "github.com/hashicorp/packer/builder/hyperv/iso"
hypervvmcxbuilder "github.com/hashicorp/packer/builder/hyperv/vmcx"
linodebuilder "github.com/hashicorp/packer/builder/linode"
lxcbuilder "github.com/hashicorp/packer/builder/lxc"
lxdbuilder "github.com/hashicorp/packer/builder/lxd"
ncloudbuilder "github.com/hashicorp/packer/builder/ncloud"
@ -109,6 +110,7 @@ var Builders = map[string]packer.Builder{
"hyperone": new(hyperonebuilder.Builder),
"hyperv-iso": new(hypervisobuilder.Builder),
"hyperv-vmcx": new(hypervvmcxbuilder.Builder),
"linode": new(linodebuilder.Builder),
"lxc": new(lxcbuilder.Builder),
"lxd": new(lxdbuilder.Builder),
"ncloud": new(ncloudbuilder.Builder),

2
go.mod
View File

@ -63,6 +63,7 @@ require (
github.com/klauspost/pgzip v0.0.0-20151221113845-47f36e165cec
github.com/kr/fs v0.0.0-20131111012553-2788f0dbd169 // indirect
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 // indirect
github.com/linode/linodego v0.7.1
github.com/marstr/guid v0.0.0-20170427235115-8bdf7d1a087c // indirect
github.com/masterzen/azure-sdk-for-go v0.0.0-20161014135628-ee4f0065d00c // indirect
github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect
@ -118,5 +119,6 @@ require (
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/h2non/gock.v1 v1.0.12 // indirect
gopkg.in/jarcoal/httpmock.v1 v1.0.0-20181117152235-275e9df93516 // indirect
gopkg.in/resty.v1 v1.12.0 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
)

5
go.sum
View File

@ -214,6 +214,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/linode/linodego v0.7.1 h1:4WZmMpSA2NRwlPZcc0+4Gyn7rr99Evk9bnr0B3gXRKE=
github.com/linode/linodego v0.7.1/go.mod h1:ga11n3ivecUrPCHN0rANxKmfWBJVkOXfLMZinAbj2sY=
github.com/marstr/guid v0.0.0-20170427235115-8bdf7d1a087c h1:N7uWGS2fTwH/4BwxbHiJZNAFTSJ5yPU0emHsQWvkxEY=
github.com/marstr/guid v0.0.0-20170427235115-8bdf7d1a087c/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
github.com/masterzen/azure-sdk-for-go v0.0.0-20161014135628-ee4f0065d00c h1:FMUOnVGy8nWk1cvlMCAoftRItQGMxI0vzJ3dQjeZTCE=
@ -386,6 +388,7 @@ golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -450,6 +453,8 @@ gopkg.in/h2non/gock.v1 v1.0.12/go.mod h1:KHI4Z1sxDW6P4N3DfTWSEza07YpkQP7KJBfglRM
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/jarcoal/httpmock.v1 v1.0.0-20181117152235-275e9df93516 h1:H6trpavCIuipdInWrab8l34Mf+GGVfphniHostMdMaQ=
gopkg.in/jarcoal/httpmock.v1 v1.0.0-20181117152235-275e9df93516/go.mod h1:d3R+NllX3X5e0zlG1Rful3uLvsGC/Q3OHut5464DEQw=
gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=

View File

@ -0,0 +1,28 @@
{
"variables": {
"linode_token": "{{env `LINODE_TOKEN`}}"
},
"builders": [
{
"type": "linode",
"linode_token": "{{user `linode_token`}}",
"region": "us-central",
"swap_size": 256,
"image": "linode/debian9",
"instance_type": "g6-nanode-1",
"instance_label": "packerbats-minimal-{{timestamp}}",
"image_label": "packerbats-minimal-image-{{timestamp}}",
"image_description": "packerbats",
"ssh_username": "root"
}
],
"provisioners": [
{
"type": "shell",
"inline": ["echo Hello > /root/message.txt"]
}
]
}

5
vendor/modules.txt vendored
View File

@ -281,6 +281,8 @@ github.com/klauspost/pgzip
github.com/konsorten/go-windows-terminal-sequences
# github.com/kr/fs v0.0.0-20131111012553-2788f0dbd169
github.com/kr/fs
# github.com/linode/linodego v0.7.1
github.com/linode/linodego
# github.com/marstr/guid v0.0.0-20170427235115-8bdf7d1a087c
github.com/marstr/guid
# github.com/masterzen/azure-sdk-for-go v0.0.0-20161014135628-ee4f0065d00c
@ -475,6 +477,7 @@ golang.org/x/net/http/httpguts
golang.org/x/net/http2/hpack
golang.org/x/net/idna
golang.org/x/net/context/ctxhttp
golang.org/x/net/publicsuffix
golang.org/x/net/html
golang.org/x/net/internal/timeseries
golang.org/x/net/html/atom
@ -575,5 +578,7 @@ google.golang.org/grpc/credentials/internal
google.golang.org/grpc/balancer/base
google.golang.org/grpc/binarylog/grpc_binarylog_v1
google.golang.org/grpc/internal/syscall
# gopkg.in/resty.v1 v1.12.0
gopkg.in/resty.v1
# gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v2

View File

@ -0,0 +1,96 @@
---
description: |
The linode Packer builder is able to create new images for use with Linode. The
builder takes a source image, runs any provisioning necessary on the image
after launching it, then snapshots it into a reusable image. This reusable
image can then be used as the foundation of new servers that are launched
within all Linode regions.
layout: docs
page_title: 'Linode - Builders'
sidebar_current: 'docs-builders-linode'
---
# Linode Builder
Type: `linode`
The `linode` Packer builder is able to create [Linode
Images](https://www.linode.com/docs/platform/disk-images/linode-images/) for
use with [Linode](https://www.linode.com). The builder takes a source image,
runs any provisioning necessary on the image after launching it, then snapshots
it into a reusable image. This reusable image can then be used as the
foundation of new servers that are launched within Linode.
The builder does *not* manage images. Once it creates an image, it is up to you
to use it or delete it.
## Configuration Reference
There are many configuration options available for the builder. They are
segmented below into two categories: required and optional parameters. Within
each category, the available configuration keys are alphabetized.
In addition to the options listed here, a
[communicator](/docs/templates/communicator.html) can be configured for this
builder.
### Required
- `linode_token` (string) - The client TOKEN to use to access your account.
- `image` (string) - An Image ID to deploy the Disk from. Official Linode
Images start with `linode/`, while user Images start with `private/`. See
[images](https://api.linode.com/v4/images) for more information on the
Images available for use. Examples are `linode/debian9`, `linode/fedora28`,
`linode/ubuntu18.04`, `linode/arch`, and `private/12345`.
- `region` (string) - The id of the region to launch the Linode instance in.
Images are available in all regions, but there will be less delay when
deploying from the region where the image was taken. Examples are
`us-east`, `us-central`, `us-west`, `ap-south`, `ca-east`, `ap-northeast`,
`eu-central`, and `eu-west`.
- `instance_type` (string) - The Linode type defines the pricing, CPU, disk,
and RAM specs of the instance. Examples are `g6-nanode-1`, `g6-standard-2`,
`g6-highmem-16`, and `g6-dedicated-16`.
### Optional
- `instance_label` (string) - The name assigned to the Linode Instance.
- `instance_tags` (list) - Tags to apply to the instance when it is created.
- `swap_size` (int) - The disk size (MiB) allocated for swap space.
- `image_label` (string) - The name of the resulting image that will appear
in your account. Defaults to "packer-{{timestamp}}" (see [configuration
templates](/docs/templates/engine.html) for more info).
- `image_description` (string) - The description of the resulting image that
will appear in your account. Defaults to "".
- `state_timeout` (string) - The time to wait, as a duration string, for the
Linode instance to enter a desired state (such as "running") before timing
out. The default state timeout is "5m".
## Basic Example
Here is a Linode builder example. The `linode_token` should be replaced with an
actual [Linode Personal Access
Token](https://www.linode.com/docs/platform/api/getting-started-with-the-linode-api/#get-an-access-token).
``` json
{
"type": "linode",
"linode_token": "YOUR API TOKEN",
"image": "linode/debian9",
"region": "us-east",
"instance_type": "g6-nanode-1",
"instance_label": "temporary-linode-{{timestamp}}",
"image_label": "private-image-{{timestamp}}",
"image_description": "My Private Image",
"ssh_username": "root"
}
```

View File

@ -116,6 +116,9 @@
</li>
</ul>
</li>
<li<%= sidebar_current("docs-builders-linode") %>>
<a href="/docs/builders/linode.html">Linode</a>
</li>
<li<%= sidebar_current("docs-builders-lxc") %>>
<a href="/docs/builders/lxc.html">LXC</a>
</li>