From 871ca8c3d91208e7d764cbf779e5dcbf931ba2cf Mon Sep 17 00:00:00 2001 From: Christopher Boumenot Date: Thu, 30 Jun 2016 16:51:52 -0700 Subject: [PATCH] azure: Support for a user define VNET. Two new configuration options have been exposed to allow users to specify an existing virtual network: virtual_network_name and virtual_network_resource_group_name. * virtual_network_name: name of the virtual network to attach a Packer VM to. * virtual_network_resource_group_name: name of the resource group that contains the virtual network. This value is optional. If the value is not specified, the builder queries Azure for the appropriate value. If the builder cannot disambiguate the value, a value must be provided for this setting. * virtual_network_subnet_name: name of the subnet attached to the virtual network. This value is optional. If the value is not specified, the builder queries Azure for the appropriate value. If the builder cannot disambiguate the value, a value must be provided for this setting. --- .gitignore | 1 + ...estVirtualMachineDeployment05.approved.txt | 120 ++++++++++++++++++ builder/azure/arm/azure_client.go | 21 +++ builder/azure/arm/builder.go | 22 +++- builder/azure/arm/builder_test.go | 1 + builder/azure/arm/config.go | 19 ++- builder/azure/arm/config_retriever.go | 12 +- builder/azure/arm/config_test.go | 72 +++++++++++ builder/azure/arm/resource_resolver.go | 101 +++++++++++++++ builder/azure/arm/resource_resolver_test.go | 76 +++++++++++ builder/azure/arm/step_get_ip_address.go | 66 +++++++--- builder/azure/arm/step_get_ip_address_test.go | 29 +++-- builder/azure/arm/template_factory.go | 7 + ...stVirtualMachineDeployment03.approved.json | 7 +- ...stVirtualMachineDeployment04.approved.json | 7 +- ...stVirtualMachineDeployment05.approved.json | 120 ++++++++++++++++++ builder/azure/arm/template_factory_test.go | 32 +++++ builder/azure/common/constants/stateBag.go | 1 + .../template/TestBuildLinux02.approved.txt | 120 ++++++++++++++++++ .../azure/common/template/template_builder.go | 61 ++++++++- ...uilder_test.TestBuildLinux00.approved.json | 7 +- ...uilder_test.TestBuildLinux01.approved.json | 7 +- ...uilder_test.TestBuildLinux02.approved.json | 120 ++++++++++++++++++ ...lder_test.TestBuildWindows00.approved.json | 7 +- .../common/template/template_builder_test.go | 26 ++++ website/source/docs/builders/azure.html.md | 13 ++ 26 files changed, 1023 insertions(+), 52 deletions(-) create mode 100644 builder/azure/arm/TestVirtualMachineDeployment05.approved.txt create mode 100644 builder/azure/arm/resource_resolver.go create mode 100644 builder/azure/arm/resource_resolver_test.go create mode 100644 builder/azure/arm/template_factory_test.TestVirtualMachineDeployment05.approved.json create mode 100644 builder/azure/common/template/TestBuildLinux02.approved.txt create mode 100644 builder/azure/common/template/template_builder_test.TestBuildLinux02.approved.json diff --git a/.gitignore b/.gitignore index 8dd382f2a..3999641cd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .idea test/.env *~ +*.received.* website/.bundle website/vendor diff --git a/builder/azure/arm/TestVirtualMachineDeployment05.approved.txt b/builder/azure/arm/TestVirtualMachineDeployment05.approved.txt new file mode 100644 index 000000000..906820f27 --- /dev/null +++ b/builder/azure/arm/TestVirtualMachineDeployment05.approved.txt @@ -0,0 +1,120 @@ +{ + "$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('apiVersion')]", + "dependsOn": [], + "location": "[variables('location')]", + "name": "[variables('nicName')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "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": { + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "image": { + "uri": "https://localhost/custom.vhd" + }, + "name": "osdisk", + "osType": "Linux", + "vhd": { + "uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]" + } + } + } + }, + "type": "Microsoft.Compute/virtualMachines" + } + ], + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2015-06-15", + "location": "[resourceGroup().location]", + "nicName": "packerNic", + "publicIPAddressName": "packerPublicIP", + "publicIPAddressType": "Dynamic", + "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", + "subnetAddressPrefix": "10.0.0.0/24", + "subnetName": "ignore", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "ignore", + "virtualNetworkResourceGroup": "ignore", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } +} \ No newline at end of file diff --git a/builder/azure/arm/azure_client.go b/builder/azure/arm/azure_client.go index 2f6e1e39a..096979b04 100644 --- a/builder/azure/arm/azure_client.go +++ b/builder/azure/arm/azure_client.go @@ -36,6 +36,9 @@ type AzureClient struct { resources.DeploymentsClient resources.GroupsClient network.PublicIPAddressesClient + network.InterfacesClient + network.SubnetsClient + network.VirtualNetworksClient compute.VirtualMachinesClient common.VaultClient armStorage.AccountsClient @@ -122,6 +125,24 @@ func NewAzureClient(subscriptionID, resourceGroupName, storageAccountName string azureClient.GroupsClient.ResponseInspector = byInspecting(maxlen) azureClient.GroupsClient.UserAgent += packerUserAgent + azureClient.InterfacesClient = network.NewInterfacesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID) + azureClient.InterfacesClient.Authorizer = servicePrincipalToken + azureClient.InterfacesClient.RequestInspector = withInspection(maxlen) + azureClient.InterfacesClient.ResponseInspector = byInspecting(maxlen) + azureClient.InterfacesClient.UserAgent += packerUserAgent + + azureClient.SubnetsClient = network.NewSubnetsClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID) + azureClient.SubnetsClient.Authorizer = servicePrincipalToken + azureClient.SubnetsClient.RequestInspector = withInspection(maxlen) + azureClient.SubnetsClient.ResponseInspector = byInspecting(maxlen) + azureClient.SubnetsClient.UserAgent += packerUserAgent + + azureClient.VirtualNetworksClient = network.NewVirtualNetworksClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID) + azureClient.VirtualNetworksClient.Authorizer = servicePrincipalToken + azureClient.VirtualNetworksClient.RequestInspector = withInspection(maxlen) + azureClient.VirtualNetworksClient.ResponseInspector = byInspecting(maxlen) + azureClient.VirtualNetworksClient.UserAgent += packerUserAgent + azureClient.PublicIPAddressesClient = network.NewPublicIPAddressesClientWithBaseURI(cloud.ResourceManagerEndpoint, subscriptionID) azureClient.PublicIPAddressesClient.Authorizer = servicePrincipalToken azureClient.PublicIPAddressesClient.RequestInspector = withInspection(maxlen) diff --git a/builder/azure/arm/builder.go b/builder/azure/arm/builder.go index 6785e3a63..0d85d3fc8 100644 --- a/builder/azure/arm/builder.go +++ b/builder/azure/arm/builder.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "log" + "strings" "time" packerAzureCommon "github.com/mitchellh/packer/builder/azure/common" @@ -20,7 +21,6 @@ import ( packerCommon "github.com/mitchellh/packer/common" "github.com/mitchellh/packer/helper/communicator" "github.com/mitchellh/packer/packer" - "strings" ) type Builder struct { @@ -30,6 +30,7 @@ type Builder struct { } const ( + DefaultNicName = "packerNic" DefaultPublicIPAddressName = "packerPublicIP" DefaultSasBlobContainer = "system/Microsoft.Compute" DefaultSasBlobPermission = "r" @@ -82,11 +83,21 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe return nil, err } + resolver := newResourceResolver(azureClient) + if err := resolver.Resolve(b.config); err != nil { + return nil, err + } + b.config.storageAccountBlobEndpoint, err = b.getBlobEndpoint(azureClient, b.config.ResourceGroupName, b.config.StorageAccount) if err != nil { return nil, err } + endpointConnectType := PublicEndpoint + if b.isPrivateNetworkCommunication() { + endpointConnectType = PrivateEndpoint + } + b.setTemplateParameters(b.stateBag) var steps []multistep.Step @@ -95,7 +106,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe NewStepCreateResourceGroup(azureClient, ui), NewStepValidateTemplate(azureClient, ui, b.config, GetVirtualMachineDeployment), NewStepDeployTemplate(azureClient, ui, b.config, GetVirtualMachineDeployment), - NewStepGetIPAddress(azureClient, ui), + NewStepGetIPAddress(azureClient, ui, endpointConnectType), &communicator.StepConnectSSH{ Config: &b.config.Comm, Host: lin.SSHHost, @@ -117,7 +128,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe NewStepSetCertificate(b.config, ui), NewStepValidateTemplate(azureClient, ui, b.config, GetVirtualMachineDeployment), NewStepDeployTemplate(azureClient, ui, b.config, GetVirtualMachineDeployment), - NewStepGetIPAddress(azureClient, ui), + NewStepGetIPAddress(azureClient, ui, endpointConnectType), &communicator.StepConnectWinRM{ Config: &b.config.Comm, Host: func(stateBag multistep.StateBag) (string, error) { @@ -176,6 +187,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe return &Artifact{}, nil } +func (b *Builder) isPrivateNetworkCommunication() bool { + return b.config.VirtualNetworkName != "" +} + func (b *Builder) Cancel() { if b.runner != nil { log.Println("Cancelling the step runner...") @@ -213,6 +228,7 @@ func (b *Builder) configureStateBag(stateBag multistep.StateBag) { stateBag.Put(constants.ArmDeploymentName, b.config.tmpDeploymentName) stateBag.Put(constants.ArmKeyVaultName, b.config.tmpKeyVaultName) stateBag.Put(constants.ArmLocation, b.config.Location) + stateBag.Put(constants.ArmNicName, DefaultNicName) stateBag.Put(constants.ArmPublicIPAddressName, DefaultPublicIPAddressName) stateBag.Put(constants.ArmResourceGroupName, b.config.tmpResourceGroupName) stateBag.Put(constants.ArmStorageAccountName, b.config.StorageAccount) diff --git a/builder/azure/arm/builder_test.go b/builder/azure/arm/builder_test.go index ecb2ea75f..9f0d26670 100644 --- a/builder/azure/arm/builder_test.go +++ b/builder/azure/arm/builder_test.go @@ -22,6 +22,7 @@ func TestStateBagShouldBePopulatedExpectedValues(t *testing.T) { constants.ArmComputeName, constants.ArmDeploymentName, constants.ArmLocation, + constants.ArmNicName, constants.ArmResourceGroupName, constants.ArmStorageAccountName, constants.ArmVirtualMachineCaptureParameters, diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go index 355ee8e47..93b7f7b59 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -70,11 +70,14 @@ type Config struct { VMSize string `mapstructure:"vm_size"` // Deployment - ResourceGroupName string `mapstructure:"resource_group_name"` - StorageAccount string `mapstructure:"storage_account"` - storageAccountBlobEndpoint string - CloudEnvironmentName string `mapstructure:"cloud_environment_name"` - cloudEnvironment *azure.Environment + ResourceGroupName string `mapstructure:"resource_group_name"` + StorageAccount string `mapstructure:"storage_account"` + storageAccountBlobEndpoint string + CloudEnvironmentName string `mapstructure:"cloud_environment_name"` + cloudEnvironment *azure.Environment + VirtualNetworkName string `mapstructure:"virtual_network_name"` + VirtualNetworkSubnetName string `mapstructure:"virtual_network_subnet_name"` + VirtualNetworkResourceGroupName string `mapstructure:"virtual_network_resource_group_name"` // OS OSType string `mapstructure:"os_type"` @@ -447,6 +450,12 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) { if c.ResourceGroupName == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A resource_group_name must be specified")) } + if c.VirtualNetworkName == "" && c.VirtualNetworkResourceGroupName != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("If virtual_network_resource_group_name is specified, so must virtual_network_name")) + } + if c.VirtualNetworkName == "" && c.VirtualNetworkSubnetName != "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("If virtual_network_subnet_name is specified, so must virtual_network_name")) + } ///////////////////////////////////////////// // OS diff --git a/builder/azure/arm/config_retriever.go b/builder/azure/arm/config_retriever.go index 428b159ea..b31340f46 100644 --- a/builder/azure/arm/config_retriever.go +++ b/builder/azure/arm/config_retriever.go @@ -1,5 +1,12 @@ package arm +// Method to resolve information about the user so that a client can be +// constructed to communicated with Azure. +// +// The following data are resolved. +// +// 1. TenantID + import ( "github.com/Azure/go-autorest/autorest/azure" "github.com/mitchellh/packer/builder/azure/common" @@ -11,7 +18,9 @@ type configRetriever struct { } func newConfigRetriever() configRetriever { - return configRetriever{common.FindTenantID} + return configRetriever{ + common.FindTenantID, + } } func (cr configRetriever) FillParameters(c *Config) error { @@ -22,5 +31,6 @@ func (cr configRetriever) FillParameters(c *Config) error { } c.TenantID = tenantID } + return nil } diff --git a/builder/azure/arm/config_test.go b/builder/azure/arm/config_test.go index ae66cc09b..9e5a89245 100644 --- a/builder/azure/arm/config_test.go +++ b/builder/azure/arm/config_test.go @@ -142,6 +142,78 @@ func TestConfigShouldRejectCustomImageAndMarketPlace(t *testing.T) { } } +func TestConfigVirtualNetworkNameIsOptional(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", + "virtual_network_name": "MyVirtualNetwork", + } + + c, _, _ := newConfig(config, getPackerConfiguration()) + if c.VirtualNetworkName != "MyVirtualNetwork" { + t.Errorf("Expected Config to set virtual_network_name to MyVirtualNetwork, but got %q", c.VirtualNetworkName) + } + if c.VirtualNetworkResourceGroupName != "" { + t.Errorf("Expected Config to leave virtual_network_resource_group_name to '', but got %q", c.VirtualNetworkResourceGroupName) + } + if c.VirtualNetworkSubnetName != "" { + t.Errorf("Expected Config to leave virtual_network_subnet_name to '', but got %q", c.VirtualNetworkSubnetName) + } +} + +// The user can pass the value virtual_network_resource_group_name to avoid the lookup of +// a virtual network's resource group, or to help with disambiguation. The value should +// only be set if virtual_network_name was set. +func TestConfigVirtualNetworkResourceGroupNameMustBeSetWithVirtualNetworkName(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", + "virtual_network_resource_group_name": "MyVirtualNetworkRG", + } + + _, _, err := newConfig(config, getPackerConfiguration()) + if err == nil { + t.Error("Expected Config to reject virtual_network_resource_group_name, if virtual_network_name is not set.") + } +} + +// The user can pass the value virtual_network_subnet_name to avoid the lookup of +// a virtual network subnet's name, or to help with disambiguation. The value should +// only be set if virtual_network_name was set. +func TestConfigVirtualNetworkSubnetNameMustBeSetWithVirtualNetworkName(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", + "virtual_network_subnet_name": "MyVirtualNetworkRG", + } + + _, _, err := newConfig(config, getPackerConfiguration()) + if err == nil { + t.Error("Expected Config to reject virtual_network_subnet_name, if virtual_network_name is not set.") + } +} + func TestConfigShouldDefaultToPublicCloud(t *testing.T) { c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) diff --git a/builder/azure/arm/resource_resolver.go b/builder/azure/arm/resource_resolver.go new file mode 100644 index 000000000..0dc278b25 --- /dev/null +++ b/builder/azure/arm/resource_resolver.go @@ -0,0 +1,101 @@ +package arm + +// Code to resolve resources that are required by the API. These resources +// can most likely be resolved without asking the user, thereby reducing the +// amount of configuration they need to provide. +// +// Resource resolver differs from config retriever because resource resolver +// requires a client to communicate with the Azure API. A config retriever is +// used to determine values without use of a client. + +import ( + "fmt" + "strings" +) + +type resourceResolver struct { + client *AzureClient + findVirtualNetworkResourceGroup func(*AzureClient, string) (string, error) + findVirtualNetworkSubnet func(*AzureClient, string, string) (string, error) +} + +func newResourceResolver(client *AzureClient) *resourceResolver { + return &resourceResolver{ + client: client, + findVirtualNetworkResourceGroup: findVirtualNetworkResourceGroup, + findVirtualNetworkSubnet: findVirtualNetworkSubnet, + } +} + +func (s *resourceResolver) Resolve(c *Config) error { + if s.shouldResolveResourceGroup(c) { + resourceGroupName, err := s.findVirtualNetworkResourceGroup(s.client, c.VirtualNetworkName) + if err != nil { + return err + } + + subnetName, err := s.findVirtualNetworkSubnet(s.client, resourceGroupName, c.VirtualNetworkName) + if err != nil { + return err + } + + c.VirtualNetworkResourceGroupName = resourceGroupName + c.VirtualNetworkSubnetName = subnetName + } + + return nil +} + +func (s *resourceResolver) shouldResolveResourceGroup(c *Config) bool { + return c.VirtualNetworkName != "" && c.VirtualNetworkResourceGroupName == "" +} + +func getResourceGroupNameFromId(id string) string { + // "/subscriptions/3f499422-dd76-4114-8859-86d526c9deb6/resourceGroups/packer-Resource-Group-yylnwsl30j/providers/... + xs := strings.Split(id, "/") + return xs[4] +} + +func findVirtualNetworkResourceGroup(client *AzureClient, name string) (string, error) { + virtualNetworks, err := client.VirtualNetworksClient.ListAll() + if err != nil { + return "", err + } + + resourceGroupNames := make([]string, 0) + + for _, virtualNetwork := range *virtualNetworks.Value { + if strings.EqualFold(name, *virtualNetwork.Name) { + rgn := getResourceGroupNameFromId(*virtualNetwork.ID) + resourceGroupNames = append(resourceGroupNames, rgn) + } + } + + if len(resourceGroupNames) == 0 { + return "", fmt.Errorf("Cannot find a resource group with a virtual network called %q", name) + } + + if len(resourceGroupNames) > 1 { + return "", fmt.Errorf("Found multiple resource groups with a virtual network called %q, please use virtual_network_resource_group_name to disambiguate", name) + } + + return resourceGroupNames[0], nil +} + +func findVirtualNetworkSubnet(client *AzureClient, resourceGroupName string, name string) (string, error) { + subnets, err := client.SubnetsClient.List(resourceGroupName, name) + if err != nil { + return "", err + } + + if len(*subnets.Value) == 0 { + return "", fmt.Errorf("Cannot find a subnet in the resource group %q associated with the virtual network called %q", resourceGroupName, name) + } + + if len(*subnets.Value) > 1 { + return "", fmt.Errorf("Found multiple subnets in the resource group %q associated with the virtual network called %q, please use virtual_network_subnet_name to disambiguate", resourceGroupName, name) + } + + subnet := (*subnets.Value)[0] + return *subnet.Name, nil +} diff --git a/builder/azure/arm/resource_resolver_test.go b/builder/azure/arm/resource_resolver_test.go new file mode 100644 index 000000000..f8e339687 --- /dev/null +++ b/builder/azure/arm/resource_resolver_test.go @@ -0,0 +1,76 @@ +package arm + +import ( + "testing" +) + +func TestResourceResolverIgnoresEmptyVirtualNetworkName(t *testing.T) { + c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) + if c.VirtualNetworkName != "" { + t.Fatalf("Expected VirtualNetworkName to be empty by default") + } + + sut := newTestResourceResolver() + sut.findVirtualNetworkResourceGroup = nil // assert that this is not even called + sut.Resolve(c) + + if c.VirtualNetworkName != "" { + t.Fatalf("Expected VirtualNetworkName to be empty") + } + if c.VirtualNetworkResourceGroupName != "" { + t.Fatalf("Expected VirtualNetworkResourceGroupName to be empty") + } +} + +// If the user fully specified the virtual network name and resource group then +// there is no need to do a lookup. +func TestResourceResolverIgnoresSetVirtualNetwork(t *testing.T) { + c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) + c.VirtualNetworkName = "--virtual-network-name--" + c.VirtualNetworkResourceGroupName = "--virtual-network-resource-group-name--" + c.VirtualNetworkSubnetName = "--virtual-network-subnet-name--" + + sut := newTestResourceResolver() + sut.findVirtualNetworkResourceGroup = nil // assert that this is not even called + sut.findVirtualNetworkSubnet = nil // assert that this is not even called + sut.Resolve(c) + + if c.VirtualNetworkName != "--virtual-network-name--" { + t.Fatalf("Expected VirtualNetworkName to be --virtual-network-name--") + } + if c.VirtualNetworkResourceGroupName != "--virtual-network-resource-group-name--" { + t.Fatalf("Expected VirtualNetworkResourceGroupName to be --virtual-network-resource-group-name--") + } + if c.VirtualNetworkSubnetName != "--virtual-network-subnet-name--" { + t.Fatalf("Expected VirtualNetworkSubnetName to be --virtual-network-subnet-name--") + } +} + +// If the user set virtual network name then the code should resolve virtual network +// resource group name. +func TestResourceResolverSetVirtualNetworkResourceGroupName(t *testing.T) { + c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration()) + c.VirtualNetworkName = "--virtual-network-name--" + + sut := newTestResourceResolver() + sut.Resolve(c) + + if c.VirtualNetworkResourceGroupName != "findVirtualNetworkResourceGroup is mocked" { + t.Fatalf("Expected VirtualNetworkResourceGroupName to be 'findVirtualNetworkResourceGroup is mocked'") + } + if c.VirtualNetworkSubnetName != "findVirtualNetworkSubnet is mocked" { + t.Fatalf("Expected findVirtualNetworkSubnet to be 'findVirtualNetworkSubnet is mocked'") + } +} + +func newTestResourceResolver() resourceResolver { + return resourceResolver{ + client: nil, + findVirtualNetworkResourceGroup: func(*AzureClient, string) (string, error) { + return "findVirtualNetworkResourceGroup is mocked", nil + }, + findVirtualNetworkSubnet: func(*AzureClient, string, string) (string, error) { + return "findVirtualNetworkSubnet is mocked", nil + }, + } +} diff --git a/builder/azure/arm/step_get_ip_address.go b/builder/azure/arm/step_get_ip_address.go index 4b664c4ed..d0b24e82c 100644 --- a/builder/azure/arm/step_get_ip_address.go +++ b/builder/azure/arm/step_get_ip_address.go @@ -11,43 +11,77 @@ import ( "github.com/mitchellh/packer/packer" ) +type EndpointType int + +const ( + PublicEndpoint EndpointType = iota + PrivateEndpoint +) + +var ( + EndpointCommunicationText = map[EndpointType]string{ + PublicEndpoint: "PublicEndpoint", + PrivateEndpoint: "PrivateEndpoint", + } +) + type StepGetIPAddress struct { - client *AzureClient - get func(resourceGroupName string, ipAddressName string) (string, error) - say func(message string) - error func(e error) + client *AzureClient + endpoint EndpointType + get func(resourceGroupName string, ipAddressName string, interfaceName string) (string, error) + say func(message string) + error func(e error) } -func NewStepGetIPAddress(client *AzureClient, ui packer.Ui) *StepGetIPAddress { +func NewStepGetIPAddress(client *AzureClient, ui packer.Ui, endpoint EndpointType) *StepGetIPAddress { var step = &StepGetIPAddress{ - client: client, - say: func(message string) { ui.Say(message) }, - error: func(e error) { ui.Error(e.Error()) }, + client: client, + endpoint: endpoint, + say: func(message string) { ui.Say(message) }, + error: func(e error) { ui.Error(e.Error()) }, + } + + switch endpoint { + case PrivateEndpoint: + step.get = step.getPrivateIP + case PublicEndpoint: + step.get = step.getPublicIP } - step.get = step.getIPAddress return step } -func (s *StepGetIPAddress) getIPAddress(resourceGroupName string, ipAddressName string) (string, error) { - res, err := s.client.PublicIPAddressesClient.Get(resourceGroupName, ipAddressName, "") +func (s *StepGetIPAddress) getPrivateIP(resourceGroupName string, ipAddressName string, interfaceName string) (string, error) { + resp, err := s.client.InterfacesClient.Get(resourceGroupName, interfaceName, "") + if err != nil { + return "", err + } + + return *(*resp.Properties.IPConfigurations)[0].Properties.PrivateIPAddress, nil +} + +func (s *StepGetIPAddress) getPublicIP(resourceGroupName string, ipAddressName string, interfaceName string) (string, error) { + resp, err := s.client.PublicIPAddressesClient.Get(resourceGroupName, ipAddressName, "") if err != nil { - return "", nil + return "", err } - return *res.Properties.IPAddress, nil + return *resp.Properties.IPAddress, nil } func (s *StepGetIPAddress) Run(state multistep.StateBag) multistep.StepAction { - s.say("Getting the public IP address ...") + s.say("Getting the VM's IP address ...") var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string) var ipAddressName = state.Get(constants.ArmPublicIPAddressName).(string) + var nicName = state.Get(constants.ArmNicName).(string) s.say(fmt.Sprintf(" -> ResourceGroupName : '%s'", resourceGroupName)) s.say(fmt.Sprintf(" -> PublicIPAddressName : '%s'", ipAddressName)) + s.say(fmt.Sprintf(" -> NicName : '%s'", nicName)) + s.say(fmt.Sprintf(" -> Network Connection : '%s'", EndpointCommunicationText[s.endpoint])) - address, err := s.get(resourceGroupName, ipAddressName) + address, err := s.get(resourceGroupName, ipAddressName, nicName) if err != nil { state.Put(constants.Error, err) s.error(err) @@ -55,8 +89,8 @@ func (s *StepGetIPAddress) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionHalt } - s.say(fmt.Sprintf(" -> Public IP : '%s'", address)) state.Put(constants.SSHHost, address) + s.say(fmt.Sprintf(" -> IP Address : '%s'", address)) return multistep.ActionContinue } diff --git a/builder/azure/arm/step_get_ip_address_test.go b/builder/azure/arm/step_get_ip_address_test.go index 8f35ad8da..d6e29a2d7 100644 --- a/builder/azure/arm/step_get_ip_address_test.go +++ b/builder/azure/arm/step_get_ip_address_test.go @@ -13,9 +13,10 @@ import ( func TestStepGetIPAddressShouldFailIfGetFails(t *testing.T) { var testSubject = &StepGetIPAddress{ - get: func(string, string) (string, error) { return "", fmt.Errorf("!! Unit Test FAIL !!") }, - say: func(message string) {}, - error: func(e error) {}, + get: func(string, string, string) (string, error) { return "", fmt.Errorf("!! Unit Test FAIL !!") }, + endpoint: PublicEndpoint, + say: func(message string) {}, + error: func(e error) {}, } stateBag := createTestStateBagStepGetIPAddress() @@ -32,9 +33,10 @@ func TestStepGetIPAddressShouldFailIfGetFails(t *testing.T) { func TestStepGetIPAddressShouldPassIfGetPasses(t *testing.T) { var testSubject = &StepGetIPAddress{ - get: func(string, string) (string, error) { return "", nil }, - say: func(message string) {}, - error: func(e error) {}, + get: func(string, string, string) (string, error) { return "", nil }, + endpoint: PublicEndpoint, + say: func(message string) {}, + error: func(e error) {}, } stateBag := createTestStateBagStepGetIPAddress() @@ -52,16 +54,19 @@ func TestStepGetIPAddressShouldPassIfGetPasses(t *testing.T) { func TestStepGetIPAddressShouldTakeStepArgumentsFromStateBag(t *testing.T) { var actualResourceGroupName string var actualIPAddressName string + var actualNicName string var testSubject = &StepGetIPAddress{ - get: func(resourceGroupName string, ipAddressName string) (string, error) { + get: func(resourceGroupName string, ipAddressName string, nicName string) (string, error) { actualResourceGroupName = resourceGroupName actualIPAddressName = ipAddressName + actualNicName = nicName return "127.0.0.1", nil }, - say: func(message string) {}, - error: func(e error) {}, + endpoint: PublicEndpoint, + say: func(message string) {}, + error: func(e error) {}, } stateBag := createTestStateBagStepGetIPAddress() @@ -73,6 +78,7 @@ func TestStepGetIPAddressShouldTakeStepArgumentsFromStateBag(t *testing.T) { var expectedResourceGroupName = stateBag.Get(constants.ArmResourceGroupName).(string) var expectedIPAddressName = stateBag.Get(constants.ArmPublicIPAddressName).(string) + var expectedNicName = stateBag.Get(constants.ArmNicName).(string) if actualIPAddressName != expectedIPAddressName { t.Fatal("Expected StepGetIPAddress to source 'constants.ArmIPAddressName' from the state bag, but it did not.") @@ -82,6 +88,10 @@ func TestStepGetIPAddressShouldTakeStepArgumentsFromStateBag(t *testing.T) { t.Fatal("Expected StepGetIPAddress to source 'constants.ArmResourceGroupName' from the state bag, but it did not.") } + if actualNicName != expectedNicName { + t.Fatalf("Expected StepGetIPAddress to source 'constants.ArmNetworkInterfaceName' from the state bag, but it did not.") + } + expectedIPAddress, ok := stateBag.GetOk(constants.SSHHost) if !ok { t.Fatalf("Expected the state bag to have a value for '%s', but it did not.", constants.SSHHost) @@ -96,6 +106,7 @@ func createTestStateBagStepGetIPAddress() multistep.StateBag { stateBag := new(multistep.BasicStateBag) stateBag.Put(constants.ArmPublicIPAddressName, "Unit Test: PublicIPAddressName") + stateBag.Put(constants.ArmNicName, "Unit Test: NicName") stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName") return stateBag diff --git a/builder/azure/arm/template_factory.go b/builder/azure/arm/template_factory.go index ca0f95fb2..31faa797f 100644 --- a/builder/azure/arm/template_factory.go +++ b/builder/azure/arm/template_factory.go @@ -51,6 +51,13 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error) builder.SetMarketPlaceImage(config.ImagePublisher, config.ImageOffer, config.ImageSku, config.ImageVersion) } + if config.VirtualNetworkName != "" { + builder.SetVirtualNetwork( + config.VirtualNetworkResourceGroupName, + config.VirtualNetworkName, + config.VirtualNetworkSubnetName) + } + doc, _ := builder.ToJSON() return createDeploymentParameters(*doc, params) } diff --git a/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment03.approved.json b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment03.approved.json index a6fce9d67..e0d7b10bb 100644 --- a/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment03.approved.json +++ b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment03.approved.json @@ -154,7 +154,8 @@ "subnetName": "packerSubnet", "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", "virtualNetworkName": "packerNetwork", - "vmStorageAccountContainerName": "images", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" - } + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "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.TestVirtualMachineDeployment04.approved.json b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment04.approved.json index 74a4b767f..faa51be82 100644 --- a/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment04.approved.json +++ b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment04.approved.json @@ -152,7 +152,8 @@ "subnetName": "packerSubnet", "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", "virtualNetworkName": "packerNetwork", - "vmStorageAccountContainerName": "images", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" - } + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "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.TestVirtualMachineDeployment05.approved.json b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment05.approved.json new file mode 100644 index 000000000..ae13afab1 --- /dev/null +++ b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment05.approved.json @@ -0,0 +1,120 @@ +{ + "$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('apiVersion')]", + "dependsOn": [], + "location": "[variables('location')]", + "name": "[variables('nicName')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "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": { + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "image": { + "uri": "https://localhost/custom.vhd" + }, + "name": "osdisk", + "osType": "Linux", + "vhd": { + "uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]" + } + } + } + }, + "type": "Microsoft.Compute/virtualMachines" + } + ], + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2015-06-15", + "location": "[resourceGroup().location]", + "nicName": "packerNic", + "publicIPAddressName": "packerPublicIP", + "publicIPAddressType": "Dynamic", + "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", + "subnetAddressPrefix": "10.0.0.0/24", + "subnetName": "virtualNetworkSubnetName", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "virtualNetworkName", + "virtualNetworkResourceGroup": "virtualNetworkResourceGroupName", + "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 4ab976f9c..708d7f379 100644 --- a/builder/azure/arm/template_factory_test.go +++ b/builder/azure/arm/template_factory_test.go @@ -145,6 +145,38 @@ func TestVirtualMachineDeployment04(t *testing.T) { } } +func TestVirtualMachineDeployment05(t *testing.T) { + config := map[string]string{ + "capture_name_prefix": "ignore", + "capture_container_name": "ignore", + "location": "ignore", + "image_url": "https://localhost/custom.vhd", + "resource_group_name": "ignore", + "storage_account": "ignore", + "subscription_id": "ignore", + "os_type": constants.Target_Linux, + "communicator": "none", + "virtual_network_name": "virtualNetworkName", + "virtual_network_resource_group_name": "virtualNetworkResourceGroupName", + "virtual_network_subnet_name": "virtualNetworkSubnetName", + } + + 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 25451967d..0955994e6 100644 --- a/builder/azure/common/constants/stateBag.go +++ b/builder/azure/common/constants/stateBag.go @@ -18,6 +18,7 @@ const ( ArmComputeName string = "arm.ComputeName" ArmCertificateUrl string = "arm.CertificateUrl" ArmDeploymentName string = "arm.DeploymentName" + ArmNicName string = "arm.NicName" ArmKeyVaultName string = "arm.KeyVaultName" ArmLocation string = "arm.Location" ArmOSDiskVhd string = "arm.OSDiskVhd" diff --git a/builder/azure/common/template/TestBuildLinux02.approved.txt b/builder/azure/common/template/TestBuildLinux02.approved.txt new file mode 100644 index 000000000..c2533dd2d --- /dev/null +++ b/builder/azure/common/template/TestBuildLinux02.approved.txt @@ -0,0 +1,120 @@ +{ + "$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" + } + }, + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2015-06-15", + "location": "[resourceGroup().location]", + "nicName": "packerNic", + "publicIPAddressName": "packerPublicIP", + "publicIPAddressType": "Dynamic", + "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", + "subnetAddressPrefix": "10.0.0.0/24", + "subnetName": "--subnet-name--", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "--virtual-network--", + "virtualNetworkResourceGroup": "--virtual-network-resource-group--", + "vmStorageAccountContainerName": "images", + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + }, + "resources": [ + { + "apiVersion": "[variables('apiVersion')]", + "name": "[variables('nicName')]", + "type": "Microsoft.Network/networkInterfaces", + "location": "[variables('location')]", + "dependsOn": [], + "properties": { + "ipConfigurations": [ + { + "properties": { + "privateIPAllocationMethod": "Dynamic", + "subnet": { + "id": "[variables('subnetRef')]" + } + }, + "name": "ipconfig" + } + ] + } + }, + { + "apiVersion": "[variables('apiVersion')]", + "name": "[parameters('vmName')]", + "type": "Microsoft.Compute/virtualMachines", + "location": "[variables('location')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + ], + "properties": { + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": false + } + }, + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('nicName'))]" + } + ] + }, + "osProfile": { + "computerName": "[parameters('vmName')]", + "adminUsername": "[parameters('adminUsername')]", + "adminPassword": "[parameters('adminPassword')]", + "linuxConfiguration": { + "ssh": { + "publicKeys": [ + { + "path": "[variables('sshKeyPath')]", + "keyData": "--test-ssh-authorized-key--" + } + ] + } + } + }, + "storageProfile": { + "osDisk": { + "osType": "Linux", + "name": "osdisk", + "vhd": { + "uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]" + }, + "image": { + "uri": "http://azure/custom.vhd" + }, + "caching": "ReadWrite", + "createOption": "FromImage" + } + } + } + } + ] +} \ No newline at end of file diff --git a/builder/azure/common/template/template_builder.go b/builder/azure/common/template/template_builder.go index d69d782e7..c8cecfcab 100644 --- a/builder/azure/common/template/template_builder.go +++ b/builder/azure/common/template/template_builder.go @@ -13,8 +13,11 @@ const ( jsonPrefix = "" jsonIndent = " " - resourceVirtualMachine = "Microsoft.Compute/virtualMachines" - resourceKeyVaults = "Microsoft.KeyVault/vaults" + resourceKeyVaults = "Microsoft.KeyVault/vaults" + resourceNetworkInterfaces = "Microsoft.Network/networkInterfaces" + resourcePublicIPAddresses = "Microsoft.Network/publicIPAddresses" + resourceVirtualMachine = "Microsoft.Compute/virtualMachines" + resourceVirtualNetworks = "Microsoft.Network/virtualNetworks" variableSshKeyPath = "sshKeyPath" ) @@ -125,6 +128,28 @@ func (s *TemplateBuilder) SetImageUrl(imageUrl string, osType compute.OperatingS return nil } +func (s *TemplateBuilder) SetVirtualNetwork(virtualNetworkResourceGroup, virtualNetworkName, subnetName string) error { + s.setVariable("virtualNetworkResourceGroup", virtualNetworkResourceGroup) + s.setVariable("virtualNetworkName", virtualNetworkName) + s.setVariable("subnetName", subnetName) + + s.deleteResourceByType(resourceVirtualNetworks) + s.deleteResourceByType(resourcePublicIPAddresses) + resource, err := s.getResourceByType(resourceNetworkInterfaces) + if err != nil { + return err + } + + s.deleteResourceDependency(resource, func(s string) bool { + return strings.Contains(s, "Microsoft.Network/virtualNetworks") || + strings.Contains(s, "Microsoft.Network/publicIPAddresses") + }) + + (*resource.Properties.IPConfigurations)[0].Properties.PublicIPAddress = nil + + return nil +} + func (s *TemplateBuilder) ToJSON() (*string, error) { bs, err := json.MarshalIndent(s.template, jsonPrefix, jsonIndent) @@ -144,6 +169,10 @@ func (s *TemplateBuilder) getResourceByType(t string) (*Resource, error) { return nil, fmt.Errorf("template: could not find a resource of type %s", t) } +func (s *TemplateBuilder) setVariable(name string, value string) { + (*s.template.Variables)[name] = value +} + func (s *TemplateBuilder) toKeyVaultID(name string) string { return s.toResourceID(resourceKeyVaults, name) } @@ -156,6 +185,31 @@ func (s *TemplateBuilder) toVariable(name string) string { return fmt.Sprintf("[variables('%s')]", name) } +func (s *TemplateBuilder) deleteResourceByType(resourceType string) { + resources := make([]Resource, 0) + + for _, resource := range *s.template.Resources { + if *resource.Type == resourceType { + continue + } + resources = append(resources, resource) + } + + s.template.Resources = &resources +} + +func (s *TemplateBuilder) deleteResourceDependency(resource *Resource, predicate func(string) bool) { + deps := make([]string, 0) + + for _, dep := range *resource.DependsOn { + if !predicate(dep) { + deps = append(deps, dep) + } + } + + *resource.DependsOn = deps +} + const basicTemplate = `{ "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", "contentVersion": "1.0.0.0", @@ -194,8 +248,9 @@ const basicTemplate = `{ "subnetAddressPrefix": "10.0.0.0/24", "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", "virtualNetworkName": "packerNetwork", + "virtualNetworkResourceGroup": "[resourceGroup().name]", "vmStorageAccountContainerName": "images", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + "vnetID": "[resourceId(variables('virtualNetworkResourceGroup'), 'Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" }, "resources": [ { diff --git a/builder/azure/common/template/template_builder_test.TestBuildLinux00.approved.json b/builder/azure/common/template/template_builder_test.TestBuildLinux00.approved.json index a8ecfc0be..59b5da643 100644 --- a/builder/azure/common/template/template_builder_test.TestBuildLinux00.approved.json +++ b/builder/azure/common/template/template_builder_test.TestBuildLinux00.approved.json @@ -154,7 +154,8 @@ "subnetName": "packerSubnet", "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", "virtualNetworkName": "packerNetwork", - "vmStorageAccountContainerName": "images", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" - } + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "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.TestBuildLinux01.approved.json b/builder/azure/common/template/template_builder_test.TestBuildLinux01.approved.json index 45bf5b271..f26c3f1c2 100644 --- a/builder/azure/common/template/template_builder_test.TestBuildLinux01.approved.json +++ b/builder/azure/common/template/template_builder_test.TestBuildLinux01.approved.json @@ -152,7 +152,8 @@ "subnetName": "packerSubnet", "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", "virtualNetworkName": "packerNetwork", - "vmStorageAccountContainerName": "images", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" - } + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "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.TestBuildLinux02.approved.json b/builder/azure/common/template/template_builder_test.TestBuildLinux02.approved.json new file mode 100644 index 000000000..217c1f415 --- /dev/null +++ b/builder/azure/common/template/template_builder_test.TestBuildLinux02.approved.json @@ -0,0 +1,120 @@ +{ + "$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('apiVersion')]", + "dependsOn": [], + "location": "[variables('location')]", + "name": "[variables('nicName')]", + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "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": "--test-ssh-authorized-key--", + "path": "[variables('sshKeyPath')]" + } + ] + } + } + }, + "storageProfile": { + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "image": { + "uri": "http://azure/custom.vhd" + }, + "name": "osdisk", + "osType": "Linux", + "vhd": { + "uri": "[concat(parameters('storageAccountBlobEndpoint'),variables('vmStorageAccountContainerName'),'/', parameters('osDiskName'),'.vhd')]" + } + } + } + }, + "type": "Microsoft.Compute/virtualMachines" + } + ], + "variables": { + "addressPrefix": "10.0.0.0/16", + "apiVersion": "2015-06-15", + "location": "[resourceGroup().location]", + "nicName": "packerNic", + "publicIPAddressName": "packerPublicIP", + "publicIPAddressType": "Dynamic", + "sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]", + "subnetAddressPrefix": "10.0.0.0/24", + "subnetName": "--subnet-name--", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "--virtual-network--", + "virtualNetworkResourceGroup": "--virtual-network-resource-group--", + "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.TestBuildWindows00.approved.json b/builder/azure/common/template/template_builder_test.TestBuildWindows00.approved.json index d2460d7b4..67dec1b2d 100644 --- a/builder/azure/common/template/template_builder_test.TestBuildWindows00.approved.json +++ b/builder/azure/common/template/template_builder_test.TestBuildWindows00.approved.json @@ -168,7 +168,8 @@ "subnetName": "packerSubnet", "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", "virtualNetworkName": "packerNetwork", - "vmStorageAccountContainerName": "images", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" - } + "virtualNetworkResourceGroup": "[resourceGroup().name]", + "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 f203acace..1097eab8b 100644 --- a/builder/azure/common/template/template_builder_test.go +++ b/builder/azure/common/template/template_builder_test.go @@ -64,6 +64,32 @@ func TestBuildLinux01(t *testing.T) { } } +// Ensure that a user can specify an existing Virtual Network +func TestBuildLinux02(t *testing.T) { + testSubject, err := NewTemplateBuilder() + if err != nil { + t.Fatal(err) + } + + testSubject.BuildLinux("--test-ssh-authorized-key--") + testSubject.SetImageUrl("http://azure/custom.vhd", compute.Linux) + + err = testSubject.SetVirtualNetwork("--virtual-network-resource-group--", "--virtual-network--", "--subnet-name--") + 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) + } +} + // Ensure that a Windows template is configured as expected. // * Include WinRM configuration. // * Include KeyVault configuration, which is needed for WinRM. diff --git a/website/source/docs/builders/azure.html.md b/website/source/docs/builders/azure.html.md index adc5655b8..71b6006db 100644 --- a/website/source/docs/builders/azure.html.md +++ b/website/source/docs/builders/azure.html.md @@ -83,6 +83,19 @@ builder. configures your Tenant ID, Object ID, Key Vault Name, Key Vault Secret, and WinRM certificate URL. +- `virtual_network_name` (string) Use a pre-existing virtual network for the VM. This option enables private + communication with the VM, no public IP address is **used** or **provisioned**. This value should only be set if + Packer is executed from a host on the same subnet / virtual network. + +- `virtual_network_resource_group_name` (string) If virtual_network_name is set, this value **may** also be set. If + virtual_network_name is set, and this value is not set the builder attempts to determine the resource group + containing the virtual network. If the resource group cannot be found, or it cannot be disambiguated, this value + should be set. + +- `virtual_network_subnet_name` (string) If virtual_network_name is set, this value **may** also be set. If + virtual_network_name is set, and this value is not set the builder attempts to determine the subnet to use with + the virtual network. If the subnet cannot be found, or it cannot be disambiguated, this value should be set. + - `vm_size` (string) Size of the VM used for building. This can be changed when you deploy a VM from your VHD. See [pricing](https://azure.microsoft.com/en-us/pricing/details/virtual-machines/) information. Defaults to `Standard_A1`.