Merge remote-tracking branch 'origin/master' into azr_selectable_temp_keygen_type_gcp

This commit is contained in:
Adrien Delorme 2020-10-26 15:47:29 +01:00
commit f5e037e8b4
452 changed files with 168232 additions and 72858 deletions

1
.gitignore vendored
View File

@ -27,3 +27,4 @@ Thumbs.db
/packer.exe
.project
cache
/.vscode/

View File

@ -25,12 +25,17 @@
* builder/oracle-oci: New option to specify image compartment separate from
build compartment. [GH-10040]
* builder/oracle-oci: New option to specify boot volume size. [GH-10017]
* builder/oracle: Add `base_image_filter` option as alternative to
`base_image_ocid` [GH-10116]
* builder/outscale: Migrate to new Outscale SDK. [GH-10056]
* builder/scaleway: Allow the user to use an image label (eg ubuntu_focal)
instead of a hardcoded UUID on the Scaleway builder. [GH-10061]
* core: Let user provide type of generated ssh key instead of always doing ssh-
rsa [GH-10101]
* hcl: Add build.name variable so users can access build name in addition to
source name. [GH-10114]
* hcl: Add consul_key function to HCL templates. [GH-10119]
* hcl: Add HCL2 aws_secretsmanager function [GH-10124]
* hcl: Add packer.version variable to hcl configs so users can access the
Packer release version. [GH-10117]

View File

@ -1,4 +1,5 @@
TEST?=$(shell go list ./...)
COUNT?=1
VET?=$(shell go list ./...)
ACC_TEST_BUILDERS?=all
ACC_TEST_PROVISIONERS?=all
@ -136,7 +137,7 @@ generate-check: generate ## Check go code generation is on par
fi
test: mode-check vet ## Run unit tests
@go test $(TEST) $(TESTARGS) -timeout=3m
@go test -count $(COUNT) $(TEST) $(TESTARGS) -timeout=3m
# acctest runs provisioners acceptance tests
provisioners-acctest: install-build-deps generate
@ -145,14 +146,14 @@ provisioners-acctest: install-build-deps generate
# testacc runs acceptance tests
testacc: install-build-deps generate ## Run acceptance tests
@echo "WARN: Acceptance tests will take a long time to run and may cost money. Ctrl-C if you want to cancel."
PACKER_ACC=1 go test -v $(TEST) $(TESTARGS) -timeout=45m
PACKER_ACC=1 go test -count $(COUNT) -v $(TEST) $(TESTARGS) -timeout=120m
testrace: mode-check vet ## Test with race detection enabled
@GO111MODULE=off go test -race $(TEST) $(TESTARGS) -timeout=3m -p=8
@GO111MODULE=off go test -count $(COUNT) -race $(TEST) $(TESTARGS) -timeout=3m -p=8
# Runs code coverage and open a html page with report
cover:
go test $(TEST) $(TESTARGS) -timeout=3m -coverprofile=coverage.out
go test -count $(COUNT) $(TEST) $(TESTARGS) -timeout=3m -coverprofile=coverage.out
go tool cover -html=coverage.out
rm coverage.out

View File

@ -46,6 +46,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -194,6 +194,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
b.config.ctx.Funcs = awscommon.TemplateFuncs
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -85,6 +85,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
b.config.ctx.Funcs = awscommon.TemplateFuncs
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -84,6 +84,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
b.config.ctx.Funcs = awscommon.TemplateFuncs
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{
@ -333,6 +334,8 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
PollingConfig: b.config.PollingConfig,
LaunchDevices: launchDevices,
SnapshotOmitMap: b.config.LaunchMappings.GetOmissions(),
SnapshotTags: b.config.SnapshotTags,
Ctx: b.config.ctx,
},
&awscommon.StepDeregisterAMI{
AccessConfig: &b.config.AccessConfig,

View File

@ -6,11 +6,13 @@ import (
"sync"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
multierror "github.com/hashicorp/go-multierror"
awscommon "github.com/hashicorp/packer/builder/amazon/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
// StepSnapshotVolumes creates snapshots of the created volumes.
@ -23,6 +25,8 @@ type StepSnapshotVolumes struct {
snapshotIds map[string]string
snapshotMutex sync.Mutex
SnapshotOmitMap map[string]bool
SnapshotTags map[string]string
Ctx interpolate.Context
}
func (s *StepSnapshotVolumes) snapshotVolume(ctx context.Context, deviceName string, state multistep.StateBag) error {
@ -40,12 +44,34 @@ func (s *StepSnapshotVolumes) snapshotVolume(ctx context.Context, deviceName str
return fmt.Errorf("Volume ID for device %s not found", deviceName)
}
ui.Say("Creating snapshot tags")
snapshotTags, err := awscommon.TagMap(s.SnapshotTags).EC2Tags(s.Ctx, *ec2conn.Config.Region, state)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return err
}
snapshotTags.Report(ui)
ui.Say(fmt.Sprintf("Creating snapshot of EBS Volume %s...", volumeId))
description := fmt.Sprintf("Packer: %s", time.Now().String())
// Collect tags for tagging on resource creation
var tagSpecs []*ec2.TagSpecification
if len(snapshotTags) > 0 {
snapTags := &ec2.TagSpecification{
ResourceType: aws.String("snapshot"),
Tags: snapshotTags,
}
tagSpecs = append(tagSpecs, snapTags)
}
createSnapResp, err := ec2conn.CreateSnapshot(&ec2.CreateSnapshotInput{
VolumeId: &volumeId,
Description: &description,
TagSpecifications: tagSpecs,
})
if err != nil {
return err

View File

@ -100,6 +100,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
SourceAMI: `{{ .SourceAMI }} `,
}
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
}, raws...)

View File

@ -107,6 +107,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
b.config.ctx.Funcs = awscommon.TemplateFuncs
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -222,7 +222,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
NewStepSnapshotDataDisks(azureClient, ui, &b.config),
NewStepCaptureImage(azureClient, ui),
NewStepPublishToSharedImageGallery(azureClient, ui, &b.config),
NewStepDeleteAdditionalDisks(azureClient, ui),
}
} else if b.config.OSType == constants.Target_Windows {
steps = []multistep.Step{
@ -263,7 +262,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
NewStepSnapshotDataDisks(azureClient, ui, &b.config),
NewStepCaptureImage(azureClient, ui),
NewStepPublishToSharedImageGallery(azureClient, ui, &b.config),
NewStepDeleteAdditionalDisks(azureClient, ui),
)
} else {
return nil, fmt.Errorf("Builder does not support the os_type '%s'", b.config.OSType)

View File

@ -6,7 +6,6 @@ package arm
// * ARM_CLIENT_ID
// * ARM_CLIENT_SECRET
// * ARM_SUBSCRIPTION_ID
// * ARM_OBJECT_ID
// * ARM_STORAGE_ACCOUNT
//
// The subscription in question should have a resource group
@ -47,6 +46,14 @@ func TestBuilderAcc_ManagedDisk_Windows_Build_Resource_Group(t *testing.T) {
})
}
func TestBuilderAcc_ManagedDisk_Windows_Build_Resource_Group_Additional_Disk(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: testBuilderAccManagedDiskWindowsBuildResourceGroupAdditionalDisk,
})
}
func TestBuilderAcc_ManagedDisk_Windows_DeviceLogin(t *testing.T) {
if os.Getenv(DeviceLoginAcceptanceTest) == "" {
t.Skip(fmt.Sprintf(
@ -151,7 +158,7 @@ const testBuilderAccManagedDiskWindowsBuildResourceGroup = `
"build_resource_group_name" : "packer-acceptance-test",
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskWindows-{{timestamp}}",
"managed_image_name": "testBuilderAccManagedDiskWindowsBuildResourceGroup-{{timestamp}}",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
@ -170,6 +177,42 @@ const testBuilderAccManagedDiskWindowsBuildResourceGroup = `
}
`
const testBuilderAccManagedDiskWindowsBuildResourceGroupAdditionalDisk = `
{
"variables": {
"client_id": "{{env ` + "`ARM_CLIENT_ID`" + `}}",
"client_secret": "{{env ` + "`ARM_CLIENT_SECRET`" + `}}",
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}"
},
"builders": [{
"type": "test",
"client_id": "{{user ` + "`client_id`" + `}}",
"client_secret": "{{user ` + "`client_secret`" + `}}",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"build_resource_group_name" : "packer-acceptance-test",
"managed_image_resource_group_name": "packer-acceptance-test",
"managed_image_name": "testBuilderAccManagedDiskWindowsBuildResourceGroupAdditionDisk-{{timestamp}}",
"os_type": "Windows",
"image_publisher": "MicrosoftWindowsServer",
"image_offer": "WindowsServer",
"image_sku": "2012-R2-Datacenter",
"communicator": "winrm",
"winrm_use_ssl": "true",
"winrm_insecure": "true",
"winrm_timeout": "3m",
"winrm_username": "packer",
"async_resourcegroup_delete": "true",
"vm_size": "Standard_DS2_v2",
"disk_additional_size": [10,15]
}]
}
`
const testBuilderAccManagedDiskWindowsDeviceLogin = `
{
"variables": {
@ -262,7 +305,6 @@ const testBuilderAccBlobWindows = `
"client_id": "{{env ` + "`ARM_CLIENT_ID`" + `}}",
"client_secret": "{{env ` + "`ARM_CLIENT_SECRET`" + `}}",
"subscription_id": "{{env ` + "`ARM_SUBSCRIPTION_ID`" + `}}",
"object_id": "{{env ` + "`ARM_OBJECT_ID`" + `}}",
"storage_account": "{{env ` + "`ARM_STORAGE_ACCOUNT`" + `}}"
},
"builders": [{
@ -271,7 +313,6 @@ const testBuilderAccBlobWindows = `
"client_id": "{{user ` + "`client_id`" + `}}",
"client_secret": "{{user ` + "`client_secret`" + `}}",
"subscription_id": "{{user ` + "`subscription_id`" + `}}",
"object_id": "{{user ` + "`object_id`" + `}}",
"storage_account": "{{user ` + "`storage_account`" + `}}",
"resource_group_name": "packer-acceptance-test",

View File

@ -580,6 +580,7 @@ func (c *Config) createCertificate() (string, error) {
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
c.ctx.Funcs = azcommon.TemplateFuncs
err := config.Decode(c, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &c.ctx,
}, raws...)

View File

@ -6,8 +6,10 @@ import (
"fmt"
"net/url"
"strings"
"time"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/common/retry"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
@ -15,7 +17,7 @@ import (
type StepDeployTemplate struct {
client *AzureClient
deploy func(ctx context.Context, resourceGroupName string, deploymentName string) error
delete func(ctx context.Context, client *AzureClient, resourceType string, resourceName string, resourceGroupName string) error
delete func(ctx context.Context, deploymentName, resourceGroupName string) error
disk func(ctx context.Context, resourceGroupName string, computeName string) (string, string, error)
deleteDisk func(ctx context.Context, imageType string, imageName string, resourceGroupName string) error
say func(message string)
@ -36,7 +38,7 @@ func NewStepDeployTemplate(client *AzureClient, ui packer.Ui, config *Config, de
}
step.deploy = step.deployTemplate
step.delete = deleteResource
step.delete = step.deleteDeploymentResources
step.disk = step.getImageDetails
step.deleteDisk = step.deleteImage
return step
@ -62,8 +64,9 @@ func (s *StepDeployTemplate) Cleanup(state multistep.StateBag) {
}
}()
//Only clean up if this was an existing resource group and the resource group
//is marked as created
// Only clean up if this is an existing resource group that has been verified to exist.
// ArmIsResourceGroupCreated is set in step_create_resource_group to true, when Packer has verified that the resource group exists.
// ArmIsExistingResourceGroup is set to true when build_resource_group is set in the Packer configuration.
existingResourceGroup := state.Get(constants.ArmIsExistingResourceGroup).(bool)
resourceGroupCreated := state.Get(constants.ArmIsResourceGroupCreated).(bool)
if !existingResourceGroup || !resourceGroupCreated {
@ -75,74 +78,12 @@ func (s *StepDeployTemplate) Cleanup(state multistep.StateBag) {
deploymentName := s.name
resourceGroupName := state.Get(constants.ArmResourceGroupName).(string)
// Get image disk details before deleting the image; otherwise we won't be able to
// delete the disk as the image request will return a 404
computeName := state.Get(constants.ArmComputeName).(string)
imageType, imageName, err := s.disk(context.TODO(), resourceGroupName, computeName)
if err != nil && !strings.Contains(err.Error(), "ResourceNotFound") {
ui.Error(fmt.Sprintf("Could not retrieve OS Image details: %s", err))
}
ui.Say(" -> Deployment Resources within: " + deploymentName)
if deploymentName != "" {
maxResources := int32(50)
deploymentOperations, err := s.client.DeploymentOperationsClient.ListComplete(context.TODO(), resourceGroupName, deploymentName, &maxResources)
err := s.deleteDeploymentResources(context.TODO(), deploymentName, resourceGroupName)
if err != nil {
ui.Error(fmt.Sprintf("Error deleting resources. Please delete them manually.\n\n"+
"Name: %s\n"+
"Error: %s", resourceGroupName, err))
s.reportIfError(err, resourceGroupName)
}
for deploymentOperations.NotDone() {
deploymentOperation := deploymentOperations.Value()
// Sometimes an empty operation is added to the list by Azure
if deploymentOperation.Properties.TargetResource == nil {
if err := deploymentOperations.Next(); err != nil {
ui.Error(fmt.Sprintf("Error moving to to next deployment operation ...\n\n"+
"Name: %s\n"+
"Error: %s", resourceGroupName, err))
break
}
continue
}
ui.Say(fmt.Sprintf(" -> %s : '%s'",
*deploymentOperation.Properties.TargetResource.ResourceType,
*deploymentOperation.Properties.TargetResource.ResourceName))
err = s.delete(context.TODO(), s.client,
*deploymentOperation.Properties.TargetResource.ResourceType,
*deploymentOperation.Properties.TargetResource.ResourceName,
resourceGroupName)
if err != nil {
ui.Error(fmt.Sprintf("Error deleting resource. Please delete manually.\n\n"+
"Name: %s\n"+
"Error: %s", *deploymentOperation.Properties.TargetResource.ResourceName, err))
}
if err = deploymentOperations.Next(); err != nil {
ui.Error(fmt.Sprintf("Error deleting resources. Please delete them manually.\n\n"+
"Name: %s\n"+
"Error: %s", resourceGroupName, err))
break
}
}
// The disk is not defined as an operation in the template so it has to be deleted separately
if imageType == "" && imageName == "" {
return
}
ui.Say(fmt.Sprintf(" -> %s : '%s'", imageType, imageName))
err = s.deleteDisk(context.TODO(), imageType, imageName, resourceGroupName)
if err != nil {
ui.Error(fmt.Sprintf("Error deleting resource. Please delete manually.\n\n"+
"Name: %s\n"+
"Error: %s", imageName, err))
}
}
NewStepDeleteAdditionalDisks(s.client, ui).Run(context.TODO(), state)
}
func (s *StepDeployTemplate) deployTemplate(ctx context.Context, resourceGroupName string, deploymentName string) error {
@ -152,27 +93,31 @@ func (s *StepDeployTemplate) deployTemplate(ctx context.Context, resourceGroupNa
}
f, err := s.client.DeploymentsClient.CreateOrUpdate(ctx, resourceGroupName, deploymentName, *deployment)
if err == nil {
err = f.WaitForCompletionRef(ctx, s.client.DeploymentsClient.Client)
}
if err != nil {
s.say(s.client.LastError.Error())
return err
}
err = f.WaitForCompletionRef(ctx, s.client.DeploymentsClient.Client)
if err == nil {
s.say(s.client.LastError.Error())
}
return err
}
func (s *StepDeployTemplate) deleteTemplate(ctx context.Context, state multistep.StateBag) error {
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
var deploymentName = s.name
deploymentName := s.name
resourceGroupName := state.Get(constants.ArmResourceGroupName).(string)
ui := state.Get("ui").(packer.Ui)
ui.Say(fmt.Sprintf("Removing the created Deployment object: '%s'", deploymentName))
f, err := s.client.DeploymentsClient.Delete(ctx, resourceGroupName, deploymentName)
if err == nil {
err = f.WaitForCompletionRef(ctx, s.client.DeploymentsClient.Client)
if err != nil {
return err
}
return err
return f.WaitForCompletionRef(ctx, s.client.DeploymentsClient.Client)
}
func (s *StepDeployTemplate) getImageDetails(ctx context.Context, resourceGroupName string, computeName string) (string, string, error) {
@ -186,10 +131,11 @@ func (s *StepDeployTemplate) getImageDetails(ctx context.Context, resourceGroupN
if vm.StorageProfile.OsDisk.Vhd != nil {
imageType = "image"
imageName = *vm.StorageProfile.OsDisk.Vhd.URI
} else {
return imageType, imageName, nil
}
imageType = "Microsoft.Compute/disks"
imageName = *vm.StorageProfile.OsDisk.ManagedDisk.ID
}
return imageType, imageName, nil
}
@ -267,3 +213,58 @@ func (s *StepDeployTemplate) deleteImage(ctx context.Context, imageType string,
return blob.Delete(nil)
}
func (s *StepDeployTemplate) deleteDeploymentResources(ctx context.Context, deploymentName, resourceGroupName string) error {
var maxResources int32 = 50
deploymentOperations, err := s.client.DeploymentOperationsClient.ListComplete(ctx, resourceGroupName, deploymentName, &maxResources)
if err != nil {
s.reportIfError(err, resourceGroupName)
return err
}
for deploymentOperations.NotDone() {
deploymentOperation := deploymentOperations.Value()
// Sometimes an empty operation is added to the list by Azure
if deploymentOperation.Properties.TargetResource == nil {
_ = deploymentOperations.Next()
continue
}
resourceName := *deploymentOperation.Properties.TargetResource.ResourceName
resourceType := *deploymentOperation.Properties.TargetResource.ResourceType
s.say(fmt.Sprintf(" -> %s : '%s'", resourceType, resourceName))
err = retry.Config{
Tries: 10,
RetryDelay: (&retry.Backoff{InitialBackoff: 10 * time.Second, MaxBackoff: 600 * time.Second, Multiplier: 2}).Linear,
}.Run(ctx, func(ctx context.Context) error {
err := deleteResource(ctx, s.client,
resourceType,
resourceName,
resourceGroupName)
if err != nil {
s.reportIfError(err, resourceName)
}
return nil
})
if err != nil {
s.reportIfError(err, resourceName)
}
if err = deploymentOperations.Next(); err != nil {
return err
}
}
return nil
}
func (s *StepDeployTemplate) reportIfError(err error, resourceName string) {
if err != nil {
s.say(fmt.Sprintf("Error deleting resource. Please delete manually.\n\n"+
"Name: %s\n"+
"Error: %s", resourceName, err.Error()))
s.error(err)
}
}

View File

@ -153,6 +153,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
b.config.ctx.Funcs["vm"] = CreateVMMetadataTemplateFunc()
md := &mapstructure.Metadata{}
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderID,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -389,6 +389,7 @@ func newConfig(raws ...interface{}) (*Config, []string, error) {
var c Config
c.ctx.Funcs = TemplateFuncs
err := config.Decode(&c, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &c.ctx,
}, raws...)

View File

@ -127,6 +127,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
var md mapstructure.Metadata
err := config.Decode(c, &config.DecodeOpts{
Metadata: &md,
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -291,6 +291,7 @@ type Config struct {
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
c.ctx.Funcs = TemplateFuncs
err := config.Decode(c, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -89,6 +89,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: hypervcommon.BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -82,6 +82,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: hypervcommon.BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -17,6 +17,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BUILDER_ID,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -3,6 +3,7 @@ package linode
import (
"strconv"
"testing"
"time"
"github.com/hashicorp/packer/packer"
)
@ -248,3 +249,43 @@ func TestBuilderPrepare_Label(t *testing.T) {
}
}
func TestBuilderPrepare_StateTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test default
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.StateTimeout != 5*time.Minute {
t.Errorf("invalid: %s", b.config.StateTimeout)
}
// Test set
config["state_timeout"] = "5m"
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["state_timeout"] = "tubes"
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}

View File

@ -9,6 +9,7 @@ import (
"fmt"
"os"
"regexp"
"time"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
@ -34,6 +35,7 @@ type Config struct {
RootSSHKey string `mapstructure:"root_ssh_key"`
ImageLabel string `mapstructure:"image_label"`
Description string `mapstructure:"image_description"`
StateTimeout time.Duration `mapstructure:"state_timeout" required:"false"`
}
func createRandomRootPassword() (string, error) {
@ -94,6 +96,11 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
}
}
if c.StateTimeout == 0 {
// Default to 5 minute timeouts waiting for state change
c.StateTimeout = 5 * time.Minute
}
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}

View File

@ -76,6 +76,7 @@ type FlatConfig struct {
RootSSHKey *string `mapstructure:"root_ssh_key" cty:"root_ssh_key" hcl:"root_ssh_key"`
ImageLabel *string `mapstructure:"image_label" cty:"image_label" hcl:"image_label"`
Description *string `mapstructure:"image_description" cty:"image_description" hcl:"image_description"`
StateTimeout *string `mapstructure:"state_timeout" required:"false" cty:"state_timeout" hcl:"state_timeout"`
}
// FlatMapstructure returns a new FlatConfig.
@ -157,6 +158,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"root_ssh_key": &hcldec.AttrSpec{Name: "root_ssh_key", Type: cty.String, Required: false},
"image_label": &hcldec.AttrSpec{Name: "image_label", Type: cty.String, Required: false},
"image_description": &hcldec.AttrSpec{Name: "image_description", Type: cty.String, Required: false},
"state_timeout": &hcldec.AttrSpec{Name: "state_timeout", Type: cty.String, Required: false},
}
return s
}

View File

@ -14,6 +14,7 @@ type stepShutdownLinode struct {
}
func (s *stepShutdownLinode) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
c := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
instance := state.Get("instance").(*linodego.Instance)
@ -25,7 +26,7 @@ func (s *stepShutdownLinode) Run(ctx context.Context, state multistep.StateBag)
return multistep.ActionHalt
}
_, err := s.client.WaitForInstanceStatus(ctx, instance.ID, linodego.InstanceOffline, 120)
_, err := s.client.WaitForInstanceStatus(ctx, instance.ID, linodego.InstanceOffline, int(c.StateTimeout.Seconds()))
if err != nil {
err = errors.New("Error shutting down Linode: " + err.Error())
state.Put("error", err)

View File

@ -21,6 +21,7 @@ type Config struct {
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(c, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{},
}, raws...)

View File

@ -40,6 +40,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
}, raws...)

View File

@ -1,4 +1,4 @@
//go:generate mapstructure-to-hcl2 -type Config,CreateVNICDetails
//go:generate mapstructure-to-hcl2 -type Config,CreateVNICDetails,ListImagesRequest
package oci
@ -35,6 +35,16 @@ type CreateVNICDetails struct {
SubnetId *string `mapstructure:"subnet_id" required:"false"`
}
type ListImagesRequest struct {
// fields that can be specified under "base_image_filter"
CompartmentId *string `mapstructure:"compartment_id"`
DisplayName *string `mapstructure:"display_name"`
DisplayNameSearch *string `mapstructure:"display_name_search"`
OperatingSystem *string `mapstructure:"operating_system"`
OperatingSystemVersion *string `mapstructure:"operating_system_version"`
Shape *string `mapstructure:"shape"`
}
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
@ -70,11 +80,12 @@ type Config struct {
// Image
BaseImageID string `mapstructure:"base_image_ocid"`
BaseImageFilter ListImagesRequest `mapstructure:"base_image_filter"`
ImageName string `mapstructure:"image_name"`
ImageCompartmentID string `mapstructure:"image_compartment_ocid"`
// Instance
InstanceName string `mapstructure:"instance_name"`
InstanceName *string `mapstructure:"instance_name"`
InstanceTags map[string]string `mapstructure:"instance_tags"`
InstanceDefinedTags map[string]map[string]interface{} `mapstructure:"instance_defined_tags"`
Shape string `mapstructure:"shape"`
@ -269,9 +280,17 @@ func (c *Config) Prepare(raws ...interface{}) error {
errs, errors.New("'subnet_ocid' must be specified"))
}
if c.BaseImageID == "" {
if (c.BaseImageID == "") && (c.BaseImageFilter == ListImagesRequest{}) {
errs = packer.MultiErrorAppend(
errs, errors.New("'base_image_ocid' must be specified"))
errs, errors.New("'base_image_ocid' or 'base_image_filter' must be specified"))
}
if c.BaseImageFilter.CompartmentId == nil {
c.BaseImageFilter.CompartmentId = &c.CompartmentID
}
if c.BaseImageFilter.Shape == nil {
c.BaseImageFilter.Shape = &c.Shape
}
// Validate tag lengths. TODO (hlowndes) maximum number of tags allowed.

View File

@ -1,4 +1,4 @@
// Code generated by "mapstructure-to-hcl2 -type Config,CreateVNICDetails"; DO NOT EDIT.
// Code generated by "mapstructure-to-hcl2 -type Config,CreateVNICDetails,ListImagesRequest"; DO NOT EDIT.
package oci
import (
@ -78,6 +78,7 @@ type FlatConfig struct {
AvailabilityDomain *string `mapstructure:"availability_domain" cty:"availability_domain" hcl:"availability_domain"`
CompartmentID *string `mapstructure:"compartment_ocid" cty:"compartment_ocid" hcl:"compartment_ocid"`
BaseImageID *string `mapstructure:"base_image_ocid" cty:"base_image_ocid" hcl:"base_image_ocid"`
BaseImageFilter *FlatListImagesRequest `mapstructure:"base_image_filter" cty:"base_image_filter" hcl:"base_image_filter"`
ImageName *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"`
ImageCompartmentID *string `mapstructure:"image_compartment_ocid" cty:"image_compartment_ocid" hcl:"image_compartment_ocid"`
InstanceName *string `mapstructure:"instance_name" cty:"instance_name" hcl:"instance_name"`
@ -175,6 +176,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"availability_domain": &hcldec.AttrSpec{Name: "availability_domain", Type: cty.String, Required: false},
"compartment_ocid": &hcldec.AttrSpec{Name: "compartment_ocid", Type: cty.String, Required: false},
"base_image_ocid": &hcldec.AttrSpec{Name: "base_image_ocid", Type: cty.String, Required: false},
"base_image_filter": &hcldec.BlockSpec{TypeName: "base_image_filter", Nested: hcldec.ObjectSpec((*FlatListImagesRequest)(nil).HCL2Spec())},
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
"image_compartment_ocid": &hcldec.AttrSpec{Name: "image_compartment_ocid", Type: cty.String, Required: false},
"instance_name": &hcldec.AttrSpec{Name: "instance_name", Type: cty.String, Required: false},
@ -231,3 +233,36 @@ func (*FlatCreateVNICDetails) HCL2Spec() map[string]hcldec.Spec {
}
return s
}
// FlatListImagesRequest is an auto-generated flat version of ListImagesRequest.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatListImagesRequest struct {
CompartmentId *string `mapstructure:"compartment_id" cty:"compartment_id" hcl:"compartment_id"`
DisplayName *string `mapstructure:"display_name" cty:"display_name" hcl:"display_name"`
DisplayNameSearch *string `mapstructure:"display_name_search" cty:"display_name_search" hcl:"display_name_search"`
OperatingSystem *string `mapstructure:"operating_system" cty:"operating_system" hcl:"operating_system"`
OperatingSystemVersion *string `mapstructure:"operating_system_version" cty:"operating_system_version" hcl:"operating_system_version"`
Shape *string `mapstructure:"shape" cty:"shape" hcl:"shape"`
}
// FlatMapstructure returns a new FlatListImagesRequest.
// FlatListImagesRequest is an auto-generated flat version of ListImagesRequest.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*ListImagesRequest) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatListImagesRequest)
}
// HCL2Spec returns the hcl spec of a ListImagesRequest.
// This spec is used by HCL to read the fields of ListImagesRequest.
// The decoded values from this spec will then be applied to a FlatListImagesRequest.
func (*FlatListImagesRequest) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"compartment_id": &hcldec.AttrSpec{Name: "compartment_id", Type: cty.String, Required: false},
"display_name": &hcldec.AttrSpec{Name: "display_name", Type: cty.String, Required: false},
"display_name_search": &hcldec.AttrSpec{Name: "display_name_search", Type: cty.String, Required: false},
"operating_system": &hcldec.AttrSpec{Name: "operating_system", Type: cty.String, Required: false},
"operating_system_version": &hcldec.AttrSpec{Name: "operating_system_version", Type: cty.String, Required: false},
"shape": &hcldec.AttrSpec{Name: "shape", Type: cty.String, Required: false},
}
return s
}

View File

@ -88,6 +88,36 @@ func TestConfig(t *testing.T) {
}
})
t.Run("BaseImageFilterWithoutOCID", func(t *testing.T) {
raw := testConfig(cfgFile)
raw["base_image_ocid"] = ""
raw["base_image_filter"] = map[string]interface{}{
"display_name": "hello_world",
}
var c Config
errs := c.Prepare(raw)
if errs != nil {
t.Fatalf("Unexpected error in configuration %+v", errs)
}
})
t.Run("BaseImageFilterDefault", func(t *testing.T) {
raw := testConfig(cfgFile)
var c Config
errs := c.Prepare(raw)
if errs != nil {
t.Fatalf("Unexpected error in configuration %+v", errs)
}
if *c.BaseImageFilter.Shape != raw["shape"] {
t.Fatalf("Default base_image_filter shape %v does not equal config shape %v",
*c.BaseImageFilter.Shape, raw["shape"])
}
})
t.Run("NoAccessConfig", func(t *testing.T) {
raw := testConfig(cfgFile)
delete(raw, "access_cfg_file")

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"regexp"
"time"
core "github.com/oracle/oci-go-sdk/core"
@ -51,22 +52,7 @@ func (d *driverOCI) CreateInstance(ctx context.Context, publicKey string) (strin
metadata["user_data"] = d.cfg.UserData
}
instanceDetails := core.LaunchInstanceDetails{
AvailabilityDomain: &d.cfg.AvailabilityDomain,
CompartmentId: &d.cfg.CompartmentID,
DefinedTags: d.cfg.InstanceDefinedTags,
FreeformTags: d.cfg.InstanceTags,
Shape: &d.cfg.Shape,
SubnetId: &d.cfg.SubnetID,
Metadata: metadata,
}
// When empty, the default display name is used.
if d.cfg.InstanceName != "" {
instanceDetails.DisplayName = &d.cfg.InstanceName
}
// Pass VNIC details, if specified, to the instance
// Create VNIC details for instance
CreateVnicDetails := core.CreateVnicDetails{
AssignPublicIp: d.cfg.CreateVnicDetails.AssignPublicIp,
DisplayName: d.cfg.CreateVnicDetails.DisplayName,
@ -79,14 +65,69 @@ func (d *driverOCI) CreateInstance(ctx context.Context, publicKey string) (strin
FreeformTags: d.cfg.CreateVnicDetails.FreeformTags,
}
instanceDetails.CreateVnicDetails = &CreateVnicDetails
// Determine base image ID
var imageId *string
if d.cfg.BaseImageID != "" {
imageId = &d.cfg.BaseImageID
} else {
// Pull images and determine which image ID to use, if BaseImageId not specified
response, err := d.computeClient.ListImages(ctx, core.ListImagesRequest{
CompartmentId: d.cfg.BaseImageFilter.CompartmentId,
DisplayName: d.cfg.BaseImageFilter.DisplayName,
OperatingSystem: d.cfg.BaseImageFilter.OperatingSystem,
OperatingSystemVersion: d.cfg.BaseImageFilter.OperatingSystemVersion,
Shape: d.cfg.BaseImageFilter.Shape,
LifecycleState: "AVAILABLE",
SortBy: "TIMECREATED",
SortOrder: "DESC",
})
if err != nil {
return "", err
}
if len(response.Items) == 0 {
return "", errors.New("base_image_filter returned no images")
}
if d.cfg.BaseImageFilter.DisplayNameSearch != nil {
// Return most recent image that matches regex
imageNameRegex, err := regexp.Compile(*d.cfg.BaseImageFilter.DisplayNameSearch)
if err != nil {
return "", err
}
for _, image := range response.Items {
if imageNameRegex.MatchString(*image.DisplayName) {
imageId = image.Id
break
}
}
if imageId == nil {
return "", errors.New("No image matched display_name_search criteria")
}
} else {
// If no regex provided, simply return most recent image pulled
imageId = response.Items[0].Id
}
}
// Create Source details which will be used to Launch Instance
instanceDetails.SourceDetails = core.InstanceSourceViaImageDetails{
ImageId: &d.cfg.BaseImageID,
InstanceSourceDetails := core.InstanceSourceViaImageDetails{
ImageId: imageId,
BootVolumeSizeInGBs: &d.cfg.BootVolumeSizeInGBs,
}
// Build instance details
instanceDetails := core.LaunchInstanceDetails{
AvailabilityDomain: &d.cfg.AvailabilityDomain,
CompartmentId: &d.cfg.CompartmentID,
CreateVnicDetails: &CreateVnicDetails,
DefinedTags: d.cfg.InstanceDefinedTags,
DisplayName: d.cfg.InstanceName,
FreeformTags: d.cfg.InstanceTags,
Shape: &d.cfg.Shape,
SourceDetails: InstanceSourceDetails,
SubnetId: &d.cfg.SubnetID,
Metadata: metadata,
}
instance, err := d.computeClient.LaunchInstance(context.TODO(), core.LaunchInstanceRequest{LaunchInstanceDetails: instanceDetails})
if err != nil {

View File

@ -45,6 +45,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
b.config.ctx.Funcs = osccommon.TemplateFuncs
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -46,6 +46,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
b.config.ctx.Funcs = osccommon.TemplateFuncs
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -53,6 +53,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
SourceOMI: `{{ .SourceOMI }} `,
}
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
}, raws...)

View File

@ -66,6 +66,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
b.config.ctx.Funcs = osccommon.TemplateFuncs
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -87,6 +87,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -51,6 +51,7 @@ type Config struct {
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(c, &config.DecodeOpts{
PluginType: parallelscommon.BuilderId,
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -1,191 +1,5 @@
package proxmox
import (
"context"
"crypto/tls"
"errors"
"fmt"
import proxmoxiso "github.com/hashicorp/packer/builder/proxmox/iso"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// The unique id for the builder
const BuilderId = "proxmox.builder"
type Builder struct {
config Config
runner multistep.Runner
proxmoxClient *proxmox.Client
}
// Builder implements packer.Builder
var _ packer.Builder = &Builder{}
var pluginVersion = "1.0.0"
func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() }
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
warnings, errs := b.config.Prepare(raws...)
if errs != nil {
return nil, warnings, errs
}
return nil, nil, nil
}
const downloadPathKey = "downloaded_iso_path"
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
var err error
tlsConfig := &tls.Config{
InsecureSkipVerify: b.config.SkipCertValidation,
}
b.proxmoxClient, err = proxmox.NewClient(b.config.proxmoxURL.String(), nil, tlsConfig)
if err != nil {
return nil, err
}
err = b.proxmoxClient.Login(b.config.Username, b.config.Password, "")
if err != nil {
return nil, err
}
// Set up the state
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
state.Put("proxmoxClient", b.proxmoxClient)
state.Put("hook", hook)
state.Put("ui", ui)
// Build the steps
steps := []multistep.Step{
&common.StepDownload{
Checksum: b.config.ISOChecksum,
Description: "ISO",
Extension: b.config.TargetExtension,
ResultKey: downloadPathKey,
TargetPath: b.config.TargetPath,
Url: b.config.ISOUrls,
}}
for idx := range b.config.AdditionalISOFiles {
steps = append(steps, &common.StepDownload{
Checksum: b.config.AdditionalISOFiles[idx].ISOChecksum,
Description: "additional ISO",
Extension: b.config.AdditionalISOFiles[idx].TargetExtension,
ResultKey: b.config.AdditionalISOFiles[idx].downloadPathKey,
TargetPath: b.config.AdditionalISOFiles[idx].downloadPathKey,
Url: b.config.AdditionalISOFiles[idx].ISOUrls,
})
}
steps = append(steps,
&stepUploadISO{},
&stepUploadAdditionalISOs{},
&stepStartVM{},
&common.StepHTTPServer{
HTTPDir: b.config.HTTPDir,
HTTPPortMin: b.config.HTTPPortMin,
HTTPPortMax: b.config.HTTPPortMax,
HTTPAddress: b.config.HTTPAddress,
},
&stepTypeBootCommand{
BootConfig: b.config.BootConfig,
Ctx: b.config.ctx,
},
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost(b.config.Comm.Host()),
SSHConfig: b.config.Comm.SSHConfigFunc(),
},
&common.StepProvision{},
&common.StepCleanupTempKeys{
Comm: &b.config.Comm,
},
&stepConvertToTemplate{},
&stepFinalizeTemplateConfig{},
&stepSuccess{},
)
// Run the steps
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(ctx, state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
// If we were interrupted or cancelled, then just exit.
if _, ok := state.GetOk(multistep.StateCancelled); ok {
return nil, errors.New("build was cancelled")
}
// Verify that the template_id was set properly, otherwise we didn't progress through the last step
tplID, ok := state.Get("template_id").(int)
if !ok {
return nil, fmt.Errorf("template ID could not be determined")
}
artifact := &Artifact{
templateID: tplID,
proxmoxClient: b.proxmoxClient,
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
}
return artifact, nil
}
// Returns ssh_host or winrm_host (see communicator.Config.Host) config
// parameter when set, otherwise gets the host IP from running VM
func commHost(host string) func(state multistep.StateBag) (string, error) {
if host != "" {
return func(state multistep.StateBag) (string, error) {
return host, nil
}
}
return getVMIP
}
// Reads the first non-loopback interface's IP address from the VM.
// qemu-guest-agent package must be installed on the VM
func getVMIP(state multistep.StateBag) (string, error) {
client := state.Get("proxmoxClient").(*proxmox.Client)
config := state.Get("config").(*Config)
vmRef := state.Get("vmRef").(*proxmox.VmRef)
ifs, err := client.GetVmAgentNetworkInterfaces(vmRef)
if err != nil {
return "", err
}
if config.VMInterface != "" {
for _, iface := range ifs {
if config.VMInterface != iface.Name {
continue
}
for _, addr := range iface.IPAddresses {
if addr.IsLoopback() {
continue
}
return addr.String(), nil
}
return "", fmt.Errorf("Interface %s only has loopback addresses", config.VMInterface)
}
return "", fmt.Errorf("Interface %s not found in VM", config.VMInterface)
}
for _, iface := range ifs {
for _, addr := range iface.IPAddresses {
if addr.IsLoopback() {
continue
}
return addr.String(), nil
}
}
return "", fmt.Errorf("Found no IP addresses on VM")
}
type Builder = proxmoxiso.Builder

View File

@ -0,0 +1,74 @@
package proxmoxclone
import (
proxmoxapi "github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/hcl/v2/hcldec"
proxmox "github.com/hashicorp/packer/builder/proxmox/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"context"
"fmt"
)
// The unique id for the builder
const BuilderID = "proxmox.clone"
type Builder struct {
config Config
}
// Builder implements packer.Builder
var _ packer.Builder = &Builder{}
func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() }
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
return b.config.Prepare(raws...)
}
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
state := new(multistep.BasicStateBag)
state.Put("clone-config", &b.config)
preSteps := []multistep.Step{
&StepSshKeyPair{
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("%s.pem", b.config.PackerBuildName),
},
}
postSteps := []multistep.Step{}
sb := proxmox.NewSharedBuilder(BuilderID, b.config.Config, preSteps, postSteps, &cloneVMCreator{})
return sb.Run(ctx, ui, hook, state)
}
type cloneVMCreator struct{}
func (*cloneVMCreator) Create(vmRef *proxmoxapi.VmRef, config proxmoxapi.ConfigQemu, state multistep.StateBag) error {
client := state.Get("proxmoxClient").(*proxmoxapi.Client)
c := state.Get("clone-config").(*Config)
comm := state.Get("config").(*proxmox.Config).Comm
fullClone := 1
if c.FullClone.False() {
fullClone = 0
}
config.FullClone = &fullClone
config.CIuser = comm.SSHUsername
config.Sshkeys = string(comm.SSHPublicKey)
sourceVmr, err := client.GetVmRefByName(c.CloneVM)
if err != nil {
return err
}
err = config.CloneVm(sourceVmr, vmRef, client)
if err != nil {
return err
}
err = config.UpdateConfig(vmRef, client)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,29 @@
//go:generate mapstructure-to-hcl2 -type Config
package proxmoxclone
import (
proxmox "github.com/hashicorp/packer/builder/proxmox/common"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
)
type Config struct {
proxmox.Config `mapstructure:",squash"`
CloneVM string `mapstructure:"clone_vm"`
FullClone config.Trilean `mapstructure:"full_clone" required:"false"`
}
func (c *Config) Prepare(raws ...interface{}) ([]string, []string, error) {
var errs *packer.MultiError
_, warnings, merrs := c.Config.Prepare(c, raws...)
if merrs != nil {
errs = packer.MultiErrorAppend(errs, merrs)
}
if errs != nil && len(errs.Errors) > 0 {
return nil, warnings, errs
}
return nil, warnings, nil
}

View File

@ -0,0 +1,211 @@
// Code generated by "mapstructure-to-hcl2 -type Config"; DO NOT EDIT.
package proxmoxclone
import (
"github.com/hashicorp/hcl/v2/hcldec"
proxmox "github.com/hashicorp/packer/builder/proxmox/common"
"github.com/zclconf/go-cty/cty"
)
// FlatConfig is an auto-generated flat version of Config.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatConfig struct {
PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"`
PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"`
PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"`
PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"`
PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"`
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
HTTPDir *string `mapstructure:"http_directory" cty:"http_directory" hcl:"http_directory"`
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"`
BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"`
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"`
Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"`
PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"`
SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"`
SSHPort *int `mapstructure:"ssh_port" cty:"ssh_port" hcl:"ssh_port"`
SSHUsername *string `mapstructure:"ssh_username" cty:"ssh_username" hcl:"ssh_username"`
SSHPassword *string `mapstructure:"ssh_password" cty:"ssh_password" hcl:"ssh_password"`
SSHKeyPairName *string `mapstructure:"ssh_keypair_name" undocumented:"true" cty:"ssh_keypair_name" hcl:"ssh_keypair_name"`
SSHTemporaryKeyPairName *string `mapstructure:"temporary_key_pair_name" undocumented:"true" cty:"temporary_key_pair_name" hcl:"temporary_key_pair_name"`
SSHCiphers []string `mapstructure:"ssh_ciphers" cty:"ssh_ciphers" hcl:"ssh_ciphers"`
SSHClearAuthorizedKeys *bool `mapstructure:"ssh_clear_authorized_keys" cty:"ssh_clear_authorized_keys" hcl:"ssh_clear_authorized_keys"`
SSHKEXAlgos []string `mapstructure:"ssh_key_exchange_algorithms" cty:"ssh_key_exchange_algorithms" hcl:"ssh_key_exchange_algorithms"`
SSHPrivateKeyFile *string `mapstructure:"ssh_private_key_file" undocumented:"true" cty:"ssh_private_key_file" hcl:"ssh_private_key_file"`
SSHCertificateFile *string `mapstructure:"ssh_certificate_file" cty:"ssh_certificate_file" hcl:"ssh_certificate_file"`
SSHPty *bool `mapstructure:"ssh_pty" cty:"ssh_pty" hcl:"ssh_pty"`
SSHTimeout *string `mapstructure:"ssh_timeout" cty:"ssh_timeout" hcl:"ssh_timeout"`
SSHWaitTimeout *string `mapstructure:"ssh_wait_timeout" undocumented:"true" cty:"ssh_wait_timeout" hcl:"ssh_wait_timeout"`
SSHAgentAuth *bool `mapstructure:"ssh_agent_auth" undocumented:"true" cty:"ssh_agent_auth" hcl:"ssh_agent_auth"`
SSHDisableAgentForwarding *bool `mapstructure:"ssh_disable_agent_forwarding" cty:"ssh_disable_agent_forwarding" hcl:"ssh_disable_agent_forwarding"`
SSHHandshakeAttempts *int `mapstructure:"ssh_handshake_attempts" cty:"ssh_handshake_attempts" hcl:"ssh_handshake_attempts"`
SSHBastionHost *string `mapstructure:"ssh_bastion_host" cty:"ssh_bastion_host" hcl:"ssh_bastion_host"`
SSHBastionPort *int `mapstructure:"ssh_bastion_port" cty:"ssh_bastion_port" hcl:"ssh_bastion_port"`
SSHBastionAgentAuth *bool `mapstructure:"ssh_bastion_agent_auth" cty:"ssh_bastion_agent_auth" hcl:"ssh_bastion_agent_auth"`
SSHBastionUsername *string `mapstructure:"ssh_bastion_username" cty:"ssh_bastion_username" hcl:"ssh_bastion_username"`
SSHBastionPassword *string `mapstructure:"ssh_bastion_password" cty:"ssh_bastion_password" hcl:"ssh_bastion_password"`
SSHBastionInteractive *bool `mapstructure:"ssh_bastion_interactive" cty:"ssh_bastion_interactive" hcl:"ssh_bastion_interactive"`
SSHBastionPrivateKeyFile *string `mapstructure:"ssh_bastion_private_key_file" cty:"ssh_bastion_private_key_file" hcl:"ssh_bastion_private_key_file"`
SSHBastionCertificateFile *string `mapstructure:"ssh_bastion_certificate_file" cty:"ssh_bastion_certificate_file" hcl:"ssh_bastion_certificate_file"`
SSHFileTransferMethod *string `mapstructure:"ssh_file_transfer_method" cty:"ssh_file_transfer_method" hcl:"ssh_file_transfer_method"`
SSHProxyHost *string `mapstructure:"ssh_proxy_host" cty:"ssh_proxy_host" hcl:"ssh_proxy_host"`
SSHProxyPort *int `mapstructure:"ssh_proxy_port" cty:"ssh_proxy_port" hcl:"ssh_proxy_port"`
SSHProxyUsername *string `mapstructure:"ssh_proxy_username" cty:"ssh_proxy_username" hcl:"ssh_proxy_username"`
SSHProxyPassword *string `mapstructure:"ssh_proxy_password" cty:"ssh_proxy_password" hcl:"ssh_proxy_password"`
SSHKeepAliveInterval *string `mapstructure:"ssh_keep_alive_interval" cty:"ssh_keep_alive_interval" hcl:"ssh_keep_alive_interval"`
SSHReadWriteTimeout *string `mapstructure:"ssh_read_write_timeout" cty:"ssh_read_write_timeout" hcl:"ssh_read_write_timeout"`
SSHRemoteTunnels []string `mapstructure:"ssh_remote_tunnels" cty:"ssh_remote_tunnels" hcl:"ssh_remote_tunnels"`
SSHLocalTunnels []string `mapstructure:"ssh_local_tunnels" cty:"ssh_local_tunnels" hcl:"ssh_local_tunnels"`
SSHPublicKey []byte `mapstructure:"ssh_public_key" undocumented:"true" cty:"ssh_public_key" hcl:"ssh_public_key"`
SSHPrivateKey []byte `mapstructure:"ssh_private_key" undocumented:"true" cty:"ssh_private_key" hcl:"ssh_private_key"`
WinRMUser *string `mapstructure:"winrm_username" cty:"winrm_username" hcl:"winrm_username"`
WinRMPassword *string `mapstructure:"winrm_password" cty:"winrm_password" hcl:"winrm_password"`
WinRMHost *string `mapstructure:"winrm_host" cty:"winrm_host" hcl:"winrm_host"`
WinRMNoProxy *bool `mapstructure:"winrm_no_proxy" cty:"winrm_no_proxy" hcl:"winrm_no_proxy"`
WinRMPort *int `mapstructure:"winrm_port" cty:"winrm_port" hcl:"winrm_port"`
WinRMTimeout *string `mapstructure:"winrm_timeout" cty:"winrm_timeout" hcl:"winrm_timeout"`
WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl" hcl:"winrm_use_ssl"`
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
ProxmoxURLRaw *string `mapstructure:"proxmox_url" cty:"proxmox_url" hcl:"proxmox_url"`
SkipCertValidation *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"`
Username *string `mapstructure:"username" cty:"username" hcl:"username"`
Password *string `mapstructure:"password" cty:"password" hcl:"password"`
Node *string `mapstructure:"node" cty:"node" hcl:"node"`
Pool *string `mapstructure:"pool" cty:"pool" hcl:"pool"`
VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"`
VMID *int `mapstructure:"vm_id" cty:"vm_id" hcl:"vm_id"`
Memory *int `mapstructure:"memory" cty:"memory" hcl:"memory"`
Cores *int `mapstructure:"cores" cty:"cores" hcl:"cores"`
CPUType *string `mapstructure:"cpu_type" cty:"cpu_type" hcl:"cpu_type"`
Sockets *int `mapstructure:"sockets" cty:"sockets" hcl:"sockets"`
OS *string `mapstructure:"os" cty:"os" hcl:"os"`
VGA *proxmox.FlatvgaConfig `mapstructure:"vga" cty:"vga" hcl:"vga"`
NICs []proxmox.FlatnicConfig `mapstructure:"network_adapters" cty:"network_adapters" hcl:"network_adapters"`
Disks []proxmox.FlatdiskConfig `mapstructure:"disks" cty:"disks" hcl:"disks"`
Agent *bool `mapstructure:"qemu_agent" cty:"qemu_agent" hcl:"qemu_agent"`
SCSIController *string `mapstructure:"scsi_controller" cty:"scsi_controller" hcl:"scsi_controller"`
Onboot *bool `mapstructure:"onboot" cty:"onboot" hcl:"onboot"`
DisableKVM *bool `mapstructure:"disable_kvm" cty:"disable_kvm" hcl:"disable_kvm"`
TemplateName *string `mapstructure:"template_name" cty:"template_name" hcl:"template_name"`
TemplateDescription *string `mapstructure:"template_description" cty:"template_description" hcl:"template_description"`
CloudInit *bool `mapstructure:"cloud_init" cty:"cloud_init" hcl:"cloud_init"`
CloudInitStoragePool *string `mapstructure:"cloud_init_storage_pool" cty:"cloud_init_storage_pool" hcl:"cloud_init_storage_pool"`
AdditionalISOFiles []proxmox.FlatstorageConfig `mapstructure:"additional_iso_files" cty:"additional_iso_files" hcl:"additional_iso_files"`
VMInterface *string `mapstructure:"vm_interface" cty:"vm_interface" hcl:"vm_interface"`
CloneVM *string `mapstructure:"clone_vm" cty:"clone_vm" hcl:"clone_vm"`
FullClone *bool `mapstructure:"full_clone" required:"false" cty:"full_clone" hcl:"full_clone"`
}
// FlatMapstructure returns a new FlatConfig.
// FlatConfig is an auto-generated flat version of Config.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatConfig)
}
// HCL2Spec returns the hcl spec of a Config.
// This spec is used by HCL to read the fields of Config.
// The decoded values from this spec will then be applied to a FlatConfig.
func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false},
"packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false},
"packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false},
"packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false},
"packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false},
"packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false},
"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
"http_directory": &hcldec.AttrSpec{Name: "http_directory", Type: cty.String, Required: false},
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false},
"boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false},
"boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false},
"boot_key_interval": &hcldec.AttrSpec{Name: "boot_key_interval", Type: cty.String, Required: false},
"communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false},
"pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false},
"ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false},
"ssh_port": &hcldec.AttrSpec{Name: "ssh_port", Type: cty.Number, Required: false},
"ssh_username": &hcldec.AttrSpec{Name: "ssh_username", Type: cty.String, Required: false},
"ssh_password": &hcldec.AttrSpec{Name: "ssh_password", Type: cty.String, Required: false},
"ssh_keypair_name": &hcldec.AttrSpec{Name: "ssh_keypair_name", Type: cty.String, Required: false},
"temporary_key_pair_name": &hcldec.AttrSpec{Name: "temporary_key_pair_name", Type: cty.String, Required: false},
"ssh_ciphers": &hcldec.AttrSpec{Name: "ssh_ciphers", Type: cty.List(cty.String), Required: false},
"ssh_clear_authorized_keys": &hcldec.AttrSpec{Name: "ssh_clear_authorized_keys", Type: cty.Bool, Required: false},
"ssh_key_exchange_algorithms": &hcldec.AttrSpec{Name: "ssh_key_exchange_algorithms", Type: cty.List(cty.String), Required: false},
"ssh_private_key_file": &hcldec.AttrSpec{Name: "ssh_private_key_file", Type: cty.String, Required: false},
"ssh_certificate_file": &hcldec.AttrSpec{Name: "ssh_certificate_file", Type: cty.String, Required: false},
"ssh_pty": &hcldec.AttrSpec{Name: "ssh_pty", Type: cty.Bool, Required: false},
"ssh_timeout": &hcldec.AttrSpec{Name: "ssh_timeout", Type: cty.String, Required: false},
"ssh_wait_timeout": &hcldec.AttrSpec{Name: "ssh_wait_timeout", Type: cty.String, Required: false},
"ssh_agent_auth": &hcldec.AttrSpec{Name: "ssh_agent_auth", Type: cty.Bool, Required: false},
"ssh_disable_agent_forwarding": &hcldec.AttrSpec{Name: "ssh_disable_agent_forwarding", Type: cty.Bool, Required: false},
"ssh_handshake_attempts": &hcldec.AttrSpec{Name: "ssh_handshake_attempts", Type: cty.Number, Required: false},
"ssh_bastion_host": &hcldec.AttrSpec{Name: "ssh_bastion_host", Type: cty.String, Required: false},
"ssh_bastion_port": &hcldec.AttrSpec{Name: "ssh_bastion_port", Type: cty.Number, Required: false},
"ssh_bastion_agent_auth": &hcldec.AttrSpec{Name: "ssh_bastion_agent_auth", Type: cty.Bool, Required: false},
"ssh_bastion_username": &hcldec.AttrSpec{Name: "ssh_bastion_username", Type: cty.String, Required: false},
"ssh_bastion_password": &hcldec.AttrSpec{Name: "ssh_bastion_password", Type: cty.String, Required: false},
"ssh_bastion_interactive": &hcldec.AttrSpec{Name: "ssh_bastion_interactive", Type: cty.Bool, Required: false},
"ssh_bastion_private_key_file": &hcldec.AttrSpec{Name: "ssh_bastion_private_key_file", Type: cty.String, Required: false},
"ssh_bastion_certificate_file": &hcldec.AttrSpec{Name: "ssh_bastion_certificate_file", Type: cty.String, Required: false},
"ssh_file_transfer_method": &hcldec.AttrSpec{Name: "ssh_file_transfer_method", Type: cty.String, Required: false},
"ssh_proxy_host": &hcldec.AttrSpec{Name: "ssh_proxy_host", Type: cty.String, Required: false},
"ssh_proxy_port": &hcldec.AttrSpec{Name: "ssh_proxy_port", Type: cty.Number, Required: false},
"ssh_proxy_username": &hcldec.AttrSpec{Name: "ssh_proxy_username", Type: cty.String, Required: false},
"ssh_proxy_password": &hcldec.AttrSpec{Name: "ssh_proxy_password", Type: cty.String, Required: false},
"ssh_keep_alive_interval": &hcldec.AttrSpec{Name: "ssh_keep_alive_interval", Type: cty.String, Required: false},
"ssh_read_write_timeout": &hcldec.AttrSpec{Name: "ssh_read_write_timeout", Type: cty.String, Required: false},
"ssh_remote_tunnels": &hcldec.AttrSpec{Name: "ssh_remote_tunnels", Type: cty.List(cty.String), Required: false},
"ssh_local_tunnels": &hcldec.AttrSpec{Name: "ssh_local_tunnels", Type: cty.List(cty.String), Required: false},
"ssh_public_key": &hcldec.AttrSpec{Name: "ssh_public_key", Type: cty.List(cty.Number), Required: false},
"ssh_private_key": &hcldec.AttrSpec{Name: "ssh_private_key", Type: cty.List(cty.Number), Required: false},
"winrm_username": &hcldec.AttrSpec{Name: "winrm_username", Type: cty.String, Required: false},
"winrm_password": &hcldec.AttrSpec{Name: "winrm_password", Type: cty.String, Required: false},
"winrm_host": &hcldec.AttrSpec{Name: "winrm_host", Type: cty.String, Required: false},
"winrm_no_proxy": &hcldec.AttrSpec{Name: "winrm_no_proxy", Type: cty.Bool, Required: false},
"winrm_port": &hcldec.AttrSpec{Name: "winrm_port", Type: cty.Number, Required: false},
"winrm_timeout": &hcldec.AttrSpec{Name: "winrm_timeout", Type: cty.String, Required: false},
"winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false},
"winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false},
"winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false},
"proxmox_url": &hcldec.AttrSpec{Name: "proxmox_url", Type: cty.String, Required: false},
"insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false},
"username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false},
"password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false},
"node": &hcldec.AttrSpec{Name: "node", Type: cty.String, Required: false},
"pool": &hcldec.AttrSpec{Name: "pool", Type: cty.String, Required: false},
"vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false},
"vm_id": &hcldec.AttrSpec{Name: "vm_id", Type: cty.Number, Required: false},
"memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false},
"cores": &hcldec.AttrSpec{Name: "cores", Type: cty.Number, Required: false},
"cpu_type": &hcldec.AttrSpec{Name: "cpu_type", Type: cty.String, Required: false},
"sockets": &hcldec.AttrSpec{Name: "sockets", Type: cty.Number, Required: false},
"os": &hcldec.AttrSpec{Name: "os", Type: cty.String, Required: false},
"vga": &hcldec.BlockSpec{TypeName: "vga", Nested: hcldec.ObjectSpec((*proxmox.FlatvgaConfig)(nil).HCL2Spec())},
"network_adapters": &hcldec.BlockListSpec{TypeName: "network_adapters", Nested: hcldec.ObjectSpec((*proxmox.FlatnicConfig)(nil).HCL2Spec())},
"disks": &hcldec.BlockListSpec{TypeName: "disks", Nested: hcldec.ObjectSpec((*proxmox.FlatdiskConfig)(nil).HCL2Spec())},
"qemu_agent": &hcldec.AttrSpec{Name: "qemu_agent", Type: cty.Bool, Required: false},
"scsi_controller": &hcldec.AttrSpec{Name: "scsi_controller", Type: cty.String, Required: false},
"onboot": &hcldec.AttrSpec{Name: "onboot", Type: cty.Bool, Required: false},
"disable_kvm": &hcldec.AttrSpec{Name: "disable_kvm", Type: cty.Bool, Required: false},
"template_name": &hcldec.AttrSpec{Name: "template_name", Type: cty.String, Required: false},
"template_description": &hcldec.AttrSpec{Name: "template_description", Type: cty.String, Required: false},
"cloud_init": &hcldec.AttrSpec{Name: "cloud_init", Type: cty.Bool, Required: false},
"cloud_init_storage_pool": &hcldec.AttrSpec{Name: "cloud_init_storage_pool", Type: cty.String, Required: false},
"additional_iso_files": &hcldec.BlockListSpec{TypeName: "additional_iso_files", Nested: hcldec.ObjectSpec((*proxmox.FlatstorageConfig)(nil).HCL2Spec())},
"vm_interface": &hcldec.AttrSpec{Name: "vm_interface", Type: cty.String, Required: false},
"clone_vm": &hcldec.AttrSpec{Name: "clone_vm", Type: cty.String, Required: false},
"full_clone": &hcldec.AttrSpec{Name: "full_clone", Type: cty.Bool, Required: false},
}
return s
}

View File

@ -0,0 +1,107 @@
package proxmoxclone
import (
"context"
"fmt"
"os"
common "github.com/hashicorp/packer/builder/proxmox/common"
"github.com/hashicorp/packer/common/uuid"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/helper/ssh"
"github.com/hashicorp/packer/packer"
)
// StepSshKeyPair executes the business logic for setting the SSH key pair in
// the specified communicator.Config.
type StepSshKeyPair struct {
Debug bool
DebugKeyPath string
}
func (s *StepSshKeyPair) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(*common.Config)
if c.Comm.SSHPassword != "" {
return multistep.ActionContinue
}
if c.Comm.SSHPrivateKeyFile != "" {
ui.Say("Using existing SSH private key for the communicator...")
privateKeyBytes, err := c.Comm.ReadSSHPrivateKeyFile()
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
kp, err := ssh.KeyPairFromPrivateKey(ssh.FromPrivateKeyConfig{
RawPrivateKeyPemBlock: privateKeyBytes,
Comment: fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()),
})
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
c.Comm.SSHPrivateKey = privateKeyBytes
c.Comm.SSHKeyPairName = kp.Comment
c.Comm.SSHTemporaryKeyPairName = kp.Comment
c.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine
return multistep.ActionContinue
}
if c.Comm.SSHAgentAuth {
ui.Say("Using local SSH Agent to authenticate connections for the communicator...")
return multistep.ActionContinue
}
ui.Say("Creating ephemeral key pair for SSH communicator...")
kp, err := ssh.NewKeyPair(ssh.CreateKeyPairConfig{
Comment: fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()),
})
if err != nil {
state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err))
return multistep.ActionHalt
}
c.Comm.SSHKeyPairName = kp.Comment
c.Comm.SSHTemporaryKeyPairName = kp.Comment
c.Comm.SSHPrivateKey = kp.PrivateKeyPemBlock
c.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine
c.Comm.SSHClearAuthorizedKeys = true
ui.Say("Created ephemeral SSH key pair for communicator")
// If we're in debug mode, output the private key to the working
// directory.
if s.Debug {
ui.Message(fmt.Sprintf("Saving communicator private key for debug purposes: %s", s.DebugKeyPath))
f, err := os.OpenFile(s.DebugKeyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt
}
defer f.Close()
// Write the key out
if _, err := f.Write(kp.PrivateKeyPemBlock); err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *StepSshKeyPair) Cleanup(state multistep.StateBag) {
if s.Debug {
if err := os.Remove(s.DebugKeyPath); err != nil {
ui := state.Get("ui").(packer.Ui)
ui.Error(fmt.Sprintf(
"Error removing debug key '%s': %s", s.DebugKeyPath, err))
}
}
}

View File

@ -10,6 +10,7 @@ import (
)
type Artifact struct {
builderID string
templateID int
proxmoxClient *proxmox.Client
@ -21,8 +22,8 @@ type Artifact struct {
// Artifact implements packer.Artifact
var _ packer.Artifact = &Artifact{}
func (*Artifact) BuilderId() string {
return BuilderId
func (a *Artifact) BuilderId() string {
return a.builderID
}
func (*Artifact) Files() []string {

View File

@ -0,0 +1,167 @@
package proxmox
import (
"context"
"crypto/tls"
"errors"
"fmt"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
func NewSharedBuilder(id string, config Config, preSteps []multistep.Step, postSteps []multistep.Step, vmCreator ProxmoxVMCreator) *Builder {
return &Builder{
id: id,
config: config,
preSteps: preSteps,
postSteps: postSteps,
vmCreator: vmCreator,
}
}
type Builder struct {
id string
config Config
preSteps []multistep.Step
postSteps []multistep.Step
runner multistep.Runner
proxmoxClient *proxmox.Client
vmCreator ProxmoxVMCreator
}
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook, state multistep.StateBag) (packer.Artifact, error) {
var err error
tlsConfig := &tls.Config{
InsecureSkipVerify: b.config.SkipCertValidation,
}
b.proxmoxClient, err = proxmox.NewClient(b.config.proxmoxURL.String(), nil, tlsConfig)
if err != nil {
return nil, err
}
err = b.proxmoxClient.Login(b.config.Username, b.config.Password, "")
if err != nil {
return nil, err
}
// Set up the state
state.Put("config", &b.config)
state.Put("proxmoxClient", b.proxmoxClient)
state.Put("hook", hook)
state.Put("ui", ui)
comm := &b.config.Comm
// Build the steps
coreSteps := []multistep.Step{
&stepStartVM{
vmCreator: b.vmCreator,
},
&common.StepHTTPServer{
HTTPDir: b.config.HTTPDir,
HTTPPortMin: b.config.HTTPPortMin,
HTTPPortMax: b.config.HTTPPortMax,
HTTPAddress: b.config.HTTPAddress,
},
&stepTypeBootCommand{
BootConfig: b.config.BootConfig,
Ctx: b.config.Ctx,
},
&communicator.StepConnect{
Config: comm,
Host: commHost((*comm).Host()),
SSHConfig: (*comm).SSHConfigFunc(),
},
&common.StepProvision{},
&common.StepCleanupTempKeys{
Comm: &b.config.Comm,
},
&stepConvertToTemplate{},
&stepFinalizeTemplateConfig{},
&stepSuccess{},
}
steps := append(b.preSteps, coreSteps...)
steps = append(steps, b.postSteps...)
// Run the steps
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(ctx, state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
// If we were interrupted or cancelled, then just exit.
if _, ok := state.GetOk(multistep.StateCancelled); ok {
return nil, errors.New("build was cancelled")
}
// Verify that the template_id was set properly, otherwise we didn't progress through the last step
tplID, ok := state.Get("template_id").(int)
if !ok {
return nil, fmt.Errorf("template ID could not be determined")
}
artifact := &Artifact{
builderID: b.id,
templateID: tplID,
proxmoxClient: b.proxmoxClient,
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
}
return artifact, nil
}
// Returns ssh_host or winrm_host (see communicator.Config.Host) config
// parameter when set, otherwise gets the host IP from running VM
func commHost(host string) func(state multistep.StateBag) (string, error) {
if host != "" {
return func(state multistep.StateBag) (string, error) {
return host, nil
}
}
return getVMIP
}
// Reads the first non-loopback interface's IP address from the VM.
// qemu-guest-agent package must be installed on the VM
func getVMIP(state multistep.StateBag) (string, error) {
client := state.Get("proxmoxClient").(*proxmox.Client)
config := state.Get("config").(*Config)
vmRef := state.Get("vmRef").(*proxmox.VmRef)
ifs, err := client.GetVmAgentNetworkInterfaces(vmRef)
if err != nil {
return "", err
}
if config.VMInterface != "" {
for _, iface := range ifs {
if config.VMInterface != iface.Name {
continue
}
for _, addr := range iface.IPAddresses {
if addr.IsLoopback() {
continue
}
return addr.String(), nil
}
return "", fmt.Errorf("Interface %s only has loopback addresses", config.VMInterface)
}
return "", fmt.Errorf("Interface %s not found in VM", config.VMInterface)
}
for _, iface := range ifs {
for _, addr := range iface.IPAddresses {
if addr.IsLoopback() {
continue
}
return addr.String(), nil
}
}
return "", fmt.Errorf("Found no IP addresses on VM")
}

View File

@ -8,7 +8,6 @@ import (
"log"
"net/url"
"os"
"strconv"
"strings"
"time"
@ -25,7 +24,6 @@ import (
type Config struct {
common.PackerConfig `mapstructure:",squash"`
common.HTTPConfig `mapstructure:",squash"`
common.ISOConfig `mapstructure:",squash"`
bootcommand.BootConfig `mapstructure:",squash"`
BootKeyInterval time.Duration `mapstructure:"boot_key_interval"`
Comm communicator.Config `mapstructure:",squash"`
@ -49,8 +47,6 @@ type Config struct {
VGA vgaConfig `mapstructure:"vga"`
NICs []nicConfig `mapstructure:"network_adapters"`
Disks []diskConfig `mapstructure:"disks"`
ISOFile string `mapstructure:"iso_file"`
ISOStoragePool string `mapstructure:"iso_storage_pool"`
Agent bool `mapstructure:"qemu_agent"`
SCSIController string `mapstructure:"scsi_controller"`
Onboot bool `mapstructure:"onboot"`
@ -58,17 +54,24 @@ type Config struct {
TemplateName string `mapstructure:"template_name"`
TemplateDescription string `mapstructure:"template_description"`
UnmountISO bool `mapstructure:"unmount_iso"`
CloudInit bool `mapstructure:"cloud_init"`
CloudInitStoragePool string `mapstructure:"cloud_init_storage_pool"`
shouldUploadISO bool
AdditionalISOFiles []storageConfig `mapstructure:"additional_iso_files"`
VMInterface string `mapstructure:"vm_interface"`
ctx interpolate.Context
Ctx interpolate.Context `mapstructure-to-hcl2:",skip"`
}
type storageConfig struct {
common.ISOConfig `mapstructure:",squash"`
Device string `mapstructure:"device"`
ISOFile string `mapstructure:"iso_file"`
ISOStoragePool string `mapstructure:"iso_storage_pool"`
Unmount bool `mapstructure:"unmount"`
ShouldUploadISO bool
DownloadPathKey string
}
type nicConfig struct {
@ -92,27 +95,18 @@ type vgaConfig struct {
Type string `mapstructure:"type"`
Memory int `mapstructure:"memory"`
}
type storageConfig struct {
common.ISOConfig `mapstructure:",squash"`
Device string `mapstructure:"device"`
ISOFile string `mapstructure:"iso_file"`
ISOStoragePool string `mapstructure:"iso_storage_pool"`
Unmount bool `mapstructure:"unmount"`
shouldUploadISO bool
downloadPathKey string
}
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
func (c *Config) Prepare(upper interface{}, raws ...interface{}) ([]string, []string, error) {
// Agent defaults to true
c.Agent = true
// Do not add a cloud-init cdrom by default
c.CloudInit = false
var md mapstructure.Metadata
err := config.Decode(c, &config.DecodeOpts{
err := config.Decode(upper, &config.DecodeOpts{
Metadata: &md,
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateContext: &c.Ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"boot_command",
@ -120,11 +114,13 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
},
}, raws...)
if err != nil {
return nil, err
return nil, nil, err
}
var errs *packer.MultiError
warnings := make([]string, 0)
var warnings []string
packer.LogSecretFilter.Set(c.Password)
// Defaults
if c.ProxmoxURLRaw == "" {
@ -208,89 +204,14 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("disk format must be specified for pool type %q", c.Disks[idx].StoragePoolType))
}
}
for idx := range c.AdditionalISOFiles {
// Check AdditionalISO config
// Either a pre-uploaded ISO should be referenced in iso_file, OR a URL
// (possibly to a local file) to an ISO file that will be downloaded and
// then uploaded to Proxmox.
if c.AdditionalISOFiles[idx].ISOFile != "" {
c.AdditionalISOFiles[idx].shouldUploadISO = false
} else {
c.AdditionalISOFiles[idx].downloadPathKey = "downloaded_additional_iso_path_" + strconv.Itoa(idx)
isoWarnings, isoErrors := c.AdditionalISOFiles[idx].ISOConfig.Prepare(&c.ctx)
errs = packer.MultiErrorAppend(errs, isoErrors...)
warnings = append(warnings, isoWarnings...)
c.AdditionalISOFiles[idx].shouldUploadISO = true
}
if c.AdditionalISOFiles[idx].Device == "" {
log.Printf("AdditionalISOFile %d Device not set, using default 'ide3'", idx)
c.AdditionalISOFiles[idx].Device = "ide3"
}
if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "ide") {
busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[3:])
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[3:]))
}
if busnumber == 2 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("IDE bus 2 is used by boot ISO"))
}
if busnumber > 3 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("IDE bus index can't be higher than 3"))
}
}
if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "sata") {
busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[4:])
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[4:]))
}
if busnumber > 5 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("SATA bus index can't be higher than 5"))
}
}
if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "scsi") {
busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[4:])
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[4:]))
}
if busnumber > 30 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("SCSI bus index can't be higher than 30"))
}
}
if (c.AdditionalISOFiles[idx].ISOFile == "" && len(c.AdditionalISOFiles[idx].ISOConfig.ISOUrls) == 0) || (c.AdditionalISOFiles[idx].ISOFile != "" && len(c.AdditionalISOFiles[idx].ISOConfig.ISOUrls) != 0) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("either iso_file or iso_url, but not both, must be specified for AdditionalISO file %s", c.AdditionalISOFiles[idx].Device))
}
if len(c.ISOConfig.ISOUrls) != 0 && c.ISOStoragePool == "" {
errs = packer.MultiErrorAppend(errs, errors.New("when specifying iso_url, iso_storage_pool must also be specified"))
}
}
if c.SCSIController == "" {
log.Printf("SCSI controller not set, using default 'lsi'")
c.SCSIController = "lsi"
}
errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...)
// Check ISO config
// Either a pre-uploaded ISO should be referenced in iso_file, OR a URL
// (possibly to a local file) to an ISO file that will be downloaded and
// then uploaded to Proxmox.
if c.ISOFile != "" {
c.shouldUploadISO = false
} else {
isoWarnings, isoErrors := c.ISOConfig.Prepare(&c.ctx)
errs = packer.MultiErrorAppend(errs, isoErrors...)
warnings = append(warnings, isoWarnings...)
c.shouldUploadISO = true
}
if (c.ISOFile == "" && len(c.ISOConfig.ISOUrls) == 0) || (c.ISOFile != "" && len(c.ISOConfig.ISOUrls) != 0) {
errs = packer.MultiErrorAppend(errs, errors.New("either iso_file or iso_url, but not both, must be specified"))
}
if len(c.ISOConfig.ISOUrls) != 0 && c.ISOStoragePool == "" {
errs = packer.MultiErrorAppend(errs, errors.New("when specifying iso_url, iso_storage_pool must also be specified"))
}
errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.Ctx)...)
errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare(&c.Ctx)...)
errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.Ctx)...)
// Required configurations that will display errors if not set
if c.Username == "" {
@ -329,11 +250,9 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
}
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
return nil, warnings, errs
}
packer.LogSecretFilter.Set(c.Password)
return nil, nil
return nil, warnings, nil
}
func contains(haystack []string, needle string) bool {

View File

@ -21,11 +21,6 @@ type FlatConfig struct {
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
TargetPath *string `mapstructure:"iso_target_path" cty:"iso_target_path" hcl:"iso_target_path"`
TargetExtension *string `mapstructure:"iso_target_extension" cty:"iso_target_extension" hcl:"iso_target_extension"`
BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"`
BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"`
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
@ -95,15 +90,12 @@ type FlatConfig struct {
VGA *FlatvgaConfig `mapstructure:"vga" cty:"vga" hcl:"vga"`
NICs []FlatnicConfig `mapstructure:"network_adapters" cty:"network_adapters" hcl:"network_adapters"`
Disks []FlatdiskConfig `mapstructure:"disks" cty:"disks" hcl:"disks"`
ISOFile *string `mapstructure:"iso_file" cty:"iso_file" hcl:"iso_file"`
ISOStoragePool *string `mapstructure:"iso_storage_pool" cty:"iso_storage_pool" hcl:"iso_storage_pool"`
Agent *bool `mapstructure:"qemu_agent" cty:"qemu_agent" hcl:"qemu_agent"`
SCSIController *string `mapstructure:"scsi_controller" cty:"scsi_controller" hcl:"scsi_controller"`
Onboot *bool `mapstructure:"onboot" cty:"onboot" hcl:"onboot"`
DisableKVM *bool `mapstructure:"disable_kvm" cty:"disable_kvm" hcl:"disable_kvm"`
TemplateName *string `mapstructure:"template_name" cty:"template_name" hcl:"template_name"`
TemplateDescription *string `mapstructure:"template_description" cty:"template_description" hcl:"template_description"`
UnmountISO *bool `mapstructure:"unmount_iso" cty:"unmount_iso" hcl:"unmount_iso"`
CloudInit *bool `mapstructure:"cloud_init" cty:"cloud_init" hcl:"cloud_init"`
CloudInitStoragePool *string `mapstructure:"cloud_init_storage_pool" cty:"cloud_init_storage_pool" hcl:"cloud_init_storage_pool"`
AdditionalISOFiles []FlatstorageConfig `mapstructure:"additional_iso_files" cty:"additional_iso_files" hcl:"additional_iso_files"`
@ -134,11 +126,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
"iso_target_path": &hcldec.AttrSpec{Name: "iso_target_path", Type: cty.String, Required: false},
"iso_target_extension": &hcldec.AttrSpec{Name: "iso_target_extension", Type: cty.String, Required: false},
"boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false},
"boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false},
"boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false},
@ -208,15 +195,12 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"vga": &hcldec.BlockSpec{TypeName: "vga", Nested: hcldec.ObjectSpec((*FlatvgaConfig)(nil).HCL2Spec())},
"network_adapters": &hcldec.BlockListSpec{TypeName: "network_adapters", Nested: hcldec.ObjectSpec((*FlatnicConfig)(nil).HCL2Spec())},
"disks": &hcldec.BlockListSpec{TypeName: "disks", Nested: hcldec.ObjectSpec((*FlatdiskConfig)(nil).HCL2Spec())},
"iso_file": &hcldec.AttrSpec{Name: "iso_file", Type: cty.String, Required: false},
"iso_storage_pool": &hcldec.AttrSpec{Name: "iso_storage_pool", Type: cty.String, Required: false},
"qemu_agent": &hcldec.AttrSpec{Name: "qemu_agent", Type: cty.Bool, Required: false},
"scsi_controller": &hcldec.AttrSpec{Name: "scsi_controller", Type: cty.String, Required: false},
"onboot": &hcldec.AttrSpec{Name: "onboot", Type: cty.Bool, Required: false},
"disable_kvm": &hcldec.AttrSpec{Name: "disable_kvm", Type: cty.Bool, Required: false},
"template_name": &hcldec.AttrSpec{Name: "template_name", Type: cty.String, Required: false},
"template_description": &hcldec.AttrSpec{Name: "template_description", Type: cty.String, Required: false},
"unmount_iso": &hcldec.AttrSpec{Name: "unmount_iso", Type: cty.Bool, Required: false},
"cloud_init": &hcldec.AttrSpec{Name: "cloud_init", Type: cty.Bool, Required: false},
"cloud_init_storage_pool": &hcldec.AttrSpec{Name: "cloud_init_storage_pool", Type: cty.String, Required: false},
"additional_iso_files": &hcldec.BlockListSpec{TypeName: "additional_iso_files", Nested: hcldec.ObjectSpec((*FlatstorageConfig)(nil).HCL2Spec())},
@ -305,6 +289,8 @@ type FlatstorageConfig struct {
ISOFile *string `mapstructure:"iso_file" cty:"iso_file" hcl:"iso_file"`
ISOStoragePool *string `mapstructure:"iso_storage_pool" cty:"iso_storage_pool" hcl:"iso_storage_pool"`
Unmount *bool `mapstructure:"unmount" cty:"unmount" hcl:"unmount"`
ShouldUploadISO *bool `cty:"should_upload_iso" hcl:"should_upload_iso"`
DownloadPathKey *string `cty:"download_path_key" hcl:"download_path_key"`
}
// FlatMapstructure returns a new FlatstorageConfig.
@ -328,6 +314,8 @@ func (*FlatstorageConfig) HCL2Spec() map[string]hcldec.Spec {
"iso_file": &hcldec.AttrSpec{Name: "iso_file", Type: cty.String, Required: false},
"iso_storage_pool": &hcldec.AttrSpec{Name: "iso_storage_pool", Type: cty.String, Required: false},
"unmount": &hcldec.AttrSpec{Name: "unmount", Type: cty.Bool, Required: false},
"should_upload_iso": &hcldec.AttrSpec{Name: "should_upload_iso", Type: cty.Bool, Required: false},
"download_path_key": &hcldec.AttrSpec{Name: "download_path_key", Type: cty.String, Required: false},
}
return s
}

View File

@ -0,0 +1,104 @@
package proxmox
import (
"strings"
"testing"
"github.com/hashicorp/packer/packer"
)
func mandatoryConfig(t *testing.T) map[string]interface{} {
return map[string]interface{}{
"proxmox_url": "https://my-proxmox.my-domain:8006/api2/json",
"username": "apiuser@pve",
"password": "supersecret",
"node": "my-proxmox",
"ssh_username": "root",
}
}
func TestRequiredParameters(t *testing.T) {
var c Config
_, _, err := c.Prepare(&c, make(map[string]interface{}))
if err == nil {
t.Fatal("Expected empty configuration to fail")
}
errs, ok := err.(*packer.MultiError)
if !ok {
t.Fatal("Expected errors to be packer.MultiError")
}
required := []string{"username", "password", "proxmox_url", "node", "ssh_username"}
for _, param := range required {
found := false
for _, err := range errs.Errors {
if strings.Contains(err.Error(), param) {
found = true
break
}
}
if !found {
t.Errorf("Expected error about missing parameter %q", required)
}
}
}
func TestAgentSetToFalse(t *testing.T) {
cfg := mandatoryConfig(t)
cfg["qemu_agent"] = false
var c Config
_, _, err := c.Prepare(&c, cfg)
if err != nil {
t.Fatal(err)
}
if c.Agent != false {
t.Errorf("Expected Agent to be false, got %t", c.Agent)
}
}
func TestPacketQueueSupportForNetworkAdapters(t *testing.T) {
drivertests := []struct {
expectedToFail bool
model string
}{
{expectedToFail: false, model: "virtio"},
{expectedToFail: true, model: "e1000"},
{expectedToFail: true, model: "e1000-82540em"},
{expectedToFail: true, model: "e1000-82544gc"},
{expectedToFail: true, model: "e1000-82545em"},
{expectedToFail: true, model: "i82551"},
{expectedToFail: true, model: "i82557b"},
{expectedToFail: true, model: "i82559er"},
{expectedToFail: true, model: "ne2k_isa"},
{expectedToFail: true, model: "ne2k_pci"},
{expectedToFail: true, model: "pcnet"},
{expectedToFail: true, model: "rtl8139"},
{expectedToFail: true, model: "vmxnet3"},
}
for _, tt := range drivertests {
device := make(map[string]interface{})
device["bridge"] = "vmbr0"
device["model"] = tt.model
device["packet_queues"] = 2
devices := make([]map[string]interface{}, 0)
devices = append(devices, device)
cfg := mandatoryConfig(t)
cfg["network_adapters"] = devices
var c Config
_, _, err := c.Prepare(&c, cfg)
if tt.expectedToFail == true && err == nil {
t.Error("expected config preparation to fail, but no error occured")
}
if tt.expectedToFail == false && err != nil {
t.Errorf("expected config preparation to succeed, but %s", err.Error())
}
}
}

View File

@ -38,24 +38,6 @@ func (s *stepFinalizeTemplateConfig) Run(ctx context.Context, state multistep.St
// set, we need to clear it
changes["description"] = c.TemplateDescription
if c.UnmountISO {
vmParams, err := client.GetVmConfig(vmRef)
if err != nil {
err := fmt.Errorf("Error fetching template config: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if vmParams["ide2"] == nil || !strings.HasSuffix(vmParams["ide2"].(string), "media=cdrom") {
err := fmt.Errorf("Cannot eject ISO from cdrom drive, ide2 is not present, or not a cdrom media")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
changes["ide2"] = "none,media=cdrom"
}
if c.CloudInit {
vmParams, err := client.GetVmConfig(vmRef)
if err != nil {
@ -93,29 +75,6 @@ func (s *stepFinalizeTemplateConfig) Run(ctx context.Context, state multistep.St
}
}
if len(c.AdditionalISOFiles) > 0 {
vmParams, err := client.GetVmConfig(vmRef)
if err != nil {
err := fmt.Errorf("Error fetching template config: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
for idx := range c.AdditionalISOFiles {
cdrom := c.AdditionalISOFiles[idx].Device
if c.AdditionalISOFiles[idx].Unmount {
if vmParams[cdrom] == nil || !strings.Contains(vmParams[cdrom].(string), "media=cdrom") {
err := fmt.Errorf("Cannot eject ISO from cdrom drive, %s is not present or not a cdrom media", cdrom)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
changes[cdrom] = "none,media=cdrom"
} else {
changes[cdrom] = c.AdditionalISOFiles[idx].ISOFile + ",media=cdrom"
}
}
}
if len(changes) > 0 {
_, err := client.SetVmConfig(vmRef, changes)
if err != nil {

View File

@ -56,18 +56,15 @@ func TestTemplateFinalize(t *testing.T) {
builderConfig: &Config{
TemplateName: "my-template",
TemplateDescription: "some-description",
UnmountISO: true,
},
initialVMConfig: map[string]interface{}{
"name": "dummy",
"description": "Packer ephemeral build VM",
"ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom",
},
expectCallSetConfig: true,
expectedVMConfig: map[string]interface{}{
"name": "my-template",
"description": "some-description",
"ide2": "none,media=cdrom",
},
expectedAction: multistep.ActionContinue,
},
@ -76,13 +73,11 @@ func TestTemplateFinalize(t *testing.T) {
builderConfig: &Config{
TemplateName: "my-template",
TemplateDescription: "some-description",
UnmountISO: true,
CloudInit: true,
},
initialVMConfig: map[string]interface{}{
"name": "dummy",
"description": "Packer ephemeral build VM",
"ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom",
"bootdisk": "virtio0",
"virtio0": "ceph01:base-223-disk-0,cache=unsafe,media=disk,size=32G",
},
@ -90,7 +85,6 @@ func TestTemplateFinalize(t *testing.T) {
expectedVMConfig: map[string]interface{}{
"name": "my-template",
"description": "some-description",
"ide2": "none,media=cdrom",
"ide3": "ceph01:cloudinit",
},
expectedAction: multistep.ActionContinue,
@ -100,7 +94,6 @@ func TestTemplateFinalize(t *testing.T) {
builderConfig: &Config{
TemplateName: "my-template",
TemplateDescription: "some-description",
UnmountISO: false,
CloudInit: true,
},
initialVMConfig: map[string]interface{}{
@ -116,27 +109,12 @@ func TestTemplateFinalize(t *testing.T) {
expectCallSetConfig: false,
expectedAction: multistep.ActionHalt,
},
{
name: "no cd-drive with unmount=true should returns halt",
builderConfig: &Config{
TemplateName: "my-template",
TemplateDescription: "some-description",
UnmountISO: true,
},
initialVMConfig: map[string]interface{}{
"name": "dummy",
"description": "Packer ephemeral build VM",
"ide1": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom",
},
expectCallSetConfig: false,
expectedAction: multistep.ActionHalt,
},
{
name: "GetVmConfig error should return halt",
builderConfig: &Config{
TemplateName: "my-template",
TemplateDescription: "some-description",
UnmountISO: true,
CloudInit: true,
},
getConfigErr: fmt.Errorf("some error"),
expectCallSetConfig: false,
@ -147,12 +125,10 @@ func TestTemplateFinalize(t *testing.T) {
builderConfig: &Config{
TemplateName: "my-template",
TemplateDescription: "some-description",
UnmountISO: true,
},
initialVMConfig: map[string]interface{}{
"name": "dummy",
"description": "Packer ephemeral build VM",
"ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom",
},
expectCallSetConfig: true,
setConfigErr: fmt.Errorf("some error"),

View File

@ -15,7 +15,13 @@ import (
//
// It sets the vmRef state which is used throughout the later steps to reference the VM
// in API calls.
type stepStartVM struct{}
type stepStartVM struct {
vmCreator ProxmoxVMCreator
}
type ProxmoxVMCreator interface {
Create(*proxmox.VmRef, proxmox.ConfigQemu, multistep.StateBag) error
}
func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
@ -32,8 +38,6 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist
kvm = false
}
isoFile := state.Get("iso_file").(string)
ui.Say("Creating VM")
config := proxmox.ConfigQemu{
Name: c.VMName,
@ -47,7 +51,6 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist
QemuSockets: c.Sockets,
QemuOs: c.OS,
QemuVga: generateProxmoxVga(c.VGA),
QemuIso: isoFile,
QemuNetworks: generateProxmoxNetworkAdapters(c.NICs),
QemuDisks: generateProxmoxDisks(c.Disks),
Scsihw: c.SCSIController,
@ -78,8 +81,9 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist
vmRef.SetPool(c.Pool)
}
err := config.CreateVm(vmRef, client)
err := s.vmCreator.Create(vmRef, config, state)
if err != nil {
err := fmt.Errorf("Error creating VM: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
@ -89,20 +93,9 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist
state.Put("vmRef", vmRef)
// instance_id is the generic term used so that users can have access to the
// instance id inside of the provisioners, used in step_provision.
state.Put("instance_id", vmRef)
for idx := range c.AdditionalISOFiles {
params := map[string]interface{}{
c.AdditionalISOFiles[idx].Device: c.AdditionalISOFiles[idx].ISOFile + ",media=cdrom",
}
_, err = client.SetVmConfig(vmRef, params)
if err != nil {
err := fmt.Errorf("Error configuring VM: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
// Note that this is just the VMID, we do not keep the node, pool and other
// info available in the vmref type.
state.Put("instance_id", vmRef.VmId())
ui.Say("Starting VM")
_, err = client.StartVm(vmRef)

View File

@ -136,7 +136,7 @@ func TestTypeBootCommand(t *testing.T) {
step := stepTypeBootCommand{
c.builderConfig.BootConfig,
c.builderConfig.ctx,
c.builderConfig.Ctx,
}
action := step.Run(context.TODO(), state)
step.Cleanup(state)

View File

@ -0,0 +1,76 @@
package proxmoxiso
import (
"context"
proxmoxapi "github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/hcl/v2/hcldec"
proxmox "github.com/hashicorp/packer/builder/proxmox/common"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// The unique id for the builder
const BuilderID = "proxmox.iso"
type Builder struct {
config Config
}
// Builder implements packer.Builder
var _ packer.Builder = &Builder{}
func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() }
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
return b.config.Prepare(raws...)
}
const downloadPathKey = "downloaded_iso_path"
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
state := new(multistep.BasicStateBag)
state.Put("iso-config", &b.config)
preSteps := []multistep.Step{
&common.StepDownload{
Checksum: b.config.ISOChecksum,
Description: "ISO",
Extension: b.config.TargetExtension,
ResultKey: downloadPathKey,
TargetPath: b.config.TargetPath,
Url: b.config.ISOUrls,
},
}
for idx := range b.config.AdditionalISOFiles {
preSteps = append(preSteps, &common.StepDownload{
Checksum: b.config.AdditionalISOFiles[idx].ISOChecksum,
Description: "additional ISO",
Extension: b.config.AdditionalISOFiles[idx].TargetExtension,
ResultKey: b.config.AdditionalISOFiles[idx].DownloadPathKey,
TargetPath: b.config.AdditionalISOFiles[idx].DownloadPathKey,
Url: b.config.AdditionalISOFiles[idx].ISOUrls,
})
}
preSteps = append(preSteps,
&stepUploadISO{},
&stepUploadAdditionalISOs{},
)
postSteps := []multistep.Step{
&stepFinalizeISOTemplate{},
}
sb := proxmox.NewSharedBuilder(BuilderID, b.config.Config, preSteps, postSteps, &isoVMCreator{})
return sb.Run(ctx, ui, hook, state)
}
type isoVMCreator struct{}
func (*isoVMCreator) Create(vmRef *proxmoxapi.VmRef, config proxmoxapi.ConfigQemu, state multistep.StateBag) error {
isoFile := state.Get("iso_file").(string)
config.QemuIso = isoFile
client := state.Get("proxmoxClient").(*proxmoxapi.Client)
return config.CreateVm(vmRef, client)
}

View File

@ -0,0 +1,114 @@
//go:generate mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig
package proxmoxiso
import (
"errors"
"fmt"
"log"
"strconv"
"strings"
proxmox "github.com/hashicorp/packer/builder/proxmox/common"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/packer"
)
type Config struct {
proxmox.Config `mapstructure:",squash"`
common.ISOConfig `mapstructure:",squash"`
ISOFile string `mapstructure:"iso_file"`
ISOStoragePool string `mapstructure:"iso_storage_pool"`
UnmountISO bool `mapstructure:"unmount_iso"`
shouldUploadISO bool
}
func (c *Config) Prepare(raws ...interface{}) ([]string, []string, error) {
var errs *packer.MultiError
_, warnings, merrs := c.Config.Prepare(c, raws...)
if merrs != nil {
errs = packer.MultiErrorAppend(errs, merrs)
}
// Check ISO config
// Either a pre-uploaded ISO should be referenced in iso_file, OR a URL
// (possibly to a local file) to an ISO file that will be downloaded and
// then uploaded to Proxmox.
if c.ISOFile != "" {
c.shouldUploadISO = false
} else {
isoWarnings, isoErrors := c.ISOConfig.Prepare(&c.Ctx)
errs = packer.MultiErrorAppend(errs, isoErrors...)
warnings = append(warnings, isoWarnings...)
c.shouldUploadISO = true
}
if (c.ISOFile == "" && len(c.ISOConfig.ISOUrls) == 0) || (c.ISOFile != "" && len(c.ISOConfig.ISOUrls) != 0) {
errs = packer.MultiErrorAppend(errs, errors.New("either iso_file or iso_url, but not both, must be specified"))
}
if len(c.ISOConfig.ISOUrls) != 0 && c.ISOStoragePool == "" {
errs = packer.MultiErrorAppend(errs, errors.New("when specifying iso_url, iso_storage_pool must also be specified"))
}
for idx := range c.AdditionalISOFiles {
// Check AdditionalISO config
// Either a pre-uploaded ISO should be referenced in iso_file, OR a URL
// (possibly to a local file) to an ISO file that will be downloaded and
// then uploaded to Proxmox.
if c.AdditionalISOFiles[idx].ISOFile != "" {
c.AdditionalISOFiles[idx].ShouldUploadISO = false
} else {
c.AdditionalISOFiles[idx].DownloadPathKey = "downloaded_additional_iso_path_" + strconv.Itoa(idx)
isoWarnings, isoErrors := c.AdditionalISOFiles[idx].ISOConfig.Prepare(&c.Ctx)
errs = packer.MultiErrorAppend(errs, isoErrors...)
warnings = append(warnings, isoWarnings...)
c.AdditionalISOFiles[idx].ShouldUploadISO = true
}
if c.AdditionalISOFiles[idx].Device == "" {
log.Printf("AdditionalISOFile %d Device not set, using default 'ide3'", idx)
c.AdditionalISOFiles[idx].Device = "ide3"
}
if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "ide") {
busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[3:])
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[3:]))
}
if busnumber == 2 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("IDE bus 2 is used by boot ISO"))
}
if busnumber > 3 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("IDE bus index can't be higher than 3"))
}
}
if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "sata") {
busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[4:])
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[4:]))
}
if busnumber > 5 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("SATA bus index can't be higher than 5"))
}
}
if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "scsi") {
busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[4:])
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[4:]))
}
if busnumber > 30 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("SCSI bus index can't be higher than 30"))
}
}
if (c.AdditionalISOFiles[idx].ISOFile == "" && len(c.AdditionalISOFiles[idx].ISOConfig.ISOUrls) == 0) || (c.AdditionalISOFiles[idx].ISOFile != "" && len(c.AdditionalISOFiles[idx].ISOConfig.ISOUrls) != 0) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("either iso_file or iso_url, but not both, must be specified for AdditionalISO file %s", c.AdditionalISOFiles[idx].Device))
}
if len(c.ISOConfig.ISOUrls) != 0 && c.ISOStoragePool == "" {
errs = packer.MultiErrorAppend(errs, errors.New("when specifying iso_url, iso_storage_pool must also be specified"))
}
}
if errs != nil && len(errs.Errors) > 0 {
return nil, warnings, errs
}
return nil, warnings, nil
}

View File

@ -0,0 +1,223 @@
// Code generated by "mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig"; DO NOT EDIT.
package proxmoxiso
import (
"github.com/hashicorp/hcl/v2/hcldec"
proxmox "github.com/hashicorp/packer/builder/proxmox/common"
"github.com/zclconf/go-cty/cty"
)
// FlatConfig is an auto-generated flat version of Config.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatConfig struct {
PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"`
PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"`
PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"`
PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"`
PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"`
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
HTTPDir *string `mapstructure:"http_directory" cty:"http_directory" hcl:"http_directory"`
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"`
BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"`
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"`
Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"`
PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"`
SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"`
SSHPort *int `mapstructure:"ssh_port" cty:"ssh_port" hcl:"ssh_port"`
SSHUsername *string `mapstructure:"ssh_username" cty:"ssh_username" hcl:"ssh_username"`
SSHPassword *string `mapstructure:"ssh_password" cty:"ssh_password" hcl:"ssh_password"`
SSHKeyPairName *string `mapstructure:"ssh_keypair_name" undocumented:"true" cty:"ssh_keypair_name" hcl:"ssh_keypair_name"`
SSHTemporaryKeyPairName *string `mapstructure:"temporary_key_pair_name" undocumented:"true" cty:"temporary_key_pair_name" hcl:"temporary_key_pair_name"`
SSHCiphers []string `mapstructure:"ssh_ciphers" cty:"ssh_ciphers" hcl:"ssh_ciphers"`
SSHClearAuthorizedKeys *bool `mapstructure:"ssh_clear_authorized_keys" cty:"ssh_clear_authorized_keys" hcl:"ssh_clear_authorized_keys"`
SSHKEXAlgos []string `mapstructure:"ssh_key_exchange_algorithms" cty:"ssh_key_exchange_algorithms" hcl:"ssh_key_exchange_algorithms"`
SSHPrivateKeyFile *string `mapstructure:"ssh_private_key_file" undocumented:"true" cty:"ssh_private_key_file" hcl:"ssh_private_key_file"`
SSHCertificateFile *string `mapstructure:"ssh_certificate_file" cty:"ssh_certificate_file" hcl:"ssh_certificate_file"`
SSHPty *bool `mapstructure:"ssh_pty" cty:"ssh_pty" hcl:"ssh_pty"`
SSHTimeout *string `mapstructure:"ssh_timeout" cty:"ssh_timeout" hcl:"ssh_timeout"`
SSHWaitTimeout *string `mapstructure:"ssh_wait_timeout" undocumented:"true" cty:"ssh_wait_timeout" hcl:"ssh_wait_timeout"`
SSHAgentAuth *bool `mapstructure:"ssh_agent_auth" undocumented:"true" cty:"ssh_agent_auth" hcl:"ssh_agent_auth"`
SSHDisableAgentForwarding *bool `mapstructure:"ssh_disable_agent_forwarding" cty:"ssh_disable_agent_forwarding" hcl:"ssh_disable_agent_forwarding"`
SSHHandshakeAttempts *int `mapstructure:"ssh_handshake_attempts" cty:"ssh_handshake_attempts" hcl:"ssh_handshake_attempts"`
SSHBastionHost *string `mapstructure:"ssh_bastion_host" cty:"ssh_bastion_host" hcl:"ssh_bastion_host"`
SSHBastionPort *int `mapstructure:"ssh_bastion_port" cty:"ssh_bastion_port" hcl:"ssh_bastion_port"`
SSHBastionAgentAuth *bool `mapstructure:"ssh_bastion_agent_auth" cty:"ssh_bastion_agent_auth" hcl:"ssh_bastion_agent_auth"`
SSHBastionUsername *string `mapstructure:"ssh_bastion_username" cty:"ssh_bastion_username" hcl:"ssh_bastion_username"`
SSHBastionPassword *string `mapstructure:"ssh_bastion_password" cty:"ssh_bastion_password" hcl:"ssh_bastion_password"`
SSHBastionInteractive *bool `mapstructure:"ssh_bastion_interactive" cty:"ssh_bastion_interactive" hcl:"ssh_bastion_interactive"`
SSHBastionPrivateKeyFile *string `mapstructure:"ssh_bastion_private_key_file" cty:"ssh_bastion_private_key_file" hcl:"ssh_bastion_private_key_file"`
SSHBastionCertificateFile *string `mapstructure:"ssh_bastion_certificate_file" cty:"ssh_bastion_certificate_file" hcl:"ssh_bastion_certificate_file"`
SSHFileTransferMethod *string `mapstructure:"ssh_file_transfer_method" cty:"ssh_file_transfer_method" hcl:"ssh_file_transfer_method"`
SSHProxyHost *string `mapstructure:"ssh_proxy_host" cty:"ssh_proxy_host" hcl:"ssh_proxy_host"`
SSHProxyPort *int `mapstructure:"ssh_proxy_port" cty:"ssh_proxy_port" hcl:"ssh_proxy_port"`
SSHProxyUsername *string `mapstructure:"ssh_proxy_username" cty:"ssh_proxy_username" hcl:"ssh_proxy_username"`
SSHProxyPassword *string `mapstructure:"ssh_proxy_password" cty:"ssh_proxy_password" hcl:"ssh_proxy_password"`
SSHKeepAliveInterval *string `mapstructure:"ssh_keep_alive_interval" cty:"ssh_keep_alive_interval" hcl:"ssh_keep_alive_interval"`
SSHReadWriteTimeout *string `mapstructure:"ssh_read_write_timeout" cty:"ssh_read_write_timeout" hcl:"ssh_read_write_timeout"`
SSHRemoteTunnels []string `mapstructure:"ssh_remote_tunnels" cty:"ssh_remote_tunnels" hcl:"ssh_remote_tunnels"`
SSHLocalTunnels []string `mapstructure:"ssh_local_tunnels" cty:"ssh_local_tunnels" hcl:"ssh_local_tunnels"`
SSHPublicKey []byte `mapstructure:"ssh_public_key" undocumented:"true" cty:"ssh_public_key" hcl:"ssh_public_key"`
SSHPrivateKey []byte `mapstructure:"ssh_private_key" undocumented:"true" cty:"ssh_private_key" hcl:"ssh_private_key"`
WinRMUser *string `mapstructure:"winrm_username" cty:"winrm_username" hcl:"winrm_username"`
WinRMPassword *string `mapstructure:"winrm_password" cty:"winrm_password" hcl:"winrm_password"`
WinRMHost *string `mapstructure:"winrm_host" cty:"winrm_host" hcl:"winrm_host"`
WinRMNoProxy *bool `mapstructure:"winrm_no_proxy" cty:"winrm_no_proxy" hcl:"winrm_no_proxy"`
WinRMPort *int `mapstructure:"winrm_port" cty:"winrm_port" hcl:"winrm_port"`
WinRMTimeout *string `mapstructure:"winrm_timeout" cty:"winrm_timeout" hcl:"winrm_timeout"`
WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl" hcl:"winrm_use_ssl"`
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
ProxmoxURLRaw *string `mapstructure:"proxmox_url" cty:"proxmox_url" hcl:"proxmox_url"`
SkipCertValidation *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"`
Username *string `mapstructure:"username" cty:"username" hcl:"username"`
Password *string `mapstructure:"password" cty:"password" hcl:"password"`
Node *string `mapstructure:"node" cty:"node" hcl:"node"`
Pool *string `mapstructure:"pool" cty:"pool" hcl:"pool"`
VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"`
VMID *int `mapstructure:"vm_id" cty:"vm_id" hcl:"vm_id"`
Memory *int `mapstructure:"memory" cty:"memory" hcl:"memory"`
Cores *int `mapstructure:"cores" cty:"cores" hcl:"cores"`
CPUType *string `mapstructure:"cpu_type" cty:"cpu_type" hcl:"cpu_type"`
Sockets *int `mapstructure:"sockets" cty:"sockets" hcl:"sockets"`
OS *string `mapstructure:"os" cty:"os" hcl:"os"`
VGA *proxmox.FlatvgaConfig `mapstructure:"vga" cty:"vga" hcl:"vga"`
NICs []proxmox.FlatnicConfig `mapstructure:"network_adapters" cty:"network_adapters" hcl:"network_adapters"`
Disks []proxmox.FlatdiskConfig `mapstructure:"disks" cty:"disks" hcl:"disks"`
Agent *bool `mapstructure:"qemu_agent" cty:"qemu_agent" hcl:"qemu_agent"`
SCSIController *string `mapstructure:"scsi_controller" cty:"scsi_controller" hcl:"scsi_controller"`
Onboot *bool `mapstructure:"onboot" cty:"onboot" hcl:"onboot"`
DisableKVM *bool `mapstructure:"disable_kvm" cty:"disable_kvm" hcl:"disable_kvm"`
TemplateName *string `mapstructure:"template_name" cty:"template_name" hcl:"template_name"`
TemplateDescription *string `mapstructure:"template_description" cty:"template_description" hcl:"template_description"`
CloudInit *bool `mapstructure:"cloud_init" cty:"cloud_init" hcl:"cloud_init"`
CloudInitStoragePool *string `mapstructure:"cloud_init_storage_pool" cty:"cloud_init_storage_pool" hcl:"cloud_init_storage_pool"`
AdditionalISOFiles []proxmox.FlatstorageConfig `mapstructure:"additional_iso_files" cty:"additional_iso_files" hcl:"additional_iso_files"`
VMInterface *string `mapstructure:"vm_interface" cty:"vm_interface" hcl:"vm_interface"`
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
TargetPath *string `mapstructure:"iso_target_path" cty:"iso_target_path" hcl:"iso_target_path"`
TargetExtension *string `mapstructure:"iso_target_extension" cty:"iso_target_extension" hcl:"iso_target_extension"`
ISOFile *string `mapstructure:"iso_file" cty:"iso_file" hcl:"iso_file"`
ISOStoragePool *string `mapstructure:"iso_storage_pool" cty:"iso_storage_pool" hcl:"iso_storage_pool"`
UnmountISO *bool `mapstructure:"unmount_iso" cty:"unmount_iso" hcl:"unmount_iso"`
}
// FlatMapstructure returns a new FlatConfig.
// FlatConfig is an auto-generated flat version of Config.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatConfig)
}
// HCL2Spec returns the hcl spec of a Config.
// This spec is used by HCL to read the fields of Config.
// The decoded values from this spec will then be applied to a FlatConfig.
func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false},
"packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false},
"packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false},
"packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false},
"packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false},
"packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false},
"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
"http_directory": &hcldec.AttrSpec{Name: "http_directory", Type: cty.String, Required: false},
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
"boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false},
"boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false},
"boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false},
"boot_key_interval": &hcldec.AttrSpec{Name: "boot_key_interval", Type: cty.String, Required: false},
"communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false},
"pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false},
"ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false},
"ssh_port": &hcldec.AttrSpec{Name: "ssh_port", Type: cty.Number, Required: false},
"ssh_username": &hcldec.AttrSpec{Name: "ssh_username", Type: cty.String, Required: false},
"ssh_password": &hcldec.AttrSpec{Name: "ssh_password", Type: cty.String, Required: false},
"ssh_keypair_name": &hcldec.AttrSpec{Name: "ssh_keypair_name", Type: cty.String, Required: false},
"temporary_key_pair_name": &hcldec.AttrSpec{Name: "temporary_key_pair_name", Type: cty.String, Required: false},
"ssh_ciphers": &hcldec.AttrSpec{Name: "ssh_ciphers", Type: cty.List(cty.String), Required: false},
"ssh_clear_authorized_keys": &hcldec.AttrSpec{Name: "ssh_clear_authorized_keys", Type: cty.Bool, Required: false},
"ssh_key_exchange_algorithms": &hcldec.AttrSpec{Name: "ssh_key_exchange_algorithms", Type: cty.List(cty.String), Required: false},
"ssh_private_key_file": &hcldec.AttrSpec{Name: "ssh_private_key_file", Type: cty.String, Required: false},
"ssh_certificate_file": &hcldec.AttrSpec{Name: "ssh_certificate_file", Type: cty.String, Required: false},
"ssh_pty": &hcldec.AttrSpec{Name: "ssh_pty", Type: cty.Bool, Required: false},
"ssh_timeout": &hcldec.AttrSpec{Name: "ssh_timeout", Type: cty.String, Required: false},
"ssh_wait_timeout": &hcldec.AttrSpec{Name: "ssh_wait_timeout", Type: cty.String, Required: false},
"ssh_agent_auth": &hcldec.AttrSpec{Name: "ssh_agent_auth", Type: cty.Bool, Required: false},
"ssh_disable_agent_forwarding": &hcldec.AttrSpec{Name: "ssh_disable_agent_forwarding", Type: cty.Bool, Required: false},
"ssh_handshake_attempts": &hcldec.AttrSpec{Name: "ssh_handshake_attempts", Type: cty.Number, Required: false},
"ssh_bastion_host": &hcldec.AttrSpec{Name: "ssh_bastion_host", Type: cty.String, Required: false},
"ssh_bastion_port": &hcldec.AttrSpec{Name: "ssh_bastion_port", Type: cty.Number, Required: false},
"ssh_bastion_agent_auth": &hcldec.AttrSpec{Name: "ssh_bastion_agent_auth", Type: cty.Bool, Required: false},
"ssh_bastion_username": &hcldec.AttrSpec{Name: "ssh_bastion_username", Type: cty.String, Required: false},
"ssh_bastion_password": &hcldec.AttrSpec{Name: "ssh_bastion_password", Type: cty.String, Required: false},
"ssh_bastion_interactive": &hcldec.AttrSpec{Name: "ssh_bastion_interactive", Type: cty.Bool, Required: false},
"ssh_bastion_private_key_file": &hcldec.AttrSpec{Name: "ssh_bastion_private_key_file", Type: cty.String, Required: false},
"ssh_bastion_certificate_file": &hcldec.AttrSpec{Name: "ssh_bastion_certificate_file", Type: cty.String, Required: false},
"ssh_file_transfer_method": &hcldec.AttrSpec{Name: "ssh_file_transfer_method", Type: cty.String, Required: false},
"ssh_proxy_host": &hcldec.AttrSpec{Name: "ssh_proxy_host", Type: cty.String, Required: false},
"ssh_proxy_port": &hcldec.AttrSpec{Name: "ssh_proxy_port", Type: cty.Number, Required: false},
"ssh_proxy_username": &hcldec.AttrSpec{Name: "ssh_proxy_username", Type: cty.String, Required: false},
"ssh_proxy_password": &hcldec.AttrSpec{Name: "ssh_proxy_password", Type: cty.String, Required: false},
"ssh_keep_alive_interval": &hcldec.AttrSpec{Name: "ssh_keep_alive_interval", Type: cty.String, Required: false},
"ssh_read_write_timeout": &hcldec.AttrSpec{Name: "ssh_read_write_timeout", Type: cty.String, Required: false},
"ssh_remote_tunnels": &hcldec.AttrSpec{Name: "ssh_remote_tunnels", Type: cty.List(cty.String), Required: false},
"ssh_local_tunnels": &hcldec.AttrSpec{Name: "ssh_local_tunnels", Type: cty.List(cty.String), Required: false},
"ssh_public_key": &hcldec.AttrSpec{Name: "ssh_public_key", Type: cty.List(cty.Number), Required: false},
"ssh_private_key": &hcldec.AttrSpec{Name: "ssh_private_key", Type: cty.List(cty.Number), Required: false},
"winrm_username": &hcldec.AttrSpec{Name: "winrm_username", Type: cty.String, Required: false},
"winrm_password": &hcldec.AttrSpec{Name: "winrm_password", Type: cty.String, Required: false},
"winrm_host": &hcldec.AttrSpec{Name: "winrm_host", Type: cty.String, Required: false},
"winrm_no_proxy": &hcldec.AttrSpec{Name: "winrm_no_proxy", Type: cty.Bool, Required: false},
"winrm_port": &hcldec.AttrSpec{Name: "winrm_port", Type: cty.Number, Required: false},
"winrm_timeout": &hcldec.AttrSpec{Name: "winrm_timeout", Type: cty.String, Required: false},
"winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false},
"winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false},
"winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false},
"proxmox_url": &hcldec.AttrSpec{Name: "proxmox_url", Type: cty.String, Required: false},
"insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false},
"username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false},
"password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false},
"node": &hcldec.AttrSpec{Name: "node", Type: cty.String, Required: false},
"pool": &hcldec.AttrSpec{Name: "pool", Type: cty.String, Required: false},
"vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false},
"vm_id": &hcldec.AttrSpec{Name: "vm_id", Type: cty.Number, Required: false},
"memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false},
"cores": &hcldec.AttrSpec{Name: "cores", Type: cty.Number, Required: false},
"cpu_type": &hcldec.AttrSpec{Name: "cpu_type", Type: cty.String, Required: false},
"sockets": &hcldec.AttrSpec{Name: "sockets", Type: cty.Number, Required: false},
"os": &hcldec.AttrSpec{Name: "os", Type: cty.String, Required: false},
"vga": &hcldec.BlockSpec{TypeName: "vga", Nested: hcldec.ObjectSpec((*proxmox.FlatvgaConfig)(nil).HCL2Spec())},
"network_adapters": &hcldec.BlockListSpec{TypeName: "network_adapters", Nested: hcldec.ObjectSpec((*proxmox.FlatnicConfig)(nil).HCL2Spec())},
"disks": &hcldec.BlockListSpec{TypeName: "disks", Nested: hcldec.ObjectSpec((*proxmox.FlatdiskConfig)(nil).HCL2Spec())},
"qemu_agent": &hcldec.AttrSpec{Name: "qemu_agent", Type: cty.Bool, Required: false},
"scsi_controller": &hcldec.AttrSpec{Name: "scsi_controller", Type: cty.String, Required: false},
"onboot": &hcldec.AttrSpec{Name: "onboot", Type: cty.Bool, Required: false},
"disable_kvm": &hcldec.AttrSpec{Name: "disable_kvm", Type: cty.Bool, Required: false},
"template_name": &hcldec.AttrSpec{Name: "template_name", Type: cty.String, Required: false},
"template_description": &hcldec.AttrSpec{Name: "template_description", Type: cty.String, Required: false},
"cloud_init": &hcldec.AttrSpec{Name: "cloud_init", Type: cty.Bool, Required: false},
"cloud_init_storage_pool": &hcldec.AttrSpec{Name: "cloud_init_storage_pool", Type: cty.String, Required: false},
"additional_iso_files": &hcldec.BlockListSpec{TypeName: "additional_iso_files", Nested: hcldec.ObjectSpec((*proxmox.FlatstorageConfig)(nil).HCL2Spec())},
"vm_interface": &hcldec.AttrSpec{Name: "vm_interface", Type: cty.String, Required: false},
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
"iso_target_path": &hcldec.AttrSpec{Name: "iso_target_path", Type: cty.String, Required: false},
"iso_target_extension": &hcldec.AttrSpec{Name: "iso_target_extension", Type: cty.String, Required: false},
"iso_file": &hcldec.AttrSpec{Name: "iso_file", Type: cty.String, Required: false},
"iso_storage_pool": &hcldec.AttrSpec{Name: "iso_storage_pool", Type: cty.String, Required: false},
"unmount_iso": &hcldec.AttrSpec{Name: "unmount_iso", Type: cty.Bool, Required: false},
}
return s
}

View File

@ -1,55 +1,17 @@
package proxmox
package proxmoxiso
import (
"strings"
"testing"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template"
)
func mandatoryConfig(t *testing.T) map[string]interface{} {
return map[string]interface{}{
"proxmox_url": "https://my-proxmox.my-domain:8006/api2/json",
"username": "apiuser@pve",
"password": "supersecret",
"iso_file": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso",
"node": "my-proxmox",
"ssh_username": "root",
}
}
func TestRequiredParameters(t *testing.T) {
var c Config
_, err := c.Prepare(make(map[string]interface{}))
if err == nil {
t.Fatal("Expected empty configuration to fail")
}
errs, ok := err.(*packer.MultiError)
if !ok {
t.Fatal("Expected errors to be packer.MultiError")
}
required := []string{"username", "password", "proxmox_url", "iso_file", "node", "ssh_username"}
for _, param := range required {
found := false
for _, err := range errs.Errors {
if strings.Contains(err.Error(), param) {
found = true
break
}
}
if !found {
t.Errorf("Expected error about missing parameter %q", required)
}
}
}
func TestBasicExampleFromDocsIsValid(t *testing.T) {
const config = `{
"builders": [
{
"type": "proxmox",
"type": "proxmox-iso",
"proxmox_url": "https://my-proxmox.my-domain:8006/api2/json",
"insecure_skip_tls_verify": true,
"username": "apiuser@pve",
@ -93,9 +55,9 @@ func TestBasicExampleFromDocsIsValid(t *testing.T) {
}
b := &Builder{}
_, warn, err := b.Prepare(tpl.Builders["proxmox"].Config)
_, _, err = b.Prepare(tpl.Builders["proxmox-iso"].Config)
if err != nil {
t.Fatal(err, warn)
t.Fatal(err)
}
// The example config does not set a number of optional fields. Validate that:
@ -154,7 +116,7 @@ func TestAgentSetToFalse(t *testing.T) {
cfg["qemu_agent"] = false
var c Config
warn, err := c.Prepare(cfg)
_, warn, err := c.Prepare(cfg)
if err != nil {
t.Fatal(err, warn)
}
@ -197,7 +159,7 @@ func TestPacketQueueSupportForNetworkAdapters(t *testing.T) {
cfg["network_adapters"] = devices
var c Config
_, err := c.Prepare(cfg)
_, _, err := c.Prepare(cfg)
if tt.expectedToFail == true && err == nil {
t.Error("expected config preparation to fail, but no error occured")
@ -246,7 +208,7 @@ func TestHardDiskControllerIOThreadSupport(t *testing.T) {
cfg["scsi_controller"] = tt.controller
var c Config
_, err := c.Prepare(cfg)
_, _, err := c.Prepare(cfg)
if tt.expectedToFail == true && err == nil {
t.Error("expected config preparation to fail, but no error occured")
@ -257,3 +219,14 @@ func TestHardDiskControllerIOThreadSupport(t *testing.T) {
}
}
}
func mandatoryConfig(t *testing.T) map[string]interface{} {
return map[string]interface{}{
"proxmox_url": "https://my-proxmox.my-domain:8006/api2/json",
"username": "apiuser@pve",
"password": "supersecret",
"node": "my-proxmox",
"ssh_username": "root",
"iso_file": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso",
}
}

View File

@ -0,0 +1,87 @@
package proxmoxiso
import (
"context"
"fmt"
"strings"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// stepFinalizeISOTemplate does any ISO-builder specific modifications after
// conversion to a template, and after the non-specific modifications in
// common.stepFinalizeTemplateConfig
type stepFinalizeISOTemplate struct{}
type templateFinalizer interface {
GetVmConfig(*proxmox.VmRef) (map[string]interface{}, error)
SetVmConfig(*proxmox.VmRef, map[string]interface{}) (interface{}, error)
}
var _ templateFinalizer = &proxmox.Client{}
func (s *stepFinalizeISOTemplate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
client := state.Get("proxmoxClient").(templateFinalizer)
c := state.Get("iso-config").(*Config)
vmRef := state.Get("vmRef").(*proxmox.VmRef)
changes := make(map[string]interface{})
if c.UnmountISO {
vmParams, err := client.GetVmConfig(vmRef)
if err != nil {
err := fmt.Errorf("Error fetching template config: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if vmParams["ide2"] == nil || !strings.HasSuffix(vmParams["ide2"].(string), "media=cdrom") {
err := fmt.Errorf("Cannot eject ISO from cdrom drive, ide2 is not present, or not a cdrom media")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
changes["ide2"] = "none,media=cdrom"
}
if len(c.AdditionalISOFiles) > 0 {
vmParams, err := client.GetVmConfig(vmRef)
if err != nil {
err := fmt.Errorf("Error fetching template config: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
for idx := range c.AdditionalISOFiles {
cdrom := c.AdditionalISOFiles[idx].Device
if c.AdditionalISOFiles[idx].Unmount {
if vmParams[cdrom] == nil || !strings.Contains(vmParams[cdrom].(string), "media=cdrom") {
err := fmt.Errorf("Cannot eject ISO from cdrom drive, %s is not present or not a cdrom media", cdrom)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
changes[cdrom] = "none,media=cdrom"
} else {
changes[cdrom] = c.AdditionalISOFiles[idx].ISOFile + ",media=cdrom"
}
}
}
if len(changes) > 0 {
_, err := client.SetVmConfig(vmRef, changes)
if err != nil {
err := fmt.Errorf("Error updating template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *stepFinalizeISOTemplate) Cleanup(state multistep.StateBag) {
}

View File

@ -0,0 +1,131 @@
package proxmoxiso
import (
"context"
"fmt"
"testing"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type finalizerMock struct {
getConfig func() (map[string]interface{}, error)
setConfig func(map[string]interface{}) (string, error)
}
func (m finalizerMock) GetVmConfig(*proxmox.VmRef) (map[string]interface{}, error) {
return m.getConfig()
}
func (m finalizerMock) SetVmConfig(vmref *proxmox.VmRef, c map[string]interface{}) (interface{}, error) {
return m.setConfig(c)
}
var _ templateFinalizer = finalizerMock{}
func TestISOTemplateFinalize(t *testing.T) {
cs := []struct {
name string
builderConfig *Config
initialVMConfig map[string]interface{}
getConfigErr error
expectCallSetConfig bool
expectedVMConfig map[string]interface{}
setConfigErr error
expectedAction multistep.StepAction
}{
{
name: "default config does nothing",
builderConfig: &Config{},
initialVMConfig: map[string]interface{}{
"ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom",
},
expectCallSetConfig: false,
expectedVMConfig: map[string]interface{}{
"ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom",
},
expectedAction: multistep.ActionContinue,
},
{
name: "should unmount when configured",
builderConfig: &Config{
UnmountISO: true,
},
initialVMConfig: map[string]interface{}{
"ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom",
},
expectCallSetConfig: true,
expectedVMConfig: map[string]interface{}{
"ide2": "none,media=cdrom",
},
expectedAction: multistep.ActionContinue,
},
{
name: "no cd-drive with unmount=true should returns halt",
builderConfig: &Config{
UnmountISO: true,
},
initialVMConfig: map[string]interface{}{
"ide1": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom",
},
expectCallSetConfig: false,
expectedAction: multistep.ActionHalt,
},
{
name: "GetVmConfig error should return halt",
builderConfig: &Config{
UnmountISO: true,
},
getConfigErr: fmt.Errorf("some error"),
expectCallSetConfig: false,
expectedAction: multistep.ActionHalt,
},
{
name: "SetVmConfig error should return halt",
builderConfig: &Config{
UnmountISO: true,
},
initialVMConfig: map[string]interface{}{
"ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom",
},
expectCallSetConfig: true,
setConfigErr: fmt.Errorf("some error"),
expectedAction: multistep.ActionHalt,
},
}
for _, c := range cs {
t.Run(c.name, func(t *testing.T) {
finalizer := finalizerMock{
getConfig: func() (map[string]interface{}, error) {
return c.initialVMConfig, c.getConfigErr
},
setConfig: func(cfg map[string]interface{}) (string, error) {
if !c.expectCallSetConfig {
t.Error("Did not expect SetVmConfig to be called")
}
for key, val := range c.expectedVMConfig {
if cfg[key] != val {
t.Errorf("Expected %q to be %q, got %q", key, val, cfg[key])
}
}
return "", c.setConfigErr
},
}
state := new(multistep.BasicStateBag)
state.Put("ui", packer.TestUi(t))
state.Put("iso-config", c.builderConfig)
state.Put("vmRef", proxmox.NewVmRef(1))
state.Put("proxmoxClient", finalizer)
step := stepFinalizeISOTemplate{}
action := step.Run(context.TODO(), state)
if action != c.expectedAction {
t.Errorf("Expected action to be %v, got %v", c.expectedAction, action)
}
})
}
}

View File

@ -1,9 +1,8 @@
package proxmox
package proxmoxiso
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
@ -16,24 +15,20 @@ import (
// to the VM
type stepUploadAdditionalISOs struct{}
type uploader interface {
Upload(node string, storage string, contentType string, filename string, file io.Reader) error
}
var _ uploader = &proxmox.Client{}
func (s *stepUploadAdditionalISOs) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
client := state.Get("proxmoxClient").(uploader)
c := state.Get("config").(*Config)
c := state.Get("iso-config").(*Config)
for idx := range c.AdditionalISOFiles {
if !c.AdditionalISOFiles[idx].shouldUploadISO {
if !c.AdditionalISOFiles[idx].ShouldUploadISO {
state.Put("additional_iso_files", c.AdditionalISOFiles)
continue
}
p := state.Get(c.AdditionalISOFiles[idx].downloadPathKey).(string)
p := state.Get(c.AdditionalISOFiles[idx].DownloadPathKey).(string)
if p == "" {
err := fmt.Errorf("Path to downloaded ISO was empty")
state.Put("erroe", err)

View File

@ -1,8 +1,9 @@
package proxmox
package proxmoxiso
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
@ -14,12 +15,16 @@ import (
// stepUploadISO uploads an ISO file to Proxmox so we can boot from it
type stepUploadISO struct{}
type uploader interface {
Upload(node string, storage string, contentType string, filename string, file io.Reader) error
}
var _ uploader = &proxmox.Client{}
func (s *stepUploadISO) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
client := state.Get("proxmoxClient").(uploader)
c := state.Get("config").(*Config)
c := state.Get("iso-config").(*Config)
if !c.shouldUploadISO {
state.Put("iso_file", c.ISOFile)
@ -53,6 +58,7 @@ func (s *stepUploadISO) Run(ctx context.Context, state multistep.StateBag) multi
isoStoragePath := fmt.Sprintf("%s:iso/%s", c.ISOStoragePool, filename)
state.Put("iso_file", isoStoragePath)
return multistep.ActionContinue
}

View File

@ -1,4 +1,4 @@
package proxmox
package proxmoxiso
import (
"context"
@ -106,7 +106,7 @@ func TestUploadISO(t *testing.T) {
state := new(multistep.BasicStateBag)
state.Put("ui", packer.TestUi(t))
state.Put("config", c.builderConfig)
state.Put("iso-config", c.builderConfig)
state.Put(downloadPathKey, c.downloadPath)
state.Put("proxmoxClient", m)

View File

@ -394,6 +394,7 @@ type Config struct {
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(c, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -282,7 +282,10 @@ func (s *stepRun) applyUserOverrides(defaultArgs map[string]interface{}, config
if len(config.QemuArgs) > 0 {
s.ui.Say("Overriding default Qemu arguments with qemuargs template option...")
commHostPort := state.Get("commHostPort").(int)
commHostPort := 0
if config.CommConfig.Comm.Type != "none" {
commHostPort = state.Get("commHostPort").(int)
}
httpIp := state.Get("http_ip").(string)
httpPort := state.Get("http_port").(int)

View File

@ -99,6 +99,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
var md mapstructure.Metadata
err := config.Decode(c, &config.DecodeOpts{
Metadata: &md,
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -35,6 +35,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -27,6 +27,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
errs := &multierror.Error{}
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
}, raws...)

View File

@ -38,6 +38,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -154,6 +154,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: BuilderId,
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -117,6 +117,7 @@ func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstruct
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
err := config.Decode(&b.config, &config.DecodeOpts{
PluginType: vboxcommon.BuilderId, // "mitchellh.virtualbox"
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -82,6 +82,7 @@ type Config struct {
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(c, &config.DecodeOpts{
PluginType: vboxcommon.BuilderId, // "mitchellh.virtualbox"
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -66,6 +66,7 @@ type Config struct {
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(c, &config.DecodeOpts{
PluginType: vboxcommon.BuilderId, // "mitchellh.virtualbox"
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{

View File

@ -52,6 +52,7 @@ type Config struct {
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(c, &config.DecodeOpts{
PluginType: common.BuilderId,
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{
@ -64,7 +65,9 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
return nil, err
}
// warnings := make([]string, 0)
errs := new(packer.MultiError)
errs = packer.MultiErrorAppend(errs, c.ConnectConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, c.CloneConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, c.LocationConfig.Prepare()...)
@ -76,7 +79,12 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.WaitIpConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare()...)
_, shutdownErrs := c.ShutdownConfig.Prepare(c.Comm)
// shutdownWarnings, shutdownErrs := c.ShutdownConfig.Prepare(c.Comm)
// warnings = append(warnings, shutdownWarnings...)
errs = packer.MultiErrorAppend(errs, shutdownErrs...)
if c.Export != nil {
errs = packer.MultiErrorAppend(errs, c.Export.Prepare(&c.ctx, &c.LocationConfig, &c.PackerConfig)...)
}

View File

@ -0,0 +1,68 @@
package common
import (
"context"
"fmt"
"net/url"
"github.com/hashicorp/packer/builder/vsphere/driver"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// Defining this interface ensures that we use the common step download, or the
// mock created to test this wrapper
type DownloadStep interface {
Run(context.Context, multistep.StateBag) multistep.StepAction
Cleanup(multistep.StateBag)
UseSourceToFindCacheTarget(source string) (*url.URL, string, error)
}
// VSphere has a specialized need -- before we waste time downloading an iso,
// we need to check whether that iso already exists on the remote datastore.
// if it does, we skip the download. This wrapping-step still uses the common
// StepDownload but only if the image isn't already present on the datastore.
type StepDownload struct {
DownloadStep DownloadStep
// These keys are VSphere-specific and used to check the remote datastore.
Url []string
ResultKey string
Datastore string
Host string
}
func (s *StepDownload) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(driver.Driver)
ui := state.Get("ui").(packer.Ui)
// Check whether iso is present on remote datastore.
ds, err := driver.FindDatastore(s.Datastore, s.Host)
if err != nil {
state.Put("error", fmt.Errorf("datastore doesn't exist: %v", err))
return multistep.ActionHalt
}
// loop over URLs to see if any are already present. If they are, store that
// one instate and continue
for _, source := range s.Url {
_, targetPath, err := s.DownloadStep.UseSourceToFindCacheTarget(source)
if err != nil {
state.Put("error", fmt.Errorf("Error getting target path: %s", err))
return multistep.ActionHalt
}
_, remotePath, _, _ := GetRemoteDirectoryAndPath(targetPath, ds)
if exists := ds.FileExists(remotePath); exists {
ui.Say(fmt.Sprintf("File %s already uploaded; continuing", targetPath))
state.Put(s.ResultKey, targetPath)
return multistep.ActionContinue
}
}
// ISO is not present on datastore, so we need to download, then upload it.
// Pass through to the common download step.
return s.DownloadStep.Run(ctx, state)
}
func (s *StepDownload) Cleanup(state multistep.StateBag) {
}

View File

@ -0,0 +1,85 @@
package common
import (
"context"
"net/url"
"testing"
"github.com/hashicorp/packer/builder/vsphere/driver"
"github.com/hashicorp/packer/helper/multistep"
)
/// create mock step
type MockDownloadStep struct {
RunCalled bool
}
func (s *MockDownloadStep) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
s.RunCalled = true
return multistep.ActionContinue
}
func (s *MockDownloadStep) Cleanup(state multistep.StateBag) {}
func (s *MockDownloadStep) UseSourceToFindCacheTarget(source string) (*url.URL, string, error) {
return nil, "sometarget", nil
}
/// start tests
func downloadStepState(exists bool) *multistep.BasicStateBag {
state := basicStateBag(nil)
dsMock := &driver.DatastoreMock{
FileExistsReturn: exists,
}
driverMock := &driver.DriverMock{
DatastoreMock: dsMock,
}
state.Put("driver", driverMock)
return state
}
func TestStepDownload_Run(t *testing.T) {
testcases := []struct {
name string
filePresent bool
expectedAction multistep.StepAction
expectInternalStepCalled bool
errMessage string
}{
{
name: "Remote iso present; download shouldn't be called",
filePresent: true,
expectedAction: multistep.ActionContinue,
expectInternalStepCalled: false,
errMessage: "",
},
{
name: "Remote iso not present; download should be called",
filePresent: false,
expectedAction: multistep.ActionContinue,
expectInternalStepCalled: true,
errMessage: "",
},
}
for _, tc := range testcases {
internalStep := &MockDownloadStep{}
state := downloadStepState(tc.filePresent)
step := &StepDownload{
DownloadStep: internalStep,
Url: []string{"https://path/to/fake-url.iso"},
Datastore: "datastore-mock",
Host: "fake-host",
}
stepAction := step.Run(context.TODO(), state)
if stepAction != tc.expectedAction {
t.Fatalf("%s: Recieved wrong step action; step exists, should return early.", tc.name)
}
if tc.expectInternalStepCalled != internalStep.RunCalled {
if tc.expectInternalStepCalled {
t.Fatalf("%s: Expected internal download step to be called", tc.name)
} else {
t.Fatalf("%s: Expected internal download step not to be called", tc.name)
}
}
}
}

View File

@ -42,24 +42,30 @@ func (s *StepRemoteUpload) Run(_ context.Context, state multistep.StateBag) mult
return multistep.ActionContinue
}
func GetRemoteDirectoryAndPath(path string, ds driver.Datastore) (string, string, string, string) {
filename := filepath.Base(path)
remotePath := fmt.Sprintf("packer_cache/%s", filename)
remoteDirectory := fmt.Sprintf("[%s] packer_cache/", ds.Name())
fullRemotePath := fmt.Sprintf("%s/%s", remoteDirectory, filename)
return filename, remotePath, remoteDirectory, fullRemotePath
}
func (s *StepRemoteUpload) uploadFile(path string, d driver.Driver, ui packer.Ui) (string, error) {
ds, err := d.FindDatastore(s.Datastore, s.Host)
if err != nil {
return "", fmt.Errorf("datastore doesn't exist: %v", err)
}
filename := filepath.Base(path)
remotePath := fmt.Sprintf("packer_cache/%s", filename)
remoteDirectory := fmt.Sprintf("[%s] packer_cache/", ds.Name())
fullRemotePath := fmt.Sprintf("%s/%s", remoteDirectory, filename)
ui.Say(fmt.Sprintf("Uploading %s to %s", filename, remotePath))
filename, remotePath, remoteDirectory, fullRemotePath := GetRemoteDirectoryAndPath(path, ds)
if exists := ds.FileExists(remotePath); exists == true {
ui.Say(fmt.Sprintf("File %s already uploaded; continuing", filename))
ui.Say(fmt.Sprintf("File %s already exists; skipping upload.", fullRemotePath))
return fullRemotePath, nil
}
ui.Say(fmt.Sprintf("Uploading %s to %s", filename, remotePath))
if err := ds.MakeDirectory(remoteDirectory); err != nil {
return "", err
}

View File

@ -11,34 +11,40 @@ import (
"time"
"github.com/hashicorp/packer/builder/vsphere/driver"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type ShutdownConfig struct {
// Specify a VM guest shutdown command. VMware guest tools are used by
// default.
// Specify a VM guest shutdown command. This command will be executed using
// the `communicator`. Otherwise the VMware guest tools are used to gracefully
// shutdown the VM guest.
Command string `mapstructure:"shutdown_command"`
// Amount of time to wait for graceful VM shutdown.
// Defaults to 5m or five minutes.
// This will likely need to be modified if the `communicator` is 'none'.
Timeout time.Duration `mapstructure:"shutdown_timeout"`
// Packer normally halts the virtual machine after all provisioners have
// run when no `shutdown_command` is defined. If this is set to `true`, Packer
// *will not* halt the virtual machine but will assume that you will send the stop
// signal yourself through the preseed.cfg or your final provisioner.
// signal yourself through a preseed.cfg, a script or the final provisioner.
// Packer will wait for a default of five minutes until the virtual machine is shutdown.
// The timeout can be changed using `shutdown_timeout` option.
DisableShutdown bool `mapstructure:"disable_shutdown"`
}
func (c *ShutdownConfig) Prepare() []error {
var errs []error
func (c *ShutdownConfig) Prepare(comm communicator.Config) (warnings []string, errs []error) {
if c.Timeout == 0 {
c.Timeout = 5 * time.Minute
}
return errs
if comm.Type == "none" && c.Command != "" {
warnings = append(warnings, "The parameter `shutdown_command` is ignored as it requires a `communicator`.")
}
return
}
type StepShutdown struct {
@ -47,7 +53,6 @@ type StepShutdown struct {
func (s *StepShutdown) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
comm := state.Get("communicator").(packer.Communicator)
vm := state.Get("vm").(*driver.VirtualMachineDriver)
if off, _ := vm.IsPoweredOff(); off {
@ -56,9 +61,17 @@ func (s *StepShutdown) Run(ctx context.Context, state multistep.StateBag) multis
return multistep.ActionContinue
}
if s.Config.DisableShutdown {
comm, _ := state.Get("communicator").(packer.Communicator)
if comm == nil {
msg := fmt.Sprintf("Please shutdown virtual machine within %s.", s.Config.Timeout)
ui.Message(msg)
} else if s.Config.DisableShutdown {
ui.Say("Automatic shutdown disabled. Please shutdown virtual machine.")
} else if s.Config.Command != "" {
// Communicator is not needed unless shutdown_command is populated
ui.Say("Executing shutdown command...")
log.Printf("Shutdown command: %s", s.Config.Command)

View File

@ -7,6 +7,10 @@ import (
type DatastoreMock struct {
FileExistsCalled bool
FileExistsReturn bool
NameReturn string
MakeDirectoryCalled bool
ResolvePathCalled bool
@ -30,12 +34,15 @@ func (ds *DatastoreMock) Info(params ...string) (*mo.Datastore, error) {
func (ds *DatastoreMock) FileExists(path string) bool {
ds.FileExistsCalled = true
return false
return ds.FileExistsReturn
}
func (ds *DatastoreMock) Name() string {
if ds.NameReturn == "" {
return "datastore-mock"
}
return ds.NameReturn
}
func (ds *DatastoreMock) Reference() types.ManagedObjectReference {
return types.ManagedObjectReference{}

View File

@ -40,7 +40,8 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
&common.StepConnect{
Config: &b.config.ConnectConfig,
},
&packerCommon.StepDownload{
&common.StepDownload{
DownloadStep: &packerCommon.StepDownload{
Checksum: b.config.ISOChecksum,
Description: "ISO",
Extension: b.config.TargetExtension,
@ -48,6 +49,11 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
TargetPath: b.config.TargetPath,
Url: b.config.ISOUrls,
},
Url: b.config.ISOUrls,
ResultKey: "iso_path",
Datastore: b.config.Datastore,
Host: b.config.Host,
},
&packerCommon.StepCreateCD{
Files: b.config.CDConfig.CDFiles,
Label: b.config.CDConfig.CDLabel,
@ -71,10 +77,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
&common.StepConfigParams{
Config: &b.config.ConfigParamsConfig,
},
)
if b.config.Comm.Type != "none" {
steps = append(steps,
&packerCommon.StepCreateFloppy{
Files: b.config.FloppyFiles,
Directories: b.config.FloppyDirectories,
@ -105,6 +107,10 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
Ctx: b.config.ctx,
VMName: b.config.VMName,
},
)
if b.config.Comm.Type != "none" {
steps = append(steps,
&common.StepWaitForIp{
Config: &b.config.WaitIpConfig,
},
@ -114,6 +120,10 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
SSHConfig: b.config.Comm.SSHConfigFunc(),
},
&packerCommon.StepProvision{},
)
}
steps = append(steps,
&common.StepShutdown{
Config: &b.config.ShutdownConfig,
},
@ -121,10 +131,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
Datastore: b.config.Datastore,
Host: b.config.Host,
},
)
}
steps = append(steps,
&common.StepRemoveCDRom{
Config: &b.config.RemoveCDRomConfig,
},

View File

@ -53,6 +53,7 @@ type Config struct {
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(c, &config.DecodeOpts{
PluginType: common.BuilderId,
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{
@ -85,7 +86,11 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.WaitIpConfig.Prepare()...)
errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare()...)
shutdownWarnings, shutdownErrs := c.ShutdownConfig.Prepare(c.Comm)
warnings = append(warnings, shutdownWarnings...)
errs = packer.MultiErrorAppend(errs, shutdownErrs...)
if c.Export != nil {
errs = packer.MultiErrorAppend(errs, c.Export.Prepare(&c.ctx, &c.LocationConfig, &c.PackerConfig)...)
}

View File

@ -22,7 +22,9 @@ type AccessConfig struct {
// is an alternative method to authenticate to Yandex.Cloud. Alternatively you may set environment variable
// `YC_SERVICE_ACCOUNT_KEY_FILE`.
ServiceAccountKeyFile string `mapstructure:"service_account_key_file" required:"false"`
// OAuth token to use to authenticate to Yandex.Cloud. Alternatively you may set
// [OAuth token](https://cloud.yandex.com/docs/iam/concepts/authorization/oauth-token)
// or [IAM token](https://cloud.yandex.com/docs/iam/concepts/authorization/iam-token)
// to use to authenticate to Yandex.Cloud. Alternatively you may set
// value by environment variable `YC_TOKEN`.
Token string `mapstructure:"token" required:"true"`
// The maximum number of times an API request is being executed.

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
"strings"
"time"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
@ -48,9 +49,13 @@ func NewDriverYC(ui packer.Ui, ac *AccessConfig) (Driver, error) {
sdkConfig.Credentials = ycsdk.InstanceServiceAccount()
case ac.Token != "":
if strings.HasPrefix(ac.Token, "t1.") && strings.Count(ac.Token, ".") == 2 {
log.Printf("[INFO] Use IAM token for authentication")
sdkConfig.Credentials = ycsdk.NewIAMTokenCredentials(ac.Token)
} else {
log.Printf("[INFO] Use OAuth token for authentication")
sdkConfig.Credentials = ycsdk.OAuthToken(ac.Token)
}
case ac.ServiceAccountKeyFile != "":
log.Printf("[INFO] Use Service Account key file %q for authentication", ac.ServiceAccountKeyFile)
key, err := iamkey.ReadFromJSONFile(ac.ServiceAccountKeyFile)

View File

@ -1,8 +1,10 @@
package main
import (
"bytes"
"flag"
"fmt"
"go/format"
"os"
"path/filepath"
"strings"
@ -11,20 +13,19 @@ import (
"github.com/hashicorp/packer/fix"
)
var deprecatedOptsTemplate = template.Must(template.New("deprecatedOptsTemplate").
Parse(`//<!-- Code generated by generate-fixer-deprecations; DO NOT EDIT MANUALLY -->
var deprecatedOptsTemplate = template.Must(template.New("deprecatedOptsTemplate").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(`//<!-- Code generated by generate-fixer-deprecations; DO NOT EDIT MANUALLY -->
package config
var DeprecatedOptions = []string{
{{- range .DeprecatedOpts}}
"{{.}}",
var DeprecatedOptions = map[string][]string{
{{- range $key, $value := .DeprecatedOpts}}
"{{$key}}": []string{"{{ StringsJoin . "\", \"" }}"},
{{- end}}
}
`))
type executeOpts struct {
DeprecatedOpts []string
DeprecatedOpts map[string][]string
}
func main() {
@ -45,7 +46,7 @@ func main() {
packerDir := paths[0]
// Load all deprecated options from all active fixers
allDeprecatedOpts := []string{}
allDeprecatedOpts := map[string][]string{}
for _, name := range fix.FixerOrder {
fixer, ok := fix.Fixers[name]
if !ok {
@ -53,20 +54,38 @@ func main() {
}
deprecated := fixer.DeprecatedOptions()
allDeprecatedOpts = append(allDeprecatedOpts, deprecated...)
for k, v := range deprecated {
if allDeprecatedOpts[k] == nil {
allDeprecatedOpts[k] = v
} else {
allDeprecatedOpts[k] = append(allDeprecatedOpts[k], v...)
}
}
}
deprecated_path := filepath.Join(packerDir, "helper", "config",
"deprecated_options.go")
buf := bytes.Buffer{}
// execute template into buffer
deprecated := &executeOpts{DeprecatedOpts: allDeprecatedOpts}
err = deprecatedOptsTemplate.Execute(&buf, deprecated)
if err != nil {
panic(err)
}
// we've written unformatted go code to the file. now we have to format it.
out, err := format.Source(buf.Bytes())
if err != nil {
panic(err)
}
outputFile, err := os.Create(deprecated_path)
if err != nil {
panic(err)
}
_, err = outputFile.Write(out)
defer outputFile.Close()
deprecated := &executeOpts{DeprecatedOpts: allDeprecatedOpts}
err = deprecatedOptsTemplate.Execute(outputFile, deprecated)
if err != nil {
fmt.Printf("%v", err)
os.Exit(1)

View File

@ -49,6 +49,8 @@ import (
parallelspvmbuilder "github.com/hashicorp/packer/builder/parallels/pvm"
profitbricksbuilder "github.com/hashicorp/packer/builder/profitbricks"
proxmoxbuilder "github.com/hashicorp/packer/builder/proxmox"
proxmoxclonebuilder "github.com/hashicorp/packer/builder/proxmox/clone"
proxmoxisobuilder "github.com/hashicorp/packer/builder/proxmox/iso"
qemubuilder "github.com/hashicorp/packer/builder/qemu"
scalewaybuilder "github.com/hashicorp/packer/builder/scaleway"
tencentcloudcvmbuilder "github.com/hashicorp/packer/builder/tencentcloud/cvm"
@ -146,6 +148,8 @@ var Builders = map[string]packer.Builder{
"parallels-pvm": new(parallelspvmbuilder.Builder),
"profitbricks": new(profitbricksbuilder.Builder),
"proxmox": new(proxmoxbuilder.Builder),
"proxmox-clone": new(proxmoxclonebuilder.Builder),
"proxmox-iso": new(proxmoxisobuilder.Builder),
"qemu": new(qemubuilder.Builder),
"scaleway": new(scalewaybuilder.Builder),
"tencentcloud-cvm": new(tencentcloudcvmbuilder.Builder),

View File

@ -108,10 +108,10 @@ func (s *StepDownload) Run(ctx context.Context, state multistep.StateBag) multis
return multistep.ActionHalt
}
func (s *StepDownload) download(ctx context.Context, ui packer.Ui, source string) (string, error) {
func (s *StepDownload) UseSourceToFindCacheTarget(source string) (*url.URL, string, error) {
u, err := parseSourceURL(source)
if err != nil {
return "", fmt.Errorf("url parse: %s", err)
return nil, "", fmt.Errorf("url parse: %s", err)
}
if checksum := u.Query().Get("checksum"); checksum != "" {
s.Checksum = checksum
@ -142,7 +142,7 @@ func (s *StepDownload) download(ctx context.Context, ui packer.Ui, source string
}
targetPath, err = packer.CachePath(targetPath)
if err != nil {
return "", fmt.Errorf("CachePath: %s", err)
return nil, "", fmt.Errorf("CachePath: %s", err)
}
} else if filepath.Ext(targetPath) == "" {
// When an absolute path is provided
@ -157,7 +157,14 @@ func (s *StepDownload) download(ctx context.Context, ui packer.Ui, source string
targetPath += ".iso"
}
}
return u, targetPath, nil
}
func (s *StepDownload) download(ctx context.Context, ui packer.Ui, source string) (string, error) {
u, targetPath, err := s.UseSourceToFindCacheTarget(source)
if err != nil {
return "", err
}
lockFile := targetPath + ".lock"
log.Printf("Acquiring lock for: %s (%s)", u.String(), lockFile)

View File

@ -58,6 +58,11 @@ EOF
boot_command = local.ubuntu_1604_boot_command
}
source "source.vmware-vmx.base-ubuntu-amd64" {
name = "16.04"
source_path = "vmware_iso_ubuntu_1604_amd64/packer-base-ubuntu-amd64.vmx"
}
source "source.vmware-iso.base-ubuntu-amd64" {
name = "18.04"
iso_url = local.iso_url_ubuntu_1804

View File

@ -0,0 +1,16 @@
source "vmware-vmx" "base-ubuntu-amd64" {
headless = var.headless
boot_wait = "10s"
http_directory = local.http_directory
shutdown_command = "echo 'vagrant' | sudo -S shutdown -P now"
ssh_password = "vagrant"
ssh_port = 22
ssh_timeout = "10000s"
ssh_username = "vagrant"
tools_upload_flavor = "linux"
vmx_data = {
"cpuid.coresPerSocket" = "1"
"ethernet0.pciSlotNumber" = "32"
}
vmx_remove_ethernet_interfaces = true
}

View File

@ -1,5 +1,5 @@
variable "ubuntu_1604_version" {
default = "16.04.6"
default = "16.04.7"
}
locals {

View File

@ -6,7 +6,7 @@ type Fixer interface {
// this fixer. It is used to generate a list of deprecated options that the
// template parser checks against to warn users that they need to call
// `packer fix` against their templates after upgrading.
DeprecatedOptions() []string
DeprecatedOptions() map[string][]string
// Fix takes a raw map structure input, potentially transforms it
// in some way, and returns the new, transformed structure. The
@ -59,6 +59,7 @@ func init() {
"iso-checksum-type-and-url": new(FixerISOChecksumTypeAndURL),
"qemu-host-port": new(FixerQEMUHostPort),
"azure-exclude_from_latest": new(FixerAzureExcludeFromLatest),
"proxmox-type": new(FixerProxmoxType),
}
FixerOrder = []string{
@ -95,5 +96,6 @@ func init() {
"iso-checksum-type-and-url",
"qemu-host-port",
"azure-exclude_from_latest",
"proxmox-type",
}
}

View File

@ -10,8 +10,10 @@ import (
// with the clearer "ena_support". This disambiguates ena_support from sriov_support.
type FixerAmazonEnhancedNetworking struct{}
func (FixerAmazonEnhancedNetworking) DeprecatedOptions() []string {
return []string{"enhanced_networking"}
func (FixerAmazonEnhancedNetworking) DeprecatedOptions() map[string][]string {
return map[string][]string{
"*amazon*": []string{"enhanced_networking"},
}
}
func (FixerAmazonEnhancedNetworking) Fix(input map[string]interface{}) (map[string]interface{}, error) {

View File

@ -12,8 +12,10 @@ import (
// true` with `"ssh_interface": "private_ip"`
type FixerAmazonPrivateIP struct{}
func (FixerAmazonPrivateIP) DeprecatedOptions() []string {
return []string{"ssh_private_ip"}
func (FixerAmazonPrivateIP) DeprecatedOptions() map[string][]string {
return map[string][]string{
"*amazon*": []string{"ssh_private_ip"},
}
}
func (FixerAmazonPrivateIP) Fix(input map[string]interface{}) (map[string]interface{}, error) {

View File

@ -10,8 +10,10 @@ import (
// template in a Amazon builder
type FixerAmazonShutdownBehavior struct{}
func (FixerAmazonShutdownBehavior) DeprecatedOptions() []string {
return []string{"shutdown_behaviour"}
func (FixerAmazonShutdownBehavior) DeprecatedOptions() map[string][]string {
return map[string][]string{
"*amazon*": []string{"shutdown_behaviour"},
}
}
func (FixerAmazonShutdownBehavior) Fix(input map[string]interface{}) (map[string]interface{}, error) {

View File

@ -8,8 +8,10 @@ import (
// from Amazon builder templates
type FixerAmazonSpotPriceProductDeprecation struct{}
func (FixerAmazonSpotPriceProductDeprecation) DeprecatedOptions() []string {
return []string{"spot_price_auto_product"}
func (FixerAmazonSpotPriceProductDeprecation) DeprecatedOptions() map[string][]string {
return map[string][]string{
"*amazon*": []string{"spot_price_auto_product"},
}
}
func (FixerAmazonSpotPriceProductDeprecation) Fix(input map[string]interface{}) (map[string]interface{}, error) {

View File

@ -8,8 +8,10 @@ import (
type FixerAmazonTemporarySecurityCIDRs struct{}
func (FixerAmazonTemporarySecurityCIDRs) DeprecatedOptions() []string {
return []string{"temporary_security_group_source_cidr"}
func (FixerAmazonTemporarySecurityCIDRs) DeprecatedOptions() map[string][]string {
return map[string][]string{
"*amazon*": []string{"temporary_security_group_source_cidr"},
}
}
func (FixerAmazonTemporarySecurityCIDRs) Fix(input map[string]interface{}) (map[string]interface{}, error) {

View File

@ -10,8 +10,10 @@ import (
// template in an Azure builder
type FixerAzureExcludeFromLatest struct{}
func (FixerAzureExcludeFromLatest) DeprecatedOptions() []string {
return []string{"exlude_from_latest"}
func (FixerAzureExcludeFromLatest) DeprecatedOptions() map[string][]string {
return map[string][]string{
"Azure*": []string{"exlude_from_latest"},
}
}
func (FixerAzureExcludeFromLatest) Fix(input map[string]interface{}) (map[string]interface{}, error) {

Some files were not shown because too many files have changed in this diff Show More