Add support for optionally building Azure VMs with additional disks.

This commit is contained in:
jmajoor 2018-02-23 15:34:13 -08:00
parent 487b1d7167
commit efcdbfeab9
21 changed files with 1689 additions and 2 deletions

View File

@ -12,6 +12,11 @@ const (
BuilderId = "Azure.ResourceManagement.VMImage"
)
type AdditionalDiskArtifact struct {
AdditionalDiskUri string
AdditionalDiskUriReadOnlySas string
}
type Artifact struct {
// VHD
StorageAccountLocation string
@ -24,6 +29,9 @@ type Artifact struct {
ManagedImageResourceGroupName string
ManagedImageName string
ManagedImageLocation string
// Additional Disks
AdditionalDisks *[]AdditionalDiskArtifact
}
func NewManagedImageArtifact(resourceGroup, name, location string) (*Artifact, error) {
@ -53,12 +61,28 @@ func NewArtifact(template *CaptureTemplate, getSasUrl func(name string) string)
return nil, err
}
var additional_disks *[]AdditionalDiskArtifact
if template.Resources[0].Properties.StorageProfile.DataDisks != nil {
data_disks := make([]AdditionalDiskArtifact, len(template.Resources[0].Properties.StorageProfile.DataDisks))
for i, additionaldisk := range template.Resources[0].Properties.StorageProfile.DataDisks {
additionalVhdUri, err := url.Parse(additionaldisk.Image.Uri)
if err != nil {
return nil, err
}
data_disks[i].AdditionalDiskUri = additionalVhdUri.String()
data_disks[i].AdditionalDiskUriReadOnlySas = getSasUrl(getStorageUrlPath(additionalVhdUri))
}
additional_disks = &data_disks
}
return &Artifact{
OSDiskUri: vhdUri.String(),
OSDiskUriReadOnlySas: getSasUrl(getStorageUrlPath(vhdUri)),
TemplateUri: templateUri.String(),
TemplateUriReadOnlySas: getSasUrl(getStorageUrlPath(templateUri)),
AdditionalDisks: additional_disks,
StorageAccountLocation: template.Resources[0].Location,
}, nil
}
@ -128,6 +152,12 @@ func (a *Artifact) String() string {
buf.WriteString(fmt.Sprintf("OSDiskUriReadOnlySas: %s\n", a.OSDiskUriReadOnlySas))
buf.WriteString(fmt.Sprintf("TemplateUri: %s\n", a.TemplateUri))
buf.WriteString(fmt.Sprintf("TemplateUriReadOnlySas: %s\n", a.TemplateUriReadOnlySas))
if a.AdditionalDisks != nil {
for i, additionaldisk := range *a.AdditionalDisks {
buf.WriteString(fmt.Sprintf("AdditionalDiskUri (datadisk-%d): %s\n", i+1, additionaldisk.AdditionalDiskUri))
buf.WriteString(fmt.Sprintf("AdditionalDiskUriReadOnlySas (datadisk-%d): %s\n", i+1, additionaldisk.AdditionalDiskUriReadOnlySas))
}
}
}
return buf.String()

View File

@ -82,6 +82,60 @@ func TestArtifactString(t *testing.T) {
}
}
func TestAdditionalDiskArtifactString(t *testing.T) {
template := CaptureTemplate{
Resources: []CaptureResources{
{
Properties: CaptureProperties{
StorageProfile: CaptureStorageProfile{
OSDisk: CaptureDisk{
Image: CaptureUri{
Uri: "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd",
},
},
DataDisks: []CaptureDisk{
CaptureDisk{
Image: CaptureUri{
Uri: "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd",
},
},
},
},
},
Location: "southcentralus",
},
},
}
artifact, err := NewArtifact(&template, getFakeSasUrl)
if err != nil {
t.Fatalf("err=%s", err)
}
testSubject := artifact.String()
if !strings.Contains(testSubject, "OSDiskUri: https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd") {
t.Errorf("Expected String() output to contain OSDiskUri")
}
if !strings.Contains(testSubject, "OSDiskUriReadOnlySas: SAS-Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd") {
t.Errorf("Expected String() output to contain OSDiskUriReadOnlySas")
}
if !strings.Contains(testSubject, "TemplateUri: https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json") {
t.Errorf("Expected String() output to contain TemplateUri")
}
if !strings.Contains(testSubject, "TemplateUriReadOnlySas: SAS-Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json") {
t.Errorf("Expected String() output to contain TemplateUriReadOnlySas")
}
if !strings.Contains(testSubject, "StorageAccountLocation: southcentralus") {
t.Errorf("Expected String() output to contain StorageAccountLocation")
}
if !strings.Contains(testSubject, "AdditionalDiskUri (datadisk-1): https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd") {
t.Errorf("Expected String() output to contain AdditionalDiskUri")
}
if !strings.Contains(testSubject, "AdditionalDiskUriReadOnlySas (datadisk-1): SAS-Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd") {
t.Errorf("Expected String() output to contain AdditionalDiskUriReadOnlySas")
}
}
func TestArtifactProperties(t *testing.T) {
template := CaptureTemplate{
Resources: []CaptureResources{
@ -122,6 +176,65 @@ func TestArtifactProperties(t *testing.T) {
}
}
func TestAdditionalDiskArtifactProperties(t *testing.T) {
template := CaptureTemplate{
Resources: []CaptureResources{
{
Properties: CaptureProperties{
StorageProfile: CaptureStorageProfile{
OSDisk: CaptureDisk{
Image: CaptureUri{
Uri: "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd",
},
},
DataDisks: []CaptureDisk{
CaptureDisk{
Image: CaptureUri{
Uri: "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd",
},
},
},
},
},
Location: "southcentralus",
},
},
}
testSubject, err := NewArtifact(&template, getFakeSasUrl)
if err != nil {
t.Fatalf("err=%s", err)
}
if testSubject.OSDiskUri != "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd" {
t.Errorf("Expected template to be 'https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd', but got %s", testSubject.OSDiskUri)
}
if testSubject.OSDiskUriReadOnlySas != "SAS-Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd" {
t.Errorf("Expected template to be 'SAS-Images/images/packer-osDisk.4085bb15-3644-4641-b9cd-f575918640b4.vhd', but got %s", testSubject.OSDiskUriReadOnlySas)
}
if testSubject.TemplateUri != "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json" {
t.Errorf("Expected template to be 'https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json', but got %s", testSubject.TemplateUri)
}
if testSubject.TemplateUriReadOnlySas != "SAS-Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json" {
t.Errorf("Expected template to be 'SAS-Images/images/packer-vmTemplate.4085bb15-3644-4641-b9cd-f575918640b4.json', but got %s", testSubject.TemplateUriReadOnlySas)
}
if testSubject.StorageAccountLocation != "southcentralus" {
t.Errorf("Expected StorageAccountLocation to be 'southcentral', but got %s", testSubject.StorageAccountLocation)
}
if testSubject.AdditionalDisks == nil {
t.Errorf("Expected AdditionalDisks to be not nil")
}
if len(*testSubject.AdditionalDisks) != 1 {
t.Errorf("Expected AdditionalDisks to have one additional disk, but got %d", len(*testSubject.AdditionalDisks))
}
if (*testSubject.AdditionalDisks)[0].AdditionalDiskUri != "https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd" {
t.Errorf("Expected additional disk uri to be 'https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd', but got %s", (*testSubject.AdditionalDisks)[0].AdditionalDiskUri)
}
if (*testSubject.AdditionalDisks)[0].AdditionalDiskUriReadOnlySas != "SAS-Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd" {
t.Errorf("Expected additional disk sas to be 'SAS-Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd', but got %s", (*testSubject.AdditionalDisks)[0].AdditionalDiskUriReadOnlySas)
}
}
func TestArtifactOverHypenatedCaptureUri(t *testing.T) {
template := CaptureTemplate{
Resources: []CaptureResources{

View File

@ -181,10 +181,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
},
&packerCommon.StepProvision{},
NewStepGetOSDisk(azureClient, ui),
NewStepGetAdditionalDisks(azureClient, ui),
NewStepPowerOffCompute(azureClient, ui),
NewStepCaptureImage(azureClient, ui),
NewStepDeleteResourceGroup(azureClient, ui),
NewStepDeleteOSDisk(azureClient, ui),
NewStepDeleteAdditionalDisks(azureClient, ui),
}
} else {
return nil, fmt.Errorf("Builder does not support the os_type '%s'", b.config.OSType)

View File

@ -23,7 +23,8 @@ type CaptureDisk struct {
}
type CaptureStorageProfile struct {
OSDisk CaptureDisk `json:"osDisk"`
OSDisk CaptureDisk `json:"osDisk"`
DataDisks []CaptureDisk `json:"dataDisks"`
}
type CaptureOSProfile struct {

View File

@ -51,7 +51,33 @@ var captureTemplate01 = `{
"uri": "http://storage.blob.core.windows.net/vmcontainerce1a1b75-f480-47cb-8e6e-55142e4a5f68/osDisk.ce1a1b75-f480-47cb-8e6e-55142e4a5f68.vhd"
},
"caching": "ReadWrite"
}
},
"dataDisks": [
{
"lun": 0,
"name": "packer-datadisk-0.32118633-6dc9-449f-83b6-a7d2983bec14.vhd",
"createOption": "Empty",
"image": {
"uri": "http://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-0.32118633-6dc9-449f-83b6-a7d2983bec14.vhd"
},
"vhd": {
"uri": "http://storage.blob.core.windows.net/vmcontainerce1a1b75-f480-47cb-8e6e-55142e4a5f68/datadisk-0.ce1a1b75-f480-47cb-8e6e-55142e4a5f68.vhd"
},
"caching": "ReadWrite"
},
{
"lun": 1,
"name": "packer-datadisk-1.32118633-6dc9-449f-83b6-a7d2983bec14.vhd",
"createOption": "Empty",
"image": {
"uri": "http://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.32118633-6dc9-449f-83b6-a7d2983bec14.vhd"
},
"vhd": {
"uri": "http://storage.blob.core.windows.net/vmcontainerce1a1b75-f480-47cb-8e6e-55142e4a5f68/datadisk-1.ce1a1b75-f480-47cb-8e6e-55142e4a5f68.vhd"
},
"caching": "ReadWrite"
}
]
},
"osProfile": {
"computerName": "[parameters('vmName')]",
@ -169,6 +195,42 @@ func TestCaptureParseJson(t *testing.T) {
t.Errorf("Resources[0].Properties.StorageProfile.OSDisk.Caching's value was unexpected: %s", osDisk.Caching)
}
// == Resources/Properties/StorageProfile/DataDisks ================
dataDisks := testSubject.Resources[0].Properties.StorageProfile.DataDisks
if len(dataDisks) != 2 {
t.Errorf("Resources[0].Properties.StorageProfile.DataDisks, 2 disks expected but was: %d", len(dataDisks))
}
if dataDisks[0].Name != "packer-datadisk-0.32118633-6dc9-449f-83b6-a7d2983bec14.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[0].Name's value was unexpected: %s", dataDisks[0].Name)
}
if dataDisks[0].CreateOption != "Empty" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[0].CreateOption's value was unexpected: %s", dataDisks[0].CreateOption)
}
if dataDisks[0].Image.Uri != "http://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-0.32118633-6dc9-449f-83b6-a7d2983bec14.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[0].Image.Uri's value was unexpected: %s", dataDisks[0].Image.Uri)
}
if dataDisks[0].Vhd.Uri != "http://storage.blob.core.windows.net/vmcontainerce1a1b75-f480-47cb-8e6e-55142e4a5f68/datadisk-0.ce1a1b75-f480-47cb-8e6e-55142e4a5f68.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[0].Vhd.Uri's value was unexpected: %s", dataDisks[0].Vhd.Uri)
}
if dataDisks[0].Caching != "ReadWrite" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[0].Caching's value was unexpected: %s", dataDisks[0].Caching)
}
if dataDisks[1].Name != "packer-datadisk-1.32118633-6dc9-449f-83b6-a7d2983bec14.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[1].Name's value was unexpected: %s", dataDisks[1].Name)
}
if dataDisks[1].CreateOption != "Empty" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[1].CreateOption's value was unexpected: %s", dataDisks[1].CreateOption)
}
if dataDisks[1].Image.Uri != "http://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.32118633-6dc9-449f-83b6-a7d2983bec14.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[1].Image.Uri's value was unexpected: %s", dataDisks[1].Image.Uri)
}
if dataDisks[1].Vhd.Uri != "http://storage.blob.core.windows.net/vmcontainerce1a1b75-f480-47cb-8e6e-55142e4a5f68/datadisk-1.ce1a1b75-f480-47cb-8e6e-55142e4a5f68.vhd" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[1].Vhd.Uri's value was unexpected: %s", dataDisks[1].Vhd.Uri)
}
if dataDisks[1].Caching != "ReadWrite" {
t.Errorf("Resources[0].Properties.StorageProfile.dataDisks[1].Caching's value was unexpected: %s", dataDisks[1].Caching)
}
// == Resources/Properties/OSProfile ============================
osProfile := testSubject.Resources[0].Properties.OSProfile
if osProfile.AdminPassword != "[parameters('adminPassword')]" {

View File

@ -112,6 +112,9 @@ type Config struct {
OSType string `mapstructure:"os_type"`
OSDiskSizeGB int32 `mapstructure:"os_disk_size_gb"`
// Additional Disks
AdditionalDiskSize []int32 `mapstructure:"disk_additional_size"`
// Runtime Values
UserName string
Password string

View File

@ -0,0 +1,106 @@
package arm
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type StepDeleteAdditionalDisk struct {
client *AzureClient
delete func(string, string) error
deleteManaged func(string, string) error
say func(message string)
error func(e error)
}
func NewStepDeleteAdditionalDisks(client *AzureClient, ui packer.Ui) *StepDeleteAdditionalDisk {
var step = &StepDeleteAdditionalDisk{
client: client,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
}
step.delete = step.deleteBlob
step.deleteManaged = step.deleteManagedDisk
return step
}
func (s *StepDeleteAdditionalDisk) deleteBlob(storageContainerName string, blobName string) error {
blob := s.client.BlobStorageClient.GetContainerReference(storageContainerName).GetBlobReference(blobName)
err := blob.Delete(nil)
if err != nil {
s.say(s.client.LastError.Error())
}
return err
}
func (s *StepDeleteAdditionalDisk) deleteManagedDisk(resourceGroupName string, imageName string) error {
xs := strings.Split(imageName, "/")
diskName := xs[len(xs)-1]
_, errChan := s.client.DisksClient.Delete(resourceGroupName, diskName, nil)
err := <-errChan
return err
}
func (s *StepDeleteAdditionalDisk) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Deleting the temporary Additional disk ...")
var dataDisks = state.Get(constants.ArmAdditionalDiskVhds).([]string)
var isManagedDisk = state.Get(constants.ArmIsManagedImage).(bool)
var isExistingResourceGroup = state.Get(constants.ArmIsExistingResourceGroup).(bool)
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
if dataDisks == nil {
s.say(fmt.Sprintf(" -> No Additional Disks specified"))
return multistep.ActionContinue
}
if isManagedDisk && !isExistingResourceGroup {
s.say(fmt.Sprintf(" -> Additional Disk : skipping, managed disk was used..."))
return multistep.ActionContinue
}
for i, additionaldisk := range dataDisks {
s.say(fmt.Sprintf(" -> Additional Disk %d: '%s'", i+1, additionaldisk))
var err error
if isManagedDisk {
err = s.deleteManaged(resourceGroupName, additionaldisk)
if err != nil {
s.say("Failed to delete the managed Additional Disk!")
return processStepResult(err, s.error, state)
}
} else {
u, err := url.Parse(additionaldisk)
if err != nil {
s.say("Failed to parse the Additional Disk's VHD URI!")
return processStepResult(err, s.error, state)
}
xs := strings.Split(u.Path, "/")
if len(xs) < 3 {
err = errors.New("Failed to parse Additional Disk's VHD URI!")
} else {
var storageAccountName = xs[1]
var blobName = strings.Join(xs[2:], "/")
err = s.delete(storageAccountName, blobName)
}
if err != nil {
return processStepResult(err, s.error, state)
}
}
}
return multistep.ActionContinue
}
func (*StepDeleteAdditionalDisk) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,227 @@
package arm
import (
"context"
"errors"
"fmt"
"testing"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/helper/multistep"
)
func TestStepDeleteAdditionalDiskShouldFailIfGetFails(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return fmt.Errorf("!! Unit Test FAIL !!") },
deleteManaged: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
}
stateBag := DeleteTestStateBagStepDeleteAdditionalDisk([]string{"http://storage.blob.core.windows.net/images/pkrvm_os.vhd"})
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepDeleteAdditionalDiskShouldPassIfGetPasses(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
}
stateBag := DeleteTestStateBagStepDeleteAdditionalDisk([]string{"http://storage.blob.core.windows.net/images/pkrvm_os.vhd"})
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepDeleteAdditionalDiskShouldTakeStepArgumentsFromStateBag(t *testing.T) {
var actualStorageContainerName string
var actualBlobName string
var testSubject = &StepDeleteAdditionalDisk{
delete: func(storageContainerName string, blobName string) error {
actualStorageContainerName = storageContainerName
actualBlobName = blobName
return nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := DeleteTestStateBagStepDeleteAdditionalDisk([]string{"http://storage.blob.core.windows.net/images/pkrvm_os.vhd"})
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if actualStorageContainerName != "images" {
t.Fatalf("Expected the storage container name to be 'images', but found '%s'.", actualStorageContainerName)
}
if actualBlobName != "pkrvm_os.vhd" {
t.Fatalf("Expected the blob name to be 'pkrvm_os.vhd', but found '%s'.", actualBlobName)
}
}
func TestStepDeleteAdditionalDiskShouldHandleComplexStorageContainerNames(t *testing.T) {
var actualStorageContainerName string
var actualBlobName string
var testSubject = &StepDeleteAdditionalDisk{
delete: func(storageContainerName string, blobName string) error {
actualStorageContainerName = storageContainerName
actualBlobName = blobName
return nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := DeleteTestStateBagStepDeleteAdditionalDisk([]string{"http://storage.blob.core.windows.net/abc/def/pkrvm_os.vhd"})
testSubject.Run(context.Background(), stateBag)
if actualStorageContainerName != "abc" {
t.Fatalf("Expected the storage container name to be 'abc/def', but found '%s'.", actualStorageContainerName)
}
if actualBlobName != "def/pkrvm_os.vhd" {
t.Fatalf("Expected the blob name to be 'pkrvm_os.vhd', but found '%s'.", actualBlobName)
}
}
func TestStepDeleteAdditionalDiskShouldFailIfVHDNameCannotBeURLParsed(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
deleteManaged: func(string, string) error { return nil },
}
// Invalid URL per https://golang.org/src/net/url/url_test.go
stateBag := DeleteTestStateBagStepDeleteAdditionalDisk([]string{"http://[fe80::1%en0]/"})
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%v'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to not stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepDeleteAdditionalDiskShouldFailIfVHDNameIsTooShort(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
deleteManaged: func(string, string) error { return nil },
}
stateBag := DeleteTestStateBagStepDeleteAdditionalDisk([]string{"storage.blob.core.windows.net/abc"})
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to not stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepDeleteAdditionalDiskShouldPassIfManagedDiskInTempResourceGroup(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
}
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmAdditionalDiskVhds, []string{"subscriptions/123-456-789/resourceGroups/existingresourcegroup/providers/Microsoft.Compute/disks/osdisk"})
stateBag.Put(constants.ArmIsManagedImage, true)
stateBag.Put(constants.ArmIsExistingResourceGroup, false)
stateBag.Put(constants.ArmResourceGroupName, "testgroup")
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepDeleteAdditionalDiskShouldFailIfManagedDiskInExistingResourceGroupFailsToDelete(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
deleteManaged: func(string, string) error { return errors.New("UNIT TEST FAIL!") },
}
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmAdditionalDiskVhds, []string{"subscriptions/123-456-789/resourceGroups/existingresourcegroup/providers/Microsoft.Compute/disks/osdisk"})
stateBag.Put(constants.ArmIsManagedImage, true)
stateBag.Put(constants.ArmIsExistingResourceGroup, true)
stateBag.Put(constants.ArmResourceGroupName, "testgroup")
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to not stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepDeleteAdditionalDiskShouldFailIfManagedDiskInExistingResourceGroupIsDeleted(t *testing.T) {
var testSubject = &StepDeleteAdditionalDisk{
delete: func(string, string) error { return nil },
say: func(message string) {},
error: func(e error) {},
deleteManaged: func(string, string) error { return nil },
}
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmAdditionalDiskVhds, []string{"subscriptions/123-456-789/resourceGroups/existingresourcegroup/providers/Microsoft.Compute/disks/osdisk"})
stateBag.Put(constants.ArmIsManagedImage, true)
stateBag.Put(constants.ArmIsExistingResourceGroup, true)
stateBag.Put(constants.ArmResourceGroupName, "testgroup")
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func DeleteTestStateBagStepDeleteAdditionalDisk(osDiskVhds []string) multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmAdditionalDiskVhds, osDiskVhds)
stateBag.Put(constants.ArmIsManagedImage, false)
stateBag.Put(constants.ArmIsExistingResourceGroup, false)
stateBag.Put(constants.ArmResourceGroupName, "testgroup")
return stateBag
}

View File

@ -0,0 +1,78 @@
package arm
import (
"context"
"fmt"
"github.com/Azure/azure-sdk-for-go/arm/compute"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type StepGetDataDisk struct {
client *AzureClient
query func(resourceGroupName string, computeName string) (compute.VirtualMachine, error)
say func(message string)
error func(e error)
}
func NewStepGetAdditionalDisks(client *AzureClient, ui packer.Ui) *StepGetDataDisk {
var step = &StepGetDataDisk{
client: client,
say: func(message string) { ui.Say(message) },
error: func(e error) { ui.Error(e.Error()) },
}
step.query = step.queryCompute
return step
}
func (s *StepGetDataDisk) queryCompute(resourceGroupName string, computeName string) (compute.VirtualMachine, error) {
vm, err := s.client.VirtualMachinesClient.Get(resourceGroupName, computeName, "")
if err != nil {
s.say(s.client.LastError.Error())
}
return vm, err
}
func (s *StepGetDataDisk) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
s.say("Querying the machine's additional disks properties ...")
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
var computeName = state.Get(constants.ArmComputeName).(string)
s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName))
s.say(fmt.Sprintf(" -> ComputeName : '%s'", computeName))
vm, err := s.query(resourceGroupName, computeName)
if err != nil {
state.Put(constants.Error, err)
s.error(err)
return multistep.ActionHalt
}
if vm.StorageProfile.DataDisks != nil {
var vhdUri string
additional_disks := make([]string, len(*vm.StorageProfile.DataDisks))
for i, additionaldisk := range *vm.StorageProfile.DataDisks {
if additionaldisk.Vhd != nil {
vhdUri = *additionaldisk.Vhd.URI
s.say(fmt.Sprintf(" -> Additional Disk %d : '%s'", i+1, vhdUri))
} else {
vhdUri = *additionaldisk.ManagedDisk.ID
s.say(fmt.Sprintf(" -> Managed Additional Disk %d : '%s'", i+1, vhdUri))
}
additional_disks[i] = vhdUri
}
state.Put(constants.ArmAdditionalDiskVhds, additional_disks)
}
return multistep.ActionContinue
}
func (*StepGetDataDisk) Cleanup(multistep.StateBag) {
}

View File

@ -0,0 +1,131 @@
package arm
import (
"context"
"fmt"
"testing"
"github.com/Azure/azure-sdk-for-go/arm/compute"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/helper/multistep"
)
func TestStepGetAdditionalDiskShouldFailIfGetFails(t *testing.T) {
var testSubject = &StepGetDataDisk{
query: func(string, string) (compute.VirtualMachine, error) {
return createVirtualMachineWithDataDisksFromUri("test.vhd"), fmt.Errorf("!! Unit Test FAIL !!")
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepGetAdditionalDisks()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionHalt {
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == false {
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
}
}
func TestStepGetAdditionalDiskShouldPassIfGetPasses(t *testing.T) {
var testSubject = &StepGetDataDisk{
query: func(string, string) (compute.VirtualMachine, error) {
return createVirtualMachineWithDataDisksFromUri("test.vhd"), nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepGetAdditionalDisks()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
if _, ok := stateBag.GetOk(constants.Error); ok == true {
t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error)
}
}
func TestStepGetAdditionalDiskShouldTakeValidateArgumentsFromStateBag(t *testing.T) {
var actualResourceGroupName string
var actualComputeName string
var testSubject = &StepGetDataDisk{
query: func(resourceGroupName string, computeName string) (compute.VirtualMachine, error) {
actualResourceGroupName = resourceGroupName
actualComputeName = computeName
return createVirtualMachineWithDataDisksFromUri("test.vhd"), nil
},
say: func(message string) {},
error: func(e error) {},
}
stateBag := createTestStateBagStepGetAdditionalDisks()
var result = testSubject.Run(context.Background(), stateBag)
if result != multistep.ActionContinue {
t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result)
}
var expectedComputeName = stateBag.Get(constants.ArmComputeName).(string)
var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string)
if actualComputeName != expectedComputeName {
t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.")
}
if actualResourceGroupName != expectedResourceGroupName {
t.Fatal("Expected the step to source 'constants.ArmResourceGroupName' from the state bag, but it did not.")
}
expectedAdditionalDiskVhds, ok := stateBag.GetOk(constants.ArmAdditionalDiskVhds)
if !ok {
t.Fatalf("Expected the state bag to have a value for '%s', but it did not.", constants.ArmAdditionalDiskVhds)
}
expectedAdditionalDiskVhd := expectedAdditionalDiskVhds.([]string)
if expectedAdditionalDiskVhd[0] != "test.vhd" {
t.Fatalf("Expected the value of stateBag[%s] to be 'test.vhd', but got '%s'.", constants.ArmAdditionalDiskVhds, expectedAdditionalDiskVhd[0])
}
}
func createTestStateBagStepGetAdditionalDisks() multistep.StateBag {
stateBag := new(multistep.BasicStateBag)
stateBag.Put(constants.ArmComputeName, "Unit Test: ComputeName")
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
return stateBag
}
func createVirtualMachineWithDataDisksFromUri(vhdUri string) compute.VirtualMachine {
vm := compute.VirtualMachine{
VirtualMachineProperties: &compute.VirtualMachineProperties{
StorageProfile: &compute.StorageProfile{
OsDisk: &compute.OSDisk{
Vhd: &compute.VirtualHardDisk{
URI: &vhdUri,
},
},
DataDisks: &[]compute.DataDisk{
compute.DataDisk{
Vhd: &compute.VirtualHardDisk{
URI: &vhdUri,
},
},
},
},
},
}
return vm
}

View File

@ -73,6 +73,10 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error)
builder.SetOSDiskSizeGB(config.OSDiskSizeGB)
}
if len(config.AdditionalDiskSize) > 0 {
builder.SetAdditionalDisks(config.AdditionalDiskSize, config.CustomManagedImageName != "" || (config.ManagedImageName != "" && config.ImagePublisher != ""))
}
if config.customData != "" {
builder.SetCustomData(config.customData)
}

View File

@ -0,0 +1,177 @@
{
"$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json",
"contentVersion": "1.0.0.0",
"parameters": {
"adminPassword": {
"type": "string"
},
"adminUsername": {
"type": "string"
},
"dnsNameForPublicIP": {
"type": "string"
},
"osDiskName": {
"type": "string"
},
"storageAccountBlobEndpoint": {
"type": "string"
},
"vmName": {
"type": "string"
},
"vmSize": {
"type": "string"
}
},
"resources": [
{
"apiVersion": "[variables('publicIPAddressApiVersion')]",
"location": "[variables('location')]",
"name": "[variables('publicIPAddressName')]",
"properties": {
"dnsSettings": {
"domainNameLabel": "[parameters('dnsNameForPublicIP')]"
},
"publicIPAllocationMethod": "[variables('publicIPAddressType')]"
},
"type": "Microsoft.Network/publicIPAddresses"
},
{
"apiVersion": "[variables('virtualNetworksApiVersion')]",
"location": "[variables('location')]",
"name": "[variables('virtualNetworkName')]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"[variables('addressPrefix')]"
]
},
"subnets": [
{
"name": "[variables('subnetName')]",
"properties": {
"addressPrefix": "[variables('subnetAddressPrefix')]"
}
}
]
},
"type": "Microsoft.Network/virtualNetworks"
},
{
"apiVersion": "[variables('networkInterfacesApiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
"[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
],
"location": "[variables('location')]",
"name": "[variables('nicName')]",
"properties": {
"ipConfigurations": [
{
"name": "ipconfig",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"publicIPAddress": {
"id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
},
"subnet": {
"id": "[variables('subnetRef')]"
}
}
}
]
},
"type": "Microsoft.Network/networkInterfaces"
},
{
"apiVersion": "[variables('apiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"location": "[variables('location')]",
"name": "[parameters('vmName')]",
"properties": {
"diagnosticsProfile": {
"bootDiagnostics": {
"enabled": false
}
},
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
}
]
},
"osProfile": {
"adminPassword": "[parameters('adminPassword')]",
"adminUsername": "[parameters('adminUsername')]",
"computerName": "[parameters('vmName')]",
"linuxConfiguration": {
"ssh": {
"publicKeys": [
{
"keyData": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"dataDisks": [
{
"caching": "ReadWrite",
"createOption": "Empty",
"diskSizeGB": 32,
"lun": 0,
"name": "datadisk-1",
"vhd": {
"uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/datadisk-', '1','.vhd')]"
}
}
],
"imageReference": {
"offer": "--image-offer--",
"publisher": "--image-publisher--",
"sku": "--image-sku--",
"version": "--version--"
},
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"name": "osdisk",
"vhd": {
"uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]"
}
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"location": "[resourceGroup().location]",
"managedDiskApiVersion": "2017-03-30",
"networkInterfacesApiVersion": "2017-04-01",
"nicName": "packerNic",
"publicIPAddressApiVersion": "2017-04-01",
"publicIPAddressName": "packerPublicIP",
"publicIPAddressType": "Dynamic",
"sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]",
"subnetAddressPrefix": "10.0.0.0/24",
"subnetName": "packerSubnet",
"subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]",
"virtualNetworkName": "packerNetwork",
"virtualNetworkResourceGroup": "[resourceGroup().name]",
"virtualNetworksApiVersion": "2017-04-01",
"vmStorageAccountContainerName": "images",
"vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
}
}

View File

@ -0,0 +1,178 @@
{
"$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json",
"contentVersion": "1.0.0.0",
"parameters": {
"adminPassword": {
"type": "string"
},
"adminUsername": {
"type": "string"
},
"dnsNameForPublicIP": {
"type": "string"
},
"osDiskName": {
"type": "string"
},
"storageAccountBlobEndpoint": {
"type": "string"
},
"vmName": {
"type": "string"
},
"vmSize": {
"type": "string"
}
},
"resources": [
{
"apiVersion": "[variables('publicIPAddressApiVersion')]",
"location": "[variables('location')]",
"name": "[variables('publicIPAddressName')]",
"properties": {
"dnsSettings": {
"domainNameLabel": "[parameters('dnsNameForPublicIP')]"
},
"publicIPAllocationMethod": "[variables('publicIPAddressType')]"
},
"type": "Microsoft.Network/publicIPAddresses"
},
{
"apiVersion": "[variables('virtualNetworksApiVersion')]",
"location": "[variables('location')]",
"name": "[variables('virtualNetworkName')]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"[variables('addressPrefix')]"
]
},
"subnets": [
{
"name": "[variables('subnetName')]",
"properties": {
"addressPrefix": "[variables('subnetAddressPrefix')]"
}
}
]
},
"type": "Microsoft.Network/virtualNetworks"
},
{
"apiVersion": "[variables('networkInterfacesApiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
"[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
],
"location": "[variables('location')]",
"name": "[variables('nicName')]",
"properties": {
"ipConfigurations": [
{
"name": "ipconfig",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"publicIPAddress": {
"id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
},
"subnet": {
"id": "[variables('subnetRef')]"
}
}
}
]
},
"type": "Microsoft.Network/networkInterfaces"
},
{
"apiVersion": "[variables('apiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"location": "[variables('location')]",
"name": "[parameters('vmName')]",
"properties": {
"diagnosticsProfile": {
"bootDiagnostics": {
"enabled": false
}
},
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
}
]
},
"osProfile": {
"adminPassword": "[parameters('adminPassword')]",
"adminUsername": "[parameters('adminUsername')]",
"computerName": "[parameters('vmName')]",
"linuxConfiguration": {
"ssh": {
"publicKeys": [
{
"keyData": "",
"path": "[variables('sshKeyPath')]"
}
]
}
}
},
"storageProfile": {
"dataDisks": [
{
"caching": "ReadWrite",
"createOption": "Empty",
"diskSizeGB": 32,
"lun": 0,
"managedDisk": {
"storageAccountType": "Standard_LRS"
},
"name": "datadisk-1"
}
],
"imageReference": {
"offer": "--image-offer--",
"publisher": "--image-publisher--",
"sku": "--image-sku--",
"version": "--version--"
},
"osDisk": {
"caching": "ReadWrite",
"createOption": "fromImage",
"managedDisk": {
"storageAccountType": "Standard_LRS"
},
"name": "osdisk",
"osType": "Linux"
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"location": "[resourceGroup().location]",
"managedDiskApiVersion": "2017-03-30",
"networkInterfacesApiVersion": "2017-04-01",
"nicName": "packerNic",
"publicIPAddressApiVersion": "2017-04-01",
"publicIPAddressName": "packerPublicIP",
"publicIPAddressType": "Dynamic",
"sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]",
"subnetAddressPrefix": "10.0.0.0/24",
"subnetName": "packerSubnet",
"subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]",
"virtualNetworkName": "packerNetwork",
"virtualNetworkResourceGroup": "[resourceGroup().name]",
"virtualNetworksApiVersion": "2017-04-01",
"vmStorageAccountContainerName": "images",
"vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
}
}

View File

@ -355,6 +355,76 @@ func TestVirtualMachineDeployment10(t *testing.T) {
}
}
// Ensure the VM template is correct when building with additional unmanaged disks
func TestVirtualMachineDeployment11(t *testing.T) {
config := map[string]interface{}{
"location": "ignore",
"subscription_id": "ignore",
"os_type": constants.Target_Linux,
"communicator": "none",
"image_publisher": "--image-publisher--",
"image_offer": "--image-offer--",
"image_sku": "--image-sku--",
"image_version": "--version--",
"disk_additional_size": []uint{32},
"resource_group_name": "packergroup",
"storage_account": "packerartifacts",
"capture_name_prefix": "packer",
"capture_container_name": "packerimages",
}
c, _, err := newConfig(config, getPackerConfiguration())
if err != nil {
t.Fatal(err)
}
deployment, err := GetVirtualMachineDeployment(c)
if err != nil {
t.Fatal(err)
}
err = approvaltests.VerifyJSONStruct(t, deployment.Properties.Template)
if err != nil {
t.Fatal(err)
}
}
// Ensure the VM template is correct when building with additional managed disks
func TestVirtualMachineDeployment12(t *testing.T) {
config := map[string]interface{}{
"location": "ignore",
"subscription_id": "ignore",
"os_type": constants.Target_Linux,
"communicator": "none",
"image_publisher": "--image-publisher--",
"image_offer": "--image-offer--",
"image_sku": "--image-sku--",
"image_version": "--version--",
"disk_additional_size": []uint{32},
"managed_image_name": "ManagedImageName",
"managed_image_resource_group_name": "ManagedImageResourceGroupName",
}
c, _, err := newConfig(config, getPackerConfiguration())
if err != nil {
t.Fatal(err)
}
deployment, err := GetVirtualMachineDeployment(c)
if err != nil {
t.Fatal(err)
}
err = approvaltests.VerifyJSONStruct(t, deployment.Properties.Template)
if err != nil {
t.Fatal(err)
}
}
// Ensure the link values are not set, and the concrete values are set.
func TestKeyVaultDeployment00(t *testing.T) {
c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration())

View File

@ -21,6 +21,7 @@ const (
ArmKeyVaultName string = "arm.KeyVaultName"
ArmLocation string = "arm.Location"
ArmOSDiskVhd string = "arm.OSDiskVhd"
ArmAdditionalDiskVhds string = "arm.AdditionalDiskVhds"
ArmPublicIPAddressName string = "arm.PublicIPAddressName"
ArmResourceGroupName string = "arm.ResourceGroupName"
ArmIsResourceGroupCreated string = "arm.IsResourceGroupCreated"

View File

@ -48,10 +48,23 @@ type OSDiskUnion struct {
ManagedDisk *compute.ManagedDiskParameters `json:"managedDisk,omitempty"`
}
type DataDiskUnion struct {
Lun *int `json:"lun,omitempty"`
BlobURI *string `json:"blobUri,omitempty"`
Name *string `json:"name,omitempty"`
Vhd *compute.VirtualHardDisk `json:"vhd,omitempty"`
Image *compute.VirtualHardDisk `json:"image,omitempty"`
Caching compute.CachingTypes `json:"caching,omitempty"`
CreateOption compute.DiskCreateOptionTypes `json:"createOption,omitempty"`
DiskSizeGB *int32 `json:"diskSizeGB,omitempty"`
ManagedDisk *compute.ManagedDiskParameters `json:"managedDisk,omitempty"`
}
// Union of the StorageProfile and ImageStorageProfile types.
type StorageProfileUnion struct {
ImageReference *compute.ImageReference `json:"imageReference,omitempty"`
OsDisk *OSDiskUnion `json:"osDisk,omitempty"`
DataDisks *[]DataDiskUnion `json:"dataDisks,omitempty"`
}
/////////////////////////////////////////////////

View File

@ -191,6 +191,35 @@ func (s *TemplateBuilder) SetOSDiskSizeGB(diskSizeGB int32) error {
return nil
}
func (s *TemplateBuilder) SetAdditionalDisks(diskSizeGB []int32, isManaged bool) error {
resource, err := s.getResourceByType(resourceVirtualMachine)
if err != nil {
return err
}
profile := resource.Properties.StorageProfile
dataDisks := make([]DataDiskUnion, len(diskSizeGB))
for i, additionalsize := range diskSizeGB {
dataDisks[i].DiskSizeGB = to.Int32Ptr(additionalsize)
dataDisks[i].Lun = to.IntPtr(i)
dataDisks[i].Name = to.StringPtr(fmt.Sprintf("datadisk-%d", i+1))
dataDisks[i].CreateOption = "Empty"
dataDisks[i].Caching = "ReadWrite"
if isManaged {
dataDisks[i].Vhd = nil
dataDisks[i].ManagedDisk = profile.OsDisk.ManagedDisk
} else {
dataDisks[i].Vhd = &compute.VirtualHardDisk{
URI: to.StringPtr(fmt.Sprintf("[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/datadisk-', '%d','.vhd')]", i+1)),
}
dataDisks[i].ManagedDisk = nil
}
}
profile.DataDisks = &dataDisks
return nil
}
func (s *TemplateBuilder) SetCustomData(customData string) error {
resource, err := s.getResourceByType(resourceVirtualMachine)
if err != nil {

View File

@ -0,0 +1,202 @@
{
"$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json",
"contentVersion": "1.0.0.0",
"parameters": {
"adminPassword": {
"type": "string"
},
"adminUsername": {
"type": "string"
},
"dnsNameForPublicIP": {
"type": "string"
},
"osDiskName": {
"type": "string"
},
"storageAccountBlobEndpoint": {
"type": "string"
},
"vmName": {
"type": "string"
},
"vmSize": {
"type": "string"
}
},
"resources": [
{
"apiVersion": "[variables('publicIPAddressApiVersion')]",
"location": "[variables('location')]",
"name": "[variables('publicIPAddressName')]",
"properties": {
"dnsSettings": {
"domainNameLabel": "[parameters('dnsNameForPublicIP')]"
},
"publicIPAllocationMethod": "[variables('publicIPAddressType')]"
},
"type": "Microsoft.Network/publicIPAddresses"
},
{
"apiVersion": "[variables('virtualNetworksApiVersion')]",
"location": "[variables('location')]",
"name": "[variables('virtualNetworkName')]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"[variables('addressPrefix')]"
]
},
"subnets": [
{
"name": "[variables('subnetName')]",
"properties": {
"addressPrefix": "[variables('subnetAddressPrefix')]"
}
}
]
},
"type": "Microsoft.Network/virtualNetworks"
},
{
"apiVersion": "[variables('networkInterfacesApiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
"[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
],
"location": "[variables('location')]",
"name": "[variables('nicName')]",
"properties": {
"ipConfigurations": [
{
"name": "ipconfig",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"publicIPAddress": {
"id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
},
"subnet": {
"id": "[variables('subnetRef')]"
}
}
}
]
},
"type": "Microsoft.Network/networkInterfaces"
},
{
"apiVersion": "[variables('apiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"location": "[variables('location')]",
"name": "[parameters('vmName')]",
"properties": {
"diagnosticsProfile": {
"bootDiagnostics": {
"enabled": false
}
},
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
}
]
},
"osProfile": {
"adminPassword": "[parameters('adminPassword')]",
"adminUsername": "[parameters('adminUsername')]",
"computerName": "[parameters('vmName')]",
"secrets": [
{
"sourceVault": {
"id": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults', '--test-key-vault-name')]"
},
"vaultCertificates": [
{
"certificateStore": "My",
"certificateUrl": "--test-winrm-certificate-url--"
}
]
}
],
"windowsConfiguration": {
"provisionVMAgent": true,
"winRM": {
"listeners": [
{
"certificateUrl": "--test-winrm-certificate-url--",
"protocol": "https"
}
]
}
}
},
"storageProfile": {
"dataDisks": [
{
"caching": "ReadWrite",
"createOption": "Empty",
"diskSizeGB": 32,
"lun": 0,
"managedDisk": {
"storageAccountType": "Premium_LRS"
},
"name": "datadisk-1"
},
{
"caching": "ReadWrite",
"createOption": "Empty",
"diskSizeGB": 64,
"lun": 1,
"managedDisk": {
"storageAccountType": "Premium_LRS"
},
"name": "datadisk-2"
}
],
"imageReference": {
"offer": "2012-R2-Datacenter",
"publisher": "WindowsServer",
"sku": "latest",
"version": "2015-1"
},
"osDisk": {
"caching": "ReadWrite",
"createOption": "fromImage",
"managedDisk": {
"storageAccountType": "Premium_LRS"
},
"name": "osdisk",
"osType": "Windows"
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"location": "[resourceGroup().location]",
"managedDiskApiVersion": "2017-03-30",
"networkInterfacesApiVersion": "2017-04-01",
"nicName": "packerNic",
"publicIPAddressApiVersion": "2017-04-01",
"publicIPAddressName": "packerPublicIP",
"publicIPAddressType": "Dynamic",
"sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]",
"subnetAddressPrefix": "10.0.0.0/24",
"subnetName": "packerSubnet",
"subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]",
"virtualNetworkName": "packerNetwork",
"virtualNetworkResourceGroup": "[resourceGroup().name]",
"virtualNetworksApiVersion": "2017-04-01",
"vmStorageAccountContainerName": "images",
"vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
}
}

View File

@ -0,0 +1,195 @@
{
"$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json",
"contentVersion": "1.0.0.0",
"parameters": {
"adminPassword": {
"type": "string"
},
"adminUsername": {
"type": "string"
},
"dnsNameForPublicIP": {
"type": "string"
},
"osDiskName": {
"type": "string"
},
"storageAccountBlobEndpoint": {
"type": "string"
},
"vmName": {
"type": "string"
},
"vmSize": {
"type": "string"
}
},
"resources": [
{
"apiVersion": "[variables('publicIPAddressApiVersion')]",
"location": "[variables('location')]",
"name": "[variables('publicIPAddressName')]",
"properties": {
"dnsSettings": {
"domainNameLabel": "[parameters('dnsNameForPublicIP')]"
},
"publicIPAllocationMethod": "[variables('publicIPAddressType')]"
},
"type": "Microsoft.Network/publicIPAddresses"
},
{
"apiVersion": "[variables('virtualNetworksApiVersion')]",
"location": "[variables('location')]",
"name": "[variables('virtualNetworkName')]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"[variables('addressPrefix')]"
]
},
"subnets": [
{
"name": "[variables('subnetName')]",
"properties": {
"addressPrefix": "[variables('subnetAddressPrefix')]"
}
}
]
},
"type": "Microsoft.Network/virtualNetworks"
},
{
"apiVersion": "[variables('networkInterfacesApiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
"[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
],
"location": "[variables('location')]",
"name": "[variables('nicName')]",
"properties": {
"ipConfigurations": [
{
"name": "ipconfig",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"publicIPAddress": {
"id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]"
},
"subnet": {
"id": "[variables('subnetRef')]"
}
}
}
]
},
"type": "Microsoft.Network/networkInterfaces"
},
{
"apiVersion": "[variables('apiVersion')]",
"dependsOn": [
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
],
"location": "[variables('location')]",
"name": "[parameters('vmName')]",
"properties": {
"diagnosticsProfile": {
"bootDiagnostics": {
"enabled": false
}
},
"hardwareProfile": {
"vmSize": "[parameters('vmSize')]"
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]"
}
]
},
"osProfile": {
"adminPassword": "[parameters('adminPassword')]",
"adminUsername": "[parameters('adminUsername')]",
"computerName": "[parameters('vmName')]",
"secrets": [
{
"sourceVault": {
"id": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults', '--test-key-vault-name')]"
},
"vaultCertificates": [
{
"certificateStore": "My",
"certificateUrl": "--test-winrm-certificate-url--"
}
]
}
],
"windowsConfiguration": {
"provisionVMAgent": true,
"winRM": {
"listeners": [
{
"certificateUrl": "--test-winrm-certificate-url--",
"protocol": "https"
}
]
}
}
},
"storageProfile": {
"dataDisks": [
{
"caching": "ReadWrite",
"createOption": "Empty",
"diskSizeGB": 32,
"lun": 0,
"name": "datadisk-1",
"vhd": {
"uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/datadisk-', '1','.vhd')]"
}
},
{
"caching": "ReadWrite",
"createOption": "Empty",
"diskSizeGB": 64,
"lun": 1,
"name": "datadisk-2",
"vhd": {
"uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/datadisk-', '2','.vhd')]"
}
}
],
"osDisk": {
"caching": "ReadWrite",
"createOption": "FromImage",
"name": "osdisk",
"vhd": {
"uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]"
}
}
}
},
"type": "Microsoft.Compute/virtualMachines"
}
],
"variables": {
"addressPrefix": "10.0.0.0/16",
"apiVersion": "2017-03-30",
"location": "[resourceGroup().location]",
"managedDiskApiVersion": "2017-03-30",
"networkInterfacesApiVersion": "2017-04-01",
"nicName": "packerNic",
"publicIPAddressApiVersion": "2017-04-01",
"publicIPAddressName": "packerPublicIP",
"publicIPAddressType": "Dynamic",
"sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]",
"subnetAddressPrefix": "10.0.0.0/24",
"subnetName": "packerSubnet",
"subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]",
"virtualNetworkName": "packerNetwork",
"virtualNetworkResourceGroup": "[resourceGroup().name]",
"virtualNetworksApiVersion": "2017-04-01",
"vmStorageAccountContainerName": "images",
"vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]"
}
}

View File

@ -120,3 +120,64 @@ func TestBuildWindows00(t *testing.T) {
t.Fatal(err)
}
}
// Windows build with additional disk for an managed build
func TestBuildWindows01(t *testing.T) {
testSubject, err := NewTemplateBuilder(BasicTemplate)
if err != nil {
t.Fatal(err)
}
err = testSubject.BuildWindows("--test-key-vault-name", "--test-winrm-certificate-url--")
if err != nil {
t.Fatal(err)
}
err = testSubject.SetManagedMarketplaceImage("MicrosoftWindowsServer", "WindowsServer", "2012-R2-Datacenter", "latest", "2015-1", "1", "Premium_LRS")
if err != nil {
t.Fatal(err)
}
err = testSubject.SetAdditionalDisks([]int32{32, 64}, true)
if err != nil {
t.Fatal(err)
}
doc, err := testSubject.ToJSON()
if err != nil {
t.Fatal(err)
}
err = approvaltests.VerifyJSONBytes(t, []byte(*doc))
if err != nil {
t.Fatal(err)
}
}
// Windows build with additional disk for an unmanaged build
func TestBuildWindows02(t *testing.T) {
testSubject, err := NewTemplateBuilder(BasicTemplate)
if err != nil {
t.Fatal(err)
}
err = testSubject.BuildWindows("--test-key-vault-name", "--test-winrm-certificate-url--")
if err != nil {
t.Fatal(err)
}
err = testSubject.SetAdditionalDisks([]int32{32, 64}, false)
if err != nil {
t.Fatal(err)
}
doc, err := testSubject.ToJSON()
if err != nil {
t.Fatal(err)
}
err = approvaltests.VerifyJSONBytes(t, []byte(*doc))
if err != nil {
t.Fatal(err)
}
}

View File

@ -149,6 +149,10 @@ Providing `temp_resource_group_name` or `location` in combination with `build_re
- `os_disk_size_gb` (number) Specify the size of the OS disk in GB (gigabytes). Values of zero or less than zero are
ignored.
- `disk_additional_size` (array of integers) - The size(s) of any additional
hard disks for the VM in gigabytes. If this is not specified then the VM
will only contain an OS disk.
- `os_type` (string) If either `Linux` or `Windows` is specified Packer will
automatically configure authentication credentials for the provisioned machine. For
`Linux` this configures an SSH authorized key. For `Windows` this