Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a39650332 | |||
| 1fd5937839 | |||
| 95435e484c | |||
| bc097abc72 | |||
| 856f27bc9b | |||
| 43c6b9e6d1 | |||
| 9a2dbd54bf | |||
| 1ec2de97a6 | |||
| 7d9c7530be | |||
| 049dc5bff5 | |||
| 724e2b3c52 | |||
| 0eb9e06637 | |||
| e00f114520 | |||
| c09c1108ca | |||
| 34556713dc | |||
| 4d7b0c2dbc | |||
| c1f8e5c3b3 | |||
| 97b4c7d22b | |||
| 4e300259c7 | |||
| db903af566 | |||
| 3c9e504475 | |||
| d88ff24343 | |||
| f7bd403151 | |||
| 1bf2bade7d | |||
| d4d58e3779 | |||
| 4f58b20479 | |||
| 758a53b146 | |||
| 336495d34c | |||
| 78d03c58e5 | |||
| 43b23c5221 | |||
| 226e6f5859 | |||
| bebed45a80 | |||
| f4a9d9d3b7 | |||
| eed6ece45b | |||
| 04c8592077 | |||
| f3bb028ec3 | |||
| f46bf47579 | |||
| ca54df1a76 | |||
| b54a5c0074 | |||
| cd62aa5003 | |||
| 10b0f920bc | |||
| f3f4ab91c7 | |||
| c8bc2bc1ba | |||
| 41370a215e | |||
| f2355a577a | |||
| 6a7450974d | |||
| 810bea7fcb | |||
| d5ab581260 | |||
| ae35cd51e2 | |||
| a4c555a5b7 | |||
| a542ff967b | |||
| 59eff6a222 | |||
| b25f0ceabf | |||
| a0a6817f21 | |||
| 5decc186a6 | |||
| cfc7715495 | |||
| 60fc565dbb | |||
| da4a7c3b70 | |||
| 2349706a68 | |||
| 777f595c8b | |||
| e7b1ba2f5e | |||
| 95926424f2 | |||
| 1e4e343333 | |||
| de13474865 | |||
| 3ca085c2c1 | |||
| 713791ba9a | |||
| bb1b3d8fe0 | |||
| 1a1b4ba715 | |||
| 41b3f9ebd9 | |||
| d0200b7be8 | |||
| a0033fba35 | |||
| 180d5c8275 | |||
| ddfd9be8cc | |||
| bf457f437c | |||
| 100458955f | |||
| ab5a1981d9 | |||
| eaf5e66f3a | |||
| 3e8b3b9327 | |||
| 7408558a55 | |||
| d87f878a44 | |||
| 7836d60c61 | |||
| c94287bbde | |||
| f909bc29ed | |||
| d736c4b180 | |||
| afe8dda7a9 | |||
| 558f919e4f | |||
| d7c9658c1c | |||
| 2a0b3cfdc2 | |||
| 1f53e047a3 | |||
| b0394c6d30 | |||
| d4d7730473 | |||
| 9dc98a6a2a | |||
| ffc73a2ffa | |||
| db5b236ad6 | |||
| bdd49619ae | |||
| 23803621e9 | |||
| 9520c6e6ed | |||
| c6e36108b3 | |||
| a9df9e91c6 | |||
| f9ffb1fc54 | |||
| 16ccfe3824 | |||
| 1b01e82d8d | |||
| f66c145ee8 | |||
| ad9575a041 | |||
| d71621a599 | |||
| 8fad60b1b4 | |||
| fde7910e85 | |||
| 66818ef89f | |||
| 1268373105 | |||
| a9b951e1fe | |||
| 28e72c7f7b | |||
| a669b4dbd0 | |||
| 8e7aa41bdf | |||
| 93ac76e9c7 | |||
| ff27abc06a | |||
| 04a378c39a | |||
| ecd598ab64 | |||
| 915f9b73be | |||
| cb756912ad | |||
| 253d32c274 | |||
| 0b471adc7d | |||
| 13ca445e13 | |||
| 87e61b643c | |||
| 6a6aa146ac | |||
| a81fc2d837 | |||
| 9eda3fa327 |
@@ -1,3 +1,59 @@
|
||||
## 0.3.1 (August 12, 2013)
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* provisioner/shell: New setting `start_retry_timeout` which is the timeout
|
||||
for the provisioner to attempt to _start_ the remote process. This allows
|
||||
the shell provisioner to work properly with reboots. [GH-260]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* core: Remote command output containing '\r' now looks much better
|
||||
within the Packer output.
|
||||
* builder/vmware: Fix issue with finding driver files. [GH-279]
|
||||
* provisioner/salt-masterless: Uploads work properly from Windows. [GH-276]
|
||||
|
||||
## 0.3.0 (August 12, 2013)
|
||||
|
||||
BACKWARDS INCOMPATIBILITIES:
|
||||
|
||||
* All `{{.CreateTime}}` variables within templates (such as for AMI names)
|
||||
are now replaced with `{{timestamp}}`. Run `packer fix` to fix your
|
||||
templates.
|
||||
|
||||
FEATURES:
|
||||
|
||||
* **User Variables** allow you to specify variables within your templates
|
||||
that can be replaced using the command-line, files, or environmental
|
||||
variables. This dramatically improves the portability of packer templates.
|
||||
See the documentation for more information.
|
||||
* **Machine-readable output** can be enabled by passing the
|
||||
`-machine-readable` flag to _any_ Packer command.
|
||||
* All strings in a template are now processed for variables/functions,
|
||||
so things like `{{timestamp}}` can be used everywhere. More features will
|
||||
be added in the future.
|
||||
* The `amazon` builders (all of them) can now have attributes of their
|
||||
resulting AMIs modified, such as access permissions and product codes.
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* builder/amazon/all: User data can be passed to start the instances. [GH-253]
|
||||
* provisioner/salt-masterless: `local_state_tree` is no longer required,
|
||||
allowing you to use shell provisioner (or others) to bring this down.
|
||||
[GH-269]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* builder/amazon/ebs,instance: Retry deleing security group a few times.
|
||||
[GH-278]
|
||||
* builder/vmware: Workstation works on Windows XP now. [GH-238]
|
||||
* builder/vmware: Look for files on Windows in multiple locations
|
||||
using multiple environmental variables. [GH-263]
|
||||
* provisioner/salt-masterless: states aren't deleted after the run
|
||||
anymore. [GH-265]
|
||||
* provisioner/salt-masterless: error if any commands exit with a non-zero
|
||||
exit status. [GH-266]
|
||||
|
||||
## 0.2.3 (August 7, 2013)
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
@@ -43,7 +43,7 @@ own.
|
||||
"source_ami": "ami-de0d9eb7",
|
||||
"instance_type": "t1.micro",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-example {{.CreateTime}}"
|
||||
"ami_name": "packer-example {{timestamp}}"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"runtime"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
@@ -25,8 +24,8 @@ const BuilderId = "mitchellh.amazon.chroot"
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
awscommon.AccessConfig `mapstructure:",squash"`
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
|
||||
CopyFiles []string `mapstructure:"copy_files"`
|
||||
DevicePath string `mapstructure:"device_path"`
|
||||
@@ -34,6 +33,8 @@ type Config struct {
|
||||
MountPath string `mapstructure:"mount_path"`
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
UnmountCommand string `mapstructure:"unmount_command"`
|
||||
|
||||
tpl *common.Template
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
@@ -47,6 +48,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = common.NewTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
|
||||
// Defaults
|
||||
if b.config.ChrootMounts == nil {
|
||||
b.config.ChrootMounts = make([][]string, 0)
|
||||
@@ -84,31 +91,56 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
|
||||
|
||||
if b.config.AMIName == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("ami_name must be specified"))
|
||||
} else {
|
||||
_, err = template.New("ami").Parse(b.config.AMIName)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
for _, mounts := range b.config.ChrootMounts {
|
||||
for i, mounts := range b.config.ChrootMounts {
|
||||
if len(mounts) != 3 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("Each chroot_mounts entry should be three elements."))
|
||||
break
|
||||
}
|
||||
|
||||
for j, entry := range mounts {
|
||||
b.config.ChrootMounts[i][j], err = b.config.tpl.Process(entry, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing chroot_mounts[%d][%d]: %s",
|
||||
i, j, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, file := range b.config.CopyFiles {
|
||||
var err error
|
||||
b.config.CopyFiles[i], err = b.config.tpl.Process(file, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing copy_files[%d]: %s",
|
||||
i, err))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.SourceAmi == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("source_ami is required."))
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"device_path": &b.config.DevicePath,
|
||||
"mount_command": &b.config.MountCommand,
|
||||
"source_ami": &b.config.SourceAmi,
|
||||
"unmount_command": &b.config.UnmountCommand,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = b.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
@@ -157,6 +189,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&StepEarlyCleanup{},
|
||||
&StepSnapshot{},
|
||||
&StepRegisterAMI{},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
},
|
||||
}
|
||||
|
||||
// Run!
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type mountPathData struct {
|
||||
@@ -30,14 +29,17 @@ func (s *StepMountDevice) Run(state map[string]interface{}) multistep.StepAction
|
||||
ui := state["ui"].(packer.Ui)
|
||||
device := state["device"].(string)
|
||||
|
||||
mountPathRaw := new(bytes.Buffer)
|
||||
t := template.Must(template.New("mountPath").Parse(config.MountPath))
|
||||
t.Execute(mountPathRaw, &mountPathData{
|
||||
mountPath, err := config.tpl.Process(config.MountPath, &mountPathData{
|
||||
Device: filepath.Base(device),
|
||||
})
|
||||
|
||||
var err error
|
||||
mountPath := mountPathRaw.String()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing mount directory: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
mountPath, err = filepath.Abs(mountPath)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing mount directory: %s", err)
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type amiNameData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
// StepRegisterAMI creates the AMI.
|
||||
type StepRegisterAMI struct{}
|
||||
|
||||
@@ -26,16 +18,6 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction
|
||||
snapshotId := state["snapshot_id"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Parse the name of the AMI
|
||||
amiNameBuf := new(bytes.Buffer)
|
||||
tData := amiNameData{
|
||||
strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
|
||||
t := template.Must(template.New("ami").Parse(config.AMIName))
|
||||
t.Execute(amiNameBuf, tData)
|
||||
amiName := amiNameBuf.String()
|
||||
|
||||
ui.Say("Registering the AMI...")
|
||||
blockDevices := make([]ec2.BlockDeviceMapping, len(image.BlockDevices))
|
||||
for i, device := range image.BlockDevices {
|
||||
@@ -48,7 +30,7 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction
|
||||
}
|
||||
|
||||
registerOpts := &ec2.RegisterImage{
|
||||
Name: amiName,
|
||||
Name: config.AMIName,
|
||||
Architecture: image.Architecture,
|
||||
KernelId: image.KernelId,
|
||||
RamdiskId: image.RamdiskId,
|
||||
|
||||
@@ -3,6 +3,7 @@ package common
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
@@ -36,12 +37,40 @@ func (c *AccessConfig) Region() (aws.Region, error) {
|
||||
return aws.Regions[region], nil
|
||||
}
|
||||
|
||||
func (c *AccessConfig) Prepare() []error {
|
||||
func (c *AccessConfig) Prepare(t *common.Template) []error {
|
||||
if t == nil {
|
||||
var err error
|
||||
t, err = common.NewTemplate()
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"access_key": &c.AccessKey,
|
||||
"secret_key": &c.SecretKey,
|
||||
"region": &c.RawRegion,
|
||||
}
|
||||
|
||||
errs := make([]error, 0)
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = t.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = append(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
if c.RawRegion != "" {
|
||||
if _, ok := aws.Regions[c.RawRegion]; !ok {
|
||||
return []error{fmt.Errorf("Unknown region: %s", c.RawRegion)}
|
||||
errs = append(errs, fmt.Errorf("Unknown region: %s", c.RawRegion))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,17 +11,17 @@ func testAccessConfig() *AccessConfig {
|
||||
func TestAccessConfigPrepare_Region(t *testing.T) {
|
||||
c := testAccessConfig()
|
||||
c.RawRegion = ""
|
||||
if err := c.Prepare(); err != nil {
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
|
||||
c.RawRegion = "us-east-12"
|
||||
if err := c.Prepare(); err == nil {
|
||||
if err := c.Prepare(nil); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
c.RawRegion = "us-east-1"
|
||||
if err := c.Prepare(); err != nil {
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/common"
|
||||
)
|
||||
|
||||
// AMIConfig is for common configuration related to creating AMIs.
|
||||
type AMIConfig struct {
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
AMIDescription string `mapstructure:"ami_description"`
|
||||
AMIUsers []string `mapstructure:"ami_users"`
|
||||
AMIGroups []string `mapstructure:"ami_groups"`
|
||||
AMIProductCodes []string `mapstructure:"ami_product_codes"`
|
||||
}
|
||||
|
||||
func (c *AMIConfig) Prepare(t *common.Template) []error {
|
||||
if t == nil {
|
||||
var err error
|
||||
t, err = common.NewTemplate()
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"ami_name": &c.AMIName,
|
||||
"ami_description": &c.AMIDescription,
|
||||
}
|
||||
|
||||
errs := make([]error, 0)
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = t.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = append(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
sliceTemplates := map[string][]string{
|
||||
"ami_users": c.AMIUsers,
|
||||
"ami_groups": c.AMIGroups,
|
||||
"ami_product_codes": c.AMIProductCodes,
|
||||
}
|
||||
|
||||
for n, slice := range sliceTemplates {
|
||||
for i, elem := range slice {
|
||||
var err error
|
||||
slice[i], err = t.Process(elem, nil)
|
||||
if err != nil {
|
||||
errs = append(
|
||||
errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.AMIName == "" {
|
||||
errs = append(errs, fmt.Errorf("ami_name must be specified"))
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testAMIConfig() *AMIConfig {
|
||||
return &AMIConfig{
|
||||
AMIName: "foo",
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMIConfigPrepare_Region(t *testing.T) {
|
||||
c := testAMIConfig()
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
|
||||
c.AMIName = ""
|
||||
if err := c.Prepare(nil); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
|
||||
return func() (interface{}, string, error) {
|
||||
resp, err := conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
|
||||
if err != nil {
|
||||
log.Printf("Error on InstanceStateRefresh: %s", err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package common
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -12,6 +14,8 @@ type RunConfig struct {
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
IamInstanceProfile string `mapstructure:"iam_instance_profile"`
|
||||
InstanceType string `mapstructure:"instance_type"`
|
||||
UserData string `mapstructure:"user_data"`
|
||||
UserDataFile string `mapstructure:"user_data_file"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort int `mapstructure:"ssh_port"`
|
||||
@@ -23,7 +27,15 @@ type RunConfig struct {
|
||||
sshTimeout time.Duration
|
||||
}
|
||||
|
||||
func (c *RunConfig) Prepare() []error {
|
||||
func (c *RunConfig) Prepare(t *common.Template) []error {
|
||||
if t == nil {
|
||||
var err error
|
||||
t, err = common.NewTemplate()
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
}
|
||||
|
||||
// Defaults
|
||||
if c.SSHPort == 0 {
|
||||
c.SSHPort = 22
|
||||
@@ -48,6 +60,34 @@ func (c *RunConfig) Prepare() []error {
|
||||
errs = append(errs, errors.New("An ssh_username must be specified"))
|
||||
}
|
||||
|
||||
if c.UserData != "" && c.UserDataFile != "" {
|
||||
errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
|
||||
} else if c.UserDataFile != "" {
|
||||
if _, err := os.Stat(c.UserDataFile); err != nil {
|
||||
errs = append(errs, fmt.Errorf("user_data_file not found: %s", c.UserDataFile))
|
||||
}
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"iam_instance_profile": &c.IamInstanceProfile,
|
||||
"instance_type": &c.InstanceType,
|
||||
"ssh_timeout": &c.RawSSHTimeout,
|
||||
"security_group_id": &c.SecurityGroupId,
|
||||
"ssh_username": &c.SSHUsername,
|
||||
"source_ami": &c.SourceAmi,
|
||||
"subnet_id": &c.SubnetId,
|
||||
"vpc_id": &c.VpcId,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = t.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = append(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
c.sshTimeout, err = time.ParseDuration(c.RawSSHTimeout)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
@@ -24,7 +25,7 @@ func testConfig() *RunConfig {
|
||||
|
||||
func TestRunConfigPrepare(t *testing.T) {
|
||||
c := testConfig()
|
||||
err := c.Prepare()
|
||||
err := c.Prepare(nil)
|
||||
if len(err) > 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
@@ -33,7 +34,7 @@ func TestRunConfigPrepare(t *testing.T) {
|
||||
func TestRunConfigPrepare_InstanceType(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.InstanceType = ""
|
||||
if err := c.Prepare(); len(err) != 1 {
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -41,7 +42,7 @@ func TestRunConfigPrepare_InstanceType(t *testing.T) {
|
||||
func TestRunConfigPrepare_SourceAmi(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.SourceAmi = ""
|
||||
if err := c.Prepare(); len(err) != 1 {
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -49,7 +50,7 @@ func TestRunConfigPrepare_SourceAmi(t *testing.T) {
|
||||
func TestRunConfigPrepare_SSHPort(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.SSHPort = 0
|
||||
if err := c.Prepare(); len(err) != 0 {
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@ func TestRunConfigPrepare_SSHPort(t *testing.T) {
|
||||
}
|
||||
|
||||
c.SSHPort = 44
|
||||
if err := c.Prepare(); len(err) != 0 {
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
@@ -70,12 +71,12 @@ func TestRunConfigPrepare_SSHPort(t *testing.T) {
|
||||
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.RawSSHTimeout = ""
|
||||
if err := c.Prepare(); len(err) != 0 {
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
c.RawSSHTimeout = "bad"
|
||||
if err := c.Prepare(); len(err) != 1 {
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -83,7 +84,45 @@ func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
|
||||
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.SSHUsername = ""
|
||||
if err := c.Prepare(); len(err) != 1 {
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_UserData(t *testing.T) {
|
||||
c := testConfig()
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer tf.Close()
|
||||
|
||||
c.UserData = "foo"
|
||||
c.UserDataFile = tf.Name()
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_UserDataFile(t *testing.T) {
|
||||
c := testConfig()
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
c.UserDataFile = "idontexistidontthink"
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer tf.Close()
|
||||
|
||||
c.UserDataFile = tf.Name()
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ func (s *StepCreateTags) Run(state map[string]interface{}) multistep.StepAction
|
||||
|
||||
var ec2Tags []ec2.Tag
|
||||
for key, value := range s.Tags {
|
||||
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value))
|
||||
ec2Tags = append(ec2Tags, ec2.Tag{key, value})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepModifyAMIAttributes struct {
|
||||
Users []string
|
||||
Groups []string
|
||||
ProductCodes []string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (s *StepModifyAMIAttributes) Run(state map[string]interface{}) multistep.StepAction {
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
amis := state["amis"].(map[string]string)
|
||||
ami := amis[ec2conn.Region.Name]
|
||||
|
||||
// Determine if there is any work to do.
|
||||
valid := false
|
||||
valid = valid || s.Description != ""
|
||||
valid = valid || (s.Users != nil && len(s.Users) > 0)
|
||||
valid = valid || (s.Groups != nil && len(s.Groups) > 0)
|
||||
valid = valid || (s.ProductCodes != nil && len(s.ProductCodes) > 0)
|
||||
|
||||
if !valid {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
options := &ec2.ModifyImageAttribute{
|
||||
Description: s.Description,
|
||||
AddUsers: s.Users,
|
||||
AddGroups: s.Groups,
|
||||
ProductCodes: s.ProductCodes,
|
||||
}
|
||||
|
||||
ui.Say("Modifying AMI attributes...")
|
||||
_, err := ec2conn.ModifyImageAttribute(ami, options)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error modify AMI attributes: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepModifyAMIAttributes) Cleanup(state map[string]interface{}) {
|
||||
// No cleanup...
|
||||
}
|
||||
@@ -5,12 +5,15 @@ import (
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
)
|
||||
|
||||
type StepRunSourceInstance struct {
|
||||
ExpectedRootDevice string
|
||||
InstanceType string
|
||||
UserData string
|
||||
UserDataFile string
|
||||
SourceAMI string
|
||||
IamInstanceProfile string
|
||||
SubnetId string
|
||||
@@ -24,10 +27,22 @@ func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
|
||||
securityGroupId := state["securityGroupId"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
userData := s.UserData
|
||||
if s.UserDataFile != "" {
|
||||
contents, err := ioutil.ReadFile(s.UserDataFile)
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Problem reading user data file: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
userData = string(contents)
|
||||
}
|
||||
|
||||
runOpts := &ec2.RunInstances{
|
||||
KeyName: keyName,
|
||||
ImageId: s.SourceAMI,
|
||||
InstanceType: s.InstanceType,
|
||||
UserData: []byte(userData),
|
||||
MinCount: 0,
|
||||
MaxCount: 0,
|
||||
SecurityGroups: []ec2.SecurityGroup{ec2.SecurityGroup{Id: securityGroupId}},
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StepSecurityGroup struct {
|
||||
@@ -83,9 +84,19 @@ func (s *StepSecurityGroup) Cleanup(state map[string]interface{}) {
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Deleting temporary security group...")
|
||||
_, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.createdGroupId})
|
||||
if err != nil {
|
||||
|
||||
var err error
|
||||
for i := 0; i < 5; i++ {
|
||||
_, err = ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.createdGroupId})
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
log.Printf("Error deleting security group: %s", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error cleaning up security group. Please delete the group manually: %s", s.createdGroupId))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
@@ -23,13 +21,13 @@ const BuilderId = "mitchellh.amazonebs"
|
||||
type config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
awscommon.AccessConfig `mapstructure:",squash"`
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
awscommon.RunConfig `mapstructure:",squash"`
|
||||
|
||||
// Configuration of the resulting AMI
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
|
||||
// Tags for the AMI
|
||||
Tags map[string]string
|
||||
|
||||
tpl *common.Template
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
@@ -43,23 +41,40 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare()...)
|
||||
b.config.tpl, err = common.NewTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
|
||||
// Accumulate any errors
|
||||
if b.config.AMIName == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("ami_name must be specified"))
|
||||
} else {
|
||||
_, err = template.New("ami").Parse(b.config.AMIName)
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
|
||||
|
||||
// Accumulate any errors
|
||||
newTags := make(map[string]string)
|
||||
for k, v := range b.config.Tags {
|
||||
k, err = b.config.tpl.Process(k, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing tag key %s: %s", k, err))
|
||||
continue
|
||||
}
|
||||
|
||||
v, err = b.config.tpl.Process(v, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing tag value '%s': %s", v, err))
|
||||
continue
|
||||
}
|
||||
|
||||
newTags[k] = v
|
||||
}
|
||||
|
||||
b.config.Tags = newTags
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
@@ -99,6 +114,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&awscommon.StepRunSourceInstance{
|
||||
ExpectedRootDevice: "ebs",
|
||||
InstanceType: b.config.InstanceType,
|
||||
UserData: b.config.UserData,
|
||||
UserDataFile: b.config.UserDataFile,
|
||||
SourceAMI: b.config.SourceAmi,
|
||||
IamInstanceProfile: b.config.IamInstanceProfile,
|
||||
SubnetId: b.config.SubnetId,
|
||||
@@ -111,7 +128,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&common.StepProvision{},
|
||||
&stepStopInstance{},
|
||||
&stepCreateAMI{},
|
||||
&awscommon.StepCreateTags{b.config.Tags},
|
||||
&awscommon.StepCreateTags{Tags: b.config.Tags},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
},
|
||||
}
|
||||
|
||||
// Run!
|
||||
|
||||
@@ -1,44 +1,26 @@
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stepCreateAMI struct{}
|
||||
|
||||
type amiNameData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
|
||||
config := state["config"].(config)
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
instance := state["instance"].(*ec2.Instance)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Parse the name of the AMI
|
||||
amiNameBuf := new(bytes.Buffer)
|
||||
tData := amiNameData{
|
||||
strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
|
||||
t := template.Must(template.New("ami").Parse(config.AMIName))
|
||||
t.Execute(amiNameBuf, tData)
|
||||
amiName := amiNameBuf.String()
|
||||
|
||||
// Create the image
|
||||
ui.Say(fmt.Sprintf("Creating the AMI: %s", amiName))
|
||||
ui.Say(fmt.Sprintf("Creating the AMI: %s", config.AMIName))
|
||||
createOpts := &ec2.CreateImage{
|
||||
InstanceId: instance.InstanceId,
|
||||
Name: amiName,
|
||||
Name: config.AMIName,
|
||||
}
|
||||
|
||||
createResp, err := ec2conn.CreateImage(createOpts)
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
@@ -24,10 +23,10 @@ const BuilderId = "mitchellh.amazon.instance"
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
awscommon.AccessConfig `mapstructure:",squash"`
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
awscommon.RunConfig `mapstructure:",squash"`
|
||||
|
||||
AccountId string `mapstructure:"account_id"`
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
BundleDestination string `mapstructure:"bundle_destination"`
|
||||
BundlePrefix string `mapstructure:"bundle_prefix"`
|
||||
BundleUploadCommand string `mapstructure:"bundle_upload_command"`
|
||||
@@ -37,6 +36,8 @@ type Config struct {
|
||||
X509CertPath string `mapstructure:"x509_cert_path"`
|
||||
X509KeyPath string `mapstructure:"x509_key_path"`
|
||||
X509UploadPath string `mapstructure:"x509_upload_path"`
|
||||
|
||||
tpl *common.Template
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
@@ -50,12 +51,18 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = common.NewTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
|
||||
if b.config.BundleDestination == "" {
|
||||
b.config.BundleDestination = "/tmp"
|
||||
}
|
||||
|
||||
if b.config.BundlePrefix == "" {
|
||||
b.config.BundlePrefix = "image-{{.CreateTime}}"
|
||||
b.config.BundlePrefix = "image-{{timestamp}}"
|
||||
}
|
||||
|
||||
if b.config.BundleUploadCommand == "" {
|
||||
@@ -87,8 +94,41 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare()...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
|
||||
|
||||
validates := map[string]*string{
|
||||
"bundle_upload_command": &b.config.BundleUploadCommand,
|
||||
"bundle_vol_command": &b.config.BundleVolCommand,
|
||||
}
|
||||
|
||||
for n, ptr := range validates {
|
||||
if err := b.config.tpl.Validate(*ptr); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error parsing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"account_id": &b.config.AccountId,
|
||||
"ami_name": &b.config.AMIName,
|
||||
"bundle_destination": &b.config.BundleDestination,
|
||||
"bundle_prefix": &b.config.BundlePrefix,
|
||||
"s3_bucket": &b.config.S3Bucket,
|
||||
"x509_cert_path": &b.config.X509CertPath,
|
||||
"x509_key_path": &b.config.X509KeyPath,
|
||||
"x509_upload_path": &b.config.X509UploadPath,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = b.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.AccountId == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("account_id is required"))
|
||||
@@ -96,17 +136,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.AccountId = strings.Replace(b.config.AccountId, "-", "", -1)
|
||||
}
|
||||
|
||||
if b.config.AMIName == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("ami_name must be specified"))
|
||||
} else {
|
||||
_, err = template.New("ami").Parse(b.config.AMIName)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.S3Bucket == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("s3_bucket is required"))
|
||||
}
|
||||
@@ -164,6 +193,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&awscommon.StepRunSourceInstance{
|
||||
ExpectedRootDevice: "instance-store",
|
||||
InstanceType: b.config.InstanceType,
|
||||
UserData: b.config.UserData,
|
||||
UserDataFile: b.config.UserDataFile,
|
||||
SourceAMI: b.config.SourceAmi,
|
||||
SubnetId: b.config.SubnetId,
|
||||
},
|
||||
@@ -177,7 +208,13 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&StepBundleVolume{},
|
||||
&StepUploadBundle{},
|
||||
&StepRegisterAMI{},
|
||||
&awscommon.StepCreateTags{b.config.Tags},
|
||||
&awscommon.StepCreateTags{Tags: b.config.Tags},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
ProductCodes: b.config.AMIProductCodes,
|
||||
},
|
||||
}
|
||||
|
||||
// Run!
|
||||
|
||||
@@ -115,7 +115,7 @@ func TestBuilderPrepare_BundlePrefix(t *testing.T) {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.BundlePrefix != "image-{{.CreateTime}}" {
|
||||
if b.config.BundlePrefix == "" {
|
||||
t.Fatalf("bad: %s", b.config.BundlePrefix)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type bundleCmdData struct {
|
||||
@@ -21,10 +17,6 @@ type bundleCmdData struct {
|
||||
PrivatePath string
|
||||
}
|
||||
|
||||
type bundlePrefixData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
type StepBundleVolume struct{}
|
||||
|
||||
func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepAction {
|
||||
@@ -55,29 +47,26 @@ func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepActio
|
||||
}
|
||||
|
||||
// Bundle the volume
|
||||
var bundlePrefix bytes.Buffer
|
||||
prefixTData := bundlePrefixData{
|
||||
CreateTime: strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
t := template.Must(template.New("bundlePrefix").Parse(config.BundlePrefix))
|
||||
t.Execute(&bundlePrefix, prefixTData)
|
||||
|
||||
var bundleCmd bytes.Buffer
|
||||
tData := bundleCmdData{
|
||||
var err error
|
||||
config.BundleVolCommand, err = config.tpl.Process(config.BundleVolCommand, bundleCmdData{
|
||||
AccountId: config.AccountId,
|
||||
Architecture: instance.Architecture,
|
||||
CertPath: x509RemoteCertPath,
|
||||
Destination: config.BundleDestination,
|
||||
KeyPath: x509RemoteKeyPath,
|
||||
Prefix: bundlePrefix.String(),
|
||||
Prefix: config.BundlePrefix,
|
||||
PrivatePath: config.X509UploadPath,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error processing bundle volume command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
t = template.Must(template.New("bundleCmd").Parse(config.BundleVolCommand))
|
||||
t.Execute(&bundleCmd, tData)
|
||||
|
||||
ui.Say("Bundling the volume...")
|
||||
cmd = new(packer.RemoteCmd)
|
||||
cmd.Command = bundleCmd.String()
|
||||
cmd.Command = config.BundleVolCommand
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
state["error"] = fmt.Errorf("Error bundling volume: %s", err)
|
||||
ui.Error(state["error"].(error).Error())
|
||||
@@ -93,7 +82,7 @@ func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepActio
|
||||
}
|
||||
|
||||
// Store the manifest path
|
||||
manifestName := bundlePrefix.String() + ".manifest.xml"
|
||||
manifestName := config.BundlePrefix + ".manifest.xml"
|
||||
state["manifest_name"] = manifestName
|
||||
state["manifest_path"] = fmt.Sprintf(
|
||||
"%s/%s", config.BundleDestination, manifestName)
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type amiNameData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
type StepRegisterAMI struct{}
|
||||
|
||||
func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction {
|
||||
@@ -24,20 +16,10 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction
|
||||
manifestPath := state["remote_manifest_path"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Parse the name of the AMI
|
||||
amiNameBuf := new(bytes.Buffer)
|
||||
tData := amiNameData{
|
||||
strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
|
||||
t := template.Must(template.New("ami").Parse(config.AMIName))
|
||||
t.Execute(amiNameBuf, tData)
|
||||
amiName := amiNameBuf.String()
|
||||
|
||||
ui.Say("Registering the AMI...")
|
||||
registerOpts := &ec2.RegisterImage{
|
||||
ImageLocation: manifestPath,
|
||||
Name: amiName,
|
||||
Name: config.AMIName,
|
||||
}
|
||||
|
||||
registerResp, err := ec2conn.RegisterImage(registerOpts)
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type uploadCmdData struct {
|
||||
@@ -25,19 +23,23 @@ func (s *StepUploadBundle) Run(state map[string]interface{}) multistep.StepActio
|
||||
manifestPath := state["manifest_path"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
var uploadCmd bytes.Buffer
|
||||
tData := uploadCmdData{
|
||||
var err error
|
||||
config.BundleUploadCommand, err = config.tpl.Process(config.BundleUploadCommand, uploadCmdData{
|
||||
AccessKey: config.AccessKey,
|
||||
BucketName: config.S3Bucket,
|
||||
BundleDirectory: config.BundleDestination,
|
||||
ManifestPath: manifestPath,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error processing bundle upload command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
t := template.Must(template.New("uploadCmd").Parse(config.BundleUploadCommand))
|
||||
t.Execute(&uploadCmd, tData)
|
||||
|
||||
ui.Say("Uploading the bundle...")
|
||||
cmd := &packer.RemoteCmd{Command: uploadCmd.String()}
|
||||
cmd := &packer.RemoteCmd{Command: config.BundleUploadCommand}
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
state["error"] = fmt.Errorf("Error uploading volume: %s", err)
|
||||
ui.Error(state["error"].(error).Error())
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
@@ -12,18 +11,12 @@ import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The unique id for the builder
|
||||
const BuilderId = "pearkes.digitalocean"
|
||||
|
||||
type snapshotNameData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
// Configuration tells the builder the credentials
|
||||
// to use while communicating with DO and describes the image
|
||||
// you are creating
|
||||
@@ -36,11 +29,10 @@ type config struct {
|
||||
SizeID uint `mapstructure:"size_id"`
|
||||
ImageID uint `mapstructure:"image_id"`
|
||||
|
||||
SnapshotName string
|
||||
SnapshotName string `mapstructure:"snapshot_name"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
|
||||
RawSnapshotName string `mapstructure:"snapshot_name"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
RawEventDelay string `mapstructure:"event_delay"`
|
||||
RawStateTimeout string `mapstructure:"state_timeout"`
|
||||
@@ -50,6 +42,8 @@ type config struct {
|
||||
sshTimeout time.Duration
|
||||
eventDelay time.Duration
|
||||
stateTimeout time.Duration
|
||||
|
||||
tpl *common.Template
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
@@ -63,6 +57,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = common.NewTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
|
||||
@@ -92,6 +92,11 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.ImageID = 284203
|
||||
}
|
||||
|
||||
if b.config.SnapshotName == "" {
|
||||
// Default to packer-{{ unix timestamp (utc) }}
|
||||
b.config.SnapshotName = "packer-{{timestamp}}"
|
||||
}
|
||||
|
||||
if b.config.SSHUsername == "" {
|
||||
// Default to "root". You can override this if your
|
||||
// SourceImage has a different user account then the DO default
|
||||
@@ -103,11 +108,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.SSHPort = 22
|
||||
}
|
||||
|
||||
if b.config.RawSnapshotName == "" {
|
||||
// Default to packer-{{ unix timestamp (utc) }}
|
||||
b.config.RawSnapshotName = "packer-{{.CreateTime}}"
|
||||
}
|
||||
|
||||
if b.config.RawSSHTimeout == "" {
|
||||
// Default to 1 minute timeouts
|
||||
b.config.RawSSHTimeout = "1m"
|
||||
@@ -125,6 +125,25 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.RawStateTimeout = "6m"
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"client_id": &b.config.ClientID,
|
||||
"api_key": &b.config.APIKey,
|
||||
"snapshot_name": &b.config.SnapshotName,
|
||||
"ssh_username": &b.config.SSHUsername,
|
||||
"ssh_timeout": &b.config.RawSSHTimeout,
|
||||
"event_delay": &b.config.RawEventDelay,
|
||||
"state_timeout": &b.config.RawStateTimeout,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = b.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
// Required configurations that will display errors if not set
|
||||
if b.config.ClientID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
@@ -157,20 +176,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
b.config.stateTimeout = stateTimeout
|
||||
|
||||
// Parse the name of the snapshot
|
||||
snapNameBuf := new(bytes.Buffer)
|
||||
tData := snapshotNameData{
|
||||
strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
t, err := template.New("snapshot").Parse(b.config.RawSnapshotName)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing snapshot_name: %s", err))
|
||||
} else {
|
||||
t.Execute(snapNameBuf, tData)
|
||||
b.config.SnapshotName = snapNameBuf.String()
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -332,8 +332,8 @@ func TestBuilderPrepare_SnapshotName(t *testing.T) {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.RawSnapshotName != "packer-{{.CreateTime}}" {
|
||||
t.Errorf("invalid: %d", b.config.RawSnapshotName)
|
||||
if b.config.SnapshotName == "" {
|
||||
t.Errorf("invalid: %s", b.config.SnapshotName)
|
||||
}
|
||||
|
||||
// Test set
|
||||
@@ -345,7 +345,7 @@ func TestBuilderPrepare_SnapshotName(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test set with template
|
||||
config["snapshot_name"] = "{{.CreateTime}}"
|
||||
config["snapshot_name"] = "{{timestamp}}"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
|
||||
@@ -57,6 +57,7 @@ type config struct {
|
||||
bootWait time.Duration ``
|
||||
shutdownTimeout time.Duration ``
|
||||
sshWaitTimeout time.Duration ``
|
||||
tpl *common.Template
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
@@ -65,6 +66,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = common.NewTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
|
||||
@@ -124,6 +131,53 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
|
||||
}
|
||||
|
||||
// Errors
|
||||
templates := map[string]*string{
|
||||
"guest_additions_path": &b.config.GuestAdditionsPath,
|
||||
"guest_additions_url": &b.config.GuestAdditionsURL,
|
||||
"guest_additions_sha256": &b.config.GuestAdditionsSHA256,
|
||||
"guest_os_type": &b.config.GuestOSType,
|
||||
"http_directory": &b.config.HTTPDir,
|
||||
"iso_checksum": &b.config.ISOChecksum,
|
||||
"iso_checksum_type": &b.config.ISOChecksumType,
|
||||
"iso_url": &b.config.ISOUrl,
|
||||
"output_directory": &b.config.OutputDir,
|
||||
"shutdown_command": &b.config.ShutdownCommand,
|
||||
"ssh_password": &b.config.SSHPassword,
|
||||
"ssh_username": &b.config.SSHUser,
|
||||
"virtualbox_version_file": &b.config.VBoxVersionFile,
|
||||
"vm_name": &b.config.VMName,
|
||||
"boot_wait": &b.config.RawBootWait,
|
||||
"shutdown_timeout": &b.config.RawShutdownTimeout,
|
||||
"ssh_wait_timeout": &b.config.RawSSHWaitTimeout,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = b.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
for i, command := range b.config.BootCommand {
|
||||
if err := b.config.tpl.Validate(command); err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing boot_command[%d]: %s", i, err))
|
||||
}
|
||||
}
|
||||
|
||||
for i, file := range b.config.FloppyFiles {
|
||||
var err error
|
||||
b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing floppy_files[%d]: %s",
|
||||
i, err))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.HTTPPortMin > b.config.HTTPPortMax {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("http_port_min must be less than http_port_max"))
|
||||
@@ -215,6 +269,15 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
|
||||
}
|
||||
|
||||
for i, args := range b.config.VBoxManage {
|
||||
for j, arg := range args {
|
||||
if err := b.config.tpl.Validate(arg); err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing vboxmanage[%d][%d]: %s", i, j, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.driver, err = b.newDriver()
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -49,11 +47,15 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
|
||||
|
||||
ui.Say("Typing the boot command...")
|
||||
for _, command := range config.BootCommand {
|
||||
var buf bytes.Buffer
|
||||
t := template.Must(template.New("boot").Parse(command))
|
||||
t.Execute(&buf, tplData)
|
||||
command, err := config.tpl.Process(command, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing boot command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
for _, code := range scancodes(buf.String()) {
|
||||
for _, code := range scancodes(command) {
|
||||
if code == "wait" {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type guestAdditionsPathTemplate struct {
|
||||
@@ -39,12 +37,16 @@ func (s *stepUploadGuestAdditions) Run(state map[string]interface{}) multistep.S
|
||||
Version: version,
|
||||
}
|
||||
|
||||
var processedPath bytes.Buffer
|
||||
t := template.Must(template.New("path").Parse(config.GuestAdditionsPath))
|
||||
t.Execute(&processedPath, tplData)
|
||||
config.GuestAdditionsPath, err = config.tpl.Process(config.GuestAdditionsPath, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing guest additions path: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Uploading VirtualBox guest additions ISO...")
|
||||
if err := comm.Upload(processedPath.String(), f); err != nil {
|
||||
if err := comm.Upload(config.GuestAdditionsPath, f); err != nil {
|
||||
state["error"] = fmt.Errorf("Error uploading guest additions: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type commandTemplate struct {
|
||||
@@ -40,10 +38,14 @@ func (s *stepVBoxManage) Run(state map[string]interface{}) multistep.StepAction
|
||||
copy(command, originalCommand)
|
||||
|
||||
for i, arg := range command {
|
||||
var buf bytes.Buffer
|
||||
t := template.Must(template.New("arg").Parse(arg))
|
||||
t.Execute(&buf, tplData)
|
||||
command[i] = buf.String()
|
||||
var err error
|
||||
command[i], err = config.tpl.Process(arg, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing vboxmanage command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf("Executing: %s", strings.Join(command, " ")))
|
||||
|
||||
@@ -58,6 +58,7 @@ type config struct {
|
||||
bootWait time.Duration ``
|
||||
shutdownTimeout time.Duration ``
|
||||
sshWaitTimeout time.Duration ``
|
||||
tpl *common.Template
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
@@ -66,6 +67,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = common.NewTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
|
||||
@@ -121,6 +128,72 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.ToolsUploadPath = "{{ .Flavor }}.iso"
|
||||
}
|
||||
|
||||
// Errors
|
||||
templates := map[string]*string{
|
||||
"disk_name": &b.config.DiskName,
|
||||
"guest_os_type": &b.config.GuestOSType,
|
||||
"http_directory": &b.config.HTTPDir,
|
||||
"iso_checksum": &b.config.ISOChecksum,
|
||||
"iso_checksum_type": &b.config.ISOChecksumType,
|
||||
"iso_url": &b.config.ISOUrl,
|
||||
"output_directory": &b.config.OutputDir,
|
||||
"shutdown_command": &b.config.ShutdownCommand,
|
||||
"ssh_password": &b.config.SSHPassword,
|
||||
"ssh_username": &b.config.SSHUser,
|
||||
"tools_upload_flavor": &b.config.ToolsUploadFlavor,
|
||||
"vm_name": &b.config.VMName,
|
||||
"boot_wait": &b.config.RawBootWait,
|
||||
"shutdown_timeout": &b.config.RawShutdownTimeout,
|
||||
"ssh_wait_timeout": &b.config.RawSSHWaitTimeout,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = b.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
for i, command := range b.config.BootCommand {
|
||||
if err := b.config.tpl.Validate(command); err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing boot_command[%d]: %s", i, err))
|
||||
}
|
||||
}
|
||||
|
||||
for i, file := range b.config.FloppyFiles {
|
||||
var err error
|
||||
b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing floppy_files[%d]: %s",
|
||||
i, err))
|
||||
}
|
||||
}
|
||||
|
||||
newVMXData := make(map[string]string)
|
||||
for k, v := range b.config.VMXData {
|
||||
k, err = b.config.tpl.Process(k, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing VMX data key %s: %s", k, err))
|
||||
continue
|
||||
}
|
||||
|
||||
v, err = b.config.tpl.Process(v, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing VMX data value '%s': %s", v, err))
|
||||
continue
|
||||
}
|
||||
|
||||
newVMXData[k] = v
|
||||
}
|
||||
|
||||
b.config.VMXData = newVMXData
|
||||
|
||||
if b.config.HTTPPortMin > b.config.HTTPPortMax {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("http_port_min must be less than http_port_max"))
|
||||
|
||||
@@ -135,3 +135,7 @@ func (d *Workstation9Driver) ToolsIsoPath(flavor string) string {
|
||||
func (d *Workstation9Driver) DhcpLeasesPath(device string) string {
|
||||
return workstationDhcpLeasesPath(device)
|
||||
}
|
||||
|
||||
func (d *Workstation9Driver) VmnetnatConfPath() string {
|
||||
return workstationVmnetnatConfPath()
|
||||
}
|
||||
|
||||
@@ -41,3 +41,7 @@ func workstationDhcpLeasesPath(device string) string {
|
||||
func workstationToolsIsoPath(flavor string) string {
|
||||
return "/usr/lib/vmware/isoimages/" + flavor + ".iso"
|
||||
}
|
||||
|
||||
func workstationVmnetnatConfPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -23,12 +23,7 @@ func workstationFindVdiskManager() (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
path, err = workstationVMwareRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(path, "vmware-vdiskmanager.exe"), nil
|
||||
return findFile("vmware-vdiskmanager.exe", workstationProgramFilePaths()), nil
|
||||
}
|
||||
|
||||
func workstationFindVMware() (string, error) {
|
||||
@@ -37,12 +32,7 @@ func workstationFindVMware() (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
path, err = workstationVMwareRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(path, "vmware.exe"), nil
|
||||
return findFile("vmware.exe", workstationProgramFilePaths()), nil
|
||||
}
|
||||
|
||||
func workstationFindVmrun() (string, error) {
|
||||
@@ -51,30 +41,26 @@ func workstationFindVmrun() (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
path, err = workstationVMwareRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(path, "vmrun.exe"), nil
|
||||
return findFile("vmrun.exe", workstationProgramFilePaths()), nil
|
||||
}
|
||||
|
||||
func workstationToolsIsoPath(flavor string) string {
|
||||
path, err := workstationVMwareRoot()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return filepath.Join(path, flavor+".iso")
|
||||
return findFile(flavor+".iso", workstationProgramFilePaths())
|
||||
}
|
||||
|
||||
func workstationDhcpLeasesPath(device string) string {
|
||||
programData := os.Getenv("ProgramData")
|
||||
if programData == "" {
|
||||
return ""
|
||||
path, err := workstationDhcpLeasesPathRegistry()
|
||||
if err != nil {
|
||||
log.Printf("Error finding leases in registry: %s", err)
|
||||
} else if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
|
||||
return filepath.Join(programData, "/VMware/vmnetdhcp.leases")
|
||||
return findFile("vmnetdhcp.leases", workstationDataFilePaths())
|
||||
}
|
||||
|
||||
func workstationVmnetnatConfPath() string {
|
||||
return findFile("vmnetnat.conf", workstationDataFilePaths())
|
||||
}
|
||||
|
||||
// See http://blog.natefinch.com/2012/11/go-win-stuff.html
|
||||
@@ -126,6 +112,105 @@ func workstationVMwareRoot() (s string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
s = strings.Replace(s, "\\", "/", -1)
|
||||
return
|
||||
return normalizePath(s), nil
|
||||
}
|
||||
|
||||
// This reads the VMware DHCP leases path from the Windows registry.
|
||||
func workstationDhcpLeasesPathRegistry() (s string, err error) {
|
||||
key := "SYSTEM\\CurrentControlSet\\services\\VMnetDHCP\\Parameters"
|
||||
subkey := "LeaseFile"
|
||||
s, err = readRegString(syscall.HKEY_LOCAL_MACHINE, key, subkey)
|
||||
if err != nil {
|
||||
log.Printf(`Unable to read registry key %s\%s`, key, subkey)
|
||||
return
|
||||
}
|
||||
|
||||
return normalizePath(s), nil
|
||||
}
|
||||
|
||||
func normalizePath(path string) string {
|
||||
path = strings.Replace(path, "\\", "/", -1)
|
||||
path = strings.Replace(path, "//", "/", -1)
|
||||
path = strings.TrimRight(path, "/")
|
||||
return path
|
||||
}
|
||||
|
||||
func findFile(file string, paths []string) string {
|
||||
for _, path := range paths {
|
||||
path = filepath.Join(path, file)
|
||||
path = normalizePath(path)
|
||||
log.Printf("Searching for file '%s'", path)
|
||||
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
log.Printf("Found file '%s'", path)
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("File not found: '%s'", file)
|
||||
return ""
|
||||
}
|
||||
|
||||
// workstationProgramFilesPaths returns a list of paths that are eligible
|
||||
// to contain program files we may want just as vmware.exe.
|
||||
func workstationProgramFilePaths() []string {
|
||||
path, err := workstationVMwareRoot()
|
||||
if err != nil {
|
||||
log.Printf("Error finding VMware root: %s", err)
|
||||
}
|
||||
|
||||
paths := make([]string, 0, 5)
|
||||
if os.Getenv("VMWARE_HOME") != "" {
|
||||
paths = append(paths, os.Getenv("VMWARE_HOME"))
|
||||
}
|
||||
|
||||
if path != "" {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
|
||||
if os.Getenv("ProgramFiles(x86)") != "" {
|
||||
paths = append(paths,
|
||||
filepath.Join(os.Getenv("ProgramFiles(x86)"), "/VMware/VMware Workstation"))
|
||||
}
|
||||
|
||||
if os.Getenv("ProgramFiles") != "" {
|
||||
paths = append(paths,
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "/VMware/VMware Workstation"))
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// workstationDataFilePaths returns a list of paths that are eligible
|
||||
// to contain data files we may want such as vmnet NAT configuration files.
|
||||
func workstationDataFilePaths() []string {
|
||||
leasesPath, err := workstationDhcpLeasesPathRegistry()
|
||||
if err != nil {
|
||||
log.Printf("Error getting DHCP leases path: %s", err)
|
||||
}
|
||||
|
||||
if leasesPath != "" {
|
||||
leasesPath = filepath.Dir(leasesPath)
|
||||
}
|
||||
|
||||
paths := make([]string, 0, 5)
|
||||
if os.Getenv("VMWARE_DATA") != "" {
|
||||
paths = append(paths, os.Getenv("VMWARE_DATA"))
|
||||
}
|
||||
|
||||
if leasesPath != "" {
|
||||
paths = append(paths, leasesPath)
|
||||
}
|
||||
|
||||
if os.Getenv("ProgramData") != "" {
|
||||
paths = append(paths,
|
||||
filepath.Join(os.Getenv("ProgramData"), "/VMware"))
|
||||
}
|
||||
|
||||
if os.Getenv("ALLUSERSPROFILE") != "" {
|
||||
paths = append(paths,
|
||||
filepath.Join(os.Getenv("ALLUSERSPROFILE"), "/Application Data/VMware"))
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
@@ -17,15 +16,15 @@ import (
|
||||
type VMnetNatConfIPFinder struct{}
|
||||
|
||||
func (*VMnetNatConfIPFinder) HostIP() (string, error) {
|
||||
programData := os.Getenv("ProgramData")
|
||||
if programData == "" {
|
||||
return "", errors.New("ProgramData directory not found.")
|
||||
driver := &Workstation9Driver{}
|
||||
|
||||
vmnetnat := driver.VmnetnatConfPath()
|
||||
if vmnetnat == "" {
|
||||
return "", errors.New("Could not find NAT vmnet conf file")
|
||||
}
|
||||
|
||||
programData = strings.Replace(programData, "\\", "/", -1)
|
||||
vmnetnat := filepath.Join(programData, "/VMware/vmnetnat.conf")
|
||||
if _, err := os.Stat(vmnetnat); err != nil {
|
||||
return "", fmt.Errorf("Error with vmnetnat.conf: %s", err)
|
||||
return "", fmt.Errorf("Could not find NAT vmnet conf file: %s", vmnetnat)
|
||||
}
|
||||
|
||||
f, err := os.Open(vmnetnat)
|
||||
@@ -62,5 +61,5 @@ func (*VMnetNatConfIPFinder) HostIP() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("host IP not found in NAT config")
|
||||
return "", errors.New("host IP not found in " + vmnetnat)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package vmware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/go-vnc"
|
||||
"github.com/mitchellh/multistep"
|
||||
@@ -10,7 +9,6 @@ import (
|
||||
"net"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -90,11 +88,15 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
|
||||
|
||||
ui.Say("Typing the boot command over VNC...")
|
||||
for _, command := range config.BootCommand {
|
||||
var buf bytes.Buffer
|
||||
t := template.Must(template.New("boot").Parse(command))
|
||||
t.Execute(&buf, tplData)
|
||||
command, err := config.tpl.Process(command, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing boot command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
vncSendString(c, buf.String())
|
||||
vncSendString(c, command)
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package vmware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type toolsUploadPathTemplate struct {
|
||||
@@ -34,11 +32,15 @@ func (*stepUploadTools) Run(state map[string]interface{}) multistep.StepAction {
|
||||
defer f.Close()
|
||||
|
||||
tplData := &toolsUploadPathTemplate{Flavor: config.ToolsUploadFlavor}
|
||||
var processedPath bytes.Buffer
|
||||
t := template.Must(template.New("path").Parse(config.ToolsUploadPath))
|
||||
t.Execute(&processedPath, tplData)
|
||||
config.ToolsUploadPath, err = config.tpl.Process(config.ToolsUploadPath, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing upload path: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if err := comm.Upload(processedPath.String(), f); err != nil {
|
||||
if err := comm.Upload(config.ToolsUploadPath, f); err != nil {
|
||||
state["error"] = fmt.Errorf("Error uploading VMware Tools: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
+49
-59
@@ -4,11 +4,12 @@ import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
cmdcommon "github.com/mitchellh/packer/common/command"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@@ -22,15 +23,13 @@ func (Command) Help() string {
|
||||
func (c Command) Run(env packer.Environment, args []string) int {
|
||||
var cfgDebug bool
|
||||
var cfgForce bool
|
||||
var cfgExcept []string
|
||||
var cfgOnly []string
|
||||
buildOptions := new(cmdcommon.BuildOptions)
|
||||
|
||||
cmdFlags := flag.NewFlagSet("build", flag.ContinueOnError)
|
||||
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||
cmdFlags.BoolVar(&cfgDebug, "debug", false, "debug mode for builds")
|
||||
cmdFlags.BoolVar(&cfgForce, "force", false, "force a build if artifacts exist")
|
||||
cmdFlags.Var((*stringSliceValue)(&cfgExcept), "except", "build all builds except these")
|
||||
cmdFlags.Var((*stringSliceValue)(&cfgOnly), "only", "only build the given builds by name")
|
||||
cmdcommon.BuildOptionFlags(cmdFlags, buildOptions)
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
@@ -41,23 +40,24 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
if len(cfgOnly) > 0 && len(cfgExcept) > 0 {
|
||||
env.Ui().Error("Only one of '-except' or '-only' may be specified.\n")
|
||||
if err := buildOptions.Validate(); err != nil {
|
||||
env.Ui().Error(err.Error())
|
||||
env.Ui().Error("")
|
||||
env.Ui().Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
userVars, err := buildOptions.AllUserVars()
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Error compiling user variables: %s", err))
|
||||
env.Ui().Error("")
|
||||
env.Ui().Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Read the file into a byte array so that we can parse the template
|
||||
log.Printf("Reading template: %s", args[0])
|
||||
tplData, err := ioutil.ReadFile(args[0])
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Failed to read template file: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Parse the template into a machine-usable format
|
||||
log.Println("Parsing template...")
|
||||
tpl, err := packer.ParseTemplate(tplData)
|
||||
tpl, err := packer.ParseTemplateFile(args[0])
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Failed to parse template: %s", err))
|
||||
return 1
|
||||
@@ -72,47 +72,10 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
}
|
||||
|
||||
// Go through each builder and compile the builds that we care about
|
||||
buildNames := tpl.BuildNames()
|
||||
builds := make([]packer.Build, 0, len(buildNames))
|
||||
for _, buildName := range buildNames {
|
||||
if len(cfgExcept) > 0 {
|
||||
found := false
|
||||
for _, only := range cfgExcept {
|
||||
if buildName == only {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
log.Printf("Skipping build '%s' because specified by -except.", buildName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfgOnly) > 0 {
|
||||
found := false
|
||||
for _, only := range cfgOnly {
|
||||
if buildName == only {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Printf("Skipping build '%s' because not specified by -only.", buildName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Creating build: %s", buildName)
|
||||
build, err := tpl.Build(buildName, components)
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Failed to create build '%s': \n\n%s", buildName, err))
|
||||
return 1
|
||||
}
|
||||
|
||||
builds = append(builds, build)
|
||||
builds, err := buildOptions.Builds(tpl, components)
|
||||
if err != nil {
|
||||
env.Ui().Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if cfgDebug {
|
||||
@@ -150,7 +113,7 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
log.Printf("Preparing build: %s", b.Name())
|
||||
b.SetDebug(cfgDebug)
|
||||
b.SetForce(cfgForce)
|
||||
err := b.Prepare()
|
||||
err := b.Prepare(userVars)
|
||||
if err != nil {
|
||||
env.Ui().Error(err.Error())
|
||||
return 1
|
||||
@@ -233,7 +196,16 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
if len(artifacts) > 0 {
|
||||
env.Ui().Say("\n==> Builds finished. The artifacts of successful builds are:")
|
||||
for name, buildArtifacts := range artifacts {
|
||||
for _, artifact := range buildArtifacts {
|
||||
// Create a UI for the machine readable stuff to be targetted
|
||||
ui := &packer.TargettedUi{
|
||||
Target: name,
|
||||
Ui: env.Ui(),
|
||||
}
|
||||
|
||||
// Machine-readable helpful
|
||||
ui.Machine("artifact-count", strconv.FormatInt(int64(len(buildArtifacts)), 10))
|
||||
|
||||
for i, artifact := range buildArtifacts {
|
||||
var message bytes.Buffer
|
||||
fmt.Fprintf(&message, "--> %s: ", name)
|
||||
|
||||
@@ -243,6 +215,24 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
fmt.Fprint(&message, "<nothing>")
|
||||
}
|
||||
|
||||
iStr := strconv.FormatInt(int64(i), 10)
|
||||
if artifact != nil {
|
||||
ui.Machine("artifact", iStr, "builder-id", artifact.BuilderId())
|
||||
ui.Machine("artifact", iStr, "id", artifact.Id())
|
||||
ui.Machine("artifact", iStr, "string", artifact.String())
|
||||
|
||||
files := artifact.Files()
|
||||
ui.Machine("artifact",
|
||||
iStr,
|
||||
"files-count", strconv.FormatInt(int64(len(files)), 10))
|
||||
for fi, file := range files {
|
||||
fiStr := strconv.FormatInt(int64(fi), 10)
|
||||
ui.Machine("artifact", iStr, "file", fiStr, file)
|
||||
}
|
||||
} else {
|
||||
ui.Machine("artifact", iStr, "nil")
|
||||
}
|
||||
|
||||
env.Ui().Say(message.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
func testEnvironment() packer.Environment {
|
||||
config := packer.DefaultEnvironmentConfig()
|
||||
config.Ui = &packer.ReaderWriterUi{
|
||||
config.Ui = &packer.BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ Options:
|
||||
|
||||
-debug Debug mode enabled for builds
|
||||
-force Force a build to continue if artifacts exist, deletes existing artifacts
|
||||
-machine-readable Machine-readable output
|
||||
-except=foo,bar,baz Build all builds other than these
|
||||
-only=foo,bar,baz Only build the given builds by name
|
||||
-var 'key=value' Variable for templates, can be used multiple times.
|
||||
-var-file=path JSON file containing user variables.
|
||||
`
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package build
|
||||
|
||||
import "strings"
|
||||
|
||||
type stringSliceValue []string
|
||||
|
||||
func (s *stringSliceValue) String() string {
|
||||
return strings.Join(*s, ",")
|
||||
}
|
||||
|
||||
func (s *stringSliceValue) Set(value string) error {
|
||||
*s = strings.Split(value, ",")
|
||||
return nil
|
||||
}
|
||||
+13
-2
@@ -6,6 +6,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@@ -49,10 +50,20 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
tplF.Close()
|
||||
|
||||
// Run the template through the various fixers
|
||||
fixers := []Fixer{Fixers["iso-md5"]}
|
||||
fixers := []string{
|
||||
"iso-md5",
|
||||
"createtime",
|
||||
}
|
||||
|
||||
input := templateData
|
||||
for _, fixer := range fixers {
|
||||
for _, name := range fixers {
|
||||
var err error
|
||||
fixer, ok := Fixers[name]
|
||||
if !ok {
|
||||
panic("fixer not found: " + name)
|
||||
}
|
||||
|
||||
log.Printf("Running fixer: %s", name)
|
||||
input, err = fixer.Fix(input)
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Error fixing: %s", err))
|
||||
|
||||
@@ -13,6 +13,7 @@ var Fixers map[string]Fixer
|
||||
|
||||
func init() {
|
||||
Fixers = map[string]Fixer{
|
||||
"iso-md5": new(FixerISOMD5),
|
||||
"iso-md5": new(FixerISOMD5),
|
||||
"createtime": new(FixerCreateTime),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package fix
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// FixerCreateTime is a Fixer that replaces the ".CreateTime" template
|
||||
// calls with "{{timestamp}"
|
||||
type FixerCreateTime struct{}
|
||||
|
||||
func (FixerCreateTime) Fix(input map[string]interface{}) (map[string]interface{}, error) {
|
||||
// Our template type we'll use for this fixer only
|
||||
type template struct {
|
||||
Builders []map[string]interface{}
|
||||
}
|
||||
|
||||
// Decode the input into our structure, if we can
|
||||
var tpl template
|
||||
if err := mapstructure.Decode(input, &tpl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
badKeys := []string{
|
||||
"ami_name",
|
||||
"bundle_prefix",
|
||||
"snapshot_name",
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`{{\s*\.CreateTime\s*}}`)
|
||||
|
||||
// Go through each builder and replace CreateTime if we can
|
||||
for _, builder := range tpl.Builders {
|
||||
for _, key := range badKeys {
|
||||
raw, ok := builder[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
v, ok := raw.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
builder[key] = re.ReplaceAllString(v, "{{timestamp}}")
|
||||
}
|
||||
}
|
||||
|
||||
input["builders"] = tpl.Builders
|
||||
return input, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package fix
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFixerCreateTime_Impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(FixerCreateTime)
|
||||
if _, ok := raw.(Fixer); !ok {
|
||||
t.Fatalf("must be a Fixer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixerCreateTime_Fix(t *testing.T) {
|
||||
var f FixerCreateTime
|
||||
|
||||
input := map[string]interface{}{
|
||||
"builders": []interface{}{
|
||||
map[string]string{
|
||||
"type": "foo",
|
||||
"ami_name": "{{.CreateTime}} foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"builders": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "foo",
|
||||
"ami_name": "{{timestamp}} foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
output, err := f.Fix(input)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(output, expected) {
|
||||
t.Fatalf("unexpected: %#v\nexpected: %#v\n", output, expected)
|
||||
}
|
||||
}
|
||||
+21
-19
@@ -3,8 +3,8 @@ package validate
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
cmdcommon "github.com/mitchellh/packer/common/command"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
@@ -17,10 +17,12 @@ func (Command) Help() string {
|
||||
|
||||
func (c Command) Run(env packer.Environment, args []string) int {
|
||||
var cfgSyntaxOnly bool
|
||||
buildOptions := new(cmdcommon.BuildOptions)
|
||||
|
||||
cmdFlags := flag.NewFlagSet("validate", flag.ContinueOnError)
|
||||
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||
cmdFlags.BoolVar(&cfgSyntaxOnly, "syntax-only", false, "check syntax only")
|
||||
cmdcommon.BuildOptionFlags(cmdFlags, buildOptions)
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
@@ -31,17 +33,24 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Read the file into a byte array so that we can parse the template
|
||||
log.Printf("Reading template: %s", args[0])
|
||||
tplData, err := ioutil.ReadFile(args[0])
|
||||
if err := buildOptions.Validate(); err != nil {
|
||||
env.Ui().Error(err.Error())
|
||||
env.Ui().Error("")
|
||||
env.Ui().Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
userVars, err := buildOptions.AllUserVars()
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Failed to read template file: %s", err))
|
||||
env.Ui().Error(fmt.Sprintf("Error compiling user variables: %s", err))
|
||||
env.Ui().Error("")
|
||||
env.Ui().Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Parse the template into a machine-usable format
|
||||
log.Println("Parsing template...")
|
||||
tpl, err := packer.ParseTemplate(tplData)
|
||||
log.Printf("Reading template: %s", args[0])
|
||||
tpl, err := packer.ParseTemplateFile(args[0])
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Failed to parse template: %s", err))
|
||||
return 1
|
||||
@@ -63,23 +72,16 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
}
|
||||
|
||||
// Otherwise, get all the builds
|
||||
buildNames := tpl.BuildNames()
|
||||
builds := make([]packer.Build, 0, len(buildNames))
|
||||
for _, buildName := range buildNames {
|
||||
log.Printf("Creating build from template for: %s", buildName)
|
||||
build, err := tpl.Build(buildName, components)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Build '%s': %s", buildName, err))
|
||||
continue
|
||||
}
|
||||
|
||||
builds = append(builds, build)
|
||||
builds, err := buildOptions.Builds(tpl, components)
|
||||
if err != nil {
|
||||
env.Ui().Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check the configuration of all builds
|
||||
for _, b := range builds {
|
||||
log.Printf("Preparing build: %s", b.Name())
|
||||
err := b.Prepare()
|
||||
err := b.Prepare(userVars)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Errors validating build '%s'. %s", b.Name(), err))
|
||||
}
|
||||
|
||||
@@ -12,5 +12,9 @@ Usage: packer validate [options] TEMPLATE
|
||||
|
||||
Options:
|
||||
|
||||
-syntax-only Only check syntax. Do not verify config of the template.
|
||||
-syntax-only Only check syntax. Do not verify config of the template.
|
||||
-except=foo,bar,baz Validate all builds other than these
|
||||
-only=foo,bar,baz Validate only these builds
|
||||
-var 'key=value' Variable for templates, can be used multiple times.
|
||||
-var-file=path JSON file containing user variables.
|
||||
`
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BuildOptionFlags sets the proper command line flags needed for
|
||||
// build options.
|
||||
func BuildOptionFlags(fs *flag.FlagSet, f *BuildOptions) {
|
||||
fs.Var((*SliceValue)(&f.Except), "except", "build all builds except these")
|
||||
fs.Var((*SliceValue)(&f.Only), "only", "only build the given builds by name")
|
||||
fs.Var((*userVarValue)(&f.UserVars), "var", "specify a user variable")
|
||||
fs.Var((*AppendSliceValue)(&f.UserVarFiles), "var-file", "file with user variables")
|
||||
}
|
||||
|
||||
// userVarValue is a flag.Value that parses out user variables in
|
||||
// the form of 'key=value' and sets it on this map.
|
||||
type userVarValue map[string]string
|
||||
|
||||
func (v *userVarValue) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *userVarValue) Set(raw string) error {
|
||||
idx := strings.Index(raw, "=")
|
||||
if idx == -1 {
|
||||
return fmt.Errorf("No '=' value in arg: %s", raw)
|
||||
}
|
||||
|
||||
if *v == nil {
|
||||
*v = make(map[string]string)
|
||||
}
|
||||
|
||||
key, value := raw[0:idx], raw[idx+1:]
|
||||
(*v)[key] = value
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildOptionFlags(t *testing.T) {
|
||||
opts := new(BuildOptions)
|
||||
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
BuildOptionFlags(fs, opts)
|
||||
|
||||
args := []string{
|
||||
"-except=foo,bar,baz",
|
||||
"-only=a,b",
|
||||
"-var=foo=bar",
|
||||
"-var", "bar=baz",
|
||||
"-var=foo=bang",
|
||||
"-var-file=foo",
|
||||
"-var-file=bar",
|
||||
}
|
||||
|
||||
err := fs.Parse(args)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{"foo", "bar", "baz"}
|
||||
if !reflect.DeepEqual(opts.Except, expected) {
|
||||
t.Fatalf("bad: %#v", opts.Except)
|
||||
}
|
||||
|
||||
expected = []string{"a", "b"}
|
||||
if !reflect.DeepEqual(opts.Only, expected) {
|
||||
t.Fatalf("bad: %#v", opts.Only)
|
||||
}
|
||||
|
||||
if len(opts.UserVars) != 2 {
|
||||
t.Fatalf("bad: %#v", opts.UserVars)
|
||||
}
|
||||
|
||||
if opts.UserVars["foo"] != "bang" {
|
||||
t.Fatalf("bad: %#v", opts.UserVars)
|
||||
}
|
||||
|
||||
if opts.UserVars["bar"] != "baz" {
|
||||
t.Fatalf("bad: %#v", opts.UserVars)
|
||||
}
|
||||
|
||||
expected = []string{"foo", "bar"}
|
||||
if !reflect.DeepEqual(opts.UserVarFiles, expected) {
|
||||
t.Fatalf("bad: %#v", opts.UserVarFiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserVarValue_implements(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(userVarValue)
|
||||
if _, ok := raw.(flag.Value); !ok {
|
||||
t.Fatalf("userVarValue should be a Value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserVarValueSet(t *testing.T) {
|
||||
sv := new(userVarValue)
|
||||
err := sv.Set("key=value")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
vars := map[string]string(*sv)
|
||||
if vars["key"] != "value" {
|
||||
t.Fatalf("Bad: %#v", vars)
|
||||
}
|
||||
|
||||
// Empty value
|
||||
err = sv.Set("key=")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
vars = map[string]string(*sv)
|
||||
if vars["key"] != "" {
|
||||
t.Fatalf("Bad: %#v", vars)
|
||||
}
|
||||
|
||||
// Equal in value
|
||||
err = sv.Set("key=foo=bar")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
vars = map[string]string(*sv)
|
||||
if vars["key"] != "foo=bar" {
|
||||
t.Fatalf("Bad: %#v", vars)
|
||||
}
|
||||
|
||||
// No equal
|
||||
err = sv.Set("key")
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package command
|
||||
|
||||
import "strings"
|
||||
|
||||
// AppendSliceValue implements the flag.Value interface and allows multiple
|
||||
// calls to the same variable to append a list.
|
||||
type AppendSliceValue []string
|
||||
|
||||
func (s *AppendSliceValue) String() string {
|
||||
return strings.Join(*s, ",")
|
||||
}
|
||||
|
||||
func (s *AppendSliceValue) Set(value string) error {
|
||||
if *s == nil {
|
||||
*s = make([]string, 0, 1)
|
||||
}
|
||||
|
||||
*s = append(*s, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SliceValue implements the flag.Value interface and allows a list of
|
||||
// strings to be given on the command line and properly parsed into a slice
|
||||
// of strings internally.
|
||||
type SliceValue []string
|
||||
|
||||
func (s *SliceValue) String() string {
|
||||
return strings.Join(*s, ",")
|
||||
}
|
||||
|
||||
func (s *SliceValue) Set(value string) error {
|
||||
*s = strings.Split(value, ",")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppendSliceValue_implements(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(AppendSliceValue)
|
||||
if _, ok := raw.(flag.Value); !ok {
|
||||
t.Fatalf("AppendSliceValue should be a Value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendSliceValueSet(t *testing.T) {
|
||||
sv := new(AppendSliceValue)
|
||||
err := sv.Set("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
err = sv.Set("bar")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{"foo", "bar"}
|
||||
if !reflect.DeepEqual([]string(*sv), expected) {
|
||||
t.Fatalf("Bad: %#v", sv)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceValue_implements(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(SliceValue)
|
||||
if _, ok := raw.(flag.Value); !ok {
|
||||
t.Fatalf("SliceValue should be a Value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceValueSet(t *testing.T) {
|
||||
sv := new(SliceValue)
|
||||
err := sv.Set("foo,bar,baz")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{"foo", "bar", "baz"}
|
||||
if !reflect.DeepEqual([]string(*sv), expected) {
|
||||
t.Fatalf("Bad: %#v", sv)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
jsonutil "github.com/mitchellh/packer/common/json"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// BuildOptions is a set of options related to builds that can be set
|
||||
// from the command line.
|
||||
type BuildOptions struct {
|
||||
UserVarFiles []string
|
||||
UserVars map[string]string
|
||||
Except []string
|
||||
Only []string
|
||||
}
|
||||
|
||||
// Validate validates the options
|
||||
func (f *BuildOptions) Validate() error {
|
||||
if len(f.Except) > 0 && len(f.Only) > 0 {
|
||||
return errors.New("Only one of '-except' or '-only' may be specified.")
|
||||
}
|
||||
|
||||
if len(f.UserVarFiles) > 0 {
|
||||
for _, path := range f.UserVarFiles {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return fmt.Errorf("Cannot access: %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllUserVars returns the user variables, compiled from both the
|
||||
// file paths and the vars on the command line.
|
||||
func (f *BuildOptions) AllUserVars() (map[string]string, error) {
|
||||
all := make(map[string]string)
|
||||
|
||||
// Copy in the variables from the files
|
||||
for _, path := range f.UserVarFiles {
|
||||
fileVars, err := readFileVars(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range fileVars {
|
||||
all[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Copy in the command-line vars
|
||||
for k, v := range f.UserVars {
|
||||
all[k] = v
|
||||
}
|
||||
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// Builds returns the builds out of the given template that pass the
|
||||
// configured options.
|
||||
func (f *BuildOptions) Builds(t *packer.Template, cf *packer.ComponentFinder) ([]packer.Build, error) {
|
||||
buildNames := t.BuildNames()
|
||||
builds := make([]packer.Build, 0, len(buildNames))
|
||||
for _, buildName := range buildNames {
|
||||
if len(f.Except) > 0 {
|
||||
found := false
|
||||
for _, except := range f.Except {
|
||||
if buildName == except {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
log.Printf("Skipping build '%s' because specified by -except.", buildName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.Only) > 0 {
|
||||
found := false
|
||||
for _, only := range f.Only {
|
||||
if buildName == only {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Printf("Skipping build '%s' because not specified by -only.", buildName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Creating build: %s", buildName)
|
||||
build, err := t.Build(buildName, cf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to create build '%s': \n\n%s", buildName, err)
|
||||
}
|
||||
|
||||
builds = append(builds, build)
|
||||
}
|
||||
|
||||
return builds, nil
|
||||
}
|
||||
|
||||
func readFileVars(path string) (map[string]string, error) {
|
||||
bytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vars := make(map[string]string)
|
||||
err = jsonutil.Unmarshal(bytes, &vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vars, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildOptionsValidate(t *testing.T) {
|
||||
bf := new(BuildOptions)
|
||||
|
||||
err := bf.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Both set
|
||||
bf.Except = make([]string, 1)
|
||||
bf.Only = make([]string, 1)
|
||||
err = bf.Validate()
|
||||
if err == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
|
||||
// One set
|
||||
bf.Except = make([]string, 1)
|
||||
bf.Only = make([]string, 0)
|
||||
err = bf.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
bf.Except = make([]string, 0)
|
||||
bf.Only = make([]string, 1)
|
||||
err = bf.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildOptionsValidate_userVarFiles(t *testing.T) {
|
||||
bf := new(BuildOptions)
|
||||
|
||||
err := bf.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Non-existent file
|
||||
bf.UserVarFiles = []string{"ireallyshouldntexistanywhere"}
|
||||
err = bf.Validate()
|
||||
if err == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Unmarshal is wrapper around json.Unmarshal that returns user-friendly
|
||||
// errors when there are syntax errors.
|
||||
func Unmarshal(data []byte, i interface{}) error {
|
||||
err := json.Unmarshal(data, i)
|
||||
if err != nil {
|
||||
syntaxErr, ok := err.(*json.SyntaxError)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// We have a syntax error. Extract out the line number and friends.
|
||||
// https://groups.google.com/forum/#!topic/golang-nuts/fizimmXtVfc
|
||||
newline := []byte{'\x0a'}
|
||||
|
||||
// Calculate the start/end position of the line where the error is
|
||||
start := bytes.LastIndex(data[:syntaxErr.Offset], newline) + 1
|
||||
end := len(data)
|
||||
if idx := bytes.Index(data[start:], newline); idx >= 0 {
|
||||
end = start + idx
|
||||
}
|
||||
|
||||
// Count the line number we're on plus the offset in the line
|
||||
line := bytes.Count(data[:start], newline) + 1
|
||||
pos := int(syntaxErr.Offset) - start - 1
|
||||
|
||||
err = fmt.Errorf("Error in line %d, char %d: %s\n%s",
|
||||
line, pos, syntaxErr, data[start:end])
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package json
|
||||
@@ -4,7 +4,9 @@ package common
|
||||
// are sent by packer, properly tagged already so mapstructure can load
|
||||
// them. Embed this structure into your configuration class to get it.
|
||||
type PackerConfig struct {
|
||||
PackerBuildName string `mapstructure:"packer_build_name"`
|
||||
PackerDebug bool `mapstructure:"packer_debug"`
|
||||
PackerForce bool `mapstructure:"packer_force"`
|
||||
PackerBuildName string `mapstructure:"packer_build_name"`
|
||||
PackerBuilderType string `mapstructure:"packer_builder_type"`
|
||||
PackerDebug bool `mapstructure:"packer_debug"`
|
||||
PackerForce bool `mapstructure:"packer_force"`
|
||||
PackerUserVars map[string]string `mapstructure:"packer_user_variables"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Template processes string data as a text/template with some common
|
||||
// elements and functions available. Plugin creators should process as
|
||||
// many fields as possible through this.
|
||||
type Template struct {
|
||||
UserVars map[string]string
|
||||
|
||||
root *template.Template
|
||||
i int
|
||||
}
|
||||
|
||||
// NewTemplate creates a new template processor.
|
||||
func NewTemplate() (*Template, error) {
|
||||
result := &Template{
|
||||
UserVars: make(map[string]string),
|
||||
}
|
||||
|
||||
result.root = template.New("configTemplateRoot")
|
||||
result.root.Funcs(template.FuncMap{
|
||||
"timestamp": templateTimestamp,
|
||||
"user": result.templateUser,
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Process processes a single string, compiling and executing the template.
|
||||
func (t *Template) Process(s string, data interface{}) (string, error) {
|
||||
tpl, err := t.root.New(t.nextTemplateName()).Parse(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := tpl.Execute(buf, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Validate the template.
|
||||
func (t *Template) Validate(s string) error {
|
||||
root, err := t.root.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = root.New("template").Parse(s)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Template) nextTemplateName() string {
|
||||
name := fmt.Sprintf("tpl%d", t.i)
|
||||
t.i++
|
||||
return name
|
||||
}
|
||||
|
||||
// User is the function exposed as "user" within the templates and
|
||||
// looks up user variables.
|
||||
func (t *Template) templateUser(n string) (string, error) {
|
||||
result, ok := t.UserVars[n]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("uknown user var: %s", n)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func templateTimestamp() string {
|
||||
return strconv.FormatInt(time.Now().UTC().Unix(), 10)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTemplateProcess_timestamp(t *testing.T) {
|
||||
tpl, err := NewTemplate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
result, err := tpl.Process(`{{timestamp}}`, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
val, err := strconv.ParseInt(result, 10, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
currentTime := time.Now().UTC().Unix()
|
||||
if math.Abs(float64(currentTime-val)) > 10 {
|
||||
t.Fatalf("val: %d (current: %d)", val, currentTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateProcess_user(t *testing.T) {
|
||||
tpl, err := NewTemplate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
tpl.UserVars["foo"] = "bar"
|
||||
|
||||
result, err := tpl.Process(`{{user "foo"}}`, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if result != "bar" {
|
||||
t.Fatalf("bad: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateValidate(t *testing.T) {
|
||||
tpl, err := NewTemplate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Valid
|
||||
err = tpl.Validate(`{{user "foo"}}`)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Invalid
|
||||
err = tpl.Validate(`{{idontexist}}`)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
@@ -205,9 +205,14 @@ func (c *comm) Download(string, io.Writer) error {
|
||||
panic("not implemented yet")
|
||||
}
|
||||
|
||||
func (c *comm) newSession() (*ssh.Session, error) {
|
||||
func (c *comm) newSession() (session *ssh.Session, err error) {
|
||||
log.Println("opening new ssh session")
|
||||
session, err := c.client.NewSession()
|
||||
if c.client == nil {
|
||||
err = errors.New("client not available")
|
||||
} else {
|
||||
session, err = c.client.NewSession()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("ssh session open error: '%s', attempting reconnect", err)
|
||||
if err := c.reconnect(); err != nil {
|
||||
@@ -225,6 +230,10 @@ func (c *comm) reconnect() (err error) {
|
||||
c.conn.Close()
|
||||
}
|
||||
|
||||
// Set the conn and client to nil since we'll recreate it
|
||||
c.conn = nil
|
||||
c.client = nil
|
||||
|
||||
log.Printf("reconnecting to TCP connection for SSH")
|
||||
c.conn, err = c.config.Connection()
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,9 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// SimpleKeychain makes it easy to use private keys in order to connect
|
||||
// via SSH, since the interface exposed by Go isn't the easiest to use
|
||||
// right away.
|
||||
type SimpleKeychain struct {
|
||||
keys []interface{}
|
||||
}
|
||||
@@ -26,6 +29,20 @@ func (k *SimpleKeychain) AddPEMKey(key string) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// AddPEMKeyPassword adds a PEM encoded private key that is protected by
|
||||
// a password to the keychain.
|
||||
func (k *SimpleKeychain) AddPEMKeyPassword(key string, password string) (err error) {
|
||||
block, _ := pem.Decode([]byte(key))
|
||||
bytes, _ := x509.DecryptPEMBlock(block, []byte(password))
|
||||
rsakey, err := x509.ParsePKCS1PrivateKey(bytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
k.keys = append(k.keys, rsakey)
|
||||
return
|
||||
}
|
||||
|
||||
// Key method for ssh.ClientKeyring interface
|
||||
func (k *SimpleKeychain) Key(i int) (interface{}, error) {
|
||||
if i < 0 || i >= len(k.keys) {
|
||||
|
||||
@@ -76,8 +76,13 @@ func main() {
|
||||
log.Printf("Setting cache directory: %s", cacheDir)
|
||||
cache := &packer.FileCache{CacheDir: cacheDir}
|
||||
|
||||
// Determine if we're in machine-readable mode by mucking around with
|
||||
// the arguments...
|
||||
args, machineReadable := extractMachineReadable(os.Args[1:])
|
||||
|
||||
defer plugin.CleanupClients()
|
||||
|
||||
// Create the environment configuration
|
||||
envConfig := packer.DefaultEnvironmentConfig()
|
||||
envConfig.Cache = cache
|
||||
envConfig.Commands = config.CommandNames()
|
||||
@@ -86,18 +91,25 @@ func main() {
|
||||
envConfig.Components.Hook = config.LoadHook
|
||||
envConfig.Components.PostProcessor = config.LoadPostProcessor
|
||||
envConfig.Components.Provisioner = config.LoadProvisioner
|
||||
if machineReadable {
|
||||
envConfig.Ui = &packer.MachineReadableUi{
|
||||
Writer: os.Stdout,
|
||||
}
|
||||
}
|
||||
|
||||
env, err := packer.NewEnvironment(envConfig)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Packer initialization error: \n\n%s\n", err)
|
||||
plugin.CleanupClients()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupSignalHandlers(env)
|
||||
|
||||
exitCode, err := env.Cli(os.Args[1:])
|
||||
exitCode, err := env.Cli(args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error())
|
||||
plugin.CleanupClients()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -105,6 +117,23 @@ func main() {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
// extractMachineReadable checks the args for the machine readable
|
||||
// flag and returns whether or not it is on. It modifies the args
|
||||
// to remove this flag.
|
||||
func extractMachineReadable(args []string) ([]string, bool) {
|
||||
for i, arg := range args {
|
||||
if arg == "-machine-readable" {
|
||||
// We found it. Slice it out.
|
||||
result := make([]string, len(args)-1)
|
||||
copy(result, args[:i])
|
||||
copy(result[i:], args[i+1:])
|
||||
return result, true
|
||||
}
|
||||
}
|
||||
|
||||
return args, false
|
||||
}
|
||||
|
||||
func loadConfig() (*config, error) {
|
||||
var config config
|
||||
if err := decodeConfig(bytes.NewBufferString(defaultConfig), &config); err != nil {
|
||||
|
||||
+46
-16
@@ -23,6 +23,10 @@ const (
|
||||
// This is the key in configurations that is set to "true" when Packer
|
||||
// force build is enabled.
|
||||
ForceConfigKey = "packer_force"
|
||||
|
||||
// This key contains a map[string]string of the user variables for
|
||||
// template processing.
|
||||
UserVariablesConfigKey = "packer_user_variables"
|
||||
)
|
||||
|
||||
// A Build represents a single job within Packer that is responsible for
|
||||
@@ -35,7 +39,7 @@ type Build interface {
|
||||
|
||||
// Prepare configures the various components of this build and reports
|
||||
// any errors in doing so (such as syntax errors, validation errors, etc.)
|
||||
Prepare() error
|
||||
Prepare(v map[string]string) error
|
||||
|
||||
// Run runs the actual builder, returning an artifact implementation
|
||||
// of what is built. If anything goes wrong, an error is returned.
|
||||
@@ -73,6 +77,7 @@ type coreBuild struct {
|
||||
hooks map[string][]Hook
|
||||
postProcessors [][]coreBuildPostProcessor
|
||||
provisioners []coreBuildProvisioner
|
||||
variables map[string]string
|
||||
|
||||
debug bool
|
||||
force bool
|
||||
@@ -102,8 +107,9 @@ func (b *coreBuild) Name() string {
|
||||
}
|
||||
|
||||
// Prepare prepares the build by doing some initialization for the builder
|
||||
// and any hooks. This _must_ be called prior to Run.
|
||||
func (b *coreBuild) Prepare() (err error) {
|
||||
// and any hooks. This _must_ be called prior to Run. The parameter is the
|
||||
// overrides for the variables within the template (if any).
|
||||
func (b *coreBuild) Prepare(userVars map[string]string) (err error) {
|
||||
b.l.Lock()
|
||||
defer b.l.Unlock()
|
||||
|
||||
@@ -113,11 +119,37 @@ func (b *coreBuild) Prepare() (err error) {
|
||||
|
||||
b.prepareCalled = true
|
||||
|
||||
// Compile the variables
|
||||
variables := make(map[string]string)
|
||||
for k, v := range b.variables {
|
||||
variables[k] = v
|
||||
}
|
||||
|
||||
if userVars != nil {
|
||||
errs := make([]error, 0)
|
||||
for k, v := range userVars {
|
||||
if _, ok := variables[k]; !ok {
|
||||
errs = append(
|
||||
errs, fmt.Errorf("Unknown user variable: %s", k))
|
||||
continue
|
||||
}
|
||||
|
||||
variables[k] = v
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return &MultiError{
|
||||
Errors: errs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packerConfig := map[string]interface{}{
|
||||
BuildNameConfigKey: b.name,
|
||||
BuilderTypeConfigKey: b.builderType,
|
||||
DebugConfigKey: b.debug,
|
||||
ForceConfigKey: b.force,
|
||||
BuildNameConfigKey: b.name,
|
||||
BuilderTypeConfigKey: b.builderType,
|
||||
DebugConfigKey: b.debug,
|
||||
ForceConfigKey: b.force,
|
||||
UserVariablesConfigKey: variables,
|
||||
}
|
||||
|
||||
// Prepare the builder
|
||||
@@ -181,11 +213,10 @@ func (b *coreBuild) Run(originalUi Ui, cache Cache) ([]Artifact, error) {
|
||||
hook := &DispatchHook{hooks}
|
||||
artifacts := make([]Artifact, 0, 1)
|
||||
|
||||
// The builder just has a normal Ui, but prefixed
|
||||
builderUi := &PrefixedUi{
|
||||
fmt.Sprintf("==> %s", b.Name()),
|
||||
fmt.Sprintf(" %s", b.Name()),
|
||||
originalUi,
|
||||
// The builder just has a normal Ui, but targetted
|
||||
builderUi := &TargettedUi{
|
||||
Target: b.Name(),
|
||||
Ui: originalUi,
|
||||
}
|
||||
|
||||
log.Printf("Running builder: %s", b.builderType)
|
||||
@@ -208,10 +239,9 @@ PostProcessorRunSeqLoop:
|
||||
for _, ppSeq := range b.postProcessors {
|
||||
priorArtifact := builderArtifact
|
||||
for i, corePP := range ppSeq {
|
||||
ppUi := &PrefixedUi{
|
||||
fmt.Sprintf("==> %s (%s)", b.Name(), corePP.processorType),
|
||||
fmt.Sprintf(" %s (%s)", b.Name(), corePP.processorType),
|
||||
originalUi,
|
||||
ppUi := &TargettedUi{
|
||||
Target: fmt.Sprintf("%s (%s)", b.Name(), corePP.processorType),
|
||||
Ui: originalUi,
|
||||
}
|
||||
|
||||
builderUi.Say(fmt.Sprintf("Running post-processor: %s", corePP.processorType))
|
||||
|
||||
+81
-23
@@ -23,6 +23,7 @@ func testBuild() *coreBuild {
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "testPP", 42, true},
|
||||
},
|
||||
},
|
||||
variables: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +31,15 @@ func testBuilder() *TestBuilder {
|
||||
return &TestBuilder{}
|
||||
}
|
||||
|
||||
func testDefaultPackerConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
BuildNameConfigKey: "test",
|
||||
BuilderTypeConfigKey: "foo",
|
||||
DebugConfigKey: false,
|
||||
ForceConfigKey: false,
|
||||
UserVariablesConfigKey: make(map[string]string),
|
||||
}
|
||||
}
|
||||
func TestBuild_Name(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
@@ -39,18 +49,12 @@ func TestBuild_Name(t *testing.T) {
|
||||
|
||||
func TestBuild_Prepare(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
packerConfig := map[string]interface{}{
|
||||
BuildNameConfigKey: "test",
|
||||
BuilderTypeConfigKey: "foo",
|
||||
DebugConfigKey: false,
|
||||
ForceConfigKey: false,
|
||||
}
|
||||
packerConfig := testDefaultPackerConfig()
|
||||
|
||||
build := testBuild()
|
||||
builder := build.builder.(*TestBuilder)
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
assert.True(builder.prepareCalled, "prepare should be called")
|
||||
assert.Equal(builder.prepareConfig, []interface{}{42, packerConfig}, "prepare config should be 42")
|
||||
|
||||
@@ -67,7 +71,7 @@ func TestBuild_Prepare(t *testing.T) {
|
||||
|
||||
func TestBuild_Prepare_Twice(t *testing.T) {
|
||||
build := testBuild()
|
||||
if err := build.Prepare(); err != nil {
|
||||
if err := build.Prepare(nil); err != nil {
|
||||
t.Fatalf("bad error: %s", err)
|
||||
}
|
||||
|
||||
@@ -82,24 +86,20 @@ func TestBuild_Prepare_Twice(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
}
|
||||
|
||||
func TestBuild_Prepare_Debug(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
packerConfig := map[string]interface{}{
|
||||
BuildNameConfigKey: "test",
|
||||
BuilderTypeConfigKey: "foo",
|
||||
DebugConfigKey: true,
|
||||
ForceConfigKey: false,
|
||||
}
|
||||
packerConfig := testDefaultPackerConfig()
|
||||
packerConfig[DebugConfigKey] = true
|
||||
|
||||
build := testBuild()
|
||||
builder := build.builder.(*TestBuilder)
|
||||
|
||||
build.SetDebug(true)
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
assert.True(builder.prepareCalled, "prepare should be called")
|
||||
assert.Equal(builder.prepareConfig, []interface{}{42, packerConfig}, "prepare config should be 42")
|
||||
|
||||
@@ -109,6 +109,64 @@ func TestBuild_Prepare_Debug(t *testing.T) {
|
||||
assert.Equal(prov.prepConfigs, []interface{}{42, packerConfig}, "prepare should be called with proper config")
|
||||
}
|
||||
|
||||
func TestBuildPrepare_variables_default(t *testing.T) {
|
||||
packerConfig := testDefaultPackerConfig()
|
||||
packerConfig[UserVariablesConfigKey] = map[string]string{
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
build := testBuild()
|
||||
build.variables["foo"] = "bar"
|
||||
builder := build.builder.(*TestBuilder)
|
||||
|
||||
err := build.Prepare(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !builder.prepareCalled {
|
||||
t.Fatal("prepare should be called")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(builder.prepareConfig[1], packerConfig) {
|
||||
t.Fatalf("prepare bad: %#v", builder.prepareConfig[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPrepare_variables_nonexist(t *testing.T) {
|
||||
build := testBuild()
|
||||
build.variables["foo"] = "bar"
|
||||
|
||||
err := build.Prepare(map[string]string{"bar": "baz"})
|
||||
if err == nil {
|
||||
t.Fatal("should have had error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPrepare_variables_override(t *testing.T) {
|
||||
packerConfig := testDefaultPackerConfig()
|
||||
packerConfig[UserVariablesConfigKey] = map[string]string{
|
||||
"foo": "baz",
|
||||
}
|
||||
|
||||
build := testBuild()
|
||||
build.variables["foo"] = "bar"
|
||||
builder := build.builder.(*TestBuilder)
|
||||
|
||||
err := build.Prepare(map[string]string{"foo": "baz"})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !builder.prepareCalled {
|
||||
t.Fatal("prepare should be called")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(builder.prepareConfig[1], packerConfig) {
|
||||
t.Fatalf("prepare bad: %#v", builder.prepareConfig[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild_Run(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
@@ -116,7 +174,7 @@ func TestBuild_Run(t *testing.T) {
|
||||
ui := testUi()
|
||||
|
||||
build := testBuild()
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
artifacts, err := build.Run(ui, cache)
|
||||
assert.Nil(err, "should not error")
|
||||
assert.Equal(len(artifacts), 2, "should have two artifacts")
|
||||
@@ -152,7 +210,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
build := testBuild()
|
||||
build.postProcessors = [][]coreBuildPostProcessor{}
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
artifacts, err := build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
@@ -177,7 +235,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
artifacts, err = build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
@@ -205,7 +263,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
artifacts, err = build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
@@ -235,7 +293,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
artifacts, err = build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
@@ -262,7 +320,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
artifacts, err = build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
|
||||
+26
-2
@@ -72,6 +72,14 @@ func (r *RemoteCmd) StartWithUi(c Communicator, ui Ui) error {
|
||||
defer stdout_w.Close()
|
||||
defer stderr_w.Close()
|
||||
|
||||
// Retain the original stdout/stderr that we can replace back in.
|
||||
originalStdout := r.Stdout
|
||||
originalStderr := r.Stderr
|
||||
defer func() {
|
||||
r.Stdout = originalStdout
|
||||
r.Stderr = originalStderr
|
||||
}()
|
||||
|
||||
// Set the writers for the output so that we get it streamed to us
|
||||
if r.Stdout == nil {
|
||||
r.Stdout = stdout_w
|
||||
@@ -108,9 +116,9 @@ OutputLoop:
|
||||
for {
|
||||
select {
|
||||
case output := <-stderrCh:
|
||||
ui.Message(strings.TrimSpace(output))
|
||||
ui.Message(r.cleanOutputLine(output))
|
||||
case output := <-stdoutCh:
|
||||
ui.Message(strings.TrimSpace(output))
|
||||
ui.Message(r.cleanOutputLine(output))
|
||||
case <-exitCh:
|
||||
break OutputLoop
|
||||
}
|
||||
@@ -156,3 +164,19 @@ func (r *RemoteCmd) Wait() {
|
||||
|
||||
<-r.exitCh
|
||||
}
|
||||
|
||||
// cleanOutputLine cleans up a line so that '\r' don't muck up the
|
||||
// UI output when we're reading from a remote command.
|
||||
func (r *RemoteCmd) cleanOutputLine(line string) string {
|
||||
// Trim surrounding whitespace
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Trim up to the first carriage return, since that text would be
|
||||
// lost anyways.
|
||||
idx := strings.LastIndex(line, "\r")
|
||||
if idx > -1 {
|
||||
line = line[idx+1:]
|
||||
}
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestRemoteCmd_StartWithUi(t *testing.T) {
|
||||
Stdout: rcOutput,
|
||||
}
|
||||
|
||||
testUi := &ReaderWriterUi{
|
||||
testUi := &BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: uiOutput,
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ type EnvironmentConfig struct {
|
||||
func DefaultEnvironmentConfig() *EnvironmentConfig {
|
||||
config := &EnvironmentConfig{}
|
||||
config.Commands = make([]string, 0)
|
||||
config.Ui = &ReaderWriterUi{
|
||||
config.Ui = &BasicUi{
|
||||
Reader: os.Stdin,
|
||||
Writer: os.Stdout,
|
||||
}
|
||||
@@ -293,12 +293,18 @@ func (e *coreEnvironment) printHelp() {
|
||||
synopsis = command.Synopsis()
|
||||
}
|
||||
|
||||
// Machine-readable output of the available command
|
||||
e.ui.Machine("command", key, synopsis)
|
||||
|
||||
// Pad the key with spaces so that they're all the same width
|
||||
key = fmt.Sprintf("%v%v", key, strings.Repeat(" ", maxKeyLen-len(key)))
|
||||
|
||||
// Output the command and the synopsis
|
||||
e.ui.Say(fmt.Sprintf(" %v %v", key, synopsis))
|
||||
}
|
||||
|
||||
e.ui.Say("\nGlobally recognized options:")
|
||||
e.ui.Say(" -machine-readable Machine-readable output format.")
|
||||
}
|
||||
|
||||
// Returns the UI for the environment. The UI is the interface that should
|
||||
|
||||
@@ -19,7 +19,7 @@ func init() {
|
||||
|
||||
func testEnvironment() Environment {
|
||||
config := DefaultEnvironmentConfig()
|
||||
config.Ui = &ReaderWriterUi{
|
||||
config.Ui = &BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
}
|
||||
@@ -45,8 +45,8 @@ func TestEnvironment_DefaultConfig_Ui(t *testing.T) {
|
||||
config := DefaultEnvironmentConfig()
|
||||
assert.NotNil(config.Ui, "default UI should not be nil")
|
||||
|
||||
rwUi, ok := config.Ui.(*ReaderWriterUi)
|
||||
assert.True(ok, "default UI should be ReaderWriterUi")
|
||||
rwUi, ok := config.Ui.(*BasicUi)
|
||||
assert.True(ok, "default UI should be BasicUi")
|
||||
assert.Equal(rwUi.Writer, os.Stdout, "default UI should go to stdout")
|
||||
assert.Equal(rwUi.Reader, os.Stdin, "default UI should read from stdin")
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func TestEnvironment_DefaultCli_Help(t *testing.T) {
|
||||
|
||||
// A little lambda to help us test the output actually contains help
|
||||
testOutput := func() {
|
||||
buffer := defaultEnv.Ui().(*ReaderWriterUi).Writer.(*bytes.Buffer)
|
||||
buffer := defaultEnv.Ui().(*BasicUi).Writer.(*bytes.Buffer)
|
||||
output := buffer.String()
|
||||
buffer.Reset()
|
||||
assert.True(strings.Contains(output, "usage: packer"), "should print help")
|
||||
@@ -341,7 +341,7 @@ func TestEnvironmentProvisioner_Error(t *testing.T) {
|
||||
func TestEnvironment_SettingUi(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
ui := &ReaderWriterUi{
|
||||
ui := &BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
}
|
||||
|
||||
+4
-4
@@ -30,8 +30,8 @@ func (b *build) Name() (result string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (b *build) Prepare() (err error) {
|
||||
if cerr := b.client.Call("Build.Prepare", new(interface{}), &err); cerr != nil {
|
||||
func (b *build) Prepare(v map[string]string) (err error) {
|
||||
if cerr := b.client.Call("Build.Prepare", v, &err); cerr != nil {
|
||||
return cerr
|
||||
}
|
||||
|
||||
@@ -86,8 +86,8 @@ func (b *BuildServer) Name(args *interface{}, reply *string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BuildServer) Prepare(args interface{}, reply *error) error {
|
||||
*reply = b.build.Prepare()
|
||||
func (b *BuildServer) Prepare(v map[string]string, reply *error) error {
|
||||
*reply = b.build.Prepare(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ var testBuildArtifact = &testArtifact{}
|
||||
type testBuild struct {
|
||||
nameCalled bool
|
||||
prepareCalled bool
|
||||
prepareVars map[string]string
|
||||
runCalled bool
|
||||
runCache packer.Cache
|
||||
runUi packer.Ui
|
||||
@@ -28,8 +29,9 @@ func (b *testBuild) Name() string {
|
||||
return "name"
|
||||
}
|
||||
|
||||
func (b *testBuild) Prepare() error {
|
||||
func (b *testBuild) Prepare(v map[string]string) error {
|
||||
b.prepareCalled = true
|
||||
b.prepareVars = v
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -78,8 +80,15 @@ func TestBuildRPC(t *testing.T) {
|
||||
assert.True(b.nameCalled, "name should be called")
|
||||
|
||||
// Test Prepare
|
||||
bClient.Prepare()
|
||||
bClient.Prepare(map[string]string{"foo": "bar"})
|
||||
assert.True(b.prepareCalled, "prepare should be called")
|
||||
if len(b.prepareVars) != 1 {
|
||||
t.Fatalf("bad vars: %#v", b.prepareVars)
|
||||
}
|
||||
|
||||
if b.prepareVars["foo"] != "bar" {
|
||||
t.Fatalf("bad vars: %#v", b.prepareVars)
|
||||
}
|
||||
|
||||
// Test Run
|
||||
cache := new(testCache)
|
||||
|
||||
@@ -4,6 +4,7 @@ import "encoding/gob"
|
||||
|
||||
func init() {
|
||||
gob.Register(new(map[string]interface{}))
|
||||
gob.Register(new(map[string]string))
|
||||
gob.Register(make([]interface{}, 0))
|
||||
gob.Register(new(BasicError))
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ type UiServer struct {
|
||||
ui packer.Ui
|
||||
}
|
||||
|
||||
// The arguments sent to Ui.Machine
|
||||
type UiMachineArgs struct {
|
||||
Category string
|
||||
Args []string
|
||||
}
|
||||
|
||||
func (u *Ui) Ask(query string) (result string, err error) {
|
||||
err = u.client.Call("Ui.Ask", query, &result)
|
||||
return
|
||||
@@ -28,6 +34,17 @@ func (u *Ui) Error(message string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Ui) Machine(t string, args ...string) {
|
||||
rpcArgs := &UiMachineArgs{
|
||||
Category: t,
|
||||
Args: args,
|
||||
}
|
||||
|
||||
if err := u.client.Call("Ui.Machine", rpcArgs, new(interface{})); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Ui) Message(message string) {
|
||||
if err := u.client.Call("Ui.Message", message, new(interface{})); err != nil {
|
||||
panic(err)
|
||||
@@ -52,6 +69,13 @@ func (u *UiServer) Error(message *string, reply *interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UiServer) Machine(args *UiMachineArgs, reply *interface{}) error {
|
||||
u.ui.Machine(args.Category, args.Args...)
|
||||
|
||||
*reply = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UiServer) Message(message *string, reply *interface{}) error {
|
||||
u.ui.Message(*message)
|
||||
*reply = nil
|
||||
|
||||
@@ -3,6 +3,7 @@ package rpc
|
||||
import (
|
||||
"cgl.tideland.biz/asserts"
|
||||
"net/rpc"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -11,6 +12,9 @@ type testUi struct {
|
||||
askQuery string
|
||||
errorCalled bool
|
||||
errorMessage string
|
||||
machineCalled bool
|
||||
machineType string
|
||||
machineArgs []string
|
||||
messageCalled bool
|
||||
messageMessage string
|
||||
sayCalled bool
|
||||
@@ -28,6 +32,12 @@ func (u *testUi) Error(message string) {
|
||||
u.errorMessage = message
|
||||
}
|
||||
|
||||
func (u *testUi) Machine(t string, args ...string) {
|
||||
u.machineCalled = true
|
||||
u.machineType = t
|
||||
u.machineArgs = args
|
||||
}
|
||||
|
||||
func (u *testUi) Message(message string) {
|
||||
u.messageCalled = true
|
||||
u.messageMessage = message
|
||||
@@ -72,4 +82,18 @@ func TestUiRPC(t *testing.T) {
|
||||
|
||||
uiClient.Say("message")
|
||||
assert.Equal(ui.sayMessage, "message", "message should be correct")
|
||||
|
||||
uiClient.Machine("foo", "bar", "baz")
|
||||
if !ui.machineCalled {
|
||||
t.Fatal("machine should be called")
|
||||
}
|
||||
|
||||
if ui.machineType != "foo" {
|
||||
t.Fatalf("bad type: %#v", ui.machineType)
|
||||
}
|
||||
|
||||
expected := []string{"bar", "baz"}
|
||||
if !reflect.DeepEqual(ui.machineArgs, expected) {
|
||||
t.Fatalf("bad: %#v", ui.machineArgs)
|
||||
}
|
||||
}
|
||||
|
||||
+23
-26
@@ -1,10 +1,10 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
jsonutil "github.com/mitchellh/packer/common/json"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
// "interface{}" pointers since we actually don't know what their contents
|
||||
// are until we read the "type" field.
|
||||
type rawTemplate struct {
|
||||
Variables map[string]string
|
||||
Builders []map[string]interface{}
|
||||
Hooks map[string][]string
|
||||
Provisioners []map[string]interface{}
|
||||
@@ -22,6 +23,7 @@ type rawTemplate struct {
|
||||
// The Template struct represents a parsed template, parsed into the most
|
||||
// completed form it can be without additional processing by the caller.
|
||||
type Template struct {
|
||||
Variables map[string]string
|
||||
Builders map[string]rawBuilderConfig
|
||||
Hooks map[string][]string
|
||||
PostProcessors [][]rawPostProcessorConfig
|
||||
@@ -65,31 +67,8 @@ type rawProvisionerConfig struct {
|
||||
// way.
|
||||
func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
var rawTplInterface interface{}
|
||||
err = json.Unmarshal(data, &rawTplInterface)
|
||||
err = jsonutil.Unmarshal(data, &rawTplInterface)
|
||||
if err != nil {
|
||||
syntaxErr, ok := err.(*json.SyntaxError)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// We have a syntax error. Extract out the line number and friends.
|
||||
// https://groups.google.com/forum/#!topic/golang-nuts/fizimmXtVfc
|
||||
newline := []byte{'\x0a'}
|
||||
|
||||
// Calculate the start/end position of the line where the error is
|
||||
start := bytes.LastIndex(data[:syntaxErr.Offset], newline) + 1
|
||||
end := len(data)
|
||||
if idx := bytes.Index(data[start:], newline); idx >= 0 {
|
||||
end = start + idx
|
||||
}
|
||||
|
||||
// Count the line number we're on plus the offset in the line
|
||||
line := bytes.Count(data[:start], newline) + 1
|
||||
pos := int(syntaxErr.Offset) - start - 1
|
||||
|
||||
err = fmt.Errorf("Error in line %d, char %d: %s\n%s",
|
||||
line, pos, syntaxErr, data[start:end])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -123,11 +102,17 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
}
|
||||
|
||||
t = &Template{}
|
||||
t.Variables = make(map[string]string)
|
||||
t.Builders = make(map[string]rawBuilderConfig)
|
||||
t.Hooks = rawTpl.Hooks
|
||||
t.PostProcessors = make([][]rawPostProcessorConfig, len(rawTpl.PostProcessors))
|
||||
t.Provisioners = make([]rawProvisionerConfig, len(rawTpl.Provisioners))
|
||||
|
||||
// Gather all the variables
|
||||
for k, v := range rawTpl.Variables {
|
||||
t.Variables[k] = v
|
||||
}
|
||||
|
||||
// Gather all the builders
|
||||
for i, v := range rawTpl.Builders {
|
||||
var raw rawBuilderConfig
|
||||
@@ -247,6 +232,17 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// ParseTemplateFile takes the given template file and parses it into
|
||||
// a single template.
|
||||
func ParseTemplateFile(path string) (*Template, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ParseTemplate(data)
|
||||
}
|
||||
|
||||
func parsePostProvisioner(i int, rawV interface{}) (result []map[string]interface{}, errors []error) {
|
||||
switch v := rawV.(type) {
|
||||
case string:
|
||||
@@ -412,6 +408,7 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err
|
||||
hooks: hooks,
|
||||
postProcessors: postProcessors,
|
||||
provisioners: provisioners,
|
||||
variables: t.Variables,
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -2,11 +2,47 @@ package packer
|
||||
|
||||
import (
|
||||
"cgl.tideland.biz/asserts"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testComponentFinder() *ComponentFinder {
|
||||
builderFactory := func(n string) (Builder, error) { return testBuilder(), nil }
|
||||
ppFactory := func(n string) (PostProcessor, error) { return new(TestPostProcessor), nil }
|
||||
provFactory := func(n string) (Provisioner, error) { return new(TestProvisioner), nil }
|
||||
return &ComponentFinder{
|
||||
Builder: builderFactory,
|
||||
PostProcessor: ppFactory,
|
||||
Provisioner: provFactory,
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTemplateFile_basic(t *testing.T) {
|
||||
data := `
|
||||
{
|
||||
"builders": [{"type": "something"}]
|
||||
}
|
||||
`
|
||||
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
tf.Write([]byte(data))
|
||||
tf.Close()
|
||||
|
||||
result, err := ParseTemplateFile(tf.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if len(result.Builders) != 1 {
|
||||
t.Fatalf("bad: %#v", result.Builders)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTemplate_Basic(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
@@ -300,6 +336,28 @@ func TestParseTemplate_Provisioners(t *testing.T) {
|
||||
assert.NotNil(result.Provisioners[0].rawConfig, "should have raw config")
|
||||
}
|
||||
|
||||
func TestParseTemplate_Variables(t *testing.T) {
|
||||
data := `
|
||||
{
|
||||
"variables": {
|
||||
"foo": "bar",
|
||||
"bar": ""
|
||||
},
|
||||
|
||||
"builders": [{"type": "something"}]
|
||||
}
|
||||
`
|
||||
|
||||
result, err := ParseTemplate([]byte(data))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if result.Variables == nil || len(result.Variables) != 2 {
|
||||
t.Fatalf("bad vars: %#v", result.Variables)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplate_BuildNames(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
@@ -613,3 +671,39 @@ func TestTemplate_Build_ProvisionerOverride(t *testing.T) {
|
||||
assert.Equal(len(coreBuild.provisioners), 1, "should have one provisioner")
|
||||
assert.Equal(len(coreBuild.provisioners[0].config), 2, "should have two configs on the provisioner")
|
||||
}
|
||||
|
||||
func TestTemplateBuild_variables(t *testing.T) {
|
||||
data := `
|
||||
{
|
||||
"variables": {
|
||||
"foo": "bar"
|
||||
},
|
||||
|
||||
"builders": [
|
||||
{
|
||||
"name": "test1",
|
||||
"type": "test-builder"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
template, err := ParseTemplate([]byte(data))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
build, err := template.Build("test1", testComponentFinder())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
coreBuild, ok := build.(*coreBuild)
|
||||
if !ok {
|
||||
t.Fatalf("couldn't convert!")
|
||||
}
|
||||
|
||||
if len(coreBuild.variables) != 1 {
|
||||
t.Fatalf("bad vars: %#v", coreBuild.variables)
|
||||
}
|
||||
}
|
||||
|
||||
+94
-23
@@ -11,6 +11,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
@@ -33,6 +34,7 @@ type Ui interface {
|
||||
Say(string)
|
||||
Message(string)
|
||||
Error(string)
|
||||
Machine(string, ...string)
|
||||
}
|
||||
|
||||
// ColoredUi is a UI that is colored using terminal colors.
|
||||
@@ -42,23 +44,32 @@ type ColoredUi struct {
|
||||
Ui Ui
|
||||
}
|
||||
|
||||
// PrefixedUi is a UI that wraps another UI implementation and adds a
|
||||
// prefix to all the messages going out.
|
||||
type PrefixedUi struct {
|
||||
SayPrefix string
|
||||
MessagePrefix string
|
||||
Ui Ui
|
||||
// TargettedUi is a UI that wraps another UI implementation and modifies
|
||||
// the output to indicate a specific target. Specifically, all Say output
|
||||
// is prefixed with the target name. Message output is not prefixed but
|
||||
// is offset by the length of the target so that output is lined up properly
|
||||
// with Say output. Machine-readable output has the proper target set.
|
||||
type TargettedUi struct {
|
||||
Target string
|
||||
Ui Ui
|
||||
}
|
||||
|
||||
// The ReaderWriterUi is a UI that writes and reads from standard Go
|
||||
// io.Reader and io.Writer.
|
||||
type ReaderWriterUi struct {
|
||||
// The BasicUI is a UI that reads and writes from a standard Go reader
|
||||
// and writer. It is safe to be called from multiple goroutines. Machine
|
||||
// readable output is simply logged for this UI.
|
||||
type BasicUi struct {
|
||||
Reader io.Reader
|
||||
Writer io.Writer
|
||||
l sync.Mutex
|
||||
interrupted bool
|
||||
}
|
||||
|
||||
// MachineReadableUi is a UI that only outputs machine-readable output
|
||||
// to the given Writer.
|
||||
type MachineReadableUi struct {
|
||||
Writer io.Writer
|
||||
}
|
||||
|
||||
func (u *ColoredUi) Ask(query string) (string, error) {
|
||||
return u.Ui.Ask(u.colorize(query, u.Color, true))
|
||||
}
|
||||
@@ -80,6 +91,11 @@ func (u *ColoredUi) Error(message string) {
|
||||
u.Ui.Error(u.colorize(message, color, true))
|
||||
}
|
||||
|
||||
func (u *ColoredUi) Machine(t string, args ...string) {
|
||||
// Don't colorize machine-readable output
|
||||
u.Ui.Machine(t, args...)
|
||||
}
|
||||
|
||||
func (u *ColoredUi) colorize(message string, color UiColor, bold bool) string {
|
||||
if !u.supportsColors() {
|
||||
return message
|
||||
@@ -107,33 +123,43 @@ func (u *ColoredUi) supportsColors() bool {
|
||||
return cygwin
|
||||
}
|
||||
|
||||
func (u *PrefixedUi) Ask(query string) (string, error) {
|
||||
return u.Ui.Ask(u.prefixLines(u.SayPrefix, query))
|
||||
func (u *TargettedUi) Ask(query string) (string, error) {
|
||||
return u.Ui.Ask(u.prefixLines(true, query))
|
||||
}
|
||||
|
||||
func (u *PrefixedUi) Say(message string) {
|
||||
u.Ui.Say(u.prefixLines(u.SayPrefix, message))
|
||||
func (u *TargettedUi) Say(message string) {
|
||||
u.Ui.Say(u.prefixLines(true, message))
|
||||
}
|
||||
|
||||
func (u *PrefixedUi) Message(message string) {
|
||||
u.Ui.Message(u.prefixLines(u.MessagePrefix, message))
|
||||
func (u *TargettedUi) Message(message string) {
|
||||
u.Ui.Message(u.prefixLines(false, message))
|
||||
}
|
||||
|
||||
func (u *PrefixedUi) Error(message string) {
|
||||
u.Ui.Error(u.prefixLines(u.SayPrefix, message))
|
||||
func (u *TargettedUi) Error(message string) {
|
||||
u.Ui.Error(u.prefixLines(true, message))
|
||||
}
|
||||
|
||||
func (u *PrefixedUi) prefixLines(prefix, message string) string {
|
||||
func (u *TargettedUi) Machine(t string, args ...string) {
|
||||
// Prefix in the target, then pass through
|
||||
u.Ui.Machine(fmt.Sprintf("%s,%s", u.Target, t), args...)
|
||||
}
|
||||
|
||||
func (u *TargettedUi) prefixLines(arrow bool, message string) string {
|
||||
arrowText := "==>"
|
||||
if !arrow {
|
||||
arrowText = strings.Repeat(" ", len(arrowText))
|
||||
}
|
||||
|
||||
var result bytes.Buffer
|
||||
|
||||
for _, line := range strings.Split(message, "\n") {
|
||||
result.WriteString(fmt.Sprintf("%s: %s\n", prefix, line))
|
||||
result.WriteString(fmt.Sprintf("%s %s: %s\n", arrowText, u.Target, line))
|
||||
}
|
||||
|
||||
return strings.TrimRightFunc(result.String(), unicode.IsSpace)
|
||||
}
|
||||
|
||||
func (rw *ReaderWriterUi) Ask(query string) (string, error) {
|
||||
func (rw *BasicUi) Ask(query string) (string, error) {
|
||||
rw.l.Lock()
|
||||
defer rw.l.Unlock()
|
||||
|
||||
@@ -177,7 +203,7 @@ func (rw *ReaderWriterUi) Ask(query string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *ReaderWriterUi) Say(message string) {
|
||||
func (rw *BasicUi) Say(message string) {
|
||||
rw.l.Lock()
|
||||
defer rw.l.Unlock()
|
||||
|
||||
@@ -188,7 +214,7 @@ func (rw *ReaderWriterUi) Say(message string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *ReaderWriterUi) Message(message string) {
|
||||
func (rw *BasicUi) Message(message string) {
|
||||
rw.l.Lock()
|
||||
defer rw.l.Unlock()
|
||||
|
||||
@@ -199,7 +225,7 @@ func (rw *ReaderWriterUi) Message(message string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *ReaderWriterUi) Error(message string) {
|
||||
func (rw *BasicUi) Error(message string) {
|
||||
rw.l.Lock()
|
||||
defer rw.l.Unlock()
|
||||
|
||||
@@ -209,3 +235,48 @@ func (rw *ReaderWriterUi) Error(message string) {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *BasicUi) Machine(t string, args ...string) {
|
||||
log.Printf("machine readable: %s %#v", t, args)
|
||||
}
|
||||
|
||||
func (u *MachineReadableUi) Ask(query string) (string, error) {
|
||||
return "", errors.New("machine-readable UI can't ask")
|
||||
}
|
||||
|
||||
func (u *MachineReadableUi) Say(message string) {
|
||||
u.Machine("ui", "say", message)
|
||||
}
|
||||
|
||||
func (u *MachineReadableUi) Message(message string) {
|
||||
u.Machine("ui", "message", message)
|
||||
}
|
||||
|
||||
func (u *MachineReadableUi) Error(message string) {
|
||||
u.Machine("ui", "error", message)
|
||||
}
|
||||
|
||||
func (u *MachineReadableUi) Machine(category string, args ...string) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
// Determine if we have a target, and set it
|
||||
target := ""
|
||||
commaIdx := strings.Index(category, ",")
|
||||
if commaIdx > -1 {
|
||||
target = category[0:commaIdx]
|
||||
category = category[commaIdx+1:]
|
||||
}
|
||||
|
||||
// Prepare the args
|
||||
for i, v := range args {
|
||||
args[i] = strings.Replace(v, ",", "%!(PACKER_COMMA)", -1)
|
||||
args[i] = strings.Replace(args[i], "\r", "\\r", -1)
|
||||
args[i] = strings.Replace(args[i], "\n", "\\n", -1)
|
||||
}
|
||||
argsString := strings.Join(args, ",")
|
||||
|
||||
_, err := fmt.Fprintf(u.Writer, "%d,%s,%s,%s\n", now.Unix(), target, category, argsString)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
+75
-21
@@ -3,11 +3,12 @@ package packer
|
||||
import (
|
||||
"bytes"
|
||||
"cgl.tideland.biz/asserts"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testUi() *ReaderWriterUi {
|
||||
return &ReaderWriterUi{
|
||||
func testUi() *BasicUi {
|
||||
return &BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
}
|
||||
@@ -36,23 +37,26 @@ func TestColoredUi(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixedUi(t *testing.T) {
|
||||
func TestTargettedUi(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
bufferUi := testUi()
|
||||
prefixUi := &PrefixedUi{"mitchell", "bar", bufferUi}
|
||||
targettedUi := &TargettedUi{
|
||||
Target: "foo",
|
||||
Ui: bufferUi,
|
||||
}
|
||||
|
||||
prefixUi.Say("foo")
|
||||
assert.Equal(readWriter(bufferUi), "mitchell: foo\n", "should have prefix")
|
||||
targettedUi.Say("foo")
|
||||
assert.Equal(readWriter(bufferUi), "==> foo: foo\n", "should have prefix")
|
||||
|
||||
prefixUi.Message("foo")
|
||||
assert.Equal(readWriter(bufferUi), "bar: foo\n", "should have prefix")
|
||||
targettedUi.Message("foo")
|
||||
assert.Equal(readWriter(bufferUi), " foo: foo\n", "should have prefix")
|
||||
|
||||
prefixUi.Error("bar")
|
||||
assert.Equal(readWriter(bufferUi), "mitchell: bar\n", "should have prefix")
|
||||
targettedUi.Error("bar")
|
||||
assert.Equal(readWriter(bufferUi), "==> foo: bar\n", "should have prefix")
|
||||
|
||||
prefixUi.Say("foo\nbar")
|
||||
assert.Equal(readWriter(bufferUi), "mitchell: foo\nmitchell: bar\n", "should multiline")
|
||||
targettedUi.Say("foo\nbar")
|
||||
assert.Equal(readWriter(bufferUi), "==> foo: foo\n==> foo: bar\n", "should multiline")
|
||||
}
|
||||
|
||||
func TestColoredUi_ImplUi(t *testing.T) {
|
||||
@@ -63,23 +67,23 @@ func TestColoredUi_ImplUi(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixedUi_ImplUi(t *testing.T) {
|
||||
func TestTargettedUi_ImplUi(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &PrefixedUi{}
|
||||
raw = &TargettedUi{}
|
||||
if _, ok := raw.(Ui); !ok {
|
||||
t.Fatalf("PrefixedUi must implement Ui")
|
||||
t.Fatalf("TargettedUi must implement Ui")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReaderWriterUi_ImplUi(t *testing.T) {
|
||||
func TestBasicUi_ImplUi(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &ReaderWriterUi{}
|
||||
raw = &BasicUi{}
|
||||
if _, ok := raw.(Ui); !ok {
|
||||
t.Fatalf("ReaderWriterUi must implement Ui")
|
||||
t.Fatalf("BasicUi must implement Ui")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReaderWriterUi_Error(t *testing.T) {
|
||||
func TestBasicUi_Error(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
bufferUi := testUi()
|
||||
@@ -91,7 +95,7 @@ func TestReaderWriterUi_Error(t *testing.T) {
|
||||
assert.Equal(readWriter(bufferUi), "5\n", "formatting")
|
||||
}
|
||||
|
||||
func TestReaderWriterUi_Say(t *testing.T) {
|
||||
func TestBasicUi_Say(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
bufferUi := testUi()
|
||||
@@ -103,9 +107,59 @@ func TestReaderWriterUi_Say(t *testing.T) {
|
||||
assert.Equal(readWriter(bufferUi), "5\n", "formatting")
|
||||
}
|
||||
|
||||
func TestMachineReadableUi_ImplUi(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &MachineReadableUi{}
|
||||
if _, ok := raw.(Ui); !ok {
|
||||
t.Fatalf("MachineReadableUi must implement Ui")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMachineReadableUi(t *testing.T) {
|
||||
var data, expected string
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
ui := &MachineReadableUi{Writer: buf}
|
||||
|
||||
// No target
|
||||
ui.Machine("foo", "bar", "baz")
|
||||
data = strings.SplitN(buf.String(), ",", 2)[1]
|
||||
expected = ",foo,bar,baz\n"
|
||||
if data != expected {
|
||||
t.Fatalf("bad: %s", data)
|
||||
}
|
||||
|
||||
// Target
|
||||
buf.Reset()
|
||||
ui.Machine("mitchellh,foo", "bar", "baz")
|
||||
data = strings.SplitN(buf.String(), ",", 2)[1]
|
||||
expected = "mitchellh,foo,bar,baz\n"
|
||||
if data != expected {
|
||||
t.Fatalf("bad: %s", data)
|
||||
}
|
||||
|
||||
// Commas
|
||||
buf.Reset()
|
||||
ui.Machine("foo", "foo,bar")
|
||||
data = strings.SplitN(buf.String(), ",", 2)[1]
|
||||
expected = ",foo,foo%!(PACKER_COMMA)bar\n"
|
||||
if data != expected {
|
||||
t.Fatalf("bad: %s", data)
|
||||
}
|
||||
|
||||
// New lines
|
||||
buf.Reset()
|
||||
ui.Machine("foo", "foo\n")
|
||||
data = strings.SplitN(buf.String(), ",", 2)[1]
|
||||
expected = ",foo,foo\\n\n"
|
||||
if data != expected {
|
||||
t.Fatalf("bad: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
// This reads the output from the bytes.Buffer in our test object
|
||||
// and then resets the buffer.
|
||||
func readWriter(ui *ReaderWriterUi) (result string) {
|
||||
func readWriter(ui *BasicUi) (result string) {
|
||||
buffer := ui.Writer.(*bytes.Buffer)
|
||||
result = buffer.String()
|
||||
buffer.Reset()
|
||||
|
||||
+5
-1
@@ -10,7 +10,7 @@ import (
|
||||
var GitCommit string
|
||||
|
||||
// The version of packer.
|
||||
const Version = "0.2.3"
|
||||
const Version = "0.3.1"
|
||||
|
||||
// Any pre-release marker for the version. If this is "" (empty string),
|
||||
// then it means that it is a final release. Otherwise, this is the
|
||||
@@ -27,6 +27,10 @@ command-line flags for this command.`
|
||||
}
|
||||
|
||||
func (versionCommand) Run(env Environment, args []string) int {
|
||||
env.Ui().Machine("version", Version)
|
||||
env.Ui().Machine("version-prelease", VersionPrerelease)
|
||||
env.Ui().Machine("version-commit", GitCommit)
|
||||
|
||||
var versionString bytes.Buffer
|
||||
fmt.Fprintf(&versionString, "Packer v%s", Version)
|
||||
if VersionPrerelease != "" {
|
||||
|
||||
@@ -1 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractMachineReadable(t *testing.T) {
|
||||
var args, expected, result []string
|
||||
var mr bool
|
||||
|
||||
// Not
|
||||
args = []string{"foo", "bar", "baz"}
|
||||
result, mr = extractMachineReadable(args)
|
||||
expected = []string{"foo", "bar", "baz"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Fatalf("bad: %#v", result)
|
||||
}
|
||||
|
||||
if mr {
|
||||
t.Fatal("should not be mr")
|
||||
}
|
||||
|
||||
// Yes
|
||||
args = []string{"foo", "-machine-readable", "baz"}
|
||||
result, mr = extractMachineReadable(args)
|
||||
expected = []string{"foo", "baz"}
|
||||
if !reflect.DeepEqual(result, expected) {
|
||||
t.Fatalf("bad: %#v", result)
|
||||
}
|
||||
|
||||
if !mr {
|
||||
t.Fatal("should be mr")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,15 @@ import (
|
||||
)
|
||||
|
||||
type config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
// The local path of the file to upload.
|
||||
Source string
|
||||
|
||||
// The remote path where the local file will be uploaded to.
|
||||
Destination string
|
||||
|
||||
tpl *common.Template
|
||||
}
|
||||
|
||||
type Provisioner struct {
|
||||
@@ -26,9 +30,29 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
p.config.tpl, err = common.NewTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.config.tpl.UserVars = p.config.PackerUserVars
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
|
||||
templates := map[string]*string{
|
||||
"source": &p.config.Source,
|
||||
"destination": &p.config.Destination,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = p.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(p.config.Source); err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Bad source '%s': %s", p.config.Source, err))
|
||||
|
||||
@@ -106,6 +106,9 @@ func (su *stubUi) Ask(string) (string, error) {
|
||||
func (su *stubUi) Error(string) {
|
||||
}
|
||||
|
||||
func (su *stubUi) Machine(string, ...string) {
|
||||
}
|
||||
|
||||
func (su *stubUi) Message(string) {
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
const DefaultTempConfigDir = "/tmp/salt"
|
||||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
// If true, run the salt-bootstrap script
|
||||
SkipBootstrap bool `mapstructure:"skip_bootstrap"`
|
||||
BootstrapArgs string `mapstructure:"bootstrap_args"`
|
||||
@@ -24,6 +26,8 @@ type Config struct {
|
||||
|
||||
// Where files will be copied before moving to the /srv/salt directory
|
||||
TempConfigDir string `mapstructure:"temp_config_dir"`
|
||||
|
||||
tpl *common.Template
|
||||
}
|
||||
|
||||
type Provisioner struct {
|
||||
@@ -36,6 +40,12 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
p.config.tpl, err = common.NewTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.config.tpl.UserVars = p.config.PackerUserVars
|
||||
|
||||
if p.config.TempConfigDir == "" {
|
||||
p.config.TempConfigDir = DefaultTempConfigDir
|
||||
}
|
||||
@@ -43,12 +53,26 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
|
||||
if p.config.LocalStateTree == "" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
errors.New("Please specify a local_state_tree"))
|
||||
} else if _, err := os.Stat(p.config.LocalStateTree); err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
errors.New("local_state_tree must exist and be accessible"))
|
||||
templates := map[string]*string{
|
||||
"bootstrap_args": &p.config.BootstrapArgs,
|
||||
"local_state_tree": &p.config.LocalStateTree,
|
||||
"temp_config_dir": &p.config.TempConfigDir,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = p.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
if p.config.LocalStateTree != "" {
|
||||
if _, err := os.Stat(p.config.LocalStateTree); err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
errors.New("local_state_tree must exist and be accessible"))
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
@@ -74,7 +98,11 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
|
||||
ui.Message(fmt.Sprintf("Creating remote directory: %s", p.config.TempConfigDir))
|
||||
cmd := &packer.RemoteCmd{Command: fmt.Sprintf("mkdir -p %s", p.config.TempConfigDir)}
|
||||
if err = cmd.StartWithUi(comm, ui); err != nil {
|
||||
if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
|
||||
}
|
||||
|
||||
return fmt.Errorf("Error creating remote salt state directory: %s", err)
|
||||
}
|
||||
|
||||
@@ -85,20 +113,22 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
|
||||
ui.Message(fmt.Sprintf("Moving %s to /srv/salt", p.config.TempConfigDir))
|
||||
cmd = &packer.RemoteCmd{Command: fmt.Sprintf("sudo mv %s /srv/salt", p.config.TempConfigDir)}
|
||||
if err = cmd.StartWithUi(comm, ui); err != nil {
|
||||
if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
|
||||
}
|
||||
|
||||
return fmt.Errorf("Unable to move %s to /srv/salt: %d", p.config.TempConfigDir, err)
|
||||
}
|
||||
|
||||
ui.Message("Running highstate")
|
||||
cmd = &packer.RemoteCmd{Command: "sudo salt-call --local state.highstate -l info"}
|
||||
if err = cmd.StartWithUi(comm, ui); err != nil {
|
||||
return fmt.Errorf("Error executing highstate: %s", err)
|
||||
}
|
||||
if err = cmd.StartWithUi(comm, ui); err != nil || cmd.ExitStatus != 0 {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus)
|
||||
}
|
||||
|
||||
ui.Message("Removing /srv/salt")
|
||||
cmd = &packer.RemoteCmd{Command: "sudo rm -r /srv/salt"}
|
||||
if err = cmd.StartWithUi(comm, ui); err != nil {
|
||||
return fmt.Errorf("Unable to remove /srv/salt: %d", err)
|
||||
return fmt.Errorf("Error executing highstate: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -107,6 +137,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
func UploadLocalDirectory(localDir string, remoteDir string, comm packer.Communicator, ui packer.Ui) (err error) {
|
||||
visitPath := func(localPath string, f os.FileInfo, err error) (err2 error) {
|
||||
localRelPath := strings.Replace(localPath, localDir, "", 1)
|
||||
localRelPath = strings.Replace(localRelPath, "\\", "/", -1)
|
||||
remotePath := fmt.Sprintf("%s%s", remoteDir, localRelPath)
|
||||
if f.IsDir() && f.Name() == ".git" {
|
||||
return filepath.SkipDir
|
||||
|
||||
@@ -4,7 +4,6 @@ package shell
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/common"
|
||||
@@ -13,12 +12,14 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultRemotePath = "/tmp/script.sh"
|
||||
|
||||
type config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
// An inline script to execute. Multiple strings are all executed
|
||||
// in the context of a single shell.
|
||||
Inline []string
|
||||
@@ -45,9 +46,13 @@ type config struct {
|
||||
// can be used to inject the environment_vars into the environment.
|
||||
ExecuteCommand string `mapstructure:"execute_command"`
|
||||
|
||||
// Packer configurations, these come from Packer itself
|
||||
PackerBuildName string `mapstructure:"packer_build_name"`
|
||||
PackerBuilderType string `mapstructure:"packer_builder_type"`
|
||||
// The timeout for retrying to start the process. Until this timeout
|
||||
// is reached, if the provisioner can't start a process, it retries.
|
||||
// This can be set high to allow for reboots.
|
||||
RawStartRetryTimeout string `mapstructure:"start_retry_timeout"`
|
||||
|
||||
startRetryTimeout time.Duration
|
||||
tpl *common.Template
|
||||
}
|
||||
|
||||
type Provisioner struct {
|
||||
@@ -65,6 +70,12 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
p.config.tpl, err = common.NewTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.config.tpl.UserVars = p.config.PackerUserVars
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
|
||||
@@ -80,6 +91,10 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
p.config.InlineShebang = "/bin/sh"
|
||||
}
|
||||
|
||||
if p.config.RawStartRetryTimeout == "" {
|
||||
p.config.RawStartRetryTimeout = "5m"
|
||||
}
|
||||
|
||||
if p.config.RemotePath == "" {
|
||||
p.config.RemotePath = DefaultRemotePath
|
||||
}
|
||||
@@ -101,6 +116,39 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
p.config.Scripts = []string{p.config.Script}
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"inline_shebang": &p.config.InlineShebang,
|
||||
"script": &p.config.Script,
|
||||
"start_retry_timeout": &p.config.RawStartRetryTimeout,
|
||||
"remote_path": &p.config.RemotePath,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = p.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
sliceTemplates := map[string][]string{
|
||||
"inline": p.config.Inline,
|
||||
"scripts": p.config.Scripts,
|
||||
"environment_vars": p.config.Vars,
|
||||
}
|
||||
|
||||
for n, slice := range sliceTemplates {
|
||||
for i, elem := range slice {
|
||||
var err error
|
||||
slice[i], err = p.config.tpl.Process(elem, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(p.config.Scripts) == 0 && p.config.Inline == nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
errors.New("Either a script file or inline script must be specified."))
|
||||
@@ -125,6 +173,14 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
if p.config.RawStartRetryTimeout != "" {
|
||||
p.config.startRetryTimeout, err = time.ParseDuration(p.config.RawStartRetryTimeout)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing start_retry_timeout: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
@@ -193,14 +249,35 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
flattendVars := strings.Join(envVars, " ")
|
||||
|
||||
// Compile the command
|
||||
var command bytes.Buffer
|
||||
t := template.Must(template.New("command").Parse(p.config.ExecuteCommand))
|
||||
t.Execute(&command, &ExecuteCommandTemplate{flattendVars, p.config.RemotePath})
|
||||
command, err := p.config.tpl.Process(p.config.ExecuteCommand, &ExecuteCommandTemplate{
|
||||
Vars: flattendVars,
|
||||
Path: p.config.RemotePath,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error processing command: %s", err)
|
||||
}
|
||||
|
||||
cmd := &packer.RemoteCmd{Command: command.String()}
|
||||
cmd := &packer.RemoteCmd{Command: command}
|
||||
startTimeout := time.After(p.config.startRetryTimeout)
|
||||
log.Printf("Executing command: %s", cmd.Command)
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
return fmt.Errorf("Failed executing command: %s", err)
|
||||
for {
|
||||
if err := cmd.StartWithUi(comm, ui); err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Create an error and log it
|
||||
err = fmt.Errorf("Error executing command: %s", err)
|
||||
log.Printf(err.Error())
|
||||
|
||||
// Check if we timed out, otherwise we retry. It is safe to
|
||||
// retry since the only error case above is if the command
|
||||
// failed to START.
|
||||
select {
|
||||
case <-startTimeout:
|
||||
return err
|
||||
default:
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.ExitStatus != 0 {
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )"
|
||||
cd $DIR
|
||||
|
||||
# Get the git commit
|
||||
GIT_COMMIT=$(git rev-parse --short HEAD)
|
||||
GIT_COMMIT=$(git rev-parse HEAD)
|
||||
GIT_DIRTY=$(test -n "`git status --porcelain`" && echo "+CHANGES" || true)
|
||||
|
||||
# Compile the main Packer app
|
||||
|
||||
@@ -48,5 +48,18 @@ list as contributors come and go.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="person">
|
||||
<img class="pull-left" src="http://www.gravatar.com/avatar/2f7fc9cb7558e3ea48f5a86fa90a78da.png?s=125">
|
||||
<div class="bio">
|
||||
<h3>Mark Peek (<a href="https://github.com/markpeek">@markpeek</a>)</h3>
|
||||
<p>
|
||||
In addition to Packer, Mark Peek helps maintain
|
||||
various open source projects such as
|
||||
<a href="https://github.com/cloudtools">cloudtools</a> and
|
||||
<a href="https://github.com/ironport">IronPort Python libraries</a>.
|
||||
Mark is also a <a href="https://FreeBSD.org">FreeBSD committer</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
@@ -62,8 +62,8 @@ Required:
|
||||
|
||||
* `ami_name` (string) - The name of the resulting AMI that will appear
|
||||
when managing AMIs in the AWS console or via APIs. This must be unique.
|
||||
To help make this unique, certain template parameters are available for
|
||||
this value, which are documented below.
|
||||
To help make this unique, use a function like `timestamp` (see
|
||||
[configuration templates](/docs/templates/configuration-templates.html) for more info)
|
||||
|
||||
* `secret_key` (string) - The secret key used to communicate with AWS.
|
||||
If not specified, Packer will attempt to read this from environmental
|
||||
@@ -78,6 +78,21 @@ Required:
|
||||
|
||||
Optional:
|
||||
|
||||
* `ami_description` (string) - The description to set for the resulting
|
||||
AMI(s). By default this description is empty.
|
||||
|
||||
* `ami_groups` (array of string) - A list of groups that have access
|
||||
to launch the resulting AMI(s). By default no groups have permission
|
||||
to launch the AMI.
|
||||
|
||||
* `ami_product_codes` (array of string) - A list of product codes to
|
||||
associate with the AMI. By default no product codes are associated with
|
||||
the AMI.
|
||||
|
||||
* `ami_users` (array of string) - A list of account IDs that have access
|
||||
to launch the resulting AMI(s). By default no additional users other than the user
|
||||
creating the AMI has permissions to launch it.
|
||||
|
||||
* `chroot_mounts` (list of list of strings) - This is a list of additional
|
||||
devices to mount into the chroot environment. This configuration parameter
|
||||
requires some additional documentation which is in the "Chroot Mounts" section
|
||||
@@ -115,22 +130,10 @@ Here is a basic example. It is completely valid except for the access keys:
|
||||
"access_key": "YOUR KEY HERE",
|
||||
"secret_key": "YOUR SECRET KEY HERE",
|
||||
"source_ami": "ami-e81d5881",
|
||||
"ami_name": "packer-amazon-chroot {{.CreateTime}}"
|
||||
"ami_name": "packer-amazon-chroot {{timestamp}}"
|
||||
}
|
||||
</pre>
|
||||
|
||||
## AMI Name Variables
|
||||
|
||||
The AMI name specified by the `ami_name` configuration variable is actually
|
||||
treated as a [configuration template](/docs/templates/configuration-templates.html).
|
||||
Packer provides a set of variables that it will replace
|
||||
within the AMI name. This helps ensure the AMI name is unique, as AWS requires.
|
||||
|
||||
The available variables are shown below:
|
||||
|
||||
* `CreateTime` - This will be replaced with the Unix timestamp of when
|
||||
the AMI was built.
|
||||
|
||||
## Chroot Mounts
|
||||
|
||||
The `chroot_mounts` configuration can be used to mount additional devices
|
||||
|
||||
@@ -37,8 +37,8 @@ Required:
|
||||
|
||||
* `ami_name` (string) - The name of the resulting AMI that will appear
|
||||
when managing AMIs in the AWS console or via APIs. This must be unique.
|
||||
To help make this unique, certain template parameters are available for
|
||||
this value, which are documented below.
|
||||
To help make this unique, use a function like `timestamp` (see
|
||||
[configuration templates](/docs/templates/configuration-templates.html) for more info)
|
||||
|
||||
* `instance_type` (string) - The EC2 instance type to use while building
|
||||
the AMI, such as "m1.small".
|
||||
@@ -58,6 +58,21 @@ Required:
|
||||
|
||||
Optional:
|
||||
|
||||
* `ami_description` (string) - The description to set for the resulting
|
||||
AMI(s). By default this description is empty.
|
||||
|
||||
* `ami_groups` (array of string) - A list of groups that have access
|
||||
to launch the resulting AMI(s). By default no groups have permission
|
||||
to launch the AMI.
|
||||
|
||||
* `ami_product_codes` (array of string) - A list of product codes to
|
||||
associate with the AMI. By default no product codes are associated with
|
||||
the AMI.
|
||||
|
||||
* `ami_users` (array of string) - A list of account IDs that have access
|
||||
to launch the resulting AMI(s). By default no additional users other than the user
|
||||
creating the AMI has permissions to launch it.
|
||||
|
||||
* `iam_instance_profile` (string) - The name of an
|
||||
[IAM instance profile](http://docs.aws.amazon.com/IAM/latest/UserGuide/instance-profiles.html)
|
||||
to launch the EC2 instance with.
|
||||
@@ -80,6 +95,14 @@ Optional:
|
||||
|
||||
* `tags` (object of key/value strings) - Tags applied to the AMI.
|
||||
|
||||
* `user_data` (string) - User data to apply when launching the instance.
|
||||
Note that you need to be careful about escaping characters due to the
|
||||
templates being JSON. It is often more convenient to use `user_data_file`,
|
||||
instead.
|
||||
|
||||
* `user_data_file` (string) - Path to a file that will be used for the
|
||||
user data when launching the instance.
|
||||
|
||||
* `vpc_id` (string) - If launching into a VPC subnet, Packer needs the
|
||||
VPC ID in order to create a temporary security group within the VPC.
|
||||
|
||||
@@ -96,7 +119,7 @@ Here is a basic example. It is completely valid except for the access keys:
|
||||
"source_ami": "ami-de0d9eb7",
|
||||
"instance_type": "t1.micro",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-quick-start {{.CreateTime}}",
|
||||
"ami_name": "packer-quick-start {{timestamp}}",
|
||||
}
|
||||
</pre>
|
||||
|
||||
@@ -121,22 +144,10 @@ Here is an example using the optional AMI tags. This will add the tags
|
||||
"source_ami": "ami-de0d9eb7",
|
||||
"instance_type": "t1.micro",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-quick-start {{.CreateTime}}",
|
||||
"ami_name": "packer-quick-start {{timestamp}}",
|
||||
"tags": {
|
||||
"OS_Version": "Ubuntu",
|
||||
"Release": "Latest"
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
|
||||
## AMI Name Variables
|
||||
|
||||
The AMI name specified by the `ami_name` configuration variable is actually
|
||||
treated as a [configuration template](/docs/templates/configuration-templates.html).
|
||||
Packer provides a set of variables that it will replace
|
||||
within the AMI name. This helps ensure the AMI name is unique, as AWS requires.
|
||||
|
||||
The available variables are shown below:
|
||||
|
||||
* `CreateTime` - This will be replaced with the Unix timestamp of when
|
||||
the AMI was built.
|
||||
|
||||
@@ -41,8 +41,8 @@ Required:
|
||||
|
||||
* `ami_name` (string) - The name of the resulting AMI that will appear
|
||||
when managing AMIs in the AWS console or via APIs. This must be unique.
|
||||
To help make this unique, certain template parameters are available for
|
||||
this value, which are documented below.
|
||||
To help make this unique, use a function like `timestamp` (see
|
||||
[configuration templates](/docs/templates/configuration-templates.html) for more info)
|
||||
|
||||
* `instance_type` (string) - The EC2 instance type to use while building
|
||||
the AMI, such as "m1.small".
|
||||
@@ -73,12 +73,27 @@ Required:
|
||||
|
||||
Optional:
|
||||
|
||||
* `ami_description` (string) - The description to set for the resulting
|
||||
AMI(s). By default this description is empty.
|
||||
|
||||
* `ami_groups` (array of string) - A list of groups that have access
|
||||
to launch the resulting AMI(s). By default no groups have permission
|
||||
to launch the AMI.
|
||||
|
||||
* `ami_product_codes` (array of string) - A list of product codes to
|
||||
associate with the AMI. By default no product codes are associated with
|
||||
the AMI.
|
||||
|
||||
* `ami_users` (array of string) - A list of account IDs that have access
|
||||
to launch the resulting AMI(s). By default no additional users other than the user
|
||||
creating the AMI has permissions to launch it.
|
||||
|
||||
* `bundle_destination` (string) - The directory on the running instance
|
||||
where the bundled AMI will be saved prior to uploading. By default this is
|
||||
"/tmp". This directory must exist and be writable.
|
||||
|
||||
* `bundle_prefix` (string) - The prefix for files created from bundling
|
||||
the root volume. By default this is "image-{{.Createtime}}". The `CreateTime`
|
||||
the root volume. By default this is "image-{{timestamp}}". The `timestamp`
|
||||
variable should be used to make sure this is unique, otherwise it can
|
||||
collide with other created AMIs by Packer in your account.
|
||||
|
||||
@@ -111,6 +126,14 @@ Optional:
|
||||
|
||||
* `tags` (object of key/value strings) - Tags applied to the AMI.
|
||||
|
||||
* `user_data` (string) - User data to apply when launching the instance.
|
||||
Note that you need to be careful about escaping characters due to the
|
||||
templates being JSON. It is often more convenient to use `user_data_file`,
|
||||
instead.
|
||||
|
||||
* `user_data_file` (string) - Path to a file that will be used for the
|
||||
user data when launching the instance.
|
||||
|
||||
* `vpc_id` (string) - If launching into a VPC subnet, Packer needs the
|
||||
VPC ID in order to create a temporary security group within the VPC.
|
||||
|
||||
@@ -140,7 +163,7 @@ Here is a basic example. It is completely valid except for the access keys:
|
||||
"x509_key_path": "x509.key",
|
||||
"x509_upload_path": "/tmp",
|
||||
|
||||
"ami_name": "packer-quick-start {{.CreateTime}}"
|
||||
"ami_name": "packer-quick-start {{timestamp}}"
|
||||
}
|
||||
</pre>
|
||||
|
||||
@@ -151,18 +174,6 @@ the section above for more information on what environmental variables Packer
|
||||
will look for.
|
||||
</div>
|
||||
|
||||
## AMI Name Variables
|
||||
|
||||
The AMI name specified by the `ami_name` configuration variable is actually
|
||||
treated as a [configuration template](/docs/templates/configuration-templates.html).
|
||||
Packer provides a set of variables that it will replace
|
||||
within the AMI name. This helps ensure the AMI name is unique, as AWS requires.
|
||||
|
||||
The available variables are shown below:
|
||||
|
||||
* `CreateTime` - This will be replaced with the Unix timestamp of when
|
||||
the AMI was built.
|
||||
|
||||
## Custom Bundle Commands
|
||||
|
||||
A lot of the process required for creating an instance-store backed AMI
|
||||
@@ -202,6 +213,14 @@ sudo -n ec2-bundle-vol \
|
||||
The available template variables should be self-explanatory based on the
|
||||
parameters they're used to satisfy the `ec2-bundle-vol` command.
|
||||
|
||||
<div class="alert alert-block">
|
||||
<strong>Warning!</strong> Some versions of ec2-bundle-vol silently
|
||||
ignore all .pem and .gpg files during the bundling of the AMI, which can
|
||||
cause problems on some systems, such as Ubuntu. You may want to
|
||||
customize the bundle volume command to include those files (see the
|
||||
<code>--no-filter</code> option of ec2-bundle-vol).
|
||||
</div>
|
||||
|
||||
### Bundle Upload Command
|
||||
|
||||
The default value for `bundle_upload_command` is shown below. It is split
|
||||
|
||||
@@ -52,9 +52,9 @@ Optional:
|
||||
which is the 512MB droplet.
|
||||
|
||||
* `snapshot_name` (string) - The name of the resulting snapshot that will
|
||||
appear in your account. This must be unique. To help make this unique,
|
||||
certain template parameters are available for this value, and are documented
|
||||
below.
|
||||
appear in your account. This must be unique.
|
||||
To help make this unique, use a function like `timestamp` (see
|
||||
[configuration templates](/docs/templates/configuration-templates.html) for more info)
|
||||
|
||||
* `ssh_port` (int) - The port that SSH will be available on. Defaults to port
|
||||
22.
|
||||
@@ -83,17 +83,6 @@ own access tokens:
|
||||
}
|
||||
</pre>
|
||||
|
||||
## Snapshot Name Variables
|
||||
|
||||
The `snapshot_name` for a DigitalOcean image must be unique. The configuration
|
||||
variable is actually treated as a [configuration template](/docs/templates/configuration-templates.html) and has various variables available to help keep this unique. Read
|
||||
the prior linked page for information on syntax if you're unfamiliar with it.
|
||||
|
||||
The available variables are shown below:
|
||||
|
||||
* `CreateTime` - This will be replaced with the Unix timestamp of when the
|
||||
image is created.
|
||||
|
||||
## Finding Image, Region, and Size IDs
|
||||
|
||||
Unfortunately, finding a list of available values for `image_id`, `region_id`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
layout: "docs"
|
||||
page_title: "Build - Command-Line"
|
||||
---
|
||||
|
||||
# Command-Line: Build
|
||||
@@ -22,7 +23,7 @@ artifacts that are created will be outputted at the end of the build.
|
||||
In general, a builder supporting the forced build will remove the artifacts from
|
||||
the previous build. This will allow the user to repeat a build without having to
|
||||
manually clean these artifacts beforehand.
|
||||
|
||||
|
||||
* `-except=foo,bar,baz` - Builds all the builds except those with the given
|
||||
comma-separated names. Build names by default are the names of their builders,
|
||||
unless a specific `name` attribute is specified within the configuration.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
layout: "docs"
|
||||
page_title: "Command-line: Fix"
|
||||
page_title: "Fix - Command-Line"
|
||||
---
|
||||
|
||||
# Command-Line: Fix
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
layout: "docs"
|
||||
page_title: "Machine-Readable Output - Command-Line"
|
||||
---
|
||||
|
||||
# Machine-Readable Output
|
||||
|
||||
By default, the output of Packer is very human-readable. It uses nice
|
||||
formatting, spacing, and colors in order to make Packer a pleasure to use.
|
||||
However, Packer was built with automation in mind. To that end, Packer
|
||||
supports a fully machine-readable output setting, allowing you to use
|
||||
Packer in automated environments.
|
||||
|
||||
The machine-readable output format is easy to use and read and was made
|
||||
with Unix tools in mind, so it is awk/sed/grep/etc. friendly.
|
||||
|
||||
## Enabling
|
||||
|
||||
The machine-readable output format can be enabled by passing the
|
||||
`-machine-readable` flag to any Packer command. This immediately enables
|
||||
all output to become machine-readable on stdout. Logging, if enabled,
|
||||
continues to appear on stderr. An example of the output is shown
|
||||
below:
|
||||
|
||||
```
|
||||
$ packer -machine-readable version
|
||||
1376289459,,version,0.2.4
|
||||
1376289459,,version-prerelease,
|
||||
1376289459,,version-commit,eed6ece
|
||||
1376289459,,ui,say,Packer v0.2.4.dev (eed6ece+CHANGES)
|
||||
```
|
||||
|
||||
The format will be covered in more detail later. But as you can see,
|
||||
the output immediately becomes machine-friendly. Try some other commands
|
||||
with the `-machine-readable` flag to see!
|
||||
|
||||
## Format
|
||||
|
||||
The machine readable format is a line-oriented, comma-delimeted text
|
||||
format. This makes it extremely to parse using standard Unix tools such
|
||||
as awk or grep in addition to full programming languages like Ruby or
|
||||
Python.
|
||||
|
||||
The format is:
|
||||
|
||||
```
|
||||
timestamp,target,type,data...
|
||||
```
|
||||
|
||||
Each component is explained below:
|
||||
|
||||
* **timestamp** is a Unix timestamp in UTC of when the message was
|
||||
printed.
|
||||
|
||||
* **target** is the target of the following output. This is empty if
|
||||
the message is related to Packer globally. Otherwise, this is generally
|
||||
a build name so you can relate output to a specific build while parallel
|
||||
builds are running.
|
||||
|
||||
* **type** is the type of machine-readable message being outputted. There
|
||||
are a set of standard types which are covered later, but each component
|
||||
of Packer (builders, provisioners, etc.) may output their own custom types
|
||||
as well, allowing the machine-readable output to be infinitely flexible.
|
||||
|
||||
* **data** is zero or more comma-seperated values associated with the prior
|
||||
type. The exact amount and meaning of this data is type-dependent, so you
|
||||
must read the documentation associated with the type to understand fully.
|
||||
|
||||
Within the format, if data contains a comma, it is replaced with
|
||||
`%!(PACKER_COMMA)`. This was preferred over an escape character such as
|
||||
`\'` because it is more friendly to tools like awk.
|
||||
|
||||
Newlines within the format are replaced with their respective standard
|
||||
escape sequence. Newlines become a literal `\n` within the output. Carriage
|
||||
returns become a literal `\r`.
|
||||
|
||||
## Message Types
|
||||
|
||||
The set of machine-readable message types can be found in the
|
||||
[machine-readable format](/docs/machine-readable/index.html)
|
||||
complete documentation section. This section contains documentation
|
||||
on all the message types exposed by Packer core as well as all the
|
||||
components that ship with Packer by default.
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
layout: "docs"
|
||||
page_title: "Validate - Command-Line"
|
||||
---
|
||||
|
||||
# Command-Line: Validate
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
layout: "docs_machine_readable"
|
||||
page_title: "Command: build - Machine-Readable Reference"
|
||||
---
|
||||
|
||||
# Build Command Types
|
||||
|
||||
These are the machine-readable types that exist as part of the output
|
||||
of `packer build`.
|
||||
|
||||
<dl>
|
||||
<dt>artifact (>= 2)</dt>
|
||||
<dd>
|
||||
<p>
|
||||
Information about an artifact of the targetted item. This is a
|
||||
fairly complex (but uniform!) machine-readable type that contains
|
||||
subtypes. The subtypes are documented within this page in the
|
||||
syntax of "artifact subtype: SUBTYPE". The number of arguments within
|
||||
that subtype is in addition to the artifact args.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Data 1: index</strong> - The zero-based index of the
|
||||
artifact being described. This goes up to "artifact-count" (see
|
||||
below).
|
||||
</p>
|
||||
<p>
|
||||
<strong>Data 2: subtype</strong> - The subtype that describes
|
||||
the remaining arguments. See the documentation for the
|
||||
subtype docs throughout this page.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Data 3..n: subtype data</strong> - Zero or more additional
|
||||
data points related to the subtype. The exact count and meaning
|
||||
of this subtypes comes from the subtype documentation.
|
||||
</p>
|
||||
</dd>
|
||||
|
||||
<dt>artifact-count (1)</dt>
|
||||
<dd>
|
||||
<p>
|
||||
The number of artifacts associated with the given target.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Data 1: count</strong> - The number of artifacts as
|
||||
a base 10 integer.
|
||||
</p>
|
||||
</dd>
|
||||
|
||||
<dt>artifact subtype: builder-id (1)</dt>
|
||||
<dd>
|
||||
<p>
|
||||
The unique ID of the builder that created this artifact.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Data 1: id</strong> - The unique ID of the builder.
|
||||
</p>
|
||||
</dd>
|
||||
|
||||
<dt>artifact subtype: file (2)</dt>
|
||||
<dd>
|
||||
<p>
|
||||
A single file associated with the artifact. There are 0 to
|
||||
"files-count" of these entries to describe every file that is
|
||||
part of the artifact.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Data 1: index</strong> - Zero-based index of the file.
|
||||
This goes from 0 to "files-count" minus one.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Data 2: filename</strong> - The filename.
|
||||
</p>
|
||||
</dd>
|
||||
|
||||
<dt>artifact subtype: files-count (1)</dt>
|
||||
<dd>
|
||||
<p>
|
||||
The number of files associated with this artifact. Not all
|
||||
artifacts have files associated with it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Data 1: count</strong> - The number of files.
|
||||
</p>
|
||||
</dd>
|
||||
|
||||
<dt>artifact subtype: id (1)</dt>
|
||||
<dd>
|
||||
<p>
|
||||
The ID (if any) of the artifact that was built. Not all artifacts
|
||||
have associated IDs. For example, AMIs built have IDs associated
|
||||
with them, but VirtualBox images do not. The exact format of the ID
|
||||
is specific to the builder.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Data 1: id</strong> - The ID of the artifact.
|
||||
</p>
|
||||
</dd>
|
||||
|
||||
<dt>artifact subtype: nil (0)</dt>
|
||||
<dd>
|
||||
<p>
|
||||
If present, this means that the artifact was nil, or that the targetted
|
||||
build completed successfully but no artifact was created.
|
||||
</p>
|
||||
</dd>
|
||||
|
||||
<dt>artifact subtype: string (1)</dt>
|
||||
<dd>
|
||||
<p>
|
||||
The human-readable string description of the artifact provided by
|
||||
the artifact itself.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Data 1: string</strong> - The string output for the artifact.
|
||||
</p>
|
||||
</dd>
|
||||
</dl>
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
layout: "docs_machine_readable"
|
||||
page_title: "Command: version - Machine-Readable Reference"
|
||||
---
|
||||
|
||||
# Version Command Types
|
||||
|
||||
These are the machine-readable types that exist as part of the output
|
||||
of `packer version`.
|
||||
|
||||
<dl>
|
||||
<dt>version (1)</dt>
|
||||
<dd>
|
||||
<p>The version number of Packer running.</p>
|
||||
|
||||
<p>
|
||||
<strong>Data 1: version</strong> - The version of Packer running,
|
||||
only including the major, minor, and patch versions. Example:
|
||||
"0.2.4".
|
||||
</p>
|
||||
</dd>
|
||||
|
||||
<dt>version-commit (1)</dt>
|
||||
<dd>
|
||||
<p>The SHA1 of the Git commit that built this version of Packer.</p>
|
||||
|
||||
<p>
|
||||
<strong>Data 1: commit SHA1</strong> - The SHA1 of the commit.
|
||||
</p>
|
||||
</dd>
|
||||
|
||||
<dt>version-prerelease (1)</dt>
|
||||
<dd>
|
||||
<p>
|
||||
The prerelease tag (if any) for the running version of Packer. This
|
||||
can be "beta", "dev", "alpha", etc. If this is empty, you can assume
|
||||
it is a release version running.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Data 1: prerelease name</strong> - The name of the
|
||||
prerelease tag.
|
||||
</p>
|
||||
</dd>
|
||||
</dl>
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
layout: "docs_machine_readable"
|
||||
page_title: "General Types - Machine-Readable Reference"
|
||||
---
|
||||
|
||||
# General Types
|
||||
|
||||
These are the machine-readable types that can appear in almost any
|
||||
machine-readable output and are provided by Packer core itself.
|
||||
|
||||
<dl>
|
||||
<dt>ui (2)</dt>
|
||||
<dd>
|
||||
<p>
|
||||
Specifies the output and type of output that would've normally
|
||||
gone to the console if Packer wasn't running in human-readable
|
||||
mode.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Data 1: type</strong> - The type of UI message that would've
|
||||
been outputted. Can be "say", "message", or "error".
|
||||
</p>
|
||||
<p>
|
||||
<strong>Data 2: output</strong> - The UI message that would have
|
||||
been outputted.
|
||||
</p>
|
||||
</dd>
|
||||
</dl>
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
layout: "docs_machine_readable"
|
||||
page_title: "Machine-Readable Reference"
|
||||
---
|
||||
|
||||
# Machine-Readable Reference
|
||||
|
||||
This is the reference for the various message categories for Packer
|
||||
[machine-readable output](/docs/command-line/machine-readable.html).
|
||||
Please read that page if you're unfamiliar with the general format and
|
||||
usage for the machine-readable output.
|
||||
|
||||
The layout of this reference is split into where the types come from.
|
||||
There are a set of core types that are from Packer core itself. Then
|
||||
there are types that come from various components of Packer such as the
|
||||
builders, provisioners, and more.
|
||||
|
||||
Within each section, the format of the documentation is the following:
|
||||
|
||||
<br>
|
||||
|
||||
<dl>
|
||||
<dt>type-name (data-count)</dt>
|
||||
<dd>
|
||||
<p>Description of the type.</p>
|
||||
<p>
|
||||
<strong>Data 1: name</strong> - Description.
|
||||
</p>
|
||||
</dd>
|
||||
</dl>
|
||||
@@ -6,7 +6,8 @@ layout: "docs"
|
||||
|
||||
Type: `salt-masterless`
|
||||
|
||||
The salt-masterless provisioner provisions machines built by Packer using [Salt](http://saltstack.com/) states.
|
||||
The `salt-masterless` provisioner provisions machines built by Packer using
|
||||
[Salt](http://saltstack.com/) states, without connecting to a Salt master.
|
||||
|
||||
## Basic Example
|
||||
|
||||
@@ -15,7 +16,6 @@ The example below is fully functional.
|
||||
<pre class="prettyprint">
|
||||
{
|
||||
"type": "salt-masterless",
|
||||
"bootstrap_args": "git v0.16.0"
|
||||
"local_state_tree": "/Users/me/salt"
|
||||
}
|
||||
</pre>
|
||||
@@ -24,15 +24,21 @@ The example below is fully functional.
|
||||
|
||||
The reference of available configuration options is listed below. The only required argument is the path to your local salt state tree.
|
||||
|
||||
Required:
|
||||
|
||||
* `local_state_tree` (string) - The path to your local [state tree](http://docs.saltstack.com/ref/states/highstate.html#the-salt-state-tree). This will be uploaded to the `/srv/salt` on the remote, and removed before shutdown.
|
||||
|
||||
Optional:
|
||||
|
||||
* `skip_bootstrap` (boolean) - By default the salt provisioner runs [salt bootstrap](https://github.com/saltstack/salt-bootstrap) to install salt. Set this to true to skip this step.
|
||||
* `local_state_tree` (string) - The path to your local
|
||||
[state tree](http://docs.saltstack.com/ref/states/highstate.html#the-salt-state-tree).
|
||||
This will be uploaded to the `/srv/salt` on the remote.
|
||||
|
||||
* `boostrap_args` (string) -
|
||||
Arguments to send to the bootstrap script. Usage is somewhat documented on [github](https://github.com/saltstack/salt-bootstrap), but the [script itself](https://github.com/saltstack/salt-bootstrap/blob/develop/bootstrap-salt.sh) has more detailed usage instructions. By default, no arguments are sent to the script.
|
||||
* `skip_bootstrap` (boolean) - By default the salt provisioner runs
|
||||
[salt bootstrap](https://github.com/saltstack/salt-bootstrap) to install
|
||||
salt. Set this to true to skip this step.
|
||||
|
||||
* `temp_config_dir` (string) - Where your local state tree will be copied before moving to the `/srv/salt` directory. Default is `/tmp/salt`.
|
||||
* `boostrap_args` (string) - Arguments to send to the bootstrap script. Usage
|
||||
is somewhat documented on [github](https://github.com/saltstack/salt-bootstrap),
|
||||
but the [script itself](https://github.com/saltstack/salt-bootstrap/blob/develop/bootstrap-salt.sh)
|
||||
has more detailed usage instructions. By default, no arguments are sent to
|
||||
the script.
|
||||
|
||||
* `temp_config_dir` (string) - Where your local state tree will be copied
|
||||
before moving to the `/srv/salt` directory. Default is `/tmp/salt`.
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
layout: "docs"
|
||||
---
|
||||
|
||||
# Salt Masterless Provisioner
|
||||
|
||||
Type: `salt-masterless`
|
||||
|
||||
The `salt-masterless` provisioner provisions machines built by Packer using
|
||||
[Salt](http://saltstack.com/) states, without connecting to a Salt master.
|
||||
|
||||
## Basic Example
|
||||
|
||||
The example below is fully functional.
|
||||
|
||||
<pre class="prettyprint">
|
||||
{
|
||||
"type": "salt-masterless",
|
||||
"local_state_tree": "/Users/me/salt"
|
||||
}
|
||||
</pre>
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
The reference of available configuration options is listed below. The only required argument is the path to your local salt state tree.
|
||||
|
||||
Required:
|
||||
|
||||
* `local_state_tree` (string) - The path to your local
|
||||
[state tree](http://docs.saltstack.com/ref/states/highstate.html#the-salt-state-tree).
|
||||
This will be uploaded to the `/srv/salt` on the remote, and removed before
|
||||
shutdown.
|
||||
|
||||
Optional:
|
||||
|
||||
* `skip_bootstrap` (boolean) - By default the salt provisioner runs
|
||||
[salt bootstrap](https://github.com/saltstack/salt-bootstrap) to install
|
||||
salt. Set this to true to skip this step.
|
||||
|
||||
* `boostrap_args` (string) - Arguments to send to the bootstrap script. Usage
|
||||
is somewhat documented on [github](https://github.com/saltstack/salt-bootstrap),
|
||||
but the [script itself](https://github.com/saltstack/salt-bootstrap/blob/develop/bootstrap-salt.sh)
|
||||
has more detailed usage instructions. By default, no arguments are sent to
|
||||
the script.
|
||||
|
||||
* `temp_config_dir` (string) - Where your local state tree will be copied
|
||||
before moving to the `/srv/salt` directory. Default is `/tmp/salt`.
|
||||
@@ -67,6 +67,12 @@ Optional parameters:
|
||||
in the machine. This defaults to "/tmp/script.sh". This value must be
|
||||
a writable location and any parent directories must already exist.
|
||||
|
||||
* `start_retry_timeout` (string) - The amount of time to attempt to
|
||||
_start_ the remote process. By default this is "5m" or 5 minutes. This
|
||||
setting exists in order to deal with times when SSH may restart, such as
|
||||
a system reboot. Set this to a higher value if reboots take a longer
|
||||
amount of time.
|
||||
|
||||
## Execute Command Example
|
||||
|
||||
To many new users, the `execute_command` is puzzling. However, it provides
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
---
|
||||
layout: "docs"
|
||||
page_title: "Configuration Templates"
|
||||
---
|
||||
|
||||
# Configuration Templates
|
||||
|
||||
Certain configuration parameters within templates are themselves a
|
||||
type of "template." These are not Packer templates, but text templates,
|
||||
where variables can be used to modify the value of a configuration parameter
|
||||
during runtime.
|
||||
All strings within templates are processed by a common Packer templating
|
||||
engine, where variables and functions can be used to modify the value of
|
||||
a configuration parameter at runtime.
|
||||
|
||||
For example, the `ami_name` configuration for the [AMI builder](/docs/builders/amazon-ebs.html)
|
||||
is a template. An example value may be `My Packer AMI {{.CreateTime}}`. At
|
||||
runtime, this will be turned into `My Packer AMI 1370900368`, where the
|
||||
"CreateTime" variable was replaced with the Unix timestamp of when the
|
||||
AMI was actually created.
|
||||
For example, the `{{timestamp}}` function can be used in any string to
|
||||
generate the current timestamp. This is useful for configurations that require
|
||||
unique keys, such as AMI names. By setting the AMI name to something like
|
||||
`My Packer AMI {{timestamp}}`, the AMI name will be unique down to the second.
|
||||
|
||||
This sort of templating is pervasive throughout Packer. Instead of documenting
|
||||
the templating syntax in each location, it is documented once here so
|
||||
you know how to use it.
|
||||
In addition to globally available functions like timestamp shown before,
|
||||
some configurations have special local variables that are available only
|
||||
for that configuration. These are recognizable because they're prefixed by
|
||||
a period, such as `{{.Name}}`.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>For advanced users:</strong> The templates are actually parsed and executed
|
||||
using Go's <a href="http://golang.org/pkg/text/template/">text/template</a>
|
||||
package. It therefore supports the complete template syntax.
|
||||
</div>
|
||||
The complete syntax is covered in the next section, followed by a reference
|
||||
of globally available functions.
|
||||
|
||||
## Syntax
|
||||
|
||||
99% of the time all you'll need within configuration templates are variables.
|
||||
Variables are accessed by using `{{.VARIABLENAME}}`. The "." prefixing the
|
||||
variable name signals that you're accessing a variable on the root
|
||||
template object. All template directives go between braces. Here is a piece
|
||||
of a VMware VMX template that uses variables:
|
||||
The syntax of templates is extremely simple. Anything template related
|
||||
happens within double-braces: `{{ }}`. Variables are prefixed with a period
|
||||
and capitalized, such as `{{.Variable}}` and functions are just directly
|
||||
within the braces, such as `{{timestamp}}`.
|
||||
|
||||
Here is an example from the VMware VMX template that shows configuration
|
||||
templates in action:
|
||||
|
||||
<pre>
|
||||
.encoding = "UTF-8"
|
||||
@@ -47,3 +46,11 @@ resulting in a VMX that looks like this:
|
||||
displayName = "packer"
|
||||
guestOS = "otherlinux"
|
||||
</pre>
|
||||
|
||||
## Global Functions
|
||||
|
||||
While some configuration settings have local variables specific to only that
|
||||
configuration, a set of functions are available globally for use in _any string_
|
||||
in Packer templates. These are listed below for reference.
|
||||
|
||||
* ``timestamp`` - The current Unix timestamp in UTC.
|
||||
|
||||
@@ -33,7 +33,7 @@ Along with each key, it is noted whether it is required or not.
|
||||
information on how to define and configure a provisioner, read the
|
||||
sub-section on [configuring provisioners in templates](/docs/templates/provisioners.html).
|
||||
|
||||
* `post-processors` (optional) is an array of one or more objects that defines the
|
||||
* `post-processors` (optional) is an array of one or more objects that defines the
|
||||
various post-processing steps to take with the built images. This is an optional
|
||||
field. If not specified, then no post-processing will be done. For more
|
||||
information on what post-processors do and how they're defined, read the
|
||||
@@ -56,7 +56,7 @@ missing valid AWS access keys. Otherwise, it would work properly with
|
||||
"source_ami": "ami-de0d9eb7",
|
||||
"instance_type": "t1.micro",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer {{.CreateTime}}"
|
||||
"ami_name": "packer {{timestamp}}"
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user