Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d597e93e70 | |||
| 1bb154de5a | |||
| 8069ae4f43 | |||
| e8f7076416 | |||
| c032d463d3 | |||
| caf65781d7 | |||
| 3739970b84 | |||
| ad4ed3c986 | |||
| f94e78077a | |||
| 53331644a6 | |||
| bcd3c33e49 | |||
| 31f08909b4 | |||
| e078bd6bf2 | |||
| 2837d56885 | |||
| 5ca9f6539f | |||
| 8b9009c571 | |||
| 8ffa0d2060 | |||
| ca650c9602 | |||
| 25b889216d | |||
| 81f9f4dd27 | |||
| 24d5824061 | |||
| ad0b413e2d | |||
| 9d5dedc3db | |||
| f8e31bff12 | |||
| 0719f906d4 | |||
| 8aaa1bd7a6 | |||
| 3a5beedddf | |||
| e5cd5a9360 | |||
| 26f3299fb7 | |||
| 40c128767e | |||
| dd54651125 | |||
| b4ff0ea4bc | |||
| 988ab66731 | |||
| 900100f591 | |||
| 254716a02a | |||
| f5dc260cf5 | |||
| 2da89db272 | |||
| e362b7f43f | |||
| 7335695c84 | |||
| 4ecade3908 | |||
| 39183d1b76 | |||
| 9a2908d1d9 | |||
| 611899f7b2 | |||
| 41a8a96821 | |||
| 3d2259a6d4 | |||
| 19f0114035 | |||
| 94a9beaac6 | |||
| 61af278b79 | |||
| bd2aa40d2c | |||
| c7356fb776 | |||
| 850fda6aa1 | |||
| f3633da200 | |||
| 00c6efd5cf | |||
| bca4439149 | |||
| d2dd158fe2 | |||
| dfe8aa51b0 | |||
| 5120ad9d9a | |||
| 20d62654ae | |||
| bce6fdf03a | |||
| 03a0beb683 | |||
| 6334fc2da4 | |||
| 7813ad5ef4 | |||
| ba74602d63 | |||
| 6c4e9e9e68 | |||
| d66f85e375 | |||
| 7a7d3ff350 | |||
| 62e1663846 | |||
| d1a61cac33 | |||
| 602729ac23 | |||
| c492199ba5 | |||
| ab23e839c8 | |||
| efd69aea2a | |||
| a70164f489 | |||
| 903deb9e6a | |||
| bddf3b03f7 | |||
| e96cc07a1a | |||
| 79bc643c17 | |||
| 10e74961d2 | |||
| bdf198594e | |||
| a25f057984 | |||
| 161879b98a | |||
| 652878059c | |||
| eb4069a1b7 | |||
| 91c5a4613c | |||
| 9eaa2e17f7 | |||
| 24dbd0a28b | |||
| 05aecc56ea | |||
| f4556fd3f1 | |||
| f74a425661 | |||
| 58a0bdd780 | |||
| 03a374f4b6 | |||
| 804fefef17 | |||
| f578b93f7e | |||
| 0df2e15d9f | |||
| 0ad26cce01 | |||
| 281af9a03d | |||
| ecd1a49a35 | |||
| e4f975fae1 | |||
| 942bfbf221 | |||
| cf622346ab | |||
| 9156563314 | |||
| 1c5bc41beb | |||
| bbc3a5b0d1 |
@@ -1,3 +1,40 @@
|
||||
## 1.6.3 (Upcoming)
|
||||
|
||||
### IMPROVEMENTS:
|
||||
* builder/azure: Support publishing to a Shared Image Gallery with a different
|
||||
subscription id [GH-9875]
|
||||
* builder/oracle-oci: Allow freeform and defined tags to be added instance.
|
||||
[GH-9802]
|
||||
* builder/proxmox: Allow the mounting of multiple ISOs via the `cd_drive`
|
||||
option. [GH-9653]
|
||||
* builder/qemu: New `skip_resize_disk` option. [GH-9896]
|
||||
* builder/scaleway: Change default boottype to local. [GH-9853]
|
||||
* builder/scaleway: Update scaleway to use non-deprecated sdk. [GH-9902]
|
||||
* builder/vsphere-iso: Add NVMe controller support. [GH-9880]
|
||||
* builder/vsphere: Look for a default resource pool when root resource pool is
|
||||
not found. [GH-9809]
|
||||
* core: New `cd_files` option to mount iso for modern OSes which don't support
|
||||
floppies. [GH-9796] [GH-9919]
|
||||
* HCL2: When the type of a variable is not known evaluate setting as a literal
|
||||
string instead of a variable name. [GH-9863]
|
||||
* post-processor/yandex-import: Allow custom API endpoint. [GH-9850]
|
||||
* provisioner/ansible: Add support for Ansible Galaxy Collections. [GH-9903]
|
||||
|
||||
### BUG FIXES:
|
||||
* builder/qemu: Fix hardcoded lowerbound causing negative ports [GH-9905]
|
||||
* builder/vsphere: Fix overly strict iso_path validation regex. [GH-9855]
|
||||
* command/console: Prevent failure when there are unknown vars. [GH-9864]
|
||||
* command/inspect: Allow unset variables in HCL2 and JSON. [GH-9832]
|
||||
* core: use $APPDATA over $HOME on Windows hosts when determining homedir.
|
||||
[GH-9830]
|
||||
* post-processor/digitalocean-import: Fix crash caused by empty artifact.Files
|
||||
slice. [GH-9857]
|
||||
* post-processor/yandex-export: Set metadata key to expected value on error.
|
||||
[GH-9849]
|
||||
* post-processor/yandex-export: Check for error after runner completes.
|
||||
[GH-9925]
|
||||
* post-processor/yandex-import: Fix S3 URL construct process. [GH-9931]
|
||||
|
||||
## 1.6.2 (August 28, 2020)
|
||||
|
||||
### FEATURES:
|
||||
|
||||
+2
-2
@@ -49,8 +49,8 @@
|
||||
/builder/proxmox/ @carlpett
|
||||
/website/pages/docs/builders/proxmox* @carlpett
|
||||
|
||||
/builder/scaleway/ @sieben @mvaude @jqueuniet @fflorens @brmzkw
|
||||
/website/pages/docs/builders/scaleway* @sieben @mvaude @jqueuniet @fflorens @brmzkw
|
||||
/builder/scaleway/ @scaleway/devtools
|
||||
/website/pages/docs/builders/scaleway* @scaleway/devtools
|
||||
|
||||
/builder/hcloud/ @LKaemmerling
|
||||
/website/pages/docs/builders/hcloud* @LKaemmerling
|
||||
|
||||
@@ -422,7 +422,6 @@ func TestBuilderAcc_ECSImageSharing(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// share with catsby
|
||||
const testBuilderAccSharing = `
|
||||
{
|
||||
"builders": [{
|
||||
|
||||
@@ -6,6 +6,7 @@ package ebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
@@ -104,10 +105,10 @@ func checkSnapshotsDeleted(snapshotIds []*string) builderT.TestCheckFunc {
|
||||
|
||||
func TestBuilderAcc_amiSharing(t *testing.T) {
|
||||
builderT.Test(t, builderT.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
PreCheck: func() { testAccSharingPreCheck(t) },
|
||||
Builder: &Builder{},
|
||||
Template: testBuilderAccSharing,
|
||||
Check: checkAMISharing(2, "932021504756", "all"),
|
||||
Template: buildSharingConfig(os.Getenv("TESTACC_AWS_ACCOUNT_ID")),
|
||||
Check: checkAMISharing(2, os.Getenv("TESTACC_AWS_ACCOUNT_ID"), "all"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -248,6 +249,12 @@ func checkBootEncrypted() builderT.TestCheckFunc {
|
||||
func testAccPreCheck(t *testing.T) {
|
||||
}
|
||||
|
||||
func testAccSharingPreCheck(t *testing.T) {
|
||||
if v := os.Getenv("TESTACC_AWS_ACCOUNT_ID"); v == "" {
|
||||
t.Fatal(fmt.Sprintf("TESTACC_AWS_ACCOUNT_ID must be set for acceptance tests"))
|
||||
}
|
||||
}
|
||||
|
||||
func testEC2Conn() (*ec2.EC2, error) {
|
||||
access := &common.AccessConfig{RawRegion: "us-east-1"}
|
||||
session, err := access.Session()
|
||||
@@ -314,7 +321,6 @@ const testBuilderAccForceDeleteSnapshot = `
|
||||
}
|
||||
`
|
||||
|
||||
// share with catsby
|
||||
const testBuilderAccSharing = `
|
||||
{
|
||||
"builders": [{
|
||||
@@ -324,7 +330,7 @@ const testBuilderAccSharing = `
|
||||
"source_ami": "ami-76b2a71e",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-test {{timestamp}}",
|
||||
"ami_users":["932021504756"],
|
||||
"ami_users":["%s"],
|
||||
"ami_groups":["all"]
|
||||
}]
|
||||
}
|
||||
@@ -351,3 +357,7 @@ func buildForceDeregisterConfig(val, name string) string {
|
||||
func buildForceDeleteSnapshotConfig(val, name string) string {
|
||||
return fmt.Sprintf(testBuilderAccForceDeleteSnapshot, val, val, name)
|
||||
}
|
||||
|
||||
func buildSharingConfig(val string) string {
|
||||
return fmt.Sprintf(testBuilderAccSharing, val)
|
||||
}
|
||||
|
||||
@@ -128,8 +128,8 @@ func byConcatDecorators(decorators ...autorest.RespondDecorator) autorest.Respon
|
||||
}
|
||||
}
|
||||
|
||||
func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string,
|
||||
cloud *azure.Environment, SharedGalleryTimeout time.Duration, PollingDuration time.Duration,
|
||||
func NewAzureClient(subscriptionID, sigSubscriptionID, resourceGroupName, storageAccountName string,
|
||||
cloud *azure.Environment, sharedGalleryTimeout time.Duration, pollingDuration time.Duration,
|
||||
servicePrincipalToken, servicePrincipalTokenVault *adal.ServicePrincipalToken) (*AzureClient, error) {
|
||||
|
||||
var azureClient = &AzureClient{}
|
||||
@@ -141,56 +141,56 @@ func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string
|
||||
azureClient.DeploymentsClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.DeploymentsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.DeploymentsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.DeploymentsClient.UserAgent)
|
||||
azureClient.DeploymentsClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.DeploymentsClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
azureClient.DeploymentOperationsClient = resources.NewDeploymentOperationsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.DeploymentOperationsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
azureClient.DeploymentOperationsClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.DeploymentOperationsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.DeploymentOperationsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.DeploymentOperationsClient.UserAgent)
|
||||
azureClient.DeploymentOperationsClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.DeploymentOperationsClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
azureClient.DisksClient = compute.NewDisksClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.DisksClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
azureClient.DisksClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.DisksClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.DisksClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.DisksClient.UserAgent)
|
||||
azureClient.DisksClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.DisksClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
azureClient.GroupsClient = resources.NewGroupsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.GroupsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
azureClient.GroupsClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.GroupsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.GroupsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.GroupsClient.UserAgent)
|
||||
azureClient.GroupsClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.GroupsClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
azureClient.ImagesClient = compute.NewImagesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.ImagesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
azureClient.ImagesClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.ImagesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.ImagesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.ImagesClient.UserAgent)
|
||||
azureClient.ImagesClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.ImagesClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
azureClient.InterfacesClient = network.NewInterfacesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.InterfacesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
azureClient.InterfacesClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.InterfacesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.InterfacesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.InterfacesClient.UserAgent)
|
||||
azureClient.InterfacesClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.InterfacesClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
azureClient.SubnetsClient = network.NewSubnetsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.SubnetsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
azureClient.SubnetsClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.SubnetsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.SubnetsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.SubnetsClient.UserAgent)
|
||||
azureClient.SubnetsClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.SubnetsClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
azureClient.VirtualNetworksClient = network.NewVirtualNetworksClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.VirtualNetworksClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
azureClient.VirtualNetworksClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.VirtualNetworksClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.VirtualNetworksClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.VirtualNetworksClient.UserAgent)
|
||||
azureClient.VirtualNetworksClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.VirtualNetworksClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
azureClient.SecurityGroupsClient = network.NewSecurityGroupsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.SecurityGroupsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
@@ -203,42 +203,44 @@ func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string
|
||||
azureClient.PublicIPAddressesClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.PublicIPAddressesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.PublicIPAddressesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.PublicIPAddressesClient.UserAgent)
|
||||
azureClient.PublicIPAddressesClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.PublicIPAddressesClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
azureClient.VirtualMachinesClient = compute.NewVirtualMachinesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.VirtualMachinesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
azureClient.VirtualMachinesClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.VirtualMachinesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), templateCapture(azureClient), errorCapture(azureClient))
|
||||
azureClient.VirtualMachinesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.VirtualMachinesClient.UserAgent)
|
||||
azureClient.VirtualMachinesClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.VirtualMachinesClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
azureClient.SnapshotsClient = compute.NewSnapshotsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.SnapshotsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
azureClient.SnapshotsClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.SnapshotsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.SnapshotsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.SnapshotsClient.UserAgent)
|
||||
azureClient.SnapshotsClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.SnapshotsClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
azureClient.AccountsClient = armStorage.NewAccountsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.AccountsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
azureClient.AccountsClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.AccountsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.AccountsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.AccountsClient.UserAgent)
|
||||
azureClient.AccountsClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.AccountsClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
azureClient.GalleryImageVersionsClient = newCompute.NewGalleryImageVersionsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.GalleryImageVersionsClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
azureClient.GalleryImageVersionsClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.GalleryImageVersionsClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.GalleryImageVersionsClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.GalleryImageVersionsClient.UserAgent)
|
||||
azureClient.GalleryImageVersionsClient.Client.PollingDuration = SharedGalleryTimeout
|
||||
azureClient.GalleryImageVersionsClient.Client.PollingDuration = sharedGalleryTimeout
|
||||
azureClient.GalleryImageVersionsClient.SubscriptionID = sigSubscriptionID
|
||||
|
||||
azureClient.GalleryImagesClient = newCompute.NewGalleryImagesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID)
|
||||
azureClient.GalleryImagesClient.Authorizer = autorest.NewBearerAuthorizer(servicePrincipalToken)
|
||||
azureClient.GalleryImagesClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.GalleryImagesClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.GalleryImagesClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.GalleryImagesClient.UserAgent)
|
||||
azureClient.GalleryImagesClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.GalleryImagesClient.Client.PollingDuration = pollingDuration
|
||||
azureClient.GalleryImagesClient.SubscriptionID = sigSubscriptionID
|
||||
|
||||
keyVaultURL, err := url.Parse(cloud.KeyVaultEndpoint)
|
||||
if err != nil {
|
||||
@@ -250,7 +252,7 @@ func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string
|
||||
azureClient.VaultClient.RequestInspector = withInspection(maxlen)
|
||||
azureClient.VaultClient.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.VaultClient.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.VaultClient.UserAgent)
|
||||
azureClient.VaultClient.Client.PollingDuration = PollingDuration
|
||||
azureClient.VaultClient.Client.PollingDuration = pollingDuration
|
||||
|
||||
// This client is different than the above because it manages the vault
|
||||
// itself rather than the contents of the vault.
|
||||
@@ -259,7 +261,7 @@ func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string
|
||||
azureClient.VaultClientDelete.RequestInspector = withInspection(maxlen)
|
||||
azureClient.VaultClientDelete.ResponseInspector = byConcatDecorators(byInspecting(maxlen), errorCapture(azureClient))
|
||||
azureClient.VaultClientDelete.UserAgent = fmt.Sprintf("%s %s", useragent.String(), azureClient.VaultClientDelete.UserAgent)
|
||||
azureClient.VaultClientDelete.Client.PollingDuration = PollingDuration
|
||||
azureClient.VaultClientDelete.Client.PollingDuration = pollingDuration
|
||||
|
||||
// If this is a managed disk build, this should be ignored.
|
||||
if resourceGroupName != "" && storageAccountName != "" {
|
||||
|
||||
@@ -84,6 +84,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
ui.Message("Creating Azure Resource Manager (ARM) client ...")
|
||||
azureClient, err := NewAzureClient(
|
||||
b.config.ClientConfig.SubscriptionID,
|
||||
b.config.SharedGalleryDestination.SigDestinationSubscription,
|
||||
b.config.ResourceGroupName,
|
||||
b.config.StorageAccount,
|
||||
b.config.ClientConfig.CloudEnvironment(),
|
||||
|
||||
+56
-19
@@ -90,6 +90,7 @@ type SharedImageGallery struct {
|
||||
}
|
||||
|
||||
type SharedImageGalleryDestination struct {
|
||||
SigDestinationSubscription string `mapstructure:"subscription"`
|
||||
SigDestinationResourceGroup string `mapstructure:"resource_group"`
|
||||
SigDestinationGalleryName string `mapstructure:"gallery_name"`
|
||||
SigDestinationImageName string `mapstructure:"image_name"`
|
||||
@@ -118,31 +119,64 @@ type Config struct {
|
||||
// Use a [Shared Gallery
|
||||
// image](https://azure.microsoft.com/en-us/blog/announcing-the-public-preview-of-shared-image-gallery/)
|
||||
// as the source for this build. *VHD targets are incompatible with this
|
||||
// build type* - the target must be a *Managed Image*.
|
||||
// build type* - the target must be a *Managed Image*. When using shared_image_gallery as a source, image_publisher,
|
||||
// image_offer, image_sku, image_version, and custom_managed_image_name should not be set.
|
||||
//
|
||||
// "shared_image_gallery": {
|
||||
// "subscription": "00000000-0000-0000-0000-00000000000",
|
||||
// "resource_group": "ResourceGroup",
|
||||
// "gallery_name": "GalleryName",
|
||||
// "image_name": "ImageName",
|
||||
// "image_version": "1.0.0"
|
||||
// }
|
||||
// "managed_image_name": "TargetImageName",
|
||||
// "managed_image_resource_group_name": "TargetResourceGroup"
|
||||
// In JSON
|
||||
// ```json
|
||||
// "shared_image_gallery": {
|
||||
// "subscription": "00000000-0000-0000-0000-00000000000",
|
||||
// "resource_group": "ResourceGroup",
|
||||
// "gallery_name": "GalleryName",
|
||||
// "image_name": "ImageName",
|
||||
// "image_version": "1.0.0"
|
||||
// }
|
||||
// "managed_image_name": "TargetImageName",
|
||||
// "managed_image_resource_group_name": "TargetResourceGroup"
|
||||
// ```
|
||||
// In HCL2
|
||||
// ```hcl
|
||||
// shared_image_gallery {
|
||||
// subscription = "00000000-0000-0000-0000-00000000000"
|
||||
// resource_group = "ResourceGroup"
|
||||
// gallery_name = "GalleryName"
|
||||
// image_name = "ImageName"
|
||||
// image_version = "1.0.0"
|
||||
// }
|
||||
// managed_image_name = "TargetImageName"
|
||||
// managed_image_resource_group_name = "TargetResourceGroup"
|
||||
// ```
|
||||
SharedGallery SharedImageGallery `mapstructure:"shared_image_gallery" required:"false"`
|
||||
// The name of the Shared Image Gallery under which the managed image will be published as Shared Gallery Image version.
|
||||
//
|
||||
// Following is an example.
|
||||
//
|
||||
// "shared_image_gallery_destination": {
|
||||
// "resource_group": "ResourceGroup",
|
||||
// "gallery_name": "GalleryName",
|
||||
// "image_name": "ImageName",
|
||||
// "image_version": "1.0.0",
|
||||
// "replication_regions": ["regionA", "regionB", "regionC"]
|
||||
// }
|
||||
// "managed_image_name": "TargetImageName",
|
||||
// "managed_image_resource_group_name": "TargetResourceGroup"
|
||||
// In JSON
|
||||
// ```json
|
||||
// "shared_image_gallery_destination": {
|
||||
// "subscription": "00000000-0000-0000-0000-00000000000",
|
||||
// "resource_group": "ResourceGroup",
|
||||
// "gallery_name": "GalleryName",
|
||||
// "image_name": "ImageName",
|
||||
// "image_version": "1.0.0",
|
||||
// "replication_regions": ["regionA", "regionB", "regionC"]
|
||||
// }
|
||||
// "managed_image_name": "TargetImageName",
|
||||
// "managed_image_resource_group_name": "TargetResourceGroup"
|
||||
// ```
|
||||
// In HCL2
|
||||
// ```hcl
|
||||
// shared_image_gallery_destination {
|
||||
// subscription = "00000000-0000-0000-0000-00000000000"
|
||||
// resource_group = "ResourceGroup"
|
||||
// gallery_name = "GalleryName"
|
||||
// image_name = "ImageName"
|
||||
// image_version = "1.0.0"
|
||||
// replication_regions = ["regionA", "regionB", "regionC"]
|
||||
// }
|
||||
// managed_image_name = "TargetImageName"
|
||||
// managed_image_resource_group_name = "TargetResourceGroup"
|
||||
// ```
|
||||
SharedGalleryDestination SharedImageGalleryDestination `mapstructure:"shared_image_gallery_destination"`
|
||||
// How long to wait for an image to be published to the shared image
|
||||
// gallery before timing out. If your Packer build is failing on the
|
||||
@@ -978,6 +1012,9 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) {
|
||||
if len(c.SharedGalleryDestination.SigDestinationReplicationRegions) == 0 {
|
||||
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A list of replication_regions must be specified for shared_image_gallery_destination"))
|
||||
}
|
||||
if c.SharedGalleryDestination.SigDestinationSubscription == "" {
|
||||
c.SharedGalleryDestination.SigDestinationSubscription = c.ClientConfig.SubscriptionID
|
||||
}
|
||||
}
|
||||
if c.SharedGalleryTimeout == 0 {
|
||||
// default to a one-hour timeout. In the sdk, the default is 15 m.
|
||||
|
||||
@@ -311,6 +311,7 @@ func (*FlatSharedImageGallery) HCL2Spec() map[string]hcldec.Spec {
|
||||
// FlatSharedImageGalleryDestination is an auto-generated flat version of SharedImageGalleryDestination.
|
||||
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||
type FlatSharedImageGalleryDestination struct {
|
||||
SigDestinationSubscription *string `mapstructure:"subscription" cty:"subscription" hcl:"subscription"`
|
||||
SigDestinationResourceGroup *string `mapstructure:"resource_group" cty:"resource_group" hcl:"resource_group"`
|
||||
SigDestinationGalleryName *string `mapstructure:"gallery_name" cty:"gallery_name" hcl:"gallery_name"`
|
||||
SigDestinationImageName *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"`
|
||||
@@ -330,6 +331,7 @@ func (*SharedImageGalleryDestination) FlatMapstructure() interface{ HCL2Spec() m
|
||||
// The decoded values from this spec will then be applied to a FlatSharedImageGalleryDestination.
|
||||
func (*FlatSharedImageGalleryDestination) HCL2Spec() map[string]hcldec.Spec {
|
||||
s := map[string]hcldec.Spec{
|
||||
"subscription": &hcldec.AttrSpec{Name: "subscription", Type: cty.String, Required: false},
|
||||
"resource_group": &hcldec.AttrSpec{Name: "resource_group", Type: cty.String, Required: false},
|
||||
"gallery_name": &hcldec.AttrSpec{Name: "gallery_name", Type: cty.String, Required: false},
|
||||
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
|
||||
|
||||
@@ -20,6 +20,7 @@ type FlatConfig struct {
|
||||
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"`
|
||||
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"`
|
||||
@@ -132,6 +133,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"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},
|
||||
|
||||
+5
-93
@@ -1,114 +1,26 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/hashicorp/packer/common/iochan"
|
||||
"github.com/hashicorp/packer/helper/builder/localexec"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
func runAndStream(cmd *exec.Cmd, ui packer.Ui) error {
|
||||
stdout_r, stdout_w := io.Pipe()
|
||||
stderr_r, stderr_w := io.Pipe()
|
||||
defer stdout_w.Close()
|
||||
defer stderr_w.Close()
|
||||
|
||||
args := make([]string, len(cmd.Args)-1)
|
||||
copy(args, cmd.Args[1:])
|
||||
|
||||
// Scrub password from the log output.
|
||||
capturedPassword := ""
|
||||
for i, v := range args {
|
||||
if v == "-p" || v == "--password" {
|
||||
args[i+1] = "<Filtered>"
|
||||
capturedPassword = args[i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Executing: %s %v", cmd.Path, args)
|
||||
cmd.Stdout = stdout_w
|
||||
cmd.Stderr = stderr_w
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the channels we'll use for data
|
||||
exitCh := make(chan int, 1)
|
||||
stdoutCh := iochan.LineReader(stdout_r)
|
||||
stderrCh := iochan.LineReader(stderr_r)
|
||||
|
||||
// Start the goroutine to watch for the exit
|
||||
go func() {
|
||||
defer stdout_w.Close()
|
||||
defer stderr_w.Close()
|
||||
exitStatus := 0
|
||||
|
||||
err := cmd.Wait()
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
exitStatus = 1
|
||||
|
||||
// There is no process-independent way to get the REAL
|
||||
// exit status so we just try to go deeper.
|
||||
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
||||
exitStatus = status.ExitStatus()
|
||||
}
|
||||
}
|
||||
|
||||
exitCh <- exitStatus
|
||||
}()
|
||||
|
||||
// This waitgroup waits for the streaming to end
|
||||
var streamWg sync.WaitGroup
|
||||
streamWg.Add(2)
|
||||
|
||||
streamFunc := func(ch <-chan string) {
|
||||
defer streamWg.Done()
|
||||
|
||||
for data := range ch {
|
||||
data = cleanOutputLine(data)
|
||||
if data != "" {
|
||||
ui.Message(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stream stderr/stdout
|
||||
go streamFunc(stderrCh)
|
||||
go streamFunc(stdoutCh)
|
||||
|
||||
// Wait for the process to end and then wait for the streaming to end
|
||||
exitStatus := <-exitCh
|
||||
streamWg.Wait()
|
||||
|
||||
if exitStatus != 0 {
|
||||
return fmt.Errorf("Bad exit status: %d", exitStatus)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanOutputLine cleans up a line so that '\r' don't muck up the
|
||||
// UI output when we're reading from a remote command.
|
||||
func cleanOutputLine(line string) string {
|
||||
// Build a regular expression that will get rid of shell codes
|
||||
re := regexp.MustCompile("(?i)\x1b\\[([0-9]{1,2}(;[0-9]{1,2})?)?[a|b|m|k]")
|
||||
line = re.ReplaceAllString(line, "")
|
||||
|
||||
// Trim surrounding whitespace
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Trim up to the first carriage return, since that text would be
|
||||
// lost anyways.
|
||||
idx := strings.LastIndex(line, "\r")
|
||||
if idx > -1 {
|
||||
line = line[idx+1:]
|
||||
}
|
||||
|
||||
return line
|
||||
// run local command and stream output to UI.
|
||||
return localexec.RunAndStream(cmd, ui, []string{capturedPassword})
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ const (
|
||||
|
||||
type CommonConfig struct {
|
||||
common.FloppyConfig `mapstructure:",squash"`
|
||||
common.CDConfig `mapstructure:",squash"`
|
||||
// The block size of the VHD to be created.
|
||||
// Recommended disk block size for Linux hyper-v guests is 1 MiB. This
|
||||
// defaults to "32" MiB.
|
||||
@@ -210,8 +211,8 @@ func (c *CommonConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig
|
||||
}
|
||||
|
||||
// Errors
|
||||
floppyerrs := c.FloppyConfig.Prepare(ctx)
|
||||
errs = append(errs, floppyerrs...)
|
||||
errs = append(errs, c.FloppyConfig.Prepare(ctx)...)
|
||||
errs = append(errs, c.CDConfig.Prepare(ctx)...)
|
||||
if c.GuestAdditionsMode == "" {
|
||||
if c.GuestAdditionsPath != "" {
|
||||
c.GuestAdditionsMode = "attach"
|
||||
|
||||
@@ -34,7 +34,17 @@ func (s *StepMountSecondaryDvdImages) Run(ctx context.Context, state multistep.S
|
||||
// For IDE, there are only 2 controllers (0,1) with 2 locations each (0,1)
|
||||
var dvdProperties []DvdControllerProperties
|
||||
|
||||
for _, isoPath := range s.IsoPaths {
|
||||
isoPaths := s.IsoPaths
|
||||
|
||||
// Add our custom CD, if it exists
|
||||
cd_path, ok := state.Get("cd_path").(string)
|
||||
if ok {
|
||||
if cd_path != "" {
|
||||
isoPaths = append(isoPaths, cd_path)
|
||||
}
|
||||
}
|
||||
|
||||
for _, isoPath := range isoPaths {
|
||||
var properties DvdControllerProperties
|
||||
|
||||
controllerNumber, controllerLocation, err := driver.CreateDvdDrive(vmName, isoPath, s.Generation)
|
||||
|
||||
@@ -254,7 +254,10 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
GuestAdditionsPath: b.config.GuestAdditionsPath,
|
||||
Generation: b.config.Generation,
|
||||
},
|
||||
|
||||
&common.StepCreateCD{
|
||||
Files: b.config.CDConfig.CDFiles,
|
||||
Label: b.config.CDConfig.CDLabel,
|
||||
},
|
||||
&hypervcommon.StepMountSecondaryDvdImages{
|
||||
IsoPaths: b.config.SecondaryDvdImages,
|
||||
Generation: b.config.Generation,
|
||||
|
||||
@@ -20,6 +20,7 @@ type FlatConfig struct {
|
||||
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"`
|
||||
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"`
|
||||
@@ -79,6 +80,8 @@ type FlatConfig struct {
|
||||
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
|
||||
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
|
||||
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
|
||||
CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"`
|
||||
CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"`
|
||||
DiskBlockSize *uint `mapstructure:"disk_block_size" required:"false" cty:"disk_block_size" hcl:"disk_block_size"`
|
||||
RamSize *uint `mapstructure:"memory" required:"false" cty:"memory" hcl:"memory"`
|
||||
SecondaryDvdImages []string `mapstructure:"secondary_iso_images" required:"false" cty:"secondary_iso_images" hcl:"secondary_iso_images"`
|
||||
@@ -136,6 +139,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"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},
|
||||
@@ -195,6 +199,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
|
||||
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
|
||||
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
|
||||
"cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false},
|
||||
"cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false},
|
||||
"disk_block_size": &hcldec.AttrSpec{Name: "disk_block_size", Type: cty.Number, Required: false},
|
||||
"memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false},
|
||||
"secondary_iso_images": &hcldec.AttrSpec{Name: "secondary_iso_images", Type: cty.List(cty.String), Required: false},
|
||||
|
||||
@@ -20,6 +20,7 @@ type FlatConfig struct {
|
||||
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"`
|
||||
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"`
|
||||
@@ -79,6 +80,8 @@ type FlatConfig struct {
|
||||
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
|
||||
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
|
||||
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
|
||||
CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"`
|
||||
CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"`
|
||||
DiskBlockSize *uint `mapstructure:"disk_block_size" required:"false" cty:"disk_block_size" hcl:"disk_block_size"`
|
||||
RamSize *uint `mapstructure:"memory" required:"false" cty:"memory" hcl:"memory"`
|
||||
SecondaryDvdImages []string `mapstructure:"secondary_iso_images" required:"false" cty:"secondary_iso_images" hcl:"secondary_iso_images"`
|
||||
@@ -138,6 +141,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"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},
|
||||
@@ -197,6 +201,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
|
||||
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
|
||||
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
|
||||
"cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false},
|
||||
"cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false},
|
||||
"disk_block_size": &hcldec.AttrSpec{Name: "disk_block_size", Type: cty.Number, Required: false},
|
||||
"memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false},
|
||||
"secondary_iso_images": &hcldec.AttrSpec{Name: "secondary_iso_images", Type: cty.List(cty.String), Required: false},
|
||||
|
||||
@@ -94,7 +94,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
errs, err)
|
||||
}
|
||||
for _, dc := range dcs {
|
||||
if strings.ToLower(dc.CountryCode) == strings.ToLower(c.DataCenterName) {
|
||||
if strings.EqualFold(dc.CountryCode, c.DataCenterName) {
|
||||
c.DataCenterId = dc.Id
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:generate mapstructure-to-hcl2 -type Config
|
||||
//go:generate mapstructure-to-hcl2 -type Config,CreateVNICDetails
|
||||
|
||||
package oci
|
||||
|
||||
@@ -22,6 +22,19 @@ import (
|
||||
ociauth "github.com/oracle/oci-go-sdk/common/auth"
|
||||
)
|
||||
|
||||
type CreateVNICDetails struct {
|
||||
// fields that can be specified under "create_vnic_details"
|
||||
AssignPublicIp *bool `mapstructure:"assign_public_ip" required:"false"`
|
||||
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" required:"false"`
|
||||
DisplayName *string `mapstructure:"display_name" required:"false"`
|
||||
FreeformTags map[string]string `mapstructure:"tags" required:"false"`
|
||||
HostnameLabel *string `mapstructure:"hostname_label" required:"false"`
|
||||
NsgIds []string `mapstructure:"nsg_ids" required:"false"`
|
||||
PrivateIp *string `mapstructure:"private_ip" required:"false"`
|
||||
SkipSourceDestCheck *bool `mapstructure:"skip_source_dest_check" required:"false"`
|
||||
SubnetId *string `mapstructure:"subnet_id" required:"false"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
@@ -57,11 +70,13 @@ type Config struct {
|
||||
|
||||
// Image
|
||||
BaseImageID string `mapstructure:"base_image_ocid"`
|
||||
Shape string `mapstructure:"shape"`
|
||||
ImageName string `mapstructure:"image_name"`
|
||||
|
||||
// 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"`
|
||||
|
||||
// Metadata optionally contains custom metadata key/value pairs provided in the
|
||||
// configuration. While this can be used to set metadata["user_data"] the explicit
|
||||
@@ -75,7 +90,8 @@ type Config struct {
|
||||
UserDataFile string `mapstructure:"user_data_file"`
|
||||
|
||||
// Networking
|
||||
SubnetID string `mapstructure:"subnet_ocid"`
|
||||
SubnetID string `mapstructure:"subnet_ocid"`
|
||||
CreateVnicDetails CreateVNICDetails `mapstructure:"create_vnic_details"`
|
||||
|
||||
// Tagging
|
||||
Tags map[string]string `mapstructure:"tags"`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by "mapstructure-to-hcl2 -type Config"; DO NOT EDIT.
|
||||
// Code generated by "mapstructure-to-hcl2 -type Config,CreateVNICDetails"; DO NOT EDIT.
|
||||
package oci
|
||||
|
||||
import (
|
||||
@@ -76,13 +76,16 @@ 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"`
|
||||
Shape *string `mapstructure:"shape" cty:"shape" hcl:"shape"`
|
||||
ImageName *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"`
|
||||
InstanceName *string `mapstructure:"instance_name" cty:"instance_name" hcl:"instance_name"`
|
||||
InstanceTags map[string]string `mapstructure:"instance_tags" cty:"instance_tags" hcl:"instance_tags"`
|
||||
InstanceDefinedTags map[string]map[string]interface{} `mapstructure:"instance_defined_tags" cty:"instance_defined_tags" hcl:"instance_defined_tags"`
|
||||
Shape *string `mapstructure:"shape" cty:"shape" hcl:"shape"`
|
||||
Metadata map[string]string `mapstructure:"metadata" cty:"metadata" hcl:"metadata"`
|
||||
UserData *string `mapstructure:"user_data" cty:"user_data" hcl:"user_data"`
|
||||
UserDataFile *string `mapstructure:"user_data_file" cty:"user_data_file" hcl:"user_data_file"`
|
||||
SubnetID *string `mapstructure:"subnet_ocid" cty:"subnet_ocid" hcl:"subnet_ocid"`
|
||||
CreateVnicDetails *FlatCreateVNICDetails `mapstructure:"create_vnic_details" cty:"create_vnic_details" hcl:"create_vnic_details"`
|
||||
Tags map[string]string `mapstructure:"tags" cty:"tags" hcl:"tags"`
|
||||
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" cty:"defined_tags" hcl:"defined_tags"`
|
||||
}
|
||||
@@ -166,15 +169,57 @@ 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},
|
||||
"shape": &hcldec.AttrSpec{Name: "shape", Type: cty.String, Required: false},
|
||||
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
|
||||
"instance_name": &hcldec.AttrSpec{Name: "instance_name", Type: cty.String, Required: false},
|
||||
"instance_tags": &hcldec.AttrSpec{Name: "instance_tags", Type: cty.Map(cty.String), Required: false},
|
||||
"instance_defined_tags": &hcldec.AttrSpec{Name: "instance_defined_tags", Type: cty.Map(cty.String), Required: false},
|
||||
"shape": &hcldec.AttrSpec{Name: "shape", Type: cty.String, Required: false},
|
||||
"metadata": &hcldec.AttrSpec{Name: "metadata", Type: cty.Map(cty.String), Required: false},
|
||||
"user_data": &hcldec.AttrSpec{Name: "user_data", Type: cty.String, Required: false},
|
||||
"user_data_file": &hcldec.AttrSpec{Name: "user_data_file", Type: cty.String, Required: false},
|
||||
"subnet_ocid": &hcldec.AttrSpec{Name: "subnet_ocid", Type: cty.String, Required: false},
|
||||
"create_vnic_details": &hcldec.BlockSpec{TypeName: "create_vnic_details", Nested: hcldec.ObjectSpec((*FlatCreateVNICDetails)(nil).HCL2Spec())},
|
||||
"tags": &hcldec.AttrSpec{Name: "tags", Type: cty.Map(cty.String), Required: false},
|
||||
"defined_tags": &hcldec.AttrSpec{Name: "defined_tags", Type: cty.Map(cty.String), Required: false},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// FlatCreateVNICDetails is an auto-generated flat version of CreateVNICDetails.
|
||||
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||
type FlatCreateVNICDetails struct {
|
||||
AssignPublicIp *bool `mapstructure:"assign_public_ip" required:"false" cty:"assign_public_ip" hcl:"assign_public_ip"`
|
||||
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" required:"false" cty:"defined_tags" hcl:"defined_tags"`
|
||||
DisplayName *string `mapstructure:"display_name" required:"false" cty:"display_name" hcl:"display_name"`
|
||||
FreeformTags map[string]string `mapstructure:"tags" required:"false" cty:"tags" hcl:"tags"`
|
||||
HostnameLabel *string `mapstructure:"hostname_label" required:"false" cty:"hostname_label" hcl:"hostname_label"`
|
||||
NsgIds []string `mapstructure:"nsg_ids" required:"false" cty:"nsg_ids" hcl:"nsg_ids"`
|
||||
PrivateIp *string `mapstructure:"private_ip" required:"false" cty:"private_ip" hcl:"private_ip"`
|
||||
SkipSourceDestCheck *bool `mapstructure:"skip_source_dest_check" required:"false" cty:"skip_source_dest_check" hcl:"skip_source_dest_check"`
|
||||
SubnetId *string `mapstructure:"subnet_id" required:"false" cty:"subnet_id" hcl:"subnet_id"`
|
||||
}
|
||||
|
||||
// FlatMapstructure returns a new FlatCreateVNICDetails.
|
||||
// FlatCreateVNICDetails is an auto-generated flat version of CreateVNICDetails.
|
||||
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||
func (*CreateVNICDetails) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||
return new(FlatCreateVNICDetails)
|
||||
}
|
||||
|
||||
// HCL2Spec returns the hcl spec of a CreateVNICDetails.
|
||||
// This spec is used by HCL to read the fields of CreateVNICDetails.
|
||||
// The decoded values from this spec will then be applied to a FlatCreateVNICDetails.
|
||||
func (*FlatCreateVNICDetails) HCL2Spec() map[string]hcldec.Spec {
|
||||
s := map[string]hcldec.Spec{
|
||||
"assign_public_ip": &hcldec.AttrSpec{Name: "assign_public_ip", Type: cty.Bool, Required: false},
|
||||
"defined_tags": &hcldec.AttrSpec{Name: "defined_tags", Type: cty.Map(cty.String), Required: false},
|
||||
"display_name": &hcldec.AttrSpec{Name: "display_name", Type: cty.String, Required: false},
|
||||
"tags": &hcldec.AttrSpec{Name: "tags", Type: cty.Map(cty.String), Required: false},
|
||||
"hostname_label": &hcldec.AttrSpec{Name: "hostname_label", Type: cty.String, Required: false},
|
||||
"nsg_ids": &hcldec.AttrSpec{Name: "nsg_ids", Type: cty.List(cty.String), Required: false},
|
||||
"private_ip": &hcldec.AttrSpec{Name: "private_ip", Type: cty.String, Required: false},
|
||||
"skip_source_dest_check": &hcldec.AttrSpec{Name: "skip_source_dest_check", Type: cty.Bool, Required: false},
|
||||
"subnet_id": &hcldec.AttrSpec{Name: "subnet_id", Type: cty.String, Required: false},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ func testConfig(accessConfFile *os.File) map[string]interface{} {
|
||||
|
||||
// Image
|
||||
"base_image_ocid": "ocd1...",
|
||||
"shape": "VM.Standard1.1",
|
||||
"image_name": "HelloWorld",
|
||||
|
||||
// Networking
|
||||
@@ -36,6 +35,16 @@ func testConfig(accessConfFile *os.File) map[string]interface{} {
|
||||
"defined_tags": map[string]map[string]interface{}{
|
||||
"namespace": {"key": "value"},
|
||||
},
|
||||
|
||||
// Instance Details
|
||||
"instance_name": "hello-world",
|
||||
"instance_tags": map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
"create_vnic_details": map[string]interface{}{
|
||||
"nsg_ids": []string{"ocd1..."},
|
||||
},
|
||||
"shape": "VM.Standard1.1",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ func (d *driverOCI) CreateInstance(ctx context.Context, publicKey string) (strin
|
||||
instanceDetails := core.LaunchInstanceDetails{
|
||||
AvailabilityDomain: &d.cfg.AvailabilityDomain,
|
||||
CompartmentId: &d.cfg.CompartmentID,
|
||||
DefinedTags: d.cfg.InstanceDefinedTags,
|
||||
FreeformTags: d.cfg.InstanceTags,
|
||||
ImageId: &d.cfg.BaseImageID,
|
||||
Shape: &d.cfg.Shape,
|
||||
SubnetId: &d.cfg.SubnetID,
|
||||
@@ -65,6 +67,21 @@ func (d *driverOCI) CreateInstance(ctx context.Context, publicKey string) (strin
|
||||
instanceDetails.DisplayName = &d.cfg.InstanceName
|
||||
}
|
||||
|
||||
// Pass VNIC details, if specified, to the instance
|
||||
CreateVnicDetails := core.CreateVnicDetails{
|
||||
AssignPublicIp: d.cfg.CreateVnicDetails.AssignPublicIp,
|
||||
DisplayName: d.cfg.CreateVnicDetails.DisplayName,
|
||||
HostnameLabel: d.cfg.CreateVnicDetails.HostnameLabel,
|
||||
NsgIds: d.cfg.CreateVnicDetails.NsgIds,
|
||||
PrivateIp: d.cfg.CreateVnicDetails.PrivateIp,
|
||||
SkipSourceDestCheck: d.cfg.CreateVnicDetails.SkipSourceDestCheck,
|
||||
SubnetId: d.cfg.CreateVnicDetails.SubnetId,
|
||||
DefinedTags: d.cfg.CreateVnicDetails.DefinedTags,
|
||||
FreeformTags: d.cfg.CreateVnicDetails.FreeformTags,
|
||||
}
|
||||
|
||||
instanceDetails.CreateVnicDetails = &CreateVnicDetails
|
||||
|
||||
instance, err := d.computeClient.LaunchInstance(context.TODO(), core.LaunchInstanceRequest{LaunchInstanceDetails: instanceDetails})
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -20,6 +20,7 @@ type FlatConfig struct {
|
||||
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"`
|
||||
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"`
|
||||
@@ -123,6 +124,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"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},
|
||||
|
||||
@@ -251,7 +251,7 @@ func (d *stepCreateServer) getImageAlias(imageAlias string, location string, ui
|
||||
if i != "" {
|
||||
alias = i
|
||||
}
|
||||
if alias != "" && strings.ToLower(alias) == strings.ToLower(imageAlias) {
|
||||
if alias != "" && strings.EqualFold(alias, imageAlias) {
|
||||
return alias
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package proxmox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
@@ -10,23 +11,33 @@ import (
|
||||
)
|
||||
|
||||
type proxmoxDriver struct {
|
||||
client commandTyper
|
||||
vmRef *proxmox.VmRef
|
||||
specialMap map[string]string
|
||||
runeMap map[rune]string
|
||||
interval time.Duration
|
||||
client commandTyper
|
||||
vmRef *proxmox.VmRef
|
||||
specialMap map[string]string
|
||||
runeMap map[rune]string
|
||||
interval time.Duration
|
||||
specialBuffer []string
|
||||
normalBuffer []string
|
||||
}
|
||||
|
||||
func NewProxmoxDriver(c commandTyper, vmRef *proxmox.VmRef, interval time.Duration) *proxmoxDriver {
|
||||
// Mappings for packer shorthand to qemu qkeycodes
|
||||
sMap := map[string]string{
|
||||
"spacebar": "spc",
|
||||
"bs": "backspace",
|
||||
"del": "delete",
|
||||
"return": "ret",
|
||||
"enter": "ret",
|
||||
"pageUp": "pgup",
|
||||
"pageDown": "pgdn",
|
||||
"spacebar": "spc",
|
||||
"bs": "backspace",
|
||||
"del": "delete",
|
||||
"return": "ret",
|
||||
"enter": "ret",
|
||||
"pageUp": "pgup",
|
||||
"pageDown": "pgdn",
|
||||
"leftshift": "shift",
|
||||
"rightshift": "shift",
|
||||
"leftalt": "alt",
|
||||
"rightalt": "alt_r",
|
||||
"leftctrl": "ctrl",
|
||||
"rightctrl": "ctrl_r",
|
||||
"leftsuper": "meta_l",
|
||||
"rightsuper": "meta_r",
|
||||
}
|
||||
// Mappings for runes that need to be translated to special qkeycodes
|
||||
// Taken from https://github.com/qemu/qemu/blob/master/pc-bios/keymaps/en-us
|
||||
@@ -78,18 +89,26 @@ func NewProxmoxDriver(c commandTyper, vmRef *proxmox.VmRef, interval time.Durati
|
||||
}
|
||||
|
||||
func (p *proxmoxDriver) SendKey(key rune, action bootcommand.KeyAction) error {
|
||||
if special, ok := p.runeMap[key]; ok {
|
||||
return p.send(special)
|
||||
switch action.String() {
|
||||
case "Press":
|
||||
if special, ok := p.runeMap[key]; ok {
|
||||
return p.send(special)
|
||||
}
|
||||
var keys string
|
||||
if unicode.IsUpper(key) {
|
||||
keys = fmt.Sprintf("shift-%c", unicode.ToLower(key))
|
||||
} else {
|
||||
keys = fmt.Sprintf("%c", key)
|
||||
}
|
||||
return p.send(keys)
|
||||
case "On":
|
||||
key := fmt.Sprintf("%c", key)
|
||||
p.normalBuffer = addKeyToBuffer(p.normalBuffer, key)
|
||||
case "Off":
|
||||
key := fmt.Sprintf("%c", key)
|
||||
p.normalBuffer = removeKeyFromBuffer(p.normalBuffer, key)
|
||||
}
|
||||
|
||||
var keys string
|
||||
if unicode.IsUpper(key) {
|
||||
keys = fmt.Sprintf("shift-%c", unicode.ToLower(key))
|
||||
} else {
|
||||
keys = fmt.Sprintf("%c", key)
|
||||
}
|
||||
|
||||
return p.send(keys)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxmoxDriver) SendSpecial(special string, action bootcommand.KeyAction) error {
|
||||
@@ -97,18 +116,48 @@ func (p *proxmoxDriver) SendSpecial(special string, action bootcommand.KeyAction
|
||||
if replacement, ok := p.specialMap[special]; ok {
|
||||
keys = replacement
|
||||
}
|
||||
|
||||
return p.send(keys)
|
||||
switch action.String() {
|
||||
case "Press":
|
||||
return p.send(keys)
|
||||
case "On":
|
||||
p.specialBuffer = addKeyToBuffer(p.specialBuffer, keys)
|
||||
case "Off":
|
||||
p.specialBuffer = removeKeyFromBuffer(p.specialBuffer, keys)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxmoxDriver) send(keys string) error {
|
||||
err := p.client.Sendkey(p.vmRef, keys)
|
||||
func (p *proxmoxDriver) send(key string) error {
|
||||
keys := append(p.specialBuffer, p.normalBuffer...)
|
||||
keys = append(keys, key)
|
||||
keyEventString := bufferToKeyEvent(keys)
|
||||
err := p.client.Sendkey(p.vmRef, keyEventString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(p.interval)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxmoxDriver) Flush() error { return nil }
|
||||
|
||||
func bufferToKeyEvent(keys []string) string {
|
||||
return strings.Join(keys, "-")
|
||||
}
|
||||
func addKeyToBuffer(buffer []string, key string) []string {
|
||||
for _, value := range buffer {
|
||||
if value == key {
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
return append(buffer, key)
|
||||
}
|
||||
func removeKeyFromBuffer(buffer []string, key string) []string {
|
||||
for index, value := range buffer {
|
||||
if value == key {
|
||||
buffer[index] = buffer[len(buffer)-1]
|
||||
return buffer[:len(buffer)-1]
|
||||
}
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
@@ -71,8 +71,21 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
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,
|
||||
@@ -96,7 +109,8 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
&stepConvertToTemplate{},
|
||||
&stepFinalizeTemplateConfig{},
|
||||
&stepSuccess{},
|
||||
}
|
||||
)
|
||||
|
||||
// Run the steps
|
||||
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
|
||||
b.runner.Run(ctx, state)
|
||||
@@ -138,16 +152,32 @@ func commHost(host string) func(state multistep.StateBag) (string, error) {
|
||||
// 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) {
|
||||
c := state.Get("proxmoxClient").(*proxmox.Client)
|
||||
client := state.Get("proxmoxClient").(*proxmox.Client)
|
||||
config := state.Get("config").(*Config)
|
||||
vmRef := state.Get("vmRef").(*proxmox.VmRef)
|
||||
|
||||
ifs, err := c.GetVmAgentNetworkInterfaces(vmRef)
|
||||
ifs, err := client.GetVmAgentNetworkInterfaces(vmRef)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// TODO: Do something smarter here? Allow specifying interface? Or address family?
|
||||
// For now, just go for first non-loopback
|
||||
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() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:generate mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig
|
||||
//go:generate mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig,storageConfig
|
||||
|
||||
package proxmox
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -64,6 +65,9 @@ type Config struct {
|
||||
|
||||
shouldUploadISO bool
|
||||
|
||||
AdditionalISOFiles []storageConfig `mapstructure:"additional_iso_files"`
|
||||
VMInterface string `mapstructure:"vm_interface"`
|
||||
|
||||
ctx interpolate.Context
|
||||
}
|
||||
|
||||
@@ -87,6 +91,15 @@ 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) {
|
||||
// Agent defaults to true
|
||||
@@ -183,6 +196,61 @@ 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"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by "mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig"; DO NOT EDIT.
|
||||
// Code generated by "mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig,storageConfig"; DO NOT EDIT.
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
@@ -9,100 +9,103 @@ import (
|
||||
// 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"`
|
||||
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"`
|
||||
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 *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"`
|
||||
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"`
|
||||
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"`
|
||||
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 *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"`
|
||||
VMInterface *string `mapstructure:"vm_interface" cty:"vm_interface" hcl:"vm_interface"`
|
||||
}
|
||||
|
||||
// FlatMapstructure returns a new FlatConfig.
|
||||
@@ -128,6 +131,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"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},
|
||||
@@ -211,6 +215,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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())},
|
||||
"vm_interface": &hcldec.AttrSpec{Name: "vm_interface", Type: cty.String, Required: false},
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -281,6 +287,45 @@ func (*FlatnicConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
return s
|
||||
}
|
||||
|
||||
// FlatstorageConfig is an auto-generated flat version of storageConfig.
|
||||
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||
type FlatstorageConfig struct {
|
||||
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"`
|
||||
Device *string `mapstructure:"device" cty:"device" hcl:"device"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// FlatMapstructure returns a new FlatstorageConfig.
|
||||
// FlatstorageConfig is an auto-generated flat version of storageConfig.
|
||||
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||
func (*storageConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||
return new(FlatstorageConfig)
|
||||
}
|
||||
|
||||
// HCL2Spec returns the hcl spec of a storageConfig.
|
||||
// This spec is used by HCL to read the fields of storageConfig.
|
||||
// The decoded values from this spec will then be applied to a FlatstorageConfig.
|
||||
func (*FlatstorageConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
s := map[string]hcldec.Spec{
|
||||
"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},
|
||||
"device": &hcldec.AttrSpec{Name: "device", 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": &hcldec.AttrSpec{Name: "unmount", Type: cty.Bool, Required: false},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// FlatvgaConfig is an auto-generated flat version of vgaConfig.
|
||||
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||
type FlatvgaConfig struct {
|
||||
|
||||
@@ -93,6 +93,29 @@ 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 {
|
||||
|
||||
@@ -91,6 +91,19 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
ui.Say("Starting VM")
|
||||
_, err = client.StartVm(vmRef)
|
||||
if err != nil {
|
||||
|
||||
@@ -53,14 +53,20 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
httpIP, err := hostIP()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Failed to determine host IP: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
var httpIP string
|
||||
var err error
|
||||
if c.HTTPAddress != "0.0.0.0" {
|
||||
httpIP = c.HTTPAddress
|
||||
} else {
|
||||
httpIP, err = hostIP(c.HTTPInterface)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Failed to determine host IP: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
state.Put("http_ip", httpIP)
|
||||
s.Ctx.Data = &bootCommandTemplateData{
|
||||
HTTPIP: httpIP,
|
||||
@@ -97,12 +103,25 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
|
||||
|
||||
func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {}
|
||||
|
||||
func hostIP() (string, error) {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
func hostIP(ifname string) (string, error) {
|
||||
var addrs []net.Addr
|
||||
var err error
|
||||
|
||||
if ifname != "" {
|
||||
iface, err := net.InterfaceByName(ifname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
addrs, err = iface.Addrs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
addrs, err = net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
@@ -110,6 +129,5 @@ func hostIP() (string, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("No host IP found")
|
||||
}
|
||||
|
||||
@@ -52,6 +52,34 @@ func TestTypeBootCommand(t *testing.T) {
|
||||
expectedKeysSent: "shift-hellospcshift-worldspc2dot0fooshift-1barshift-2baz",
|
||||
expectedAction: multistep.ActionContinue,
|
||||
},
|
||||
{
|
||||
name: "holding and releasing keys",
|
||||
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"<leftShiftOn>hello<rightAltOn>world<leftShiftOff><rightAltOff>"}}},
|
||||
expectCallSendkey: true,
|
||||
expectedKeysSent: "shift-hshift-eshift-lshift-lshift-oshift-alt_r-wshift-alt_r-oshift-alt_r-rshift-alt_r-lshift-alt_r-d",
|
||||
expectedAction: multistep.ActionContinue,
|
||||
},
|
||||
{
|
||||
name: "holding multiple alphabetical keys and shift",
|
||||
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"<cOn><leftShiftOn>n<leftShiftOff><cOff>"}}},
|
||||
expectCallSendkey: true,
|
||||
expectedKeysSent: "shift-c-n",
|
||||
expectedAction: multistep.ActionContinue,
|
||||
},
|
||||
{
|
||||
name: "noop keystrokes",
|
||||
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"<cOn><leftShiftOn><cOff><leftAltOn><leftShiftOff><leftAltOff>"}}},
|
||||
expectCallSendkey: true,
|
||||
expectedKeysSent: "",
|
||||
expectedAction: multistep.ActionContinue,
|
||||
},
|
||||
{
|
||||
name: "noop keystrokes mixed",
|
||||
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"<cOn><leftShiftOn><cOff>h<leftShiftOff>"}}},
|
||||
expectCallSendkey: true,
|
||||
expectedKeysSent: "shift-h",
|
||||
expectedAction: multistep.ActionContinue,
|
||||
},
|
||||
{
|
||||
name: "without boot command sendkey should not be called",
|
||||
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{}}},
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Telmate/proxmox-api-go/proxmox"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
// stepUploadAdditionalISOs uploads all additional ISO files that are mountet
|
||||
// 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)
|
||||
|
||||
for idx := range c.AdditionalISOFiles {
|
||||
if !c.AdditionalISOFiles[idx].shouldUploadISO {
|
||||
state.Put("additional_iso_files", c.AdditionalISOFiles)
|
||||
continue
|
||||
}
|
||||
|
||||
p := state.Get(c.AdditionalISOFiles[idx].downloadPathKey).(string)
|
||||
if p == "" {
|
||||
err := fmt.Errorf("Path to downloaded ISO was empty")
|
||||
state.Put("erroe", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
isoPath, _ := filepath.EvalSymlinks(p)
|
||||
r, err := os.Open(isoPath)
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
filename := filepath.Base(c.AdditionalISOFiles[idx].ISOConfig.ISOUrls[0])
|
||||
err = client.Upload(c.Node, c.AdditionalISOFiles[idx].ISOStoragePool, "iso", filename, r)
|
||||
if err != nil {
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
isoStoragePath := fmt.Sprintf("%s:iso/%s", c.AdditionalISOFiles[idx].ISOStoragePool, filename)
|
||||
c.AdditionalISOFiles[idx].ISOFile = isoStoragePath
|
||||
state.Put("additional_iso_files", c.AdditionalISOFiles)
|
||||
}
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepUploadAdditionalISOs) Cleanup(state multistep.StateBag) {
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package proxmox
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -15,10 +14,6 @@ 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 {
|
||||
@@ -58,7 +53,6 @@ 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
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,10 @@ type Config struct {
|
||||
// for disk_size, Packer uses a default of `40960M` (40 GB). If a disk_size
|
||||
// number is provided with no units, Packer will default to Megabytes.
|
||||
DiskSize string `mapstructure:"disk_size" required:"false"`
|
||||
// Packer resizes the QCOW2 image using
|
||||
// qemu-img resize. Set this option to true to disable resizing.
|
||||
// Defaults to false.
|
||||
SkipResizeDisk bool `mapstructure:"skip_resize_disk" required:"false"`
|
||||
// The cache mode to use for disk. Allowed values include any of
|
||||
// `writethrough`, `writeback`, `none`, `unsafe` or `directsync`. By
|
||||
// default, this is set to `writeback`.
|
||||
@@ -509,6 +513,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
|
||||
errs, errors.New("disk_additional_size can only be used when disk_image is false"))
|
||||
}
|
||||
|
||||
if b.config.SkipResizeDisk && !(b.config.DiskImage) {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("skip_resize_disk can only be used when disk_image is true"))
|
||||
}
|
||||
|
||||
if _, ok := accels[b.config.Accelerator]; !ok {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', 'whpx', or 'none' are allowed"))
|
||||
|
||||
@@ -20,6 +20,7 @@ type FlatConfig struct {
|
||||
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"`
|
||||
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"`
|
||||
@@ -93,6 +94,7 @@ type FlatConfig struct {
|
||||
CpuCount *int `mapstructure:"cpus" required:"false" cty:"cpus" hcl:"cpus"`
|
||||
DiskInterface *string `mapstructure:"disk_interface" required:"false" cty:"disk_interface" hcl:"disk_interface"`
|
||||
DiskSize *string `mapstructure:"disk_size" required:"false" cty:"disk_size" hcl:"disk_size"`
|
||||
SkipResizeDisk *bool `mapstructure:"skip_resize_disk" required:"false" cty:"skip_resize_disk" hcl:"skip_resize_disk"`
|
||||
DiskCache *string `mapstructure:"disk_cache" required:"false" cty:"disk_cache" hcl:"disk_cache"`
|
||||
DiskDiscard *string `mapstructure:"disk_discard" required:"false" cty:"disk_discard" hcl:"disk_discard"`
|
||||
DetectZeroes *string `mapstructure:"disk_detect_zeroes" required:"false" cty:"disk_detect_zeroes" hcl:"disk_detect_zeroes"`
|
||||
@@ -145,6 +147,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"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},
|
||||
@@ -218,6 +221,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"cpus": &hcldec.AttrSpec{Name: "cpus", Type: cty.Number, Required: false},
|
||||
"disk_interface": &hcldec.AttrSpec{Name: "disk_interface", Type: cty.String, Required: false},
|
||||
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.String, Required: false},
|
||||
"skip_resize_disk": &hcldec.AttrSpec{Name: "skip_resize_disk", Type: cty.Bool, Required: false},
|
||||
"disk_cache": &hcldec.AttrSpec{Name: "disk_cache", Type: cty.String, Required: false},
|
||||
"disk_discard": &hcldec.AttrSpec{Name: "disk_discard", Type: cty.String, Required: false},
|
||||
"disk_detect_zeroes": &hcldec.AttrSpec{Name: "disk_detect_zeroes", Type: cty.String, Required: false},
|
||||
|
||||
@@ -302,6 +302,21 @@ func TestBuilderPrepare_UseBackingFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SkipResizeDisk(t *testing.T) {
|
||||
config := testConfig()
|
||||
config["skip_resize_disk"] = true
|
||||
config["disk_image"] = false
|
||||
|
||||
b := Builder{}
|
||||
_, warns, err := b.Prepare(config)
|
||||
if len(warns) > 0 {
|
||||
t.Errorf("unexpected warns when calling prepare with skip_resize_disk set to true: %#v", warns)
|
||||
}
|
||||
if err == nil {
|
||||
t.Errorf("setting skip_resize_disk to true when disk_image is false should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package qemu
|
||||
|
||||
import "sync"
|
||||
|
||||
type DriverMock struct {
|
||||
sync.Mutex
|
||||
|
||||
StopCalled bool
|
||||
StopErr error
|
||||
|
||||
QemuCalls [][]string
|
||||
QemuErrs []error
|
||||
|
||||
WaitForShutdownCalled bool
|
||||
WaitForShutdownState bool
|
||||
|
||||
QemuImgCalls [][]string
|
||||
QemuImgErrs []error
|
||||
|
||||
VerifyCalled bool
|
||||
VerifyErr error
|
||||
|
||||
VersionCalled bool
|
||||
VersionResult string
|
||||
VersionErr error
|
||||
}
|
||||
|
||||
func (d *DriverMock) Stop() error {
|
||||
d.StopCalled = true
|
||||
return d.StopErr
|
||||
}
|
||||
|
||||
func (d *DriverMock) Qemu(args ...string) error {
|
||||
d.QemuCalls = append(d.QemuCalls, args)
|
||||
|
||||
if len(d.QemuErrs) >= len(d.QemuCalls) {
|
||||
return d.QemuErrs[len(d.QemuCalls)-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DriverMock) WaitForShutdown(cancelCh <-chan struct{}) bool {
|
||||
d.WaitForShutdownCalled = true
|
||||
return d.WaitForShutdownState
|
||||
}
|
||||
|
||||
func (d *DriverMock) QemuImg(args ...string) error {
|
||||
d.QemuImgCalls = append(d.QemuImgCalls, args)
|
||||
|
||||
if len(d.QemuImgErrs) >= len(d.QemuImgCalls) {
|
||||
return d.QemuImgErrs[len(d.QemuImgCalls)-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DriverMock) Verify() error {
|
||||
d.VerifyCalled = true
|
||||
return d.VerifyErr
|
||||
}
|
||||
|
||||
func (d *DriverMock) Version() (string, error) {
|
||||
d.VersionCalled = true
|
||||
return d.VersionResult, d.VersionErr
|
||||
}
|
||||
@@ -25,7 +25,8 @@ func (s *stepResizeDisk) Run(ctx context.Context, state multistep.StateBag) mult
|
||||
path,
|
||||
config.DiskSize,
|
||||
}
|
||||
if config.DiskImage == false {
|
||||
|
||||
if config.DiskImage == false || config.SkipResizeDisk == true {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package qemu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
)
|
||||
|
||||
func TestStepResizeDisk_Run(t *testing.T) {
|
||||
state := testState(t)
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
|
||||
config := &Config{
|
||||
DiskImage: true,
|
||||
SkipResizeDisk: false,
|
||||
DiskSize: "4096M",
|
||||
Format: "qcow2",
|
||||
OutputDir: "/test/",
|
||||
VMName: "test",
|
||||
}
|
||||
state.Put("config", config)
|
||||
step := new(stepResizeDisk)
|
||||
|
||||
// Test the run
|
||||
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
if _, ok := state.GetOk("error"); ok {
|
||||
t.Fatal("should NOT have error")
|
||||
}
|
||||
if len(driver.QemuImgCalls) == 0 {
|
||||
t.Fatal("should qemu-img called")
|
||||
}
|
||||
if len(driver.QemuImgCalls[0]) != 5 {
|
||||
t.Fatal("should 5 qemu-img parameters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepResizeDisk_SkipIso(t *testing.T) {
|
||||
state := testState(t)
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
config := &Config{
|
||||
DiskImage: false,
|
||||
SkipResizeDisk: false,
|
||||
}
|
||||
state.Put("config", config)
|
||||
step := new(stepResizeDisk)
|
||||
|
||||
// Test the run
|
||||
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
if _, ok := state.GetOk("error"); ok {
|
||||
t.Fatal("should NOT have error")
|
||||
}
|
||||
if len(driver.QemuImgCalls) > 0 {
|
||||
t.Fatal("should NOT qemu-img called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepResizeDisk_SkipOption(t *testing.T) {
|
||||
state := testState(t)
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
config := &Config{
|
||||
DiskImage: false,
|
||||
SkipResizeDisk: true,
|
||||
}
|
||||
state.Put("config", config)
|
||||
step := new(stepResizeDisk)
|
||||
|
||||
// Test the run
|
||||
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
if _, ok := state.GetOk("error"); ok {
|
||||
t.Fatal("should NOT have error")
|
||||
}
|
||||
if len(driver.QemuImgCalls) > 0 {
|
||||
t.Fatal("should NOT qemu-img called")
|
||||
}
|
||||
}
|
||||
@@ -73,12 +73,11 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
|
||||
var deviceArgs []string
|
||||
var driveArgs []string
|
||||
var commHostPort int
|
||||
var vnc string
|
||||
|
||||
if !config.VNCUsePassword {
|
||||
vnc = fmt.Sprintf("%s:%d", vncIP, vncPort-5900)
|
||||
} else {
|
||||
vnc = fmt.Sprintf("%s:%d,password", vncIP, vncPort-5900)
|
||||
vncPort = vncPort - config.VNCPortMin
|
||||
vnc := fmt.Sprintf("%s:%d", vncIP, vncPort)
|
||||
if config.VNCUsePassword {
|
||||
vnc = fmt.Sprintf("%s:%d,password", vncIP, vncPort)
|
||||
}
|
||||
|
||||
if config.QMPEnable {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package qemu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
func testState(t *testing.T) multistep.StateBag {
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("driver", new(DriverMock))
|
||||
state.Put("ui", &packer.BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
})
|
||||
return state
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type Artifact struct {
|
||||
@@ -20,11 +21,11 @@ type Artifact struct {
|
||||
// The ID of the snapshot
|
||||
snapshotID string
|
||||
|
||||
// The name of the region
|
||||
regionName string
|
||||
// The name of the zone
|
||||
zoneName string
|
||||
|
||||
// The client for making API calls
|
||||
client *api.ScalewayAPI
|
||||
client *scw.Client
|
||||
|
||||
// StateData should store data such as GeneratedData
|
||||
// to be shared with post-processors
|
||||
@@ -41,12 +42,12 @@ func (*Artifact) Files() []string {
|
||||
}
|
||||
|
||||
func (a *Artifact) Id() string {
|
||||
return fmt.Sprintf("%s:%s", a.regionName, a.imageID)
|
||||
return fmt.Sprintf("%s:%s", a.zoneName, a.imageID)
|
||||
}
|
||||
|
||||
func (a *Artifact) String() string {
|
||||
return fmt.Sprintf("An image was created: '%v' (ID: %v) in region '%v' based on snapshot '%v' (ID: %v)",
|
||||
a.imageName, a.imageID, a.regionName, a.snapshotName, a.snapshotID)
|
||||
return fmt.Sprintf("An image was created: '%v' (ID: %v) in zone '%v' based on snapshot '%v' (ID: %v)",
|
||||
a.imageName, a.imageID, a.zoneName, a.snapshotName, a.snapshotID)
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
@@ -55,11 +56,19 @@ func (a *Artifact) State(name string) interface{} {
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
log.Printf("Destroying image: %s (%s)", a.imageID, a.imageName)
|
||||
if err := a.client.DeleteImage(a.imageID); err != nil {
|
||||
instanceAPI := instance.NewAPI(a.client)
|
||||
|
||||
err := instanceAPI.DeleteImage(&instance.DeleteImageRequest{
|
||||
ImageID: a.imageID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Destroying snapshot: %s (%s)", a.snapshotID, a.snapshotName)
|
||||
if err := a.client.DeleteSnapshot(a.snapshotID); err != nil {
|
||||
err = instanceAPI.DeleteSnapshot(&instance.DeleteSnapshotRequest{
|
||||
SnapshotID: a.snapshotID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestArtifactId(t *testing.T) {
|
||||
func TestArtifactString(t *testing.T) {
|
||||
generatedData := make(map[string]interface{})
|
||||
a := &Artifact{"packer-foobar-image", "cc586e45-5156-4f71-b223-cf406b10dd1d", "packer-foobar-snapshot", "cc586e45-5156-4f71-b223-cf406b10dd1c", "ams1", nil, generatedData}
|
||||
expected := "An image was created: 'packer-foobar-image' (ID: cc586e45-5156-4f71-b223-cf406b10dd1d) in region 'ams1' based on snapshot 'packer-foobar-snapshot' (ID: cc586e45-5156-4f71-b223-cf406b10dd1c)"
|
||||
expected := "An image was created: 'packer-foobar-image' (ID: cc586e45-5156-4f71-b223-cf406b10dd1d) in zone 'ams1' based on snapshot 'packer-foobar-snapshot' (ID: cc586e45-5156-4f71-b223-cf406b10dd1c)"
|
||||
|
||||
if a.String() != expected {
|
||||
t.Fatalf("artifact string should match: %v", expected)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/hashicorp/packer/helper/communicator"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
// The unique id for the builder
|
||||
@@ -32,12 +32,27 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
|
||||
return nil, warnings, errs
|
||||
}
|
||||
|
||||
return nil, nil, nil
|
||||
return nil, warnings, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
|
||||
client, err := api.NewScalewayAPI(b.config.Organization, b.config.Token, b.config.UserAgent, b.config.Region)
|
||||
scwZone, err := scw.ParseZone(b.config.Zone)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientOpts := []scw.ClientOption{
|
||||
scw.WithDefaultProjectID(b.config.ProjectID),
|
||||
scw.WithAuth(b.config.AccessKey, b.config.SecretKey),
|
||||
scw.WithDefaultZone(scwZone),
|
||||
}
|
||||
|
||||
if b.config.APIURL != "" {
|
||||
clientOpts = append(clientOpts, scw.WithAPIURL(b.config.APIURL))
|
||||
}
|
||||
|
||||
client, err := scw.NewClient(clientOpts...)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return nil, err
|
||||
@@ -96,7 +111,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
imageID: state.Get("image_id").(string),
|
||||
snapshotName: state.Get("snapshot_name").(string),
|
||||
snapshotID: state.Get("snapshot_id").(string),
|
||||
regionName: state.Get("region").(string),
|
||||
zoneName: b.config.Zone,
|
||||
client: client,
|
||||
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"organization_id": "foo",
|
||||
"api_token": "bar",
|
||||
"region": "ams1",
|
||||
"project_id": "00000000-1111-2222-3333-444444444444",
|
||||
"access_key": "SCWABCXXXXXXXXXXXXXX",
|
||||
"secret_key": "00000000-1111-2222-3333-444444444444",
|
||||
"zone": "fr-par-1",
|
||||
"commercial_type": "START1-S",
|
||||
"ssh_username": "root",
|
||||
"image": "image-uuid",
|
||||
@@ -32,10 +33,7 @@ func TestBuilder_Prepare_BadType(t *testing.T) {
|
||||
"api_token": []string{},
|
||||
}
|
||||
|
||||
_, warnings, err := b.Prepare(c)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
_, _, err := b.Prepare(c)
|
||||
if err == nil {
|
||||
t.Fatalf("prepare should fail")
|
||||
}
|
||||
@@ -68,11 +66,11 @@ func TestBuilderPrepare_InvalidKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_Region(t *testing.T) {
|
||||
func TestBuilderPrepare_Zone(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "region")
|
||||
delete(config, "zone")
|
||||
_, warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
@@ -81,9 +79,9 @@ func TestBuilderPrepare_Region(t *testing.T) {
|
||||
t.Fatalf("should error")
|
||||
}
|
||||
|
||||
expected := "ams1"
|
||||
expected := "fr-par-1"
|
||||
|
||||
config["region"] = expected
|
||||
config["zone"] = expected
|
||||
b = Builder{}
|
||||
_, warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
@@ -93,8 +91,8 @@ func TestBuilderPrepare_Region(t *testing.T) {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.Region != expected {
|
||||
t.Errorf("found %s, expected %s", b.config.Region, expected)
|
||||
if b.config.Zone != expected {
|
||||
t.Errorf("found %s, expected %s", b.config.Zone, expected)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+90
-25
@@ -17,27 +17,29 @@ import (
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
// The token to use to authenticate with your account.
|
||||
// It can also be specified via environment variable SCALEWAY_API_TOKEN. You
|
||||
// can see and generate tokens in the "Credentials"
|
||||
// section of the control panel.
|
||||
Token string `mapstructure:"api_token" required:"true"`
|
||||
// The organization id to use to identify your
|
||||
// organization. It can also be specified via environment variable
|
||||
// SCALEWAY_ORGANIZATION. Your organization id is available in the
|
||||
// "Account" section of the
|
||||
// control panel.
|
||||
// Previously named: api_access_key with environment variable: SCALEWAY_API_ACCESS_KEY
|
||||
Organization string `mapstructure:"organization_id" required:"true"`
|
||||
// The name of the region to launch the server in (par1
|
||||
// or ams1). Consequently, this is the region where the snapshot will be
|
||||
// available.
|
||||
Region string `mapstructure:"region" required:"true"`
|
||||
// The AccessKey corresponding to the secret key.
|
||||
// It can also be specified via the environment variable SCW_ACCESS_KEY.
|
||||
AccessKey string `mapstructure:"access_key" required:"true"`
|
||||
// The SecretKey to authenticate against the Scaleway API.
|
||||
// It can also be specified via the environment variable SCW_SECRET_KEY.
|
||||
SecretKey string `mapstructure:"secret_key" required:"true"`
|
||||
// The Project ID in which the instances, volumes and snapshots will be created.
|
||||
// It can also be specified via the environment variable SCW_DEFAULT_PROJECT_ID.
|
||||
ProjectID string `mapstructure:"project_id" required:"true"`
|
||||
// The Zone in which the instances, volumes and snapshots will be created.
|
||||
// It can also be specified via the environment variable SCW_DEFAULT_ZONE
|
||||
Zone string `mapstructure:"zone" required:"true"`
|
||||
// The Scaleway API URL to use
|
||||
// It can also be specified via the environment variable SCW_API_URL
|
||||
APIURL string `mapstructure:"api_url"`
|
||||
|
||||
// The UUID of the base image to use. This is the image
|
||||
// that will be used to launch a new server and provision it. See
|
||||
// the images list
|
||||
@@ -69,6 +71,28 @@ type Config struct {
|
||||
|
||||
UserAgent string `mapstructure-to-hcl2:",skip"`
|
||||
ctx interpolate.Context
|
||||
|
||||
// Deprecated configs
|
||||
|
||||
// The token to use to authenticate with your account.
|
||||
// It can also be specified via environment variable SCALEWAY_API_TOKEN. You
|
||||
// can see and generate tokens in the "Credentials"
|
||||
// section of the control panel.
|
||||
// Deprecated, use SecretKey instead
|
||||
Token string `mapstructure:"api_token" required:"false"`
|
||||
// The organization id to use to identify your
|
||||
// organization. It can also be specified via environment variable
|
||||
// SCALEWAY_ORGANIZATION. Your organization id is available in the
|
||||
// "Account" section of the
|
||||
// control panel.
|
||||
// Previously named: api_access_key with environment variable: SCALEWAY_API_ACCESS_KEY
|
||||
// Deprecated, use ProjectID instead
|
||||
Organization string `mapstructure:"organization_id" required:"false"`
|
||||
// The name of the region to launch the server in (par1
|
||||
// or ams1). Consequently, this is the region where the snapshot will be
|
||||
// available.
|
||||
// Deprecated, use Zone instead
|
||||
Region string `mapstructure:"region" required:"false"`
|
||||
}
|
||||
|
||||
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
@@ -88,8 +112,11 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var warnings []string
|
||||
|
||||
c.UserAgent = useragent.String()
|
||||
|
||||
// Deprecated variables
|
||||
if c.Organization == "" {
|
||||
if os.Getenv("SCALEWAY_ORGANIZATION") != "" {
|
||||
c.Organization = os.Getenv("SCALEWAY_ORGANIZATION")
|
||||
@@ -98,10 +125,43 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
c.Organization = os.Getenv("SCALEWAY_API_ACCESS_KEY")
|
||||
}
|
||||
}
|
||||
if c.Organization != "" {
|
||||
warnings = append(warnings, "organization_id is deprecated in favor of project_id")
|
||||
c.ProjectID = c.Organization
|
||||
}
|
||||
|
||||
if c.Token == "" {
|
||||
c.Token = os.Getenv("SCALEWAY_API_TOKEN")
|
||||
}
|
||||
if c.Token != "" {
|
||||
warnings = append(warnings, "token is deprecated in favor of secret_key")
|
||||
c.SecretKey = c.Token
|
||||
}
|
||||
|
||||
if c.Region != "" {
|
||||
warnings = append(warnings, "region is deprecated in favor of zone")
|
||||
c.Zone = c.Region
|
||||
}
|
||||
|
||||
if c.AccessKey == "" {
|
||||
c.AccessKey = os.Getenv(scw.ScwAccessKeyEnv)
|
||||
}
|
||||
|
||||
if c.SecretKey == "" {
|
||||
c.SecretKey = os.Getenv(scw.ScwSecretKeyEnv)
|
||||
}
|
||||
|
||||
if c.ProjectID == "" {
|
||||
c.ProjectID = os.Getenv(scw.ScwDefaultProjectIDEnv)
|
||||
}
|
||||
|
||||
if c.Zone == "" {
|
||||
c.Zone = os.Getenv(scw.ScwDefaultZoneEnv)
|
||||
}
|
||||
|
||||
if c.APIURL == "" {
|
||||
c.APIURL = os.Getenv(scw.ScwAPIURLEnv)
|
||||
}
|
||||
|
||||
if c.SnapshotName == "" {
|
||||
def, err := interpolate.Render("snapshot-packer-{{timestamp}}", nil)
|
||||
@@ -127,26 +187,31 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
}
|
||||
|
||||
if c.BootType == "" {
|
||||
c.BootType = "bootscript"
|
||||
c.BootType = instance.BootTypeLocal.String()
|
||||
}
|
||||
|
||||
var errs *packer.MultiError
|
||||
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
|
||||
errs = packer.MultiErrorAppend(errs, es...)
|
||||
}
|
||||
if c.Organization == "" {
|
||||
if c.ProjectID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("Scaleway Organization ID must be specified"))
|
||||
errs, errors.New("Scaleway Project ID must be specified"))
|
||||
}
|
||||
|
||||
if c.Token == "" {
|
||||
if c.SecretKey == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("Scaleway Token must be specified"))
|
||||
errs, errors.New("Scaleway Secret Key must be specified"))
|
||||
}
|
||||
|
||||
if c.Region == "" {
|
||||
if c.AccessKey == "" {
|
||||
warnings = append(warnings, "access_key will be required in future versions")
|
||||
c.AccessKey = "SCWXXXXXXXXXXXXXXXXX"
|
||||
}
|
||||
|
||||
if c.Zone == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("region is required"))
|
||||
errs, errors.New("Scaleway Zone is required"))
|
||||
}
|
||||
|
||||
if c.CommercialType == "" {
|
||||
@@ -160,9 +225,9 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return nil, errs
|
||||
return warnings, errs
|
||||
}
|
||||
|
||||
packer.LogSecretFilter.Set(c.Token)
|
||||
return nil, nil
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
@@ -63,9 +63,11 @@ type FlatConfig struct {
|
||||
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"`
|
||||
Token *string `mapstructure:"api_token" required:"true" cty:"api_token" hcl:"api_token"`
|
||||
Organization *string `mapstructure:"organization_id" required:"true" cty:"organization_id" hcl:"organization_id"`
|
||||
Region *string `mapstructure:"region" required:"true" cty:"region" hcl:"region"`
|
||||
AccessKey *string `mapstructure:"access_key" required:"true" cty:"access_key" hcl:"access_key"`
|
||||
SecretKey *string `mapstructure:"secret_key" required:"true" cty:"secret_key" hcl:"secret_key"`
|
||||
ProjectID *string `mapstructure:"project_id" required:"true" cty:"project_id" hcl:"project_id"`
|
||||
Zone *string `mapstructure:"zone" required:"true" cty:"zone" hcl:"zone"`
|
||||
APIURL *string `mapstructure:"api_url" cty:"api_url" hcl:"api_url"`
|
||||
Image *string `mapstructure:"image" required:"true" cty:"image" hcl:"image"`
|
||||
CommercialType *string `mapstructure:"commercial_type" required:"true" cty:"commercial_type" hcl:"commercial_type"`
|
||||
SnapshotName *string `mapstructure:"snapshot_name" required:"false" cty:"snapshot_name" hcl:"snapshot_name"`
|
||||
@@ -74,6 +76,9 @@ type FlatConfig struct {
|
||||
Bootscript *string `mapstructure:"bootscript" required:"false" cty:"bootscript" hcl:"bootscript"`
|
||||
BootType *string `mapstructure:"boottype" required:"false" cty:"boottype" hcl:"boottype"`
|
||||
RemoveVolume *bool `mapstructure:"remove_volume" cty:"remove_volume" hcl:"remove_volume"`
|
||||
Token *string `mapstructure:"api_token" required:"false" cty:"api_token" hcl:"api_token"`
|
||||
Organization *string `mapstructure:"organization_id" required:"false" cty:"organization_id" hcl:"organization_id"`
|
||||
Region *string `mapstructure:"region" required:"false" cty:"region" hcl:"region"`
|
||||
}
|
||||
|
||||
// FlatMapstructure returns a new FlatConfig.
|
||||
@@ -142,9 +147,11 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"api_token": &hcldec.AttrSpec{Name: "api_token", Type: cty.String, Required: false},
|
||||
"organization_id": &hcldec.AttrSpec{Name: "organization_id", Type: cty.String, Required: false},
|
||||
"region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false},
|
||||
"access_key": &hcldec.AttrSpec{Name: "access_key", Type: cty.String, Required: false},
|
||||
"secret_key": &hcldec.AttrSpec{Name: "secret_key", Type: cty.String, Required: false},
|
||||
"project_id": &hcldec.AttrSpec{Name: "project_id", Type: cty.String, Required: false},
|
||||
"zone": &hcldec.AttrSpec{Name: "zone", Type: cty.String, Required: false},
|
||||
"api_url": &hcldec.AttrSpec{Name: "api_url", Type: cty.String, Required: false},
|
||||
"image": &hcldec.AttrSpec{Name: "image", Type: cty.String, Required: false},
|
||||
"commercial_type": &hcldec.AttrSpec{Name: "commercial_type", Type: cty.String, Required: false},
|
||||
"snapshot_name": &hcldec.AttrSpec{Name: "snapshot_name", Type: cty.String, Required: false},
|
||||
@@ -153,6 +160,9 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"bootscript": &hcldec.AttrSpec{Name: "bootscript", Type: cty.String, Required: false},
|
||||
"boottype": &hcldec.AttrSpec{Name: "boottype", Type: cty.String, Required: false},
|
||||
"remove_volume": &hcldec.AttrSpec{Name: "remove_volume", Type: cty.Bool, Required: false},
|
||||
"api_token": &hcldec.AttrSpec{Name: "api_token", Type: cty.String, Required: false},
|
||||
"organization_id": &hcldec.AttrSpec{Name: "organization_id", Type: cty.String, Required: false},
|
||||
"region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ import (
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type stepImage struct{}
|
||||
|
||||
func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(*Config)
|
||||
snapshotID := state.Get("snapshot_id").(string)
|
||||
@@ -21,7 +22,9 @@ func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep
|
||||
|
||||
ui.Say(fmt.Sprintf("Creating image: %v", c.ImageName))
|
||||
|
||||
image, err := client.GetImage(c.Image)
|
||||
imageResp, err := instanceAPI.GetImage(&instance.GetImageRequest{
|
||||
ImageID: c.Image,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error getting initial image info: %s", err)
|
||||
state.Put("error", err)
|
||||
@@ -29,11 +32,16 @@ func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if image.DefaultBootscript != nil {
|
||||
bootscriptID = image.DefaultBootscript.Identifier
|
||||
if imageResp.Image.DefaultBootscript != nil {
|
||||
bootscriptID = imageResp.Image.DefaultBootscript.ID
|
||||
}
|
||||
|
||||
imageID, err := client.PostImage(snapshotID, c.ImageName, bootscriptID, image.Arch)
|
||||
createImageResp, err := instanceAPI.CreateImage(&instance.CreateImageRequest{
|
||||
Arch: imageResp.Image.Arch,
|
||||
DefaultBootscript: bootscriptID,
|
||||
Name: c.ImageName,
|
||||
RootVolume: snapshotID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating image: %s", err)
|
||||
state.Put("error", err)
|
||||
@@ -41,10 +49,11 @@ func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Image ID: %s", imageID)
|
||||
state.Put("image_id", imageID)
|
||||
log.Printf("Image ID: %s", createImageResp.Image.ID)
|
||||
state.Put("image_id", createImageResp.Image.ID)
|
||||
state.Put("image_name", c.ImageName)
|
||||
state.Put("region", c.Region)
|
||||
state.Put("region", c.Zone) // Deprecated
|
||||
state.Put("zone", c.Zone)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type stepCreateServer struct {
|
||||
@@ -15,7 +16,7 @@ type stepCreateServer struct {
|
||||
}
|
||||
|
||||
func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(*Config)
|
||||
tags := []string{}
|
||||
@@ -31,16 +32,16 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
|
||||
tags = []string{fmt.Sprintf("AUTHORIZED_KEY=%s", strings.Replace(strings.TrimSpace(string(c.Comm.SSHPublicKey)), " ", "_", -1))}
|
||||
}
|
||||
|
||||
server, err := client.PostServer(api.ScalewayServerDefinition{
|
||||
Name: c.ServerName,
|
||||
Image: &c.Image,
|
||||
Organization: c.Organization,
|
||||
CommercialType: c.CommercialType,
|
||||
Tags: tags,
|
||||
Bootscript: bootscript,
|
||||
BootType: c.BootType,
|
||||
})
|
||||
bootType := instance.BootType(c.BootType)
|
||||
|
||||
createServerResp, err := instanceAPI.CreateServer(&instance.CreateServerRequest{
|
||||
BootType: &bootType,
|
||||
Bootscript: bootscript,
|
||||
CommercialType: c.CommercialType,
|
||||
Name: c.ServerName,
|
||||
Image: c.Image,
|
||||
Tags: tags,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating server: %s", err)
|
||||
state.Put("error", err)
|
||||
@@ -48,8 +49,10 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
err = client.PostServerAction(server, "poweron")
|
||||
|
||||
_, err = instanceAPI.ServerAction(&instance.ServerActionRequest{
|
||||
Action: instance.ServerActionPoweron,
|
||||
ServerID: createServerResp.Server.ID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error starting server: %s", err)
|
||||
state.Put("error", err)
|
||||
@@ -57,9 +60,9 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.serverID = server
|
||||
s.serverID = createServerResp.Server.ID
|
||||
|
||||
state.Put("server_id", server)
|
||||
state.Put("server_id", createServerResp.Server.ID)
|
||||
// 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", s.serverID)
|
||||
@@ -72,16 +75,22 @@ func (s *stepCreateServer) Cleanup(state multistep.StateBag) {
|
||||
return
|
||||
}
|
||||
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Destroying server...")
|
||||
|
||||
err := client.DeleteServerForce(s.serverID)
|
||||
|
||||
err := instanceAPI.DeleteServer(&instance.DeleteServerRequest{
|
||||
ServerID: s.serverID,
|
||||
})
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error destroying server. Please destroy it manually: %s", err))
|
||||
_, err = instanceAPI.ServerAction(&instance.ServerActionRequest{
|
||||
Action: instance.ServerActionTerminate,
|
||||
ServerID: s.serverID,
|
||||
})
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error destroying server. Please destroy it manually: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type stepRemoveVolume struct{}
|
||||
@@ -24,7 +24,7 @@ func (s *stepRemoveVolume) Cleanup(state multistep.StateBag) {
|
||||
return
|
||||
}
|
||||
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(*Config)
|
||||
volumeID := state.Get("root_volume_id").(string)
|
||||
@@ -35,7 +35,9 @@ func (s *stepRemoveVolume) Cleanup(state multistep.StateBag) {
|
||||
|
||||
ui.Say("Removing Volume ...")
|
||||
|
||||
err := client.DeleteVolume(volumeID)
|
||||
err := instanceAPI.DeleteVolume(&instance.DeleteVolumeRequest{
|
||||
VolumeID: volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error removing volume: %s", err)
|
||||
state.Put("error", err)
|
||||
|
||||
@@ -6,19 +6,22 @@ import (
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type stepServerInfo struct{}
|
||||
|
||||
func (s *stepServerInfo) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
serverID := state.Get("server_id").(string)
|
||||
|
||||
ui.Say("Waiting for server to become active...")
|
||||
|
||||
_, err := api.WaitForServerState(client, serverID, "running")
|
||||
instanceResp, err := instanceAPI.WaitForServer(&instance.WaitForServerRequest{
|
||||
ServerID: serverID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for server to become booted: %s", err)
|
||||
state.Put("error", err)
|
||||
@@ -26,16 +29,22 @@ func (s *stepServerInfo) Run(ctx context.Context, state multistep.StateBag) mult
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
server, err := client.GetServer(serverID)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error retrieving server: %s", err)
|
||||
if instanceResp.State != instance.ServerStateRunning {
|
||||
err := fmt.Errorf("Server is in state %s", instanceResp.State.String())
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("server_ip", server.PublicAddress.IP)
|
||||
state.Put("root_volume_id", server.Volumes["0"].Identifier)
|
||||
if instanceResp.PublicIP == nil {
|
||||
err := fmt.Errorf("Server does not have a public IP")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("server_ip", instanceResp.PublicIP.Address.String())
|
||||
state.Put("root_volume_id", instanceResp.Volumes["0"].ID)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
@@ -6,20 +6,23 @@ import (
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type stepShutdown struct{}
|
||||
|
||||
func (s *stepShutdown) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
serverID := state.Get("server_id").(string)
|
||||
|
||||
ui.Say("Shutting down server...")
|
||||
|
||||
err := client.PostServerAction(serverID, "poweroff")
|
||||
|
||||
_, err := instanceAPI.ServerAction(&instance.ServerActionRequest{
|
||||
Action: instance.ServerActionPoweroff,
|
||||
ServerID: serverID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error stopping server: %s", err)
|
||||
state.Put("error", err)
|
||||
@@ -27,8 +30,9 @@ func (s *stepShutdown) Run(ctx context.Context, state multistep.StateBag) multis
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
_, err = api.WaitForServerState(client, serverID, "stopped")
|
||||
|
||||
instanceResp, err := instanceAPI.WaitForServer(&instance.WaitForServerRequest{
|
||||
ServerID: serverID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error shutting down server: %s", err)
|
||||
state.Put("error", err)
|
||||
@@ -36,6 +40,13 @@ func (s *stepShutdown) Run(ctx context.Context, state multistep.StateBag) multis
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if instanceResp.State != instance.ServerStateStopped {
|
||||
err := fmt.Errorf("Server is in state %s instead of stopped", instanceResp.State.String())
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
|
||||
@@ -7,19 +7,23 @@ import (
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type stepSnapshot struct{}
|
||||
|
||||
func (s *stepSnapshot) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(*Config)
|
||||
volumeID := state.Get("root_volume_id").(string)
|
||||
|
||||
ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName))
|
||||
snapshot, err := client.PostSnapshot(volumeID, c.SnapshotName)
|
||||
createSnapshotResp, err := instanceAPI.CreateSnapshot(&instance.CreateSnapshotRequest{
|
||||
Name: c.SnapshotName,
|
||||
VolumeID: volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating snapshot: %s", err)
|
||||
state.Put("error", err)
|
||||
@@ -27,10 +31,11 @@ func (s *stepSnapshot) Run(ctx context.Context, state multistep.StateBag) multis
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Snapshot ID: %s", snapshot)
|
||||
state.Put("snapshot_id", snapshot)
|
||||
log.Printf("Snapshot ID: %s", createSnapshotResp.Snapshot.ID)
|
||||
state.Put("snapshot_id", createSnapshotResp.Snapshot.ID)
|
||||
state.Put("snapshot_name", c.SnapshotName)
|
||||
state.Put("region", c.Region)
|
||||
state.Put("region", c.Zone) // Deprecated
|
||||
state.Put("zone", c.Zone)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type FlatConfig struct {
|
||||
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"`
|
||||
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"`
|
||||
@@ -125,6 +126,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"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},
|
||||
|
||||
@@ -20,6 +20,7 @@ type FlatConfig struct {
|
||||
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"`
|
||||
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"`
|
||||
@@ -146,6 +147,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"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},
|
||||
|
||||
@@ -20,6 +20,7 @@ type FlatConfig struct {
|
||||
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"`
|
||||
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
|
||||
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
|
||||
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
|
||||
@@ -133,6 +134,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
|
||||
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
|
||||
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
|
||||
|
||||
@@ -20,6 +20,7 @@ type FlatConfig struct {
|
||||
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"`
|
||||
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
|
||||
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
|
||||
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
|
||||
@@ -130,6 +131,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
|
||||
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
|
||||
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
//go:generate struct-markdown
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/packer/common/bootcommand"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
)
|
||||
|
||||
type BootConfigWrapper struct {
|
||||
bootcommand.VNCConfig `mapstructure:",squash"`
|
||||
// If set to true, Packer will use USB HID Keyboard scan codes to send the boot command to the VM and
|
||||
// the [disable_vnc](#disable_vnc) option will be ignored and automatically set to true.
|
||||
// This option is not supported by hosts with free license.
|
||||
//
|
||||
// ~> **Note:** The ESXi 6.5+ removes support to VNC. In this case, the `usb_keyboard` should be set to true
|
||||
// in order to send boot command keystrokes to the VM.
|
||||
USBKeyBoard bool `mapstructure:"usb_keyboard"`
|
||||
}
|
||||
|
||||
func (c *BootConfigWrapper) Prepare(ctx *interpolate.Context, driverConfig *DriverConfig) (warnings []string, errs []error) {
|
||||
if c.USBKeyBoard {
|
||||
if driverConfig.RemoteType == "" {
|
||||
warnings = append(warnings, "[WARN] `usb_keyboard` can only be used with remote VMWare builds. "+
|
||||
"The `usb_keyboard` option will be ignored and automatically set to false.")
|
||||
c.USBKeyBoard = false
|
||||
} else if !c.DisableVNC {
|
||||
warnings = append(warnings, "[WARN] `usb_keyboard` is set to true then the remote VMWare builds "+
|
||||
"will not use VNC to connect to the host. The `disable_vnc` option will be ignored and automatically set to true.")
|
||||
c.DisableVNC = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.BootCommand) > 0 && c.DisableVNC {
|
||||
errs = append(errs,
|
||||
fmt.Errorf("A boot command cannot be used when vnc is disabled."))
|
||||
}
|
||||
errs = append(errs, c.BootConfig.Prepare(ctx)...)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hashicorp/packer/common/bootcommand"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
)
|
||||
|
||||
func TestVNCConfigWrapper_Prepare(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
config *BootConfigWrapper
|
||||
expectedConfig *BootConfigWrapper
|
||||
driver *DriverConfig
|
||||
errs []error
|
||||
warnings []string
|
||||
}{
|
||||
{
|
||||
name: "VNC and boot command for local build",
|
||||
config: &BootConfigWrapper{
|
||||
VNCConfig: bootcommand.VNCConfig{
|
||||
BootConfig: bootcommand.BootConfig{
|
||||
BootCommand: []string{"<boot><command>"},
|
||||
},
|
||||
DisableVNC: true,
|
||||
},
|
||||
USBKeyBoard: false,
|
||||
},
|
||||
expectedConfig: nil,
|
||||
driver: new(DriverConfig),
|
||||
errs: []error{fmt.Errorf("A boot command cannot be used when vnc is disabled.")},
|
||||
warnings: nil,
|
||||
},
|
||||
{
|
||||
name: "Disable VNC warning for remote build",
|
||||
config: &BootConfigWrapper{
|
||||
VNCConfig: bootcommand.VNCConfig{
|
||||
BootConfig: bootcommand.BootConfig{
|
||||
BootCommand: []string{"<boot><command>"},
|
||||
},
|
||||
DisableVNC: false,
|
||||
},
|
||||
USBKeyBoard: true,
|
||||
},
|
||||
expectedConfig: &BootConfigWrapper{
|
||||
VNCConfig: bootcommand.VNCConfig{
|
||||
DisableVNC: true,
|
||||
},
|
||||
USBKeyBoard: true,
|
||||
},
|
||||
driver: &DriverConfig{
|
||||
RemoteType: "esxi",
|
||||
},
|
||||
errs: nil,
|
||||
warnings: []string{"[WARN] `usb_keyboard` is set to true then the remote VMWare builds " +
|
||||
"will not use VNC to connect to the host. The `disable_vnc` option will be ignored and automatically set to true."},
|
||||
},
|
||||
{
|
||||
name: "Disable USBKeyBoard warning for local build",
|
||||
config: &BootConfigWrapper{
|
||||
VNCConfig: bootcommand.VNCConfig{
|
||||
BootConfig: bootcommand.BootConfig{
|
||||
BootCommand: []string{"<boot><command>"},
|
||||
},
|
||||
DisableVNC: false,
|
||||
},
|
||||
USBKeyBoard: true,
|
||||
},
|
||||
expectedConfig: &BootConfigWrapper{
|
||||
VNCConfig: bootcommand.VNCConfig{
|
||||
DisableVNC: false,
|
||||
},
|
||||
USBKeyBoard: false,
|
||||
},
|
||||
driver: &DriverConfig{},
|
||||
errs: nil,
|
||||
warnings: []string{"[WARN] `usb_keyboard` can only be used with remote VMWare builds. " +
|
||||
"The `usb_keyboard` option will be ignored and automatically set to false."},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range tc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
warnings, errs := c.config.Prepare(interpolate.NewContext(), c.driver)
|
||||
if !reflect.DeepEqual(errs, c.errs) {
|
||||
t.Fatalf("bad: \n expected '%v' \nactual '%v'", c.errs, errs)
|
||||
}
|
||||
if diff := cmp.Diff(warnings, c.warnings); diff != "" {
|
||||
t.Fatalf("unexpected warnings: %s", diff)
|
||||
}
|
||||
if len(c.errs) == 0 {
|
||||
if diff := cmp.Diff(c.config, c.expectedConfig,
|
||||
cmpopts.IgnoreFields(bootcommand.VNCConfig{},
|
||||
"BootConfig",
|
||||
)); diff != "" {
|
||||
t.Fatalf("unexpected config: %s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,9 @@ type Driver interface {
|
||||
|
||||
// Export the vm to ovf or ova format using ovftool
|
||||
Export([]string) error
|
||||
|
||||
// OvfTool
|
||||
VerifyOvfTool(bool, bool) error
|
||||
}
|
||||
|
||||
// NewDriver returns a new driver implementation for this operating
|
||||
@@ -88,20 +91,11 @@ func NewDriver(dconfig *DriverConfig, config *SSHConfig, vmName string) (Driver,
|
||||
drivers := []Driver{}
|
||||
|
||||
if dconfig.RemoteType != "" {
|
||||
drivers = []Driver{
|
||||
&ESX5Driver{
|
||||
Host: dconfig.RemoteHost,
|
||||
Port: dconfig.RemotePort,
|
||||
Username: dconfig.RemoteUser,
|
||||
Password: dconfig.RemotePassword,
|
||||
PrivateKeyFile: dconfig.RemotePrivateKey,
|
||||
Datastore: dconfig.RemoteDatastore,
|
||||
CacheDatastore: dconfig.RemoteCacheDatastore,
|
||||
CacheDirectory: dconfig.RemoteCacheDirectory,
|
||||
VMName: vmName,
|
||||
CommConfig: config.Comm,
|
||||
},
|
||||
esx5Driver, err := NewESX5Driver(dconfig, config, vmName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
drivers = []Driver{esx5Driver}
|
||||
|
||||
} else {
|
||||
switch runtime.GOOS {
|
||||
@@ -628,3 +622,22 @@ func (d *VmwareDriver) Export(args []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *VmwareDriver) VerifyOvfTool(SkipExport, _ bool) error {
|
||||
if SkipExport {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Verifying that ovftool exists...")
|
||||
// Validate that tool exists, but no need to validate credentials.
|
||||
ovftool := GetOVFTool()
|
||||
if ovftool != "" {
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("Couldn't find ovftool in path! Please either " +
|
||||
"set `skip_export = true` and remove the `format` option " +
|
||||
"from your template, or make sure ovftool is installed on " +
|
||||
"your build system. ")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,14 +3,8 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
)
|
||||
@@ -58,6 +52,8 @@ type DriverConfig struct {
|
||||
}
|
||||
|
||||
func (c *DriverConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
var errs []error
|
||||
|
||||
if c.FusionAppPath == "" {
|
||||
c.FusionAppPath = os.Getenv("FUSION_APP_PATH")
|
||||
}
|
||||
@@ -80,7 +76,19 @@ func (c *DriverConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
c.RemotePort = 22
|
||||
}
|
||||
|
||||
return nil
|
||||
if c.RemoteType != "" {
|
||||
if c.RemoteHost == "" {
|
||||
errs = append(errs,
|
||||
fmt.Errorf("remote_host must be specified"))
|
||||
}
|
||||
|
||||
if c.RemoteType != "esx5" {
|
||||
errs = append(errs,
|
||||
fmt.Errorf("Only 'esx5' value is accepted for remote_type"))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *DriverConfig) Validate(SkipExport bool) error {
|
||||
@@ -93,65 +101,5 @@ func (c *DriverConfig) Validate(SkipExport bool) error {
|
||||
"that you set a value for remote_password")
|
||||
}
|
||||
|
||||
if c.RemoteType == "" {
|
||||
// Validate that tool exists, but no need to validate credentials.
|
||||
ovftool := GetOVFTool()
|
||||
if ovftool != "" {
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("Couldn't find ovftool in path! Please either " +
|
||||
"set `skip_export = true` and remove the `format` option " +
|
||||
"from your template, or make sure ovftool is installed on " +
|
||||
"your build system. ")
|
||||
}
|
||||
}
|
||||
|
||||
if c.SkipValidateCredentials {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check that password is valid by sending a dummy ovftool command
|
||||
// now, so that we don't fail for a simple mistake after a long
|
||||
// build
|
||||
ovftool := GetOVFTool()
|
||||
|
||||
// Generate the uri of the host, with embedded credentials
|
||||
ovftool_uri := fmt.Sprintf("vi://%s", c.RemoteHost)
|
||||
u, err := url.Parse(ovftool_uri)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't generate uri for ovftool: %s", err)
|
||||
}
|
||||
u.User = url.UserPassword(c.RemoteUser, c.RemotePassword)
|
||||
|
||||
ovfToolArgs := []string{"--noSSLVerify", "--verifyOnly", u.String()}
|
||||
|
||||
var out bytes.Buffer
|
||||
cmdCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(cmdCtx, ovftool, ovfToolArgs...)
|
||||
cmd.Stdout = &out
|
||||
|
||||
// Need to manually close stdin or else the ofvtool call will hang
|
||||
// forever in a situation where the user has provided an invalid
|
||||
// password or username
|
||||
stdin, _ := cmd.StdinPipe()
|
||||
defer stdin.Close()
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
outString := out.String()
|
||||
// The command *should* fail with this error, if it
|
||||
// authenticates properly.
|
||||
if !strings.Contains(outString, "Found wrong kind of object") {
|
||||
err := fmt.Errorf("ovftool validation error: %s; %s",
|
||||
err, outString)
|
||||
if strings.Contains(outString,
|
||||
"Enter login information for source") {
|
||||
err = fmt.Errorf("The username or password you " +
|
||||
"provided to ovftool is invalid.")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,32 +1,84 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
)
|
||||
|
||||
func TestDriverConfigPrepare(t *testing.T) {
|
||||
var c *DriverConfig
|
||||
|
||||
// Test a default boot_wait
|
||||
c = new(DriverConfig)
|
||||
errs := c.Prepare(interpolate.NewContext())
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("bad: %#v", errs)
|
||||
}
|
||||
if c.FusionAppPath != "/Applications/VMware Fusion.app" {
|
||||
t.Fatalf("bad value: %s", c.FusionAppPath)
|
||||
tc := []struct {
|
||||
name string
|
||||
config *DriverConfig
|
||||
expectedConfig *DriverConfig
|
||||
errs []error
|
||||
}{
|
||||
{
|
||||
name: "Set default values",
|
||||
config: new(DriverConfig),
|
||||
expectedConfig: &DriverConfig{
|
||||
FusionAppPath: "/Applications/VMware Fusion.app",
|
||||
RemoteDatastore: "datastore1",
|
||||
RemoteCacheDatastore: "datastore1",
|
||||
RemoteCacheDirectory: "packer_cache",
|
||||
RemotePort: 22,
|
||||
RemoteUser: "root",
|
||||
},
|
||||
errs: nil,
|
||||
},
|
||||
{
|
||||
name: "Override default values",
|
||||
config: &DriverConfig{
|
||||
FusionAppPath: "foo",
|
||||
RemoteDatastore: "set-datastore1",
|
||||
RemoteCacheDatastore: "set-datastore1",
|
||||
RemoteCacheDirectory: "set_packer_cache",
|
||||
RemotePort: 443,
|
||||
RemoteUser: "admin",
|
||||
},
|
||||
expectedConfig: &DriverConfig{
|
||||
FusionAppPath: "foo",
|
||||
RemoteDatastore: "set-datastore1",
|
||||
RemoteCacheDatastore: "set-datastore1",
|
||||
RemoteCacheDirectory: "set_packer_cache",
|
||||
RemotePort: 443,
|
||||
RemoteUser: "admin",
|
||||
},
|
||||
errs: nil,
|
||||
},
|
||||
{
|
||||
name: "Invalid remote type",
|
||||
config: &DriverConfig{
|
||||
RemoteType: "invalid",
|
||||
RemoteHost: "host",
|
||||
},
|
||||
expectedConfig: nil,
|
||||
errs: []error{fmt.Errorf("Only 'esx5' value is accepted for remote_type")},
|
||||
},
|
||||
{
|
||||
name: "Remote host not set",
|
||||
config: &DriverConfig{
|
||||
RemoteType: "esx5",
|
||||
},
|
||||
expectedConfig: nil,
|
||||
errs: []error{fmt.Errorf("remote_host must be specified")},
|
||||
},
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
c = new(DriverConfig)
|
||||
c.FusionAppPath = "foo"
|
||||
errs = c.Prepare(interpolate.NewContext())
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("bad: %#v", errs)
|
||||
}
|
||||
if c.FusionAppPath != "foo" {
|
||||
t.Fatalf("bad value: %s", c.FusionAppPath)
|
||||
for _, c := range tc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
errs := c.config.Prepare(interpolate.NewContext())
|
||||
if !reflect.DeepEqual(errs, c.errs) {
|
||||
t.Fatalf("bad: \n expected '%v' \nactual '%v'", c.errs, errs)
|
||||
}
|
||||
if len(c.errs) == 0 {
|
||||
if diff := cmp.Diff(c.config, c.expectedConfig); diff != "" {
|
||||
t.Fatalf("bad value: %s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -24,7 +26,15 @@ import (
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
helperssh "github.com/hashicorp/packer/helper/ssh"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/vmware/govmomi"
|
||||
"github.com/vmware/govmomi/find"
|
||||
"github.com/vmware/govmomi/session"
|
||||
"github.com/vmware/govmomi/vim25"
|
||||
"github.com/vmware/govmomi/vim25/methods"
|
||||
"github.com/vmware/govmomi/vim25/soap"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"golang.org/x/mobile/event/key"
|
||||
)
|
||||
|
||||
// ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build
|
||||
@@ -43,11 +53,66 @@ type ESX5Driver struct {
|
||||
VMName string
|
||||
CommConfig communicator.Config
|
||||
|
||||
ctx context.Context
|
||||
client *govmomi.Client
|
||||
finder *find.Finder
|
||||
|
||||
comm packer.Communicator
|
||||
outputDir string
|
||||
vmId string
|
||||
}
|
||||
|
||||
func NewESX5Driver(dconfig *DriverConfig, config *SSHConfig, vmName string) (Driver, error) {
|
||||
ctx := context.TODO()
|
||||
|
||||
vsphereUrl, err := url.Parse(fmt.Sprintf("https://%v/sdk", dconfig.RemoteHost))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
credentials := url.UserPassword(dconfig.RemoteUser, dconfig.RemotePassword)
|
||||
vsphereUrl.User = credentials
|
||||
|
||||
soapClient := soap.NewClient(vsphereUrl, true)
|
||||
vimClient, err := vim25.NewClient(ctx, soapClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vimClient.RoundTripper = session.KeepAlive(vimClient.RoundTripper, 10*time.Minute)
|
||||
client := &govmomi.Client{
|
||||
Client: vimClient,
|
||||
SessionManager: session.NewManager(vimClient),
|
||||
}
|
||||
|
||||
err = client.SessionManager.Login(ctx, credentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
finder := find.NewFinder(client.Client, false)
|
||||
datacenter, err := finder.DefaultDatacenter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
finder.SetDatacenter(datacenter)
|
||||
|
||||
return &ESX5Driver{
|
||||
Host: dconfig.RemoteHost,
|
||||
Port: dconfig.RemotePort,
|
||||
Username: dconfig.RemoteUser,
|
||||
Password: dconfig.RemotePassword,
|
||||
PrivateKeyFile: dconfig.RemotePrivateKey,
|
||||
Datastore: dconfig.RemoteDatastore,
|
||||
CacheDatastore: dconfig.RemoteCacheDatastore,
|
||||
CacheDirectory: dconfig.RemoteCacheDirectory,
|
||||
VMName: vmName,
|
||||
CommConfig: config.Comm,
|
||||
ctx: ctx,
|
||||
client: client,
|
||||
finder: finder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *ESX5Driver) Clone(dst, src string, linked bool) error {
|
||||
|
||||
linesToArray := func(lines string) []string { return strings.Split(strings.Trim(lines, "\r\n"), "\n") }
|
||||
@@ -263,6 +328,68 @@ func (d *ESX5Driver) Verify() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *ESX5Driver) VerifyOvfTool(SkipExport, skipValidateCredentials bool) error {
|
||||
err := d.base.VerifyOvfTool(SkipExport, skipValidateCredentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Verifying that ovftool credentials are valid...")
|
||||
// check that password is valid by sending a dummy ovftool command
|
||||
// now, so that we don't fail for a simple mistake after a long
|
||||
// build
|
||||
ovftool := GetOVFTool()
|
||||
|
||||
if skipValidateCredentials {
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.Password == "" {
|
||||
return fmt.Errorf("exporting the vm from esxi with ovftool requires " +
|
||||
"that you set a value for remote_password")
|
||||
}
|
||||
|
||||
// Generate the uri of the host, with embedded credentials
|
||||
ovftool_uri := fmt.Sprintf("vi://%s", d.Host)
|
||||
u, err := url.Parse(ovftool_uri)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't generate uri for ovftool: %s", err)
|
||||
}
|
||||
u.User = url.UserPassword(d.Username, d.Password)
|
||||
|
||||
ovfToolArgs := []string{"--noSSLVerify", "--verifyOnly", u.String()}
|
||||
|
||||
var out bytes.Buffer
|
||||
cmdCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(cmdCtx, ovftool, ovfToolArgs...)
|
||||
cmd.Stdout = &out
|
||||
|
||||
// Need to manually close stdin or else the ofvtool call will hang
|
||||
// forever in a situation where the user has provided an invalid
|
||||
// password or username
|
||||
stdin, _ := cmd.StdinPipe()
|
||||
defer stdin.Close()
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
outString := out.String()
|
||||
// The command *should* fail with this error, if it
|
||||
// authenticates properly.
|
||||
if !strings.Contains(outString, "Found wrong kind of object") {
|
||||
err := fmt.Errorf("ovftool validation error: %s; %s",
|
||||
err, outString)
|
||||
if strings.Contains(outString,
|
||||
"Enter login information for source") {
|
||||
err = fmt.Errorf("The username or password you " +
|
||||
"provided to ovftool is invalid.")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *ESX5Driver) HostIP(multistep.StateBag) (string, error) {
|
||||
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port))
|
||||
if err != nil {
|
||||
@@ -823,3 +950,38 @@ func (r *esxcliReader) find(key, val string) (map[string]string, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type KeyInput struct {
|
||||
Scancode key.Code
|
||||
Alt bool
|
||||
Ctrl bool
|
||||
Shift bool
|
||||
}
|
||||
|
||||
func (d *ESX5Driver) TypeOnKeyboard(input KeyInput) (int32, error) {
|
||||
vm, err := d.finder.VirtualMachine(d.ctx, d.VMName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var spec types.UsbScanCodeSpec
|
||||
spec.KeyEvents = append(spec.KeyEvents, types.UsbScanCodeSpecKeyEvent{
|
||||
UsbHidCode: int32(input.Scancode)<<16 | 7,
|
||||
Modifiers: &types.UsbScanCodeSpecModifierType{
|
||||
LeftControl: &input.Ctrl,
|
||||
LeftAlt: &input.Alt,
|
||||
LeftShift: &input.Shift,
|
||||
},
|
||||
})
|
||||
|
||||
req := &types.PutUsbScanCodes{
|
||||
This: vm.Reference(),
|
||||
Spec: spec,
|
||||
}
|
||||
|
||||
resp, err := methods.PutUsbScanCodes(d.ctx, d.client.RoundTripper, req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return resp.Returnval, nil
|
||||
}
|
||||
|
||||
@@ -95,6 +95,8 @@ type DriverMock struct {
|
||||
|
||||
VerifyCalled bool
|
||||
VerifyErr error
|
||||
|
||||
VerifyOvftoolCalled bool
|
||||
}
|
||||
|
||||
type NetworkMapperMock struct {
|
||||
@@ -279,3 +281,7 @@ func (d *DriverMock) GetVmwareDriver() VmwareDriver {
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func (d *DriverMock) VerifyOvfTool(_ bool, _ bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -954,20 +954,20 @@ func createDeclaration(node pDeclaration) configDeclaration {
|
||||
|
||||
// update configDeclaration parameters
|
||||
for _, p := range hierarchy[i].parameters {
|
||||
switch p.(type) {
|
||||
switch p := p.(type) {
|
||||
case pParameterOption:
|
||||
result.options[p.(pParameterOption).name] = p.(pParameterOption).value
|
||||
result.options[p.name] = p.value
|
||||
case pParameterGrant:
|
||||
Grant := map[string]grant{"ignore": IGNORE, "allow": ALLOW, "deny": DENY}
|
||||
result.grants[p.(pParameterGrant).attribute] = Grant[p.(pParameterGrant).verb]
|
||||
result.grants[p.attribute] = Grant[p.verb]
|
||||
case pParameterBoolean:
|
||||
result.attributes[p.(pParameterBoolean).parameter] = p.(pParameterBoolean).truancy
|
||||
result.attributes[p.parameter] = p.truancy
|
||||
case pParameterClientMatch:
|
||||
result.hostid = append(result.hostid, p.(pParameterClientMatch))
|
||||
result.hostid = append(result.hostid, p)
|
||||
case pParameterExpression:
|
||||
result.expressions[p.(pParameterExpression).parameter] = p.(pParameterExpression).expression
|
||||
result.expressions[p.parameter] = p.expression
|
||||
case pParameterOther:
|
||||
result.parameters[p.(pParameterOther).parameter] = p.(pParameterOther).value
|
||||
result.parameters[p.parameter] = p.value
|
||||
default:
|
||||
result.address = append(result.address, p)
|
||||
}
|
||||
@@ -1196,7 +1196,7 @@ func (e *DhcpConfiguration) HostByName(host string) (configDeclaration, error) {
|
||||
switch entry.id[0].(type) {
|
||||
case pDeclarationHost:
|
||||
id := entry.id[0].(pDeclarationHost)
|
||||
if strings.ToLower(id.name) == strings.ToLower(host) {
|
||||
if strings.EqualFold(id.name, host) {
|
||||
result = append(result, entry)
|
||||
}
|
||||
}
|
||||
@@ -1237,7 +1237,7 @@ func (e NetworkMap) NameIntoDevices(name string) ([]string, error) {
|
||||
var devices []string
|
||||
|
||||
for _, val := range e {
|
||||
if strings.ToLower(val["name"]) == strings.ToLower(name) {
|
||||
if strings.EqualFold(val["name"], name) {
|
||||
devices = append(devices, val["device"])
|
||||
}
|
||||
}
|
||||
@@ -1250,7 +1250,7 @@ func (e NetworkMap) NameIntoDevices(name string) ([]string, error) {
|
||||
}
|
||||
func (e NetworkMap) DeviceIntoName(device string) (string, error) {
|
||||
for _, val := range e {
|
||||
if strings.ToLower(val["device"]) == strings.ToLower(device) {
|
||||
if strings.EqualFold(val["device"], device) {
|
||||
return val["name"], nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
)
|
||||
|
||||
// ~> **Note:** If [usb_keyboard](#usb_keyboard) is set to true, any VNC configuration will be ignored.
|
||||
type RunConfig struct {
|
||||
// Packer defaults to building VMware virtual machines
|
||||
// by launching a GUI that shows the console of the machine being built. When
|
||||
@@ -39,25 +40,26 @@ type RunConfig struct {
|
||||
VNCDisablePassword bool `mapstructure:"vnc_disable_password" required:"false"`
|
||||
}
|
||||
|
||||
func (c *RunConfig) Prepare(ctx *interpolate.Context) (errs []error) {
|
||||
if c.VNCPortMin == 0 {
|
||||
c.VNCPortMin = 5900
|
||||
}
|
||||
func (c *RunConfig) Prepare(_ *interpolate.Context, bootConfig *BootConfigWrapper) (errs []error) {
|
||||
if !bootConfig.USBKeyBoard {
|
||||
if c.VNCPortMin == 0 {
|
||||
c.VNCPortMin = 5900
|
||||
}
|
||||
|
||||
if c.VNCPortMax == 0 {
|
||||
c.VNCPortMax = 6000
|
||||
}
|
||||
if c.VNCPortMax == 0 {
|
||||
c.VNCPortMax = 6000
|
||||
}
|
||||
|
||||
if c.VNCBindAddress == "" {
|
||||
c.VNCBindAddress = "127.0.0.1"
|
||||
}
|
||||
if c.VNCBindAddress == "" {
|
||||
c.VNCBindAddress = "127.0.0.1"
|
||||
}
|
||||
|
||||
if c.VNCPortMin > c.VNCPortMax {
|
||||
errs = append(errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
|
||||
if c.VNCPortMin > c.VNCPortMax {
|
||||
errs = append(errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
|
||||
}
|
||||
if c.VNCPortMin < 0 {
|
||||
errs = append(errs, fmt.Errorf("vnc_port_min must be positive"))
|
||||
}
|
||||
}
|
||||
if c.VNCPortMin < 0 {
|
||||
errs = append(errs, fmt.Errorf("vnc_port_min must be positive"))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/packer/common/bootcommand"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
"golang.org/x/mobile/event/key"
|
||||
)
|
||||
|
||||
// This step "types" the boot command into the VM using USB Scan Codes.
|
||||
type StepUSBBootCommand struct {
|
||||
Config bootcommand.BootConfig
|
||||
KeyInterval time.Duration
|
||||
VMName string
|
||||
Ctx interpolate.Context
|
||||
}
|
||||
|
||||
type USBBootCommandTemplateData struct {
|
||||
HTTPIP string
|
||||
HTTPPort int
|
||||
Name string
|
||||
}
|
||||
|
||||
func (s *StepUSBBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
debug := state.Get("debug").(bool)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
driver := state.Get("driver").(*ESX5Driver)
|
||||
|
||||
if s.Config.BootCommand == nil {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Wait the for the vm to boot.
|
||||
if int64(s.Config.BootWait) > 0 {
|
||||
ui.Say(fmt.Sprintf("Waiting %s for boot...", s.Config.BootWait.String()))
|
||||
select {
|
||||
case <-time.After(s.Config.BootWait):
|
||||
break
|
||||
case <-ctx.Done():
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
var pauseFn multistep.DebugPauseFn
|
||||
if debug {
|
||||
pauseFn = state.Get("pauseFn").(multistep.DebugPauseFn)
|
||||
}
|
||||
|
||||
port := state.Get("http_port").(int)
|
||||
if port > 0 {
|
||||
ip := state.Get("http_ip").(string)
|
||||
s.Ctx.Data = &USBBootCommandTemplateData{
|
||||
HTTPIP: ip,
|
||||
HTTPPort: port,
|
||||
Name: s.VMName,
|
||||
}
|
||||
ui.Say(fmt.Sprintf("HTTP server is working at http://%v:%v/", ip, port))
|
||||
}
|
||||
|
||||
var keyAlt, keyCtrl, keyShift bool
|
||||
sendCodes := func(code key.Code, down bool) error {
|
||||
switch code {
|
||||
case key.CodeLeftAlt:
|
||||
keyAlt = down
|
||||
case key.CodeLeftControl:
|
||||
keyCtrl = down
|
||||
case key.CodeLeftShift:
|
||||
keyShift = down
|
||||
}
|
||||
|
||||
shift := down
|
||||
if keyShift {
|
||||
shift = keyShift
|
||||
}
|
||||
|
||||
_, err := driver.TypeOnKeyboard(KeyInput{
|
||||
Scancode: code,
|
||||
Ctrl: keyCtrl,
|
||||
Alt: keyAlt,
|
||||
Shift: shift,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error typing a boot command (code, down) `%d, %t`: %w", code, down, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
d := bootcommand.NewUSBDriver(sendCodes, s.KeyInterval)
|
||||
|
||||
ui.Say("Typing boot command...")
|
||||
flatBootCommand := s.Config.FlatBootCommand()
|
||||
command, err := interpolate.Render(flatBootCommand, &s.Ctx)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing boot command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
seq, err := bootcommand.GenerateExpressionSequence(command)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error generating boot command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if err := seq.Do(ctx, d); err != nil {
|
||||
err := fmt.Errorf("Error running boot command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if pauseFn != nil {
|
||||
pauseFn(multistep.DebugLocationAfterRun, fmt.Sprintf("boot_command: %s", command), state)
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (*StepUSBBootCommand) Cleanup(multistep.StateBag) {}
|
||||
+19
-20
@@ -23,22 +23,20 @@ import (
|
||||
//
|
||||
// Produces:
|
||||
// <nothing>
|
||||
type StepTypeBootCommand struct {
|
||||
BootCommand string
|
||||
VNCEnabled bool
|
||||
BootWait time.Duration
|
||||
VMName string
|
||||
Ctx interpolate.Context
|
||||
KeyInterval time.Duration
|
||||
type StepVNCBootCommand struct {
|
||||
Config bootcommand.VNCConfig
|
||||
VMName string
|
||||
Ctx interpolate.Context
|
||||
}
|
||||
type bootCommandTemplateData struct {
|
||||
|
||||
type VNCBootCommandTemplateData struct {
|
||||
HTTPIP string
|
||||
HTTPPort int
|
||||
Name string
|
||||
}
|
||||
|
||||
func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
if !s.VNCEnabled {
|
||||
func (s *StepVNCBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
if s.Config.DisableVNC {
|
||||
log.Println("Skipping boot command step...")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
@@ -51,10 +49,10 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
|
||||
vncPassword := state.Get("vnc_password")
|
||||
|
||||
// Wait the for the vm to boot.
|
||||
if int64(s.BootWait) > 0 {
|
||||
ui.Say(fmt.Sprintf("Waiting %s for boot...", s.BootWait.String()))
|
||||
if int64(s.Config.BootWait) > 0 {
|
||||
ui.Say(fmt.Sprintf("Waiting %s for boot...", s.Config.BootWait.String()))
|
||||
select {
|
||||
case <-time.After(s.BootWait):
|
||||
case <-time.After(s.Config.BootWait):
|
||||
break
|
||||
case <-ctx.Done():
|
||||
return multistep.ActionHalt
|
||||
@@ -98,16 +96,17 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
|
||||
log.Printf("Connected to VNC desktop: %s", c.DesktopName)
|
||||
|
||||
hostIP := state.Get("http_ip").(string)
|
||||
s.Ctx.Data = &bootCommandTemplateData{
|
||||
hostIP,
|
||||
httpPort,
|
||||
s.VMName,
|
||||
s.Ctx.Data = &VNCBootCommandTemplateData{
|
||||
HTTPIP: hostIP,
|
||||
HTTPPort: httpPort,
|
||||
Name: s.VMName,
|
||||
}
|
||||
|
||||
d := bootcommand.NewVNCDriver(c, s.KeyInterval)
|
||||
d := bootcommand.NewVNCDriver(c, s.Config.BootKeyInterval)
|
||||
|
||||
ui.Say("Typing the boot command over VNC...")
|
||||
command, err := interpolate.Render(s.BootCommand, &s.Ctx)
|
||||
flatBootCommand := s.Config.FlatBootCommand()
|
||||
command, err := interpolate.Render(flatBootCommand, &s.Ctx)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing boot command: %s", err)
|
||||
state.Put("error", err)
|
||||
@@ -138,4 +137,4 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (*StepTypeBootCommand) Cleanup(multistep.StateBag) {}
|
||||
func (*StepVNCBootCommand) Cleanup(multistep.StateBag) {}
|
||||
@@ -35,6 +35,11 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed creating VMware driver: %s", err)
|
||||
}
|
||||
// Before we get deep into the build, make sure ovftool is present and
|
||||
// credentials are valid, if we're going to use ovftool.
|
||||
if err := driver.VerifyOvfTool(b.config.SkipExport, b.config.SkipValidateCredentials); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Setup the state bag
|
||||
state := new(multistep.BasicStateBag)
|
||||
@@ -47,6 +52,20 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
state.Put("driverConfig", &b.config.DriverConfig)
|
||||
state.Put("temporaryDevices", []string{}) // Devices (in .vmx) created by packer during building
|
||||
|
||||
var stepBootCommand multistep.Step = &vmwcommon.StepVNCBootCommand{
|
||||
Config: b.config.VNCConfig,
|
||||
VMName: b.config.VMName,
|
||||
Ctx: b.config.ctx,
|
||||
}
|
||||
if b.config.USBKeyBoard {
|
||||
stepBootCommand = &vmwcommon.StepUSBBootCommand{
|
||||
Config: b.config.VNCConfig.BootConfig,
|
||||
KeyInterval: b.config.VNCConfig.BootKeyInterval,
|
||||
VMName: b.config.VMName,
|
||||
Ctx: b.config.ctx,
|
||||
}
|
||||
}
|
||||
|
||||
steps := []multistep.Step{
|
||||
&vmwcommon.StepPrepareTools{
|
||||
RemoteType: b.config.RemoteType,
|
||||
@@ -71,12 +90,22 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
Directories: b.config.FloppyConfig.FloppyDirectories,
|
||||
Label: b.config.FloppyConfig.FloppyLabel,
|
||||
},
|
||||
&common.StepCreateCD{
|
||||
Files: b.config.CDConfig.CDFiles,
|
||||
Label: b.config.CDConfig.CDLabel,
|
||||
},
|
||||
&vmwcommon.StepRemoteUpload{
|
||||
Key: "floppy_path",
|
||||
Message: "Uploading Floppy to remote machine...",
|
||||
DoCleanup: true,
|
||||
Checksum: "none",
|
||||
},
|
||||
&vmwcommon.StepRemoteUpload{
|
||||
Key: "cd_path",
|
||||
Message: "Uploading CD to remote machine...",
|
||||
DoCleanup: true,
|
||||
Checksum: "none",
|
||||
},
|
||||
&vmwcommon.StepRemoteUpload{
|
||||
Key: "iso_path",
|
||||
Message: "Uploading ISO to remote machine...",
|
||||
@@ -122,14 +151,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
DurationBeforeStop: 5 * time.Second,
|
||||
Headless: b.config.Headless,
|
||||
},
|
||||
&vmwcommon.StepTypeBootCommand{
|
||||
BootWait: b.config.BootWait,
|
||||
VNCEnabled: !b.config.DisableVNC,
|
||||
BootCommand: b.config.FlatBootCommand(),
|
||||
VMName: b.config.VMName,
|
||||
Ctx: b.config.ctx,
|
||||
KeyInterval: b.config.VNCConfig.BootKeyInterval,
|
||||
},
|
||||
stepBootCommand,
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.SSHConfig.Comm,
|
||||
Host: driver.CommHost,
|
||||
|
||||
@@ -198,6 +198,123 @@ func TestBuilderPrepare_RemoteType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_Export(t *testing.T) {
|
||||
type testCase struct {
|
||||
InputConfigVals map[string]string
|
||||
ExpectedSkipExportValue bool
|
||||
ExpectedFormat string
|
||||
ExpectedErr bool
|
||||
Reason string
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "",
|
||||
"format": "",
|
||||
},
|
||||
ExpectedSkipExportValue: true,
|
||||
ExpectedFormat: "vmx",
|
||||
ExpectedErr: false,
|
||||
Reason: "should have defaulted format to vmx.",
|
||||
},
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "esx5",
|
||||
"format": "",
|
||||
"remote_host": "fakehost.com",
|
||||
"remote_password": "fakepassword",
|
||||
"remote_username": "fakeuser",
|
||||
},
|
||||
ExpectedSkipExportValue: false,
|
||||
ExpectedFormat: "ovf",
|
||||
ExpectedErr: false,
|
||||
Reason: "should have defaulted format to ovf with remote set to esx5.",
|
||||
},
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "esx5",
|
||||
"format": "",
|
||||
},
|
||||
ExpectedSkipExportValue: false,
|
||||
ExpectedFormat: "ovf",
|
||||
ExpectedErr: true,
|
||||
Reason: "should have errored because remote host isn't set for remote build.",
|
||||
},
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "invalid",
|
||||
"format": "",
|
||||
"remote_host": "fakehost.com",
|
||||
"remote_password": "fakepassword",
|
||||
"remote_username": "fakeuser",
|
||||
},
|
||||
ExpectedSkipExportValue: false,
|
||||
ExpectedFormat: "ovf",
|
||||
ExpectedErr: true,
|
||||
Reason: "should error with invalid remote type",
|
||||
},
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "",
|
||||
"format": "invalid",
|
||||
},
|
||||
ExpectedSkipExportValue: false,
|
||||
ExpectedFormat: "invalid",
|
||||
ExpectedErr: true,
|
||||
Reason: "should error with invalid format",
|
||||
},
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "",
|
||||
"format": "ova",
|
||||
},
|
||||
ExpectedSkipExportValue: false,
|
||||
ExpectedFormat: "ova",
|
||||
ExpectedErr: false,
|
||||
Reason: "should set user-given ova format",
|
||||
},
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "esx5",
|
||||
"format": "ova",
|
||||
"remote_host": "fakehost.com",
|
||||
"remote_password": "fakepassword",
|
||||
"remote_username": "fakeuser",
|
||||
},
|
||||
ExpectedSkipExportValue: false,
|
||||
ExpectedFormat: "ova",
|
||||
ExpectedErr: false,
|
||||
Reason: "should set user-given ova format",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
config := testConfig()
|
||||
for k, v := range tc.InputConfigVals {
|
||||
config[k] = v
|
||||
}
|
||||
config["skip_validate_credentials"] = true
|
||||
outCfg := &Config{}
|
||||
warns, errs := (outCfg).Prepare(config)
|
||||
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
|
||||
if (errs != nil) != tc.ExpectedErr {
|
||||
t.Fatalf("received error: \n %s \n but 'expected err' was %t", errs, tc.ExpectedErr)
|
||||
}
|
||||
|
||||
if outCfg.Format != tc.ExpectedFormat {
|
||||
t.Fatalf("Expected: %s. Actual: %s. Reason: %s", tc.ExpectedFormat,
|
||||
outCfg.Format, tc.Reason)
|
||||
}
|
||||
if outCfg.SkipExport != tc.ExpectedSkipExportValue {
|
||||
t.Fatalf("For SkipExport expected %t but recieved %t",
|
||||
tc.ExpectedSkipExportValue, outCfg.SkipExport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_RemoteExport(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
|
||||
"github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/common/bootcommand"
|
||||
"github.com/hashicorp/packer/common/shutdowncommand"
|
||||
"github.com/hashicorp/packer/helper/config"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
@@ -23,7 +22,8 @@ type Config struct {
|
||||
common.HTTPConfig `mapstructure:",squash"`
|
||||
common.ISOConfig `mapstructure:",squash"`
|
||||
common.FloppyConfig `mapstructure:",squash"`
|
||||
bootcommand.VNCConfig `mapstructure:",squash"`
|
||||
common.CDConfig `mapstructure:",squash"`
|
||||
vmwcommon.BootConfigWrapper `mapstructure:",squash"`
|
||||
vmwcommon.DriverConfig `mapstructure:",squash"`
|
||||
vmwcommon.HWConfig `mapstructure:",squash"`
|
||||
vmwcommon.OutputConfig `mapstructure:",squash"`
|
||||
@@ -93,24 +93,27 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
}
|
||||
|
||||
// Accumulate any errors and warnings
|
||||
var warnings []string
|
||||
var errs *packer.MultiError
|
||||
warnings := make([]string, 0)
|
||||
|
||||
vncWarnings, vncErrs := c.BootConfigWrapper.Prepare(&c.ctx, &c.DriverConfig)
|
||||
warnings = append(warnings, vncWarnings...)
|
||||
errs = packer.MultiErrorAppend(errs, vncErrs...)
|
||||
|
||||
isoWarnings, isoErrs := c.ISOConfig.Prepare(&c.ctx)
|
||||
warnings = append(warnings, isoWarnings...)
|
||||
errs = packer.MultiErrorAppend(errs, isoErrs...)
|
||||
errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.HWConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.DriverConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx, &c.BootConfigWrapper)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.SSHConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.ToolsConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.CDConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.VMXConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.DiskConfig.Prepare(&c.ctx)...)
|
||||
|
||||
@@ -163,32 +166,26 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
c.HWConfig.Network = "nat"
|
||||
}
|
||||
|
||||
// Remote configuration validation
|
||||
if c.RemoteType != "" {
|
||||
if c.RemoteHost == "" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("remote_host must be specified"))
|
||||
}
|
||||
|
||||
if c.RemoteType != "esx5" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Only 'esx5' value is accepted for remote_type"))
|
||||
}
|
||||
}
|
||||
|
||||
if c.Format == "" {
|
||||
if c.RemoteType != "esx5" {
|
||||
if c.RemoteType == "" {
|
||||
c.Format = "vmx"
|
||||
} else {
|
||||
c.Format = "ovf"
|
||||
}
|
||||
}
|
||||
|
||||
if c.RemoteType != "esx5" && c.Format == "vmx" {
|
||||
// if we're building locally and want a vmx, there's nothing to export.
|
||||
// Set skip export flag here to keep the export step from attempting
|
||||
// an unneded export
|
||||
c.SkipExport = true
|
||||
if c.RemoteType == "" {
|
||||
if c.Format == "vmx" {
|
||||
// if we're building locally and want a vmx, there's nothing to export.
|
||||
// Set skip export flag here to keep the export step from attempting
|
||||
// an unneded export
|
||||
c.SkipExport = true
|
||||
}
|
||||
if c.Headless && c.DisableVNC {
|
||||
warnings = append(warnings,
|
||||
"Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+
|
||||
"you won't be able to see any output.")
|
||||
}
|
||||
}
|
||||
|
||||
err = c.DriverConfig.Validate(c.SkipExport)
|
||||
@@ -196,19 +193,12 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
errs = packer.MultiErrorAppend(errs, err)
|
||||
}
|
||||
|
||||
// Warnings
|
||||
if c.ShutdownCommand == "" {
|
||||
warnings = append(warnings,
|
||||
"A shutdown_command was not specified. Without a shutdown command, Packer\n"+
|
||||
"will forcibly halt the virtual machine, which may result in data loss.")
|
||||
}
|
||||
|
||||
if c.Headless && c.DisableVNC {
|
||||
warnings = append(warnings,
|
||||
"Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+
|
||||
"you won't be able to see any output.")
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return warnings, errs
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type FlatConfig struct {
|
||||
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"`
|
||||
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"`
|
||||
@@ -28,11 +29,14 @@ type FlatConfig struct {
|
||||
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
|
||||
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
|
||||
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
|
||||
CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"`
|
||||
CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"`
|
||||
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"`
|
||||
DisableVNC *bool `mapstructure:"disable_vnc" cty:"disable_vnc" hcl:"disable_vnc"`
|
||||
BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"`
|
||||
USBKeyBoard *bool `mapstructure:"usb_keyboard" cty:"usb_keyboard" hcl:"usb_keyboard"`
|
||||
CleanUpRemoteCache *bool `mapstructure:"cleanup_remote_cache" required:"false" cty:"cleanup_remote_cache" hcl:"cleanup_remote_cache"`
|
||||
FusionAppPath *string `mapstructure:"fusion_app_path" required:"false" cty:"fusion_app_path" hcl:"fusion_app_path"`
|
||||
RemoteType *string `mapstructure:"remote_type" required:"false" cty:"remote_type" hcl:"remote_type"`
|
||||
@@ -159,6 +163,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"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},
|
||||
@@ -167,11 +172,14 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
|
||||
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
|
||||
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
|
||||
"cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false},
|
||||
"cd_label": &hcldec.AttrSpec{Name: "cd_label", 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},
|
||||
"disable_vnc": &hcldec.AttrSpec{Name: "disable_vnc", Type: cty.Bool, Required: false},
|
||||
"boot_key_interval": &hcldec.AttrSpec{Name: "boot_key_interval", Type: cty.String, Required: false},
|
||||
"usb_keyboard": &hcldec.AttrSpec{Name: "usb_keyboard", Type: cty.Bool, Required: false},
|
||||
"cleanup_remote_cache": &hcldec.AttrSpec{Name: "cleanup_remote_cache", Type: cty.Bool, Required: false},
|
||||
"fusion_app_path": &hcldec.AttrSpec{Name: "fusion_app_path", Type: cty.String, Required: false},
|
||||
"remote_type": &hcldec.AttrSpec{Name: "remote_type", Type: cty.String, Required: false},
|
||||
|
||||
@@ -34,6 +34,7 @@ type vmxTemplateData struct {
|
||||
DiskType string
|
||||
CDROMType string
|
||||
CDROMType_PrimarySecondary string
|
||||
CDROM_PATH string
|
||||
|
||||
Network_Type string
|
||||
Network_Device string
|
||||
@@ -112,6 +113,7 @@ func (s *stepCreateVMX) Run(ctx context.Context, state multistep.StateBag) multi
|
||||
|
||||
ictx := config.ctx
|
||||
|
||||
// Mount extra vmdks we created earlier.
|
||||
if len(config.AdditionalDiskSize) > 0 {
|
||||
for i := range config.AdditionalDiskSize {
|
||||
ictx.Data = &additionalDiskTemplateData{
|
||||
@@ -238,6 +240,15 @@ func (s *stepCreateVMX) Run(ctx context.Context, state multistep.StateBag) multi
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Add our custom CD, if it exists
|
||||
cd_path, ok := state.Get("cd_path").(string)
|
||||
if ok {
|
||||
if cd_path != "" {
|
||||
vmxTemplate += ExtraCDRomTemplate
|
||||
templateData.CDROM_PATH = cd_path
|
||||
}
|
||||
}
|
||||
|
||||
/// Now that we figured out the CDROM device to add, store it
|
||||
/// to the list of temporary build devices in our statebag
|
||||
tmpBuildDevices := state.Get("temporaryDevices").([]string)
|
||||
@@ -596,3 +607,9 @@ scsi0:{{ .DiskNumber }}.fileName = "{{ .DiskName}}-{{ .DiskNumber }}.vmdk"
|
||||
scsi0:{{ .DiskNumber }}.present = "TRUE"
|
||||
scsi0:{{ .DiskNumber }}.redo = ""
|
||||
`
|
||||
|
||||
const ExtraCDRomTemplate = `
|
||||
{{ .CDROMType }}1:{{ .CDROMType_PrimarySecondary }}.present = "TRUE"
|
||||
{{ .CDROMType }}1:{{ .CDROMType_PrimarySecondary }}.fileName = "{{ .CDROM_PATH }}"
|
||||
{{ .CDROMType }}1:{{ .CDROMType_PrimarySecondary }}.deviceType = "cdrom-image"
|
||||
`
|
||||
|
||||
@@ -40,6 +40,11 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed creating VMware driver: %s", err)
|
||||
}
|
||||
// Before we get deep into the build, make sure ovftool is present and
|
||||
// credentials are valid, if we're going to use ovftool.
|
||||
if err := driver.VerifyOvfTool(b.config.SkipExport, b.config.SkipValidateCredentials); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set up the state.
|
||||
state := new(multistep.BasicStateBag)
|
||||
@@ -51,6 +56,20 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
state.Put("driverConfig", &b.config.DriverConfig)
|
||||
state.Put("temporaryDevices", []string{}) // Devices (in .vmx) created by packer during building
|
||||
|
||||
var stepBootCommand multistep.Step = &vmwcommon.StepVNCBootCommand{
|
||||
Config: b.config.VNCConfig,
|
||||
VMName: b.config.VMName,
|
||||
Ctx: b.config.ctx,
|
||||
}
|
||||
if b.config.USBKeyBoard {
|
||||
stepBootCommand = &vmwcommon.StepUSBBootCommand{
|
||||
Config: b.config.VNCConfig.BootConfig,
|
||||
KeyInterval: b.config.VNCConfig.BootKeyInterval,
|
||||
VMName: b.config.VMName,
|
||||
Ctx: b.config.ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// Build the steps.
|
||||
steps := []multistep.Step{
|
||||
&vmwcommon.StepPrepareTools{
|
||||
@@ -121,14 +140,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
DurationBeforeStop: 5 * time.Second,
|
||||
Headless: b.config.Headless,
|
||||
},
|
||||
&vmwcommon.StepTypeBootCommand{
|
||||
BootWait: b.config.BootWait,
|
||||
VNCEnabled: !b.config.DisableVNC,
|
||||
BootCommand: b.config.FlatBootCommand(),
|
||||
VMName: b.config.VMName,
|
||||
Ctx: b.config.ctx,
|
||||
KeyInterval: b.config.VNCConfig.BootKeyInterval,
|
||||
},
|
||||
stepBootCommand,
|
||||
&communicator.StepConnect{
|
||||
Config: &b.config.SSHConfig.Comm,
|
||||
Host: driver.CommHost,
|
||||
@@ -198,5 +210,3 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
return vmwcommon.NewArtifact(b.config.RemoteType, b.config.Format, exportOutputPath,
|
||||
b.config.VMName, b.config.SkipExport, b.config.KeepRegistered, state)
|
||||
}
|
||||
|
||||
// Cancel.
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
|
||||
"github.com/hashicorp/packer/common"
|
||||
"github.com/hashicorp/packer/common/bootcommand"
|
||||
"github.com/hashicorp/packer/common/shutdowncommand"
|
||||
"github.com/hashicorp/packer/helper/config"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
@@ -21,7 +20,7 @@ type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
common.HTTPConfig `mapstructure:",squash"`
|
||||
common.FloppyConfig `mapstructure:",squash"`
|
||||
bootcommand.VNCConfig `mapstructure:",squash"`
|
||||
vmwcommon.BootConfigWrapper `mapstructure:",squash"`
|
||||
vmwcommon.DriverConfig `mapstructure:",squash"`
|
||||
vmwcommon.OutputConfig `mapstructure:",squash"`
|
||||
vmwcommon.RunConfig `mapstructure:",squash"`
|
||||
@@ -77,16 +76,20 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
"packer-%s-%d", c.PackerBuildName, interpolate.InitTime.Unix())
|
||||
}
|
||||
|
||||
// Prepare the errors
|
||||
// Accumulate any errors and warnings
|
||||
var warnings []string
|
||||
var errs *packer.MultiError
|
||||
|
||||
vncWarnings, vncErrs := c.BootConfigWrapper.Prepare(&c.ctx, &c.DriverConfig)
|
||||
warnings = append(warnings, vncWarnings...)
|
||||
errs = packer.MultiErrorAppend(errs, vncErrs...)
|
||||
errs = packer.MultiErrorAppend(errs, c.DriverConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx, &c.BootConfigWrapper)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.SSHConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.ToolsConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.VMXConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...)
|
||||
errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...)
|
||||
@@ -101,16 +104,10 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
fmt.Errorf("source_path is invalid: %s", err))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Remote configuration validation
|
||||
if c.RemoteHost == "" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("remote_host must be specified"))
|
||||
}
|
||||
|
||||
if c.RemoteType != "esx5" {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Only 'esx5' value is accepted for remote_type"))
|
||||
if c.Headless && c.DisableVNC {
|
||||
warnings = append(warnings,
|
||||
"Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+
|
||||
"you won't be able to see any output.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,14 +121,14 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
}
|
||||
|
||||
if c.Format == "" {
|
||||
if c.RemoteType != "esx5" {
|
||||
if c.RemoteType == "" {
|
||||
c.Format = "vmx"
|
||||
} else {
|
||||
c.Format = "ovf"
|
||||
}
|
||||
}
|
||||
|
||||
if c.RemoteType != "esx5" && c.Format == "vmx" {
|
||||
if c.RemoteType == "" && c.Format == "vmx" {
|
||||
// if we're building locally and want a vmx, there's nothing to export.
|
||||
// Set skip export flag here to keep the export step from attempting
|
||||
// an unneded export
|
||||
@@ -143,28 +140,12 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
errs = packer.MultiErrorAppend(errs, err)
|
||||
}
|
||||
|
||||
if c.Format == "" {
|
||||
if c.RemoteType != "esx5" {
|
||||
c.Format = "vmx"
|
||||
} else {
|
||||
c.Format = "ovf"
|
||||
}
|
||||
}
|
||||
|
||||
// Warnings
|
||||
var warnings []string
|
||||
if c.ShutdownCommand == "" {
|
||||
warnings = append(warnings,
|
||||
"A shutdown_command was not specified. Without a shutdown command, Packer\n"+
|
||||
"will forcibly halt the virtual machine, which may result in data loss.")
|
||||
}
|
||||
|
||||
if c.Headless && c.DisableVNC {
|
||||
warnings = append(warnings,
|
||||
"Headless mode uses VNC to retrieve output. Since VNC has been disabled,\n"+
|
||||
"you won't be able to see any output.")
|
||||
}
|
||||
|
||||
// Check for any errors.
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return warnings, errs
|
||||
|
||||
@@ -20,6 +20,7 @@ type FlatConfig struct {
|
||||
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"`
|
||||
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
|
||||
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
|
||||
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
|
||||
@@ -28,6 +29,7 @@ type FlatConfig struct {
|
||||
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
|
||||
DisableVNC *bool `mapstructure:"disable_vnc" cty:"disable_vnc" hcl:"disable_vnc"`
|
||||
BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"`
|
||||
USBKeyBoard *bool `mapstructure:"usb_keyboard" cty:"usb_keyboard" hcl:"usb_keyboard"`
|
||||
CleanUpRemoteCache *bool `mapstructure:"cleanup_remote_cache" required:"false" cty:"cleanup_remote_cache" hcl:"cleanup_remote_cache"`
|
||||
FusionAppPath *string `mapstructure:"fusion_app_path" required:"false" cty:"fusion_app_path" hcl:"fusion_app_path"`
|
||||
RemoteType *string `mapstructure:"remote_type" required:"false" cty:"remote_type" hcl:"remote_type"`
|
||||
@@ -140,6 +142,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
|
||||
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
|
||||
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
|
||||
@@ -148,6 +151,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false},
|
||||
"disable_vnc": &hcldec.AttrSpec{Name: "disable_vnc", Type: cty.Bool, Required: false},
|
||||
"boot_key_interval": &hcldec.AttrSpec{Name: "boot_key_interval", Type: cty.String, Required: false},
|
||||
"usb_keyboard": &hcldec.AttrSpec{Name: "usb_keyboard", Type: cty.Bool, Required: false},
|
||||
"cleanup_remote_cache": &hcldec.AttrSpec{Name: "cleanup_remote_cache", Type: cty.Bool, Required: false},
|
||||
"fusion_app_path": &hcldec.AttrSpec{Name: "fusion_app_path", Type: cty.String, Required: false},
|
||||
"remote_type": &hcldec.AttrSpec{Name: "remote_type", Type: cty.String, Required: false},
|
||||
|
||||
@@ -58,3 +58,120 @@ func TestNewConfig_sourcePath(t *testing.T) {
|
||||
warns, errs = (&Config{}).Prepare(cfg)
|
||||
testConfigOk(t, warns, errs)
|
||||
}
|
||||
|
||||
func TestNewConfig_exportConfig(t *testing.T) {
|
||||
type testCase struct {
|
||||
InputConfigVals map[string]string
|
||||
ExpectedSkipExportValue bool
|
||||
ExpectedFormat string
|
||||
ExpectedErr bool
|
||||
Reason string
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "",
|
||||
"format": "",
|
||||
},
|
||||
ExpectedSkipExportValue: true,
|
||||
ExpectedFormat: "vmx",
|
||||
ExpectedErr: false,
|
||||
Reason: "should have defaulted format to vmx.",
|
||||
},
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "esx5",
|
||||
"format": "",
|
||||
"remote_host": "fakehost.com",
|
||||
"remote_password": "fakepassword",
|
||||
"remote_username": "fakeuser",
|
||||
},
|
||||
ExpectedSkipExportValue: false,
|
||||
ExpectedFormat: "ovf",
|
||||
ExpectedErr: false,
|
||||
Reason: "should have defaulted format to ovf with remote set to esx5.",
|
||||
},
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "esx5",
|
||||
"format": "",
|
||||
},
|
||||
ExpectedSkipExportValue: false,
|
||||
ExpectedFormat: "ovf",
|
||||
ExpectedErr: true,
|
||||
Reason: "should have errored because remote host isn't set for remote build.",
|
||||
},
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "invalid",
|
||||
"format": "",
|
||||
"remote_host": "fakehost.com",
|
||||
"remote_password": "fakepassword",
|
||||
"remote_username": "fakeuser",
|
||||
},
|
||||
ExpectedSkipExportValue: false,
|
||||
ExpectedFormat: "ovf",
|
||||
ExpectedErr: true,
|
||||
Reason: "should error with invalid remote type",
|
||||
},
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "",
|
||||
"format": "invalid",
|
||||
},
|
||||
ExpectedSkipExportValue: false,
|
||||
ExpectedFormat: "invalid",
|
||||
ExpectedErr: true,
|
||||
Reason: "should error with invalid format",
|
||||
},
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "",
|
||||
"format": "ova",
|
||||
},
|
||||
ExpectedSkipExportValue: false,
|
||||
ExpectedFormat: "ova",
|
||||
ExpectedErr: false,
|
||||
Reason: "should set user-given ova format",
|
||||
},
|
||||
{
|
||||
InputConfigVals: map[string]string{
|
||||
"remote_type": "esx5",
|
||||
"format": "ova",
|
||||
"remote_host": "fakehost.com",
|
||||
"remote_password": "fakepassword",
|
||||
"remote_username": "fakeuser",
|
||||
},
|
||||
ExpectedSkipExportValue: false,
|
||||
ExpectedFormat: "ova",
|
||||
ExpectedErr: false,
|
||||
Reason: "should set user-given ova format",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cfg := testConfig(t)
|
||||
for k, v := range tc.InputConfigVals {
|
||||
cfg[k] = v
|
||||
}
|
||||
cfg["skip_validate_credentials"] = true
|
||||
outCfg := &Config{}
|
||||
warns, errs := (outCfg).Prepare(cfg)
|
||||
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
|
||||
if (errs != nil) != tc.ExpectedErr {
|
||||
t.Fatalf("received error: \n %s \n but 'expected err' was %t", errs, tc.ExpectedErr)
|
||||
}
|
||||
|
||||
if outCfg.Format != tc.ExpectedFormat {
|
||||
t.Fatalf("Expected: %s. Actual: %s. Reason: %s", tc.ExpectedFormat,
|
||||
outCfg.Format, tc.Reason)
|
||||
}
|
||||
if outCfg.SkipExport != tc.ExpectedSkipExportValue {
|
||||
t.Fatalf("For SkipExport expected %t but recieved %t",
|
||||
tc.ExpectedSkipExportValue, outCfg.SkipExport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
||||
}
|
||||
artifact := &common.Artifact{
|
||||
Name: b.config.VMName,
|
||||
VM: state.Get("vm").(*driver.VirtualMachine),
|
||||
VM: state.Get("vm").(*driver.VirtualMachineDriver),
|
||||
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
|
||||
}
|
||||
if b.config.Export != nil {
|
||||
|
||||
@@ -21,6 +21,7 @@ type FlatConfig struct {
|
||||
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"`
|
||||
VCenterServer *string `mapstructure:"vcenter_server" cty:"vcenter_server" hcl:"vcenter_server"`
|
||||
Username *string `mapstructure:"username" cty:"username" hcl:"username"`
|
||||
Password *string `mapstructure:"password" cty:"password" hcl:"password"`
|
||||
@@ -30,6 +31,7 @@ type FlatConfig struct {
|
||||
DiskSize *int64 `mapstructure:"disk_size" cty:"disk_size" hcl:"disk_size"`
|
||||
LinkedClone *bool `mapstructure:"linked_clone" cty:"linked_clone" hcl:"linked_clone"`
|
||||
Network *string `mapstructure:"network" cty:"network" hcl:"network"`
|
||||
MacAddress *string `mapstructure:"mac_address" cty:"mac_address" hcl:"mac_address"`
|
||||
Notes *string `mapstructure:"notes" cty:"notes" hcl:"notes"`
|
||||
VAppConfig *FlatvAppConfig `mapstructure:"vapp" cty:"vapp" hcl:"vapp"`
|
||||
VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"`
|
||||
@@ -144,6 +146,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"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},
|
||||
"vcenter_server": &hcldec.AttrSpec{Name: "vcenter_server", Type: cty.String, Required: false},
|
||||
"username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false},
|
||||
"password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false},
|
||||
@@ -153,6 +156,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.Number, Required: false},
|
||||
"linked_clone": &hcldec.AttrSpec{Name: "linked_clone", Type: cty.Bool, Required: false},
|
||||
"network": &hcldec.AttrSpec{Name: "network", Type: cty.String, Required: false},
|
||||
"mac_address": &hcldec.AttrSpec{Name: "mac_address", Type: cty.String, Required: false},
|
||||
"notes": &hcldec.AttrSpec{Name: "notes", Type: cty.String, Required: false},
|
||||
"vapp": &hcldec.BlockSpec{TypeName: "vapp", Nested: hcldec.ObjectSpec((*FlatvAppConfig)(nil).HCL2Spec())},
|
||||
"vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false},
|
||||
|
||||
@@ -37,6 +37,8 @@ type CloneConfig struct {
|
||||
// available network. If the network is inside a network folder in vCenter,
|
||||
// you need to provide the full path to the network.
|
||||
Network string `mapstructure:"network"`
|
||||
// Sets a custom Mac Address to the network adapter. If set, the [network](#network) must be also specified.
|
||||
MacAddress string `mapstructure:"mac_address"`
|
||||
// VM notes.
|
||||
Notes string `mapstructure:"notes"`
|
||||
// Set the vApp Options to a virtual machine.
|
||||
@@ -56,6 +58,10 @@ func (c *CloneConfig) Prepare() []error {
|
||||
errs = append(errs, fmt.Errorf("'linked_clone' and 'disk_size' cannot be used together"))
|
||||
}
|
||||
|
||||
if c.MacAddress != "" && c.Network == "" {
|
||||
errs = append(errs, fmt.Errorf("'network' is required when 'mac_address' is specified"))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -67,7 +73,7 @@ type StepCloneVM struct {
|
||||
|
||||
func (s *StepCloneVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
d := state.Get("driver").(*driver.Driver)
|
||||
d := state.Get("driver").(*driver.VCenterDriver)
|
||||
vmPath := path.Join(s.Location.Folder, s.Location.VMName)
|
||||
|
||||
err := d.PreCleanVM(ui, vmPath, s.Force)
|
||||
@@ -92,6 +98,7 @@ func (s *StepCloneVM) Run(ctx context.Context, state multistep.StateBag) multist
|
||||
Datastore: s.Location.Datastore,
|
||||
LinkedClone: s.Config.LinkedClone,
|
||||
Network: s.Config.Network,
|
||||
MacAddress: s.Config.MacAddress,
|
||||
Annotation: s.Config.Notes,
|
||||
VAppProperties: s.Config.VAppConfig.Properties,
|
||||
})
|
||||
@@ -128,7 +135,7 @@ func (s *StepCloneVM) Cleanup(state multistep.StateBag) {
|
||||
if st == nil {
|
||||
return
|
||||
}
|
||||
vm := st.(*driver.VirtualMachine)
|
||||
vm := st.(*driver.VirtualMachineDriver)
|
||||
|
||||
ui.Say("Destroying VM...")
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ type FlatCloneConfig struct {
|
||||
DiskSize *int64 `mapstructure:"disk_size" cty:"disk_size" hcl:"disk_size"`
|
||||
LinkedClone *bool `mapstructure:"linked_clone" cty:"linked_clone" hcl:"linked_clone"`
|
||||
Network *string `mapstructure:"network" cty:"network" hcl:"network"`
|
||||
MacAddress *string `mapstructure:"mac_address" cty:"mac_address" hcl:"mac_address"`
|
||||
Notes *string `mapstructure:"notes" cty:"notes" hcl:"notes"`
|
||||
VAppConfig *FlatvAppConfig `mapstructure:"vapp" cty:"vapp" hcl:"vapp"`
|
||||
}
|
||||
@@ -33,6 +34,7 @@ func (*FlatCloneConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.Number, Required: false},
|
||||
"linked_clone": &hcldec.AttrSpec{Name: "linked_clone", Type: cty.Bool, Required: false},
|
||||
"network": &hcldec.AttrSpec{Name: "network", Type: cty.String, Required: false},
|
||||
"mac_address": &hcldec.AttrSpec{Name: "mac_address", Type: cty.String, Required: false},
|
||||
"notes": &hcldec.AttrSpec{Name: "notes", Type: cty.String, Required: false},
|
||||
"vapp": &hcldec.BlockSpec{TypeName: "vapp", Nested: hcldec.ObjectSpec((*FlatvAppConfig)(nil).HCL2Spec())},
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ func (c *CustomizeConfig) Prepare() []error {
|
||||
}
|
||||
|
||||
func (s *StepCustomize) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
identity, err := s.identitySettings()
|
||||
|
||||
@@ -11,7 +11,7 @@ const BuilderId = "jetbrains.vsphere"
|
||||
type Artifact struct {
|
||||
Outconfig *OutputConfig
|
||||
Name string
|
||||
VM *driver.VirtualMachine
|
||||
VM *driver.VirtualMachineDriver
|
||||
|
||||
// StateData should store data such as GeneratedData
|
||||
// to be shared with post-processors
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
func basicStateBag(errorBuffer *strings.Builder) *multistep.BasicStateBag {
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("ui", &packer.BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
ErrorWriter: errorBuffer,
|
||||
})
|
||||
return state
|
||||
}
|
||||
@@ -21,8 +21,8 @@ type LocationConfig struct {
|
||||
// the host is in a folder. For example `folder/host`. See the
|
||||
// `Specifying Clusters and Hosts` section above for more details.
|
||||
Host string `mapstructure:"host"`
|
||||
// VMWare resource pool. Defaults to the root resource pool of the
|
||||
// `host` or `cluster`.
|
||||
// VMWare resource pool. If not set, it will look for the root resource pool of the `host` or `cluster`.
|
||||
// If a root resource is not found, it will then look for a default resource pool.
|
||||
ResourcePool string `mapstructure:"resource_pool"`
|
||||
// VMWare datastore. Required if `host` is a cluster, or if `host` has
|
||||
// multiple datastores.
|
||||
|
||||
@@ -43,7 +43,7 @@ type StepBootCommand struct {
|
||||
func (s *StepBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
debug := state.Get("debug").(bool)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
||||
|
||||
if s.Config.BootCommand == nil {
|
||||
return multistep.ActionContinue
|
||||
|
||||
@@ -33,7 +33,7 @@ type StepConfigParams struct {
|
||||
|
||||
func (s *StepConfigParams) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
||||
configParams := make(map[string]string)
|
||||
|
||||
if s.Config.ConfigParams != nil {
|
||||
|
||||
@@ -154,7 +154,7 @@ func (s *StepExport) Cleanup(multistep.StateBag) {
|
||||
|
||||
func (s *StepExport) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
||||
|
||||
ui.Message("Starting export...")
|
||||
lease, err := vm.Export()
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
type HardwareConfig struct {
|
||||
// Number of CPU sockets.
|
||||
// Number of CPU cores.
|
||||
CPUs int32 `mapstructure:"CPUs"`
|
||||
// Number of CPU cores per socket.
|
||||
CpuCores int32 `mapstructure:"cpu_cores"`
|
||||
@@ -65,7 +65,7 @@ type StepConfigureHardware struct {
|
||||
|
||||
func (s *StepConfigureHardware) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||
vm := state.Get("vm").(driver.VirtualMachine)
|
||||
|
||||
if *s.Config != (HardwareConfig{}) {
|
||||
ui.Say("Customizing hardware...")
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/packer/builder/vsphere/driver"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
)
|
||||
|
||||
func TestHardwareConfig_Prepare(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
config *HardwareConfig
|
||||
fail bool
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "Validate empty config",
|
||||
config: &HardwareConfig{},
|
||||
fail: false,
|
||||
},
|
||||
{
|
||||
name: "Validate RAMReservation RAMReserveAll cannot be used together",
|
||||
config: &HardwareConfig{
|
||||
RAMReservation: 2,
|
||||
RAMReserveAll: true,
|
||||
},
|
||||
fail: true,
|
||||
expectedErrMsg: "'RAM_reservation' and 'RAM_reserve_all' cannot be used together",
|
||||
},
|
||||
{
|
||||
name: "Invalid firmware",
|
||||
config: &HardwareConfig{
|
||||
Firmware: "invalid",
|
||||
},
|
||||
fail: true,
|
||||
expectedErrMsg: "'firmware' must be '', 'bios', 'efi' or 'efi-secure'",
|
||||
},
|
||||
{
|
||||
name: "Validate 'bios' firmware",
|
||||
config: &HardwareConfig{
|
||||
Firmware: "bios",
|
||||
},
|
||||
fail: false,
|
||||
},
|
||||
{
|
||||
name: "Validate 'efi' firmware",
|
||||
config: &HardwareConfig{
|
||||
Firmware: "efi",
|
||||
},
|
||||
fail: false,
|
||||
},
|
||||
{
|
||||
name: "Validate 'efi-secure' firmware",
|
||||
config: &HardwareConfig{
|
||||
Firmware: "efi-secure",
|
||||
},
|
||||
fail: false,
|
||||
},
|
||||
}
|
||||
for _, c := range tc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
errs := c.config.Prepare()
|
||||
if c.fail {
|
||||
if len(errs) == 0 {
|
||||
t.Fatalf("Config preprare should fail")
|
||||
}
|
||||
if errs[0].Error() != c.expectedErrMsg {
|
||||
t.Fatalf("Expected error message: %s but was '%s'", c.expectedErrMsg, errs[0].Error())
|
||||
}
|
||||
} else {
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("Config preprare should not fail")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepConfigureHardware_Run(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
step *StepConfigureHardware
|
||||
action multistep.StepAction
|
||||
configureError error
|
||||
configureCalled bool
|
||||
hardwareConfig *driver.HardwareConfig
|
||||
}{
|
||||
{
|
||||
name: "Configure hardware",
|
||||
step: basicStepConfigureHardware(),
|
||||
action: multistep.ActionContinue,
|
||||
configureError: nil,
|
||||
configureCalled: true,
|
||||
hardwareConfig: driverHardwareConfigFromConfig(basicStepConfigureHardware().Config),
|
||||
},
|
||||
{
|
||||
name: "Don't configure hardware when config is empty",
|
||||
step: &StepConfigureHardware{Config: &HardwareConfig{}},
|
||||
action: multistep.ActionContinue,
|
||||
configureError: nil,
|
||||
configureCalled: false,
|
||||
},
|
||||
{
|
||||
name: "Halt when configure return error",
|
||||
step: basicStepConfigureHardware(),
|
||||
action: multistep.ActionHalt,
|
||||
configureError: errors.New("failed to configure"),
|
||||
configureCalled: true,
|
||||
hardwareConfig: driverHardwareConfigFromConfig(basicStepConfigureHardware().Config),
|
||||
},
|
||||
}
|
||||
for _, c := range tc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
state := basicStateBag(nil)
|
||||
vmMock := new(driver.VirtualMachineMock)
|
||||
vmMock.ConfigureError = c.configureError
|
||||
state.Put("vm", vmMock)
|
||||
|
||||
action := c.step.Run(context.TODO(), state)
|
||||
if action != c.action {
|
||||
t.Fatalf("expected action '%v' but actual action was '%v'", c.action, action)
|
||||
}
|
||||
if vmMock.ConfigureCalled != c.configureCalled {
|
||||
t.Fatalf("expecting vm.Configure called to %t but was %t", c.configureCalled, vmMock.ConfigureCalled)
|
||||
}
|
||||
if diff := cmp.Diff(vmMock.ConfigureHardwareConfig, c.hardwareConfig); diff != "" {
|
||||
t.Fatalf("wrong driver.HardwareConfig: %s", diff)
|
||||
}
|
||||
|
||||
err, ok := state.GetOk("error")
|
||||
containsError := c.configureError != nil
|
||||
if containsError != ok {
|
||||
t.Fatalf("Contain error - expecting %t but was %t", containsError, ok)
|
||||
}
|
||||
if containsError {
|
||||
if !strings.Contains(err.(error).Error(), c.configureError.Error()) {
|
||||
t.Fatalf("Destroy should fail with error message '%s' but failed with '%s'", c.configureError.Error(), err.(error).Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func basicStepConfigureHardware() *StepConfigureHardware {
|
||||
return &StepConfigureHardware{
|
||||
Config: &HardwareConfig{
|
||||
CPUs: 1,
|
||||
CpuCores: 1,
|
||||
CPUReservation: 1,
|
||||
CPULimit: 4000,
|
||||
RAM: 1024,
|
||||
RAMReserveAll: true,
|
||||
Firmware: "efi-secure",
|
||||
ForceBIOSSetup: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func driverHardwareConfigFromConfig(config *HardwareConfig) *driver.HardwareConfig {
|
||||
return &driver.HardwareConfig{
|
||||
CPUs: config.CPUs,
|
||||
CpuCores: config.CpuCores,
|
||||
CPUReservation: config.CPUReservation,
|
||||
CPULimit: config.CPULimit,
|
||||
RAM: config.RAM,
|
||||
RAMReservation: config.RAMReservation,
|
||||
RAMReserveAll: config.RAMReserveAll,
|
||||
NestedHV: config.NestedHV,
|
||||
CpuHotAddEnabled: config.CpuHotAddEnabled,
|
||||
MemoryHotAddEnabled: config.MemoryHotAddEnabled,
|
||||
VideoRAM: config.VideoRAM,
|
||||
VGPUProfile: config.VGPUProfile,
|
||||
Firmware: config.Firmware,
|
||||
ForceBIOSSetup: config.ForceBIOSSetup,
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@ type StepImportToContentLibrary struct {
|
||||
|
||||
func (s *StepImportToContentLibrary) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
||||
var err error
|
||||
|
||||
if s.ContentLibConfig.Ovf {
|
||||
@@ -142,7 +142,7 @@ func (s *StepImportToContentLibrary) Run(_ context.Context, state multistep.Stat
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepImportToContentLibrary) importOvfTemplate(vm *driver.VirtualMachine) error {
|
||||
func (s *StepImportToContentLibrary) importOvfTemplate(vm *driver.VirtualMachineDriver) error {
|
||||
ovf := vcenter.OVF{
|
||||
Spec: vcenter.CreateSpec{
|
||||
Name: s.ContentLibConfig.Name,
|
||||
@@ -154,7 +154,7 @@ func (s *StepImportToContentLibrary) importOvfTemplate(vm *driver.VirtualMachine
|
||||
return vm.ImportOvfToContentLibrary(ovf)
|
||||
}
|
||||
|
||||
func (s *StepImportToContentLibrary) importVmTemplate(vm *driver.VirtualMachine) error {
|
||||
func (s *StepImportToContentLibrary) importVmTemplate(vm *driver.VirtualMachineDriver) error {
|
||||
template := vcenter.Template{
|
||||
Name: s.ContentLibConfig.Name,
|
||||
Description: s.ContentLibConfig.Description,
|
||||
|
||||
@@ -24,7 +24,7 @@ type StepRun struct {
|
||||
|
||||
func (s *StepRun) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
||||
|
||||
if s.Config.BootOrder != "" {
|
||||
ui.Say("Set boot order...")
|
||||
@@ -55,7 +55,7 @@ func (s *StepRun) Run(_ context.Context, state multistep.StateBag) multistep.Ste
|
||||
|
||||
func (s *StepRun) Cleanup(state multistep.StateBag) {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
||||
|
||||
if s.Config.BootOrder == "" && s.SetOrder {
|
||||
ui.Say("Clear boot order...")
|
||||
|
||||
@@ -48,7 +48,7 @@ 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.VirtualMachine)
|
||||
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
||||
|
||||
if off, _ := vm.IsPoweredOff(); off {
|
||||
// Probably power off initiated by last provisioner, though disable_shutdown is not set
|
||||
|
||||
@@ -14,7 +14,7 @@ type StepCreateSnapshot struct {
|
||||
|
||||
func (s *StepCreateSnapshot) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
||||
|
||||
if s.CreateSnapshot {
|
||||
ui.Say("Creating snapshot...")
|
||||
|
||||
@@ -81,7 +81,7 @@ func (s *StepSshKeyPair) Run(ctx context.Context, state multistep.StateBag) mult
|
||||
s.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine
|
||||
s.Comm.SSHClearAuthorizedKeys = true
|
||||
|
||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
||||
err = vm.AddPublicKeys(ctx, string(s.Comm.SSHPublicKey))
|
||||
if err != nil {
|
||||
state.Put("error", fmt.Errorf("error saving temporary keypair in the vm: %s", err))
|
||||
|
||||
@@ -14,7 +14,7 @@ type StepConvertToTemplate struct {
|
||||
|
||||
func (s *StepConvertToTemplate) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
||||
|
||||
if s.ConvertToTemplate {
|
||||
ui.Say("Convert VM into template...")
|
||||
|
||||
@@ -75,7 +75,7 @@ func (c *WaitIpConfig) GetIPNet() *net.IPNet {
|
||||
|
||||
func (s *StepWaitForIp) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
vm := state.Get("vm").(*driver.VirtualMachine)
|
||||
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
||||
|
||||
var ip string
|
||||
var err error
|
||||
@@ -128,7 +128,7 @@ func (s *StepWaitForIp) Run(ctx context.Context, state multistep.StateBag) multi
|
||||
}
|
||||
}
|
||||
|
||||
func doGetIp(vm *driver.VirtualMachine, ctx context.Context, c *WaitIpConfig) (string, error) {
|
||||
func doGetIp(vm *driver.VirtualMachineDriver, ctx context.Context, c *WaitIpConfig) (string, error) {
|
||||
var prevIp = ""
|
||||
var stopTime time.Time
|
||||
var interval time.Duration
|
||||
|
||||
@@ -34,7 +34,7 @@ func RenderConfig(config map[string]interface{}) string {
|
||||
return string(j)
|
||||
}
|
||||
|
||||
func TestConn(t *testing.T) *driver.Driver {
|
||||
func TestConn(t *testing.T) driver.Driver {
|
||||
username := os.Getenv("VSPHERE_USERNAME")
|
||||
if username == "" {
|
||||
username = "root"
|
||||
@@ -56,7 +56,7 @@ func TestConn(t *testing.T) *driver.Driver {
|
||||
return d
|
||||
}
|
||||
|
||||
func GetVM(t *testing.T, d *driver.Driver, artifacts []packer.Artifact) *driver.VirtualMachine {
|
||||
func GetVM(t *testing.T, d driver.Driver, artifacts []packer.Artifact) driver.VirtualMachine {
|
||||
artifactRaw := artifacts[0]
|
||||
artifact, _ := artifactRaw.(*common.Artifact)
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ package driver
|
||||
import "github.com/vmware/govmomi/object"
|
||||
|
||||
type Cluster struct {
|
||||
driver *Driver
|
||||
driver *VCenterDriver
|
||||
cluster *object.ClusterComputeResource
|
||||
}
|
||||
|
||||
func (d *Driver) FindCluster(name string) (*Cluster, error) {
|
||||
func (d *VCenterDriver) FindCluster(name string) (*Cluster, error) {
|
||||
c, err := d.finder.ClusterComputeResource(d.ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -13,20 +13,31 @@ import (
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
)
|
||||
|
||||
type Datastore struct {
|
||||
ds *object.Datastore
|
||||
driver *Driver
|
||||
type Datastore interface {
|
||||
Info(params ...string) (*mo.Datastore, error)
|
||||
FileExists(path string) bool
|
||||
Name() string
|
||||
ResolvePath(path string) string
|
||||
UploadFile(src, dst, host string, setHost bool) error
|
||||
Delete(path string) error
|
||||
MakeDirectory(path string) error
|
||||
Reference() types.ManagedObjectReference
|
||||
}
|
||||
|
||||
func (d *Driver) NewDatastore(ref *types.ManagedObjectReference) *Datastore {
|
||||
return &Datastore{
|
||||
type DatastoreDriver struct {
|
||||
ds *object.Datastore
|
||||
driver *VCenterDriver
|
||||
}
|
||||
|
||||
func (d *VCenterDriver) NewDatastore(ref *types.ManagedObjectReference) Datastore {
|
||||
return &DatastoreDriver{
|
||||
ds: object.NewDatastore(d.client.Client, *ref),
|
||||
driver: d,
|
||||
}
|
||||
}
|
||||
|
||||
// If name is an empty string, then resolve host's one
|
||||
func (d *Driver) FindDatastore(name string, host string) (*Datastore, error) {
|
||||
func (d *VCenterDriver) FindDatastore(name string, host string) (Datastore, error) {
|
||||
if name == "" {
|
||||
h, err := d.FindHost(host)
|
||||
if err != nil {
|
||||
@@ -55,13 +66,13 @@ func (d *Driver) FindDatastore(name string, host string) (*Datastore, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Datastore{
|
||||
return &DatastoreDriver{
|
||||
ds: ds,
|
||||
driver: d,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Driver) GetDatastoreName(id string) (string, error) {
|
||||
func (d *VCenterDriver) GetDatastoreName(id string) (string, error) {
|
||||
obj := types.ManagedObjectReference{
|
||||
Type: "Datastore",
|
||||
Value: id,
|
||||
@@ -76,7 +87,7 @@ func (d *Driver) GetDatastoreName(id string) (string, error) {
|
||||
return me.Name, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) Info(params ...string) (*mo.Datastore, error) {
|
||||
func (ds *DatastoreDriver) Info(params ...string) (*mo.Datastore, error) {
|
||||
var p []string
|
||||
if len(params) == 0 {
|
||||
p = []string{"*"}
|
||||
@@ -91,21 +102,25 @@ func (ds *Datastore) Info(params ...string) (*mo.Datastore, error) {
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) FileExists(path string) bool {
|
||||
func (ds *DatastoreDriver) FileExists(path string) bool {
|
||||
_, err := ds.ds.Stat(ds.driver.ctx, path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) Name() string {
|
||||
func (ds *DatastoreDriver) Name() string {
|
||||
return ds.ds.Name()
|
||||
}
|
||||
|
||||
func (ds *Datastore) ResolvePath(path string) string {
|
||||
func (ds *DatastoreDriver) Reference() types.ManagedObjectReference {
|
||||
return ds.ds.Reference()
|
||||
}
|
||||
|
||||
func (ds *DatastoreDriver) ResolvePath(path string) string {
|
||||
return ds.ds.Path(path)
|
||||
}
|
||||
|
||||
// The file ID isn't available via the API, so we use DatastoreBrowser to search
|
||||
func (d *Driver) GetDatastoreFilePath(datastoreID, dir, filename string) (string, error) {
|
||||
func (d *VCenterDriver) GetDatastoreFilePath(datastoreID, dir, filename string) (string, error) {
|
||||
ref := types.ManagedObjectReference{Type: "Datastore", Value: datastoreID}
|
||||
ds := object.NewDatastore(d.vimClient, ref)
|
||||
|
||||
@@ -140,11 +155,11 @@ func (d *Driver) GetDatastoreFilePath(datastoreID, dir, filename string) (string
|
||||
return res.File[0].GetFileInfo().Path, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) UploadFile(src, dst, host string, set_host_for_datastore_uploads bool) error {
|
||||
func (ds *DatastoreDriver) UploadFile(src, dst, host string, setHost bool) error {
|
||||
p := soap.DefaultUpload
|
||||
ctx := ds.driver.ctx
|
||||
|
||||
if set_host_for_datastore_uploads && host != "" {
|
||||
if setHost && host != "" {
|
||||
h, err := ds.driver.FindHost(host)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -155,7 +170,7 @@ func (ds *Datastore) UploadFile(src, dst, host string, set_host_for_datastore_up
|
||||
return ds.ds.UploadFile(ctx, src, dst, &p)
|
||||
}
|
||||
|
||||
func (ds *Datastore) Delete(path string) error {
|
||||
func (ds *DatastoreDriver) Delete(path string) error {
|
||||
dc, err := ds.driver.finder.Datacenter(ds.driver.ctx, ds.ds.DatacenterPath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -164,7 +179,7 @@ func (ds *Datastore) Delete(path string) error {
|
||||
return fm.Delete(ds.driver.ctx, path)
|
||||
}
|
||||
|
||||
func (ds *Datastore) MakeDirectory(path string) error {
|
||||
func (ds *DatastoreDriver) MakeDirectory(path string) error {
|
||||
dc, err := ds.driver.finder.Datacenter(ds.driver.ctx, ds.ds.DatacenterPath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -193,8 +208,9 @@ func (d *DatastoreIsoPath) Validate() bool {
|
||||
// [datastore] /dir/subdir/file
|
||||
// [datastore] dir/subdir/file
|
||||
// [] /dir/subdir/file
|
||||
// /dir/subdir/file or dir/subdir/file
|
||||
matched, _ := regexp.MatchString(`^((\[\w*\])?\s*([^\[\]]+))$`, d.path)
|
||||
// [data-store] /dir/subdir/file
|
||||
// dir/subdir/file or dir/subdir/file
|
||||
matched, _ := regexp.MatchString(`^\s*(\[[^\[\]\/]*\])?\s*[^\[\]]+\s*$`, d.path)
|
||||
return matched
|
||||
}
|
||||
|
||||
@@ -204,7 +220,7 @@ func (d *DatastoreIsoPath) GetFilePath() string {
|
||||
if len(parts) > 1 {
|
||||
// removes datastore name from path
|
||||
filePath = parts[1]
|
||||
filePath = strings.TrimLeft(filePath, " ")
|
||||
filePath = strings.TrimSpace(filePath)
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"github.com/vmware/govmomi/vim25/mo"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
)
|
||||
|
||||
type DatastoreMock struct {
|
||||
FileExistsCalled bool
|
||||
MakeDirectoryCalled bool
|
||||
UploadFileCalled bool
|
||||
}
|
||||
|
||||
func (ds *DatastoreMock) Info(params ...string) (*mo.Datastore, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (ds *DatastoreMock) FileExists(path string) bool {
|
||||
ds.FileExistsCalled = true
|
||||
return false
|
||||
}
|
||||
|
||||
func (ds *DatastoreMock) Name() string {
|
||||
return "datastore-mock"
|
||||
}
|
||||
|
||||
func (ds *DatastoreMock) Reference() types.ManagedObjectReference {
|
||||
return types.ManagedObjectReference{}
|
||||
}
|
||||
|
||||
func (ds *DatastoreMock) ResolvePath(path string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ds *DatastoreMock) UploadFile(src, dst, host string, setHost bool) error {
|
||||
ds.UploadFileCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *DatastoreMock) Delete(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *DatastoreMock) MakeDirectory(path string) error {
|
||||
ds.MakeDirectoryCalled = true
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user