From efcdbfeab97b8a65d5a514f4671c53227468b61a Mon Sep 17 00:00:00 2001 From: jmajoor Date: Fri, 23 Feb 2018 15:34:13 -0800 Subject: [PATCH 1/5] Add support for optionally building Azure VMs with additional disks. --- builder/azure/arm/artifact.go | 30 +++ builder/azure/arm/artifact_test.go | 113 +++++++++ builder/azure/arm/builder.go | 2 + builder/azure/arm/capture_template.go | 3 +- builder/azure/arm/capture_template_test.go | 64 ++++- builder/azure/arm/config.go | 3 + .../azure/arm/step_delete_additional_disks.go | 106 ++++++++ .../arm/step_delete_additional_disks_test.go | 227 ++++++++++++++++++ .../azure/arm/step_get_additional_disks.go | 78 ++++++ .../arm/step_get_additional_disks_test.go | 131 ++++++++++ builder/azure/arm/template_factory.go | 4 + ...stVirtualMachineDeployment11.approved.json | 177 ++++++++++++++ ...stVirtualMachineDeployment12.approved.json | 178 ++++++++++++++ builder/azure/arm/template_factory_test.go | 70 ++++++ builder/azure/common/constants/stateBag.go | 1 + builder/azure/common/template/template.go | 13 + .../azure/common/template/template_builder.go | 29 +++ ...lder_test.TestBuildWindows01.approved.json | 202 ++++++++++++++++ ...lder_test.TestBuildWindows02.approved.json | 195 +++++++++++++++ .../common/template/template_builder_test.go | 61 +++++ website/source/docs/builders/azure.html.md | 4 + 21 files changed, 1689 insertions(+), 2 deletions(-) create mode 100644 builder/azure/arm/step_delete_additional_disks.go create mode 100644 builder/azure/arm/step_delete_additional_disks_test.go create mode 100644 builder/azure/arm/step_get_additional_disks.go create mode 100644 builder/azure/arm/step_get_additional_disks_test.go create mode 100644 builder/azure/arm/template_factory_test.TestVirtualMachineDeployment11.approved.json create mode 100644 builder/azure/arm/template_factory_test.TestVirtualMachineDeployment12.approved.json create mode 100644 builder/azure/common/template/template_builder_test.TestBuildWindows01.approved.json create mode 100644 builder/azure/common/template/template_builder_test.TestBuildWindows02.approved.json diff --git a/builder/azure/arm/artifact.go b/builder/azure/arm/artifact.go index e8a72091f..c2abcc4d5 100644 --- a/builder/azure/arm/artifact.go +++ b/builder/azure/arm/artifact.go @@ -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() diff --git a/builder/azure/arm/artifact_test.go b/builder/azure/arm/artifact_test.go index 1b9a472af..13c77e2e3 100644 --- a/builder/azure/arm/artifact_test.go +++ b/builder/azure/arm/artifact_test.go @@ -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{ diff --git a/builder/azure/arm/builder.go b/builder/azure/arm/builder.go index 62afe6fa5..64d791ffa 100644 --- a/builder/azure/arm/builder.go +++ b/builder/azure/arm/builder.go @@ -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) diff --git a/builder/azure/arm/capture_template.go b/builder/azure/arm/capture_template.go index 0332a02f4..2ba2c48b8 100644 --- a/builder/azure/arm/capture_template.go +++ b/builder/azure/arm/capture_template.go @@ -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 { diff --git a/builder/azure/arm/capture_template_test.go b/builder/azure/arm/capture_template_test.go index 8c7a71405..10be51ee8 100644 --- a/builder/azure/arm/capture_template_test.go +++ b/builder/azure/arm/capture_template_test.go @@ -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')]" { diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go index f384c88bc..afa5f62a7 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -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 diff --git a/builder/azure/arm/step_delete_additional_disks.go b/builder/azure/arm/step_delete_additional_disks.go new file mode 100644 index 000000000..d43bf8253 --- /dev/null +++ b/builder/azure/arm/step_delete_additional_disks.go @@ -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) { +} diff --git a/builder/azure/arm/step_delete_additional_disks_test.go b/builder/azure/arm/step_delete_additional_disks_test.go new file mode 100644 index 000000000..80ec5b42e --- /dev/null +++ b/builder/azure/arm/step_delete_additional_disks_test.go @@ -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 +} diff --git a/builder/azure/arm/step_get_additional_disks.go b/builder/azure/arm/step_get_additional_disks.go new file mode 100644 index 000000000..68af51724 --- /dev/null +++ b/builder/azure/arm/step_get_additional_disks.go @@ -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) { +} diff --git a/builder/azure/arm/step_get_additional_disks_test.go b/builder/azure/arm/step_get_additional_disks_test.go new file mode 100644 index 000000000..1c6dd3380 --- /dev/null +++ b/builder/azure/arm/step_get_additional_disks_test.go @@ -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 +} diff --git a/builder/azure/arm/template_factory.go b/builder/azure/arm/template_factory.go index 07f2313b5..ebc7ef91c 100644 --- a/builder/azure/arm/template_factory.go +++ b/builder/azure/arm/template_factory.go @@ -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) } diff --git a/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment11.approved.json b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment11.approved.json new file mode 100644 index 000000000..b5fbfaf2f --- /dev/null +++ b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment11.approved.json @@ -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'))]" + } +} \ No newline at end of file diff --git a/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment12.approved.json b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment12.approved.json new file mode 100644 index 000000000..531b2654b --- /dev/null +++ b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment12.approved.json @@ -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'))]" + } +} \ No newline at end of file diff --git a/builder/azure/arm/template_factory_test.go b/builder/azure/arm/template_factory_test.go index b571871ef..7909a0cee 100644 --- a/builder/azure/arm/template_factory_test.go +++ b/builder/azure/arm/template_factory_test.go @@ -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()) diff --git a/builder/azure/common/constants/stateBag.go b/builder/azure/common/constants/stateBag.go index 720979b9c..3576f6820 100644 --- a/builder/azure/common/constants/stateBag.go +++ b/builder/azure/common/constants/stateBag.go @@ -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" diff --git a/builder/azure/common/template/template.go b/builder/azure/common/template/template.go index cdea487f0..070ca3392 100644 --- a/builder/azure/common/template/template.go +++ b/builder/azure/common/template/template.go @@ -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"` } ///////////////////////////////////////////////// diff --git a/builder/azure/common/template/template_builder.go b/builder/azure/common/template/template_builder.go index 7311817c2..835aff096 100644 --- a/builder/azure/common/template/template_builder.go +++ b/builder/azure/common/template/template_builder.go @@ -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 { diff --git a/builder/azure/common/template/template_builder_test.TestBuildWindows01.approved.json b/builder/azure/common/template/template_builder_test.TestBuildWindows01.approved.json new file mode 100644 index 000000000..b7a145df9 --- /dev/null +++ b/builder/azure/common/template/template_builder_test.TestBuildWindows01.approved.json @@ -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'))]" + } +} \ No newline at end of file diff --git a/builder/azure/common/template/template_builder_test.TestBuildWindows02.approved.json b/builder/azure/common/template/template_builder_test.TestBuildWindows02.approved.json new file mode 100644 index 000000000..61b05a125 --- /dev/null +++ b/builder/azure/common/template/template_builder_test.TestBuildWindows02.approved.json @@ -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'))]" + } +} \ No newline at end of file diff --git a/builder/azure/common/template/template_builder_test.go b/builder/azure/common/template/template_builder_test.go index ec93a61a7..6eac5334f 100644 --- a/builder/azure/common/template/template_builder_test.go +++ b/builder/azure/common/template/template_builder_test.go @@ -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) + } +} diff --git a/website/source/docs/builders/azure.html.md b/website/source/docs/builders/azure.html.md index 8fa59f4e8..71e8be385 100644 --- a/website/source/docs/builders/azure.html.md +++ b/website/source/docs/builders/azure.html.md @@ -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 From 47947bd0f6753cfad327177705efc485023166cf Mon Sep 17 00:00:00 2001 From: jmajoor Date: Fri, 23 Feb 2018 18:43:55 -0800 Subject: [PATCH 2/5] Apply gofmt --- builder/azure/arm/artifact_test.go | 4 ++-- builder/azure/arm/step_get_additional_disks_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/builder/azure/arm/artifact_test.go b/builder/azure/arm/artifact_test.go index 13c77e2e3..4aff7cee6 100644 --- a/builder/azure/arm/artifact_test.go +++ b/builder/azure/arm/artifact_test.go @@ -94,7 +94,7 @@ func TestAdditionalDiskArtifactString(t *testing.T) { }, }, 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", }, @@ -188,7 +188,7 @@ func TestAdditionalDiskArtifactProperties(t *testing.T) { }, }, 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", }, diff --git a/builder/azure/arm/step_get_additional_disks_test.go b/builder/azure/arm/step_get_additional_disks_test.go index 1c6dd3380..b0ca112c3 100644 --- a/builder/azure/arm/step_get_additional_disks_test.go +++ b/builder/azure/arm/step_get_additional_disks_test.go @@ -117,7 +117,7 @@ func createVirtualMachineWithDataDisksFromUri(vhdUri string) compute.VirtualMach }, }, DataDisks: &[]compute.DataDisk{ - compute.DataDisk{ + { Vhd: &compute.VirtualHardDisk{ URI: &vhdUri, }, From 33c3d2885de042d027ec1460c4818aaa9a14fdcd Mon Sep 17 00:00:00 2001 From: jmajoor Date: Mon, 26 Feb 2018 17:33:40 -0800 Subject: [PATCH 3/5] Add the additional disk steps to the Linux build --- builder/azure/arm/builder.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/builder/azure/arm/builder.go b/builder/azure/arm/builder.go index 64d791ffa..f2bc66135 100644 --- a/builder/azure/arm/builder.go +++ b/builder/azure/arm/builder.go @@ -151,10 +151,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 if b.config.OSType == constants.Target_Windows { keyVaultDeploymentName := b.stateBag.Get(constants.ArmKeyVaultDeploymentName).(string) From 675dc0696782f4927f424c053ebe523b3db36581 Mon Sep 17 00:00:00 2001 From: jmajoor Date: Mon, 26 Feb 2018 17:37:46 -0800 Subject: [PATCH 4/5] Tests for the optional disk_additional_size configuration. --- builder/azure/arm/config_test.go | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/builder/azure/arm/config_test.go b/builder/azure/arm/config_test.go index 4cefd6730..d69c24a33 100644 --- a/builder/azure/arm/config_test.go +++ b/builder/azure/arm/config_test.go @@ -1057,6 +1057,46 @@ func TestConfigShouldRejectManagedDiskNames(t *testing.T) { } } +func TestConfigAdditionalDiskDefaultIsNil(t *testing.T) { + + c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) + if c.AdditionalDiskSize != nil { + t.Errorf("Expected Config to not have a set of additional disks, but got a non nil value") + } +} + +func TestConfigAdditionalDiskOverrideDefault(t *testing.T) { + config := map[string]string{ + "capture_name_prefix": "ignore", + "capture_container_name": "ignore", + "location": "ignore", + "image_url": "ignore", + "storage_account": "ignore", + "resource_group_name": "ignore", + "subscription_id": "ignore", + "os_type": constants.Target_Linux, + "communicator": "none", + } + + diskconfig := map[string][]int32{ + "disk_additional_size": {32, 64}, + } + + c, _, _ := newConfig(config, diskconfig, getPackerConfiguration()) + if c.AdditionalDiskSize == nil { + t.Errorf("Expected Config to have a set of additional disks, but got nil") + } + if len(c.AdditionalDiskSize) != 2 { + t.Errorf("Expected Config to have a 2 additional disks, but got %d additional disks", len(c.AdditionalDiskSize)) + } + if c.AdditionalDiskSize[0] != 32 { + t.Errorf("Expected Config to have the first additional disks of size 32Gb, but got %dGb", c.AdditionalDiskSize[0]) + } + if c.AdditionalDiskSize[1] != 64 { + t.Errorf("Expected Config to have the second additional disks of size 64Gb, but got %dGb", c.AdditionalDiskSize[1]) + } +} + func getArmBuilderConfiguration() map[string]string { m := make(map[string]string) for _, v := range requiredConfigValues { From 863f4ec1daaced25afdab7e217015601a276f692 Mon Sep 17 00:00:00 2001 From: jmajoor Date: Mon, 26 Feb 2018 17:41:28 -0800 Subject: [PATCH 5/5] Add additional documentation for the additional disks configuration --- website/source/docs/builders/azure.html.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/source/docs/builders/azure.html.md b/website/source/docs/builders/azure.html.md index 71e8be385..b7d937acd 100644 --- a/website/source/docs/builders/azure.html.md +++ b/website/source/docs/builders/azure.html.md @@ -151,7 +151,11 @@ Providing `temp_resource_group_name` or `location` in combination with `build_re - `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. + will only contain an OS disk. The number of additional disks and maximum size of a disk depends on the configuration of your VM. See [Windows](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/about-disks-and-vhds) or [Linux](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/about-disks-and-vhds) for more information. + + For VHD builds the final artifacts will be named `PREFIX-dataDisk-.UUID.vhd` and stored in the specified capture container along side the OS disk. The additional disks are included in the deployment template `PREFIX-vmTemplate.UUID`. + + For Managed build the final artifacts are included in the managed image. The additional disk will have the same storage account type as the OS disk, as specified with the `managed_image_storage_account_type` setting. - `os_type` (string) If either `Linux` or `Windows` is specified Packer will automatically configure authentication credentials for the provisioned machine. For