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:
parent
45af9f0cbc
commit
99987c2d56
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}]
|
||||
}
|
||||
`
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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) {}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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) {}
|
|
@ -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) {}
|
|
@ -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
2
go.mod
|
@ -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
5
go.sum
|
@ -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=
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
```
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue