diff --git a/builder/azure/arm/builder.go b/builder/azure/arm/builder.go index 5377b7bb6..b8658cd8d 100644 --- a/builder/azure/arm/builder.go +++ b/builder/azure/arm/builder.go @@ -119,7 +119,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe } endpointConnectType := PublicEndpoint - if b.isPrivateNetworkCommunication() { + if b.isPublicPrivateNetworkCommunication() && b.isPrivateNetworkCommunication() { + endpointConnectType = PublicEndpointInPrivateNetwork + } else if b.isPrivateNetworkCommunication() { endpointConnectType = PrivateEndpoint } @@ -245,6 +247,10 @@ func (b *Builder) writeSSHPrivateKey(ui packer.Ui, debugKeyPath string) { } } +func (b *Builder) isPublicPrivateNetworkCommunication() bool { + return DefaultPrivateVirtualNetworkWithPublicIp != b.config.PrivateVirtualNetworkWithPublicIp +} + func (b *Builder) isPrivateNetworkCommunication() bool { return b.config.VirtualNetworkName != "" } diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go index 1d0c98d14..08ab3ffb2 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -34,10 +34,11 @@ import ( ) const ( - DefaultCloudEnvironmentName = "Public" - DefaultImageVersion = "latest" - DefaultUserName = "packer" - DefaultVMSize = "Standard_A1" + DefaultCloudEnvironmentName = "Public" + DefaultImageVersion = "latest" + DefaultUserName = "packer" + DefaultPrivateVirtualNetworkWithPublicIp = false + DefaultVMSize = "Standard_A1" ) var ( @@ -78,19 +79,20 @@ type Config struct { manageImageLocation string // Deployment - AzureTags map[string]*string `mapstructure:"azure_tags"` - ResourceGroupName string `mapstructure:"resource_group_name"` - StorageAccount string `mapstructure:"storage_account"` - TempComputeName string `mapstructure:"temp_compute_name"` - TempResourceGroupName string `mapstructure:"temp_resource_group_name"` - 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"` - CustomDataFile string `mapstructure:"custom_data_file"` - customData string + AzureTags map[string]*string `mapstructure:"azure_tags"` + ResourceGroupName string `mapstructure:"resource_group_name"` + StorageAccount string `mapstructure:"storage_account"` + TempComputeName string `mapstructure:"temp_compute_name"` + TempResourceGroupName string `mapstructure:"temp_resource_group_name"` + storageAccountBlobEndpoint string + CloudEnvironmentName string `mapstructure:"cloud_environment_name"` + cloudEnvironment *azure.Environment + PrivateVirtualNetworkWithPublicIp bool `mapstructure:"private_virtual_network_with_public_ip"` + VirtualNetworkName string `mapstructure:"virtual_network_name"` + VirtualNetworkSubnetName string `mapstructure:"virtual_network_subnet_name"` + VirtualNetworkResourceGroupName string `mapstructure:"virtual_network_resource_group_name"` + CustomDataFile string `mapstructure:"custom_data_file"` + customData string // OS OSType string `mapstructure:"os_type"` diff --git a/builder/azure/arm/step_get_ip_address.go b/builder/azure/arm/step_get_ip_address.go index fed78afbb..69b17a64f 100644 --- a/builder/azure/arm/step_get_ip_address.go +++ b/builder/azure/arm/step_get_ip_address.go @@ -16,12 +16,14 @@ type EndpointType int const ( PublicEndpoint EndpointType = iota PrivateEndpoint + PublicEndpointInPrivateNetwork ) var ( EndpointCommunicationText = map[EndpointType]string{ - PublicEndpoint: "PublicEndpoint", - PrivateEndpoint: "PrivateEndpoint", + PublicEndpoint: "PublicEndpoint", + PrivateEndpoint: "PrivateEndpoint", + PublicEndpointInPrivateNetwork: "PublicEndpointInPrivateNetwork", } ) @@ -46,6 +48,8 @@ func NewStepGetIPAddress(client *AzureClient, ui packer.Ui, endpoint EndpointTyp step.get = step.getPrivateIP case PublicEndpoint: step.get = step.getPublicIP + case PublicEndpointInPrivateNetwork: + step.get = step.getPublicIPInPrivateNetwork } return step @@ -70,6 +74,11 @@ func (s *StepGetIPAddress) getPublicIP(resourceGroupName string, ipAddressName s return *resp.IPAddress, nil } +func (s *StepGetIPAddress) getPublicIPInPrivateNetwork(resourceGroupName string, ipAddressName string, interfaceName string) (string, error) { + s.getPrivateIP(resourceGroupName, ipAddressName, interfaceName) + return s.getPublicIP(resourceGroupName, ipAddressName, interfaceName) +} + func (s *StepGetIPAddress) Run(state multistep.StateBag) multistep.StepAction { s.say("Getting the VM's IP address ...") diff --git a/builder/azure/arm/step_get_ip_address_test.go b/builder/azure/arm/step_get_ip_address_test.go index a0b854ed0..f19c14425 100644 --- a/builder/azure/arm/step_get_ip_address_test.go +++ b/builder/azure/arm/step_get_ip_address_test.go @@ -12,42 +12,50 @@ import ( ) func TestStepGetIPAddressShouldFailIfGetFails(t *testing.T) { - var testSubject = &StepGetIPAddress{ - 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() - - var result = testSubject.Run(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) + endpoints := []EndpointType{PublicEndpoint, PublicEndpointInPrivateNetwork} + + for _, endpoint := range endpoints { + var testSubject = &StepGetIPAddress{ + get: func(string, string, string) (string, error) { return "", fmt.Errorf("!! Unit Test FAIL !!") }, + endpoint: endpoint, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepGetIPAddress() + + var result = testSubject.Run(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 TestStepGetIPAddressShouldPassIfGetPasses(t *testing.T) { - var testSubject = &StepGetIPAddress{ - get: func(string, string, string) (string, error) { return "", nil }, - endpoint: PublicEndpoint, - say: func(message string) {}, - error: func(e error) {}, - } - - stateBag := createTestStateBagStepGetIPAddress() - - var result = testSubject.Run(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) + endpoints := []EndpointType{PublicEndpoint, PublicEndpointInPrivateNetwork} + + for _, endpoint := range endpoints { + var testSubject = &StepGetIPAddress{ + get: func(string, string, string) (string, error) { return "", nil }, + endpoint: endpoint, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepGetIPAddress() + + var result = testSubject.Run(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) + } } } @@ -55,50 +63,53 @@ func TestStepGetIPAddressShouldTakeStepArgumentsFromStateBag(t *testing.T) { var actualResourceGroupName string var actualIPAddressName string var actualNicName string - - var testSubject = &StepGetIPAddress{ - get: func(resourceGroupName string, ipAddressName string, nicName string) (string, error) { - actualResourceGroupName = resourceGroupName - actualIPAddressName = ipAddressName - actualNicName = nicName - - return "127.0.0.1", nil - }, - endpoint: PublicEndpoint, - say: func(message string) {}, - error: func(e error) {}, - } - - stateBag := createTestStateBagStepGetIPAddress() - var result = testSubject.Run(stateBag) - - if result != multistep.ActionContinue { - t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) - } - - 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.") - } - - if actualResourceGroupName != expectedResourceGroupName { - 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) - } - - if expectedIPAddress != "127.0.0.1" { - t.Fatalf("Expected the value of stateBag[%s] to be '127.0.0.1', but got '%s'.", constants.SSHHost, expectedIPAddress) + endpoints := []EndpointType{PublicEndpoint, PublicEndpointInPrivateNetwork} + + for _, endpoint := range endpoints { + var testSubject = &StepGetIPAddress{ + get: func(resourceGroupName string, ipAddressName string, nicName string) (string, error) { + actualResourceGroupName = resourceGroupName + actualIPAddressName = ipAddressName + actualNicName = nicName + + return "127.0.0.1", nil + }, + endpoint: endpoint, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepGetIPAddress() + var result = testSubject.Run(stateBag) + + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + 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.") + } + + if actualResourceGroupName != expectedResourceGroupName { + 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) + } + + if expectedIPAddress != "127.0.0.1" { + t.Fatalf("Expected the value of stateBag[%s] to be '127.0.0.1', but got '%s'.", constants.SSHHost, expectedIPAddress) + } } } diff --git a/builder/azure/arm/template_factory.go b/builder/azure/arm/template_factory.go index 3cd00845f..fd4d96fe5 100644 --- a/builder/azure/arm/template_factory.go +++ b/builder/azure/arm/template_factory.go @@ -76,7 +76,12 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error) builder.SetCustomData(config.customData) } - if config.VirtualNetworkName != "" { + if config.VirtualNetworkName != "" && DefaultPrivateVirtualNetworkWithPublicIp != config.PrivateVirtualNetworkWithPublicIp { + builder.SetPrivateVirtualNetworWithPublicIp( + config.VirtualNetworkResourceGroupName, + config.VirtualNetworkName, + config.VirtualNetworkSubnetName) + } else if config.VirtualNetworkName != "" { builder.SetVirtualNetwork( config.VirtualNetworkResourceGroupName, config.VirtualNetworkName, diff --git a/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment10.approved.json b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment10.approved.json new file mode 100644 index 000000000..9ad7217c7 --- /dev/null +++ b/builder/azure/arm/template_factory_test.TestVirtualMachineDeployment10.approved.json @@ -0,0 +1,141 @@ +{ + "$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('networkInterfacesApiVersion')]", + "dependsOn": [ + "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]" + ], + "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": { + "imageReference": { + "offer": "--image-offer--", + "publisher": "--image-publisher--", + "sku": "--image-sku--", + "version": "--version--" + }, + "osDisk": { + "caching": "ReadWrite", + "createOption": "fromImage", + "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": "--virtual_network_subnet_name--", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", + "virtualNetworkName": "--virtual_network_name--", + "virtualNetworkResourceGroup": "--virtual_network_resource_group_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 8a6e0c650..b571871ef 100644 --- a/builder/azure/arm/template_factory_test.go +++ b/builder/azure/arm/template_factory_test.go @@ -318,6 +318,43 @@ func TestVirtualMachineDeployment09(t *testing.T) { } } +// Ensure the VM template is correct when building with PublicIp and connect to Private Network +func TestVirtualMachineDeployment10(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--", + + "virtual_network_resource_group_name": "--virtual_network_resource_group_name--", + "virtual_network_name": "--virtual_network_name--", + "virtual_network_subnet_name": "--virtual_network_subnet_name--", + "private_virtual_network_with_public_ip": true, + + "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/template/template_builder.go b/builder/azure/common/template/template_builder.go index 898bf77e9..7950052dd 100644 --- a/builder/azure/common/template/template_builder.go +++ b/builder/azure/common/template/template_builder.go @@ -219,6 +219,24 @@ func (s *TemplateBuilder) SetVirtualNetwork(virtualNetworkResourceGroup, virtual return nil } +func (s *TemplateBuilder) SetPrivateVirtualNetworWithPublicIp(virtualNetworkResourceGroup, virtualNetworkName, subnetName string) error { + s.setVariable("virtualNetworkResourceGroup", virtualNetworkResourceGroup) + s.setVariable("virtualNetworkName", virtualNetworkName) + s.setVariable("subnetName", subnetName) + + s.deleteResourceByType(resourceVirtualNetworks) + resource, err := s.getResourceByType(resourceNetworkInterfaces) + if err != nil { + return err + } + + s.deleteResourceDependency(resource, func(s string) bool { + return strings.Contains(s, "Microsoft.Network/virtualNetworks") + }) + + return nil +} + func (s *TemplateBuilder) SetTags(tags *map[string]*string) error { if tags == nil || len(*tags) == 0 { return nil diff --git a/website/source/docs/builders/azure.html.md b/website/source/docs/builders/azure.html.md index ec3916318..2e7422fc3 100644 --- a/website/source/docs/builders/azure.html.md +++ b/website/source/docs/builders/azure.html.md @@ -131,9 +131,12 @@ When creating a managed image the following two options are required. - `tenant_id` (string) The account identifier with which your `client_id` and `subscription_id` are associated. If not specified, `tenant_id` will be looked up using `subscription_id`. +- `private_virtual_network_with_public_ip` (boolean) This value allows you to set a `virtual_network_name` and obtain + a public IP. If this value is not set and `virtual_network_name` is defined Packer is only allowed to be executed + from a host on the same subnet / virtual network. + - `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. + communication with the VM, no public IP address is **used** or **provisioned** (unless you set `private_virtual_network_with_public_ip`). - `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