Repository: ansible/ansible

Issue:
azure_rm_virtualmachine_scaleset should support Azure Azure Load Balancer with inbound NAT rules
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Feature Idea

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
azure_rm_virtualmachine_scaleset

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.5.0
  config file = None
  configured module search path = [u'/Users/zhijzhao/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python2.7/site-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 2.7.14 (default, Sep 25 2017, 09:53:22) [GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.37)]

```

##### CONFIGURATION
<!---
If using Ansible 2.4 or above, paste the results of "ansible-config dump --only-changed"

Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).

-->
No change at all
##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->
macOS 10.13.1
##### SUMMARY
<!--- Explain the problem briefly -->
Right now, only subnet is passed to VirtualMachineScaleSetIPConfiguration in the module implementation. In other words, user can not access the provisioned vmss with public ip address. 

Actually, the API of VirtualMachineScaleSetIPConfiguration support load balancer. 

My understanding is that vmss with load balancer is the most common scenario, which is also the default behavior in Azure Portal. 

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->
```yaml

```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```

```



Discussion:
@yaweiw @zikalino 
cc @sozercan
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/cloud/azure/azure_rm_virtualmachine_scaleset.py b/lib/ansible/modules/cloud/azure/azure_rm_virtualmachine_scaleset.py
--- a/lib/ansible/modules/cloud/azure/azure_rm_virtualmachine_scaleset.py
+++ b/lib/ansible/modules/cloud/azure/azure_rm_virtualmachine_scaleset.py
@@ -54,17 +54,17 @@
         required: true
     capacity:
         description:
-            - Capacity of VMSS
+            - Capacity of VMSS.
         required: true
     tier:
         description:
-            - SKU Tier
+            - SKU Tier.
         choices:
             - Basic
             - Standard
     upgrade_policy:
         description:
-            - Upgrade policy
+            - Upgrade policy.
         choices:
             - Manual
             - Automatic
@@ -111,7 +111,7 @@
             - Linux
     managed_disk_type:
         description:
-            - Managed disk type
+            - Managed disk type.
         choices:
             - Standard_LRS
             - Premium_LRS
@@ -124,16 +124,16 @@
         suboptions:
             lun:
                 description:
-                    - The logical unit number for data disk
+                    - The logical unit number for data disk.
                 default: 0
                 version_added: "2.4"
             disk_size_gb:
                 description:
-                    - The initial disk size in GB for blank data disks
+                    - The initial disk size in GB for blank data disks.
                 version_added: "2.4"
             managed_disk_type:
                 description:
-                    - Managed data disk type
+                    - Managed data disk type.
                 choices:
                     - Standard_LRS
                     - Premium_LRS
@@ -148,19 +148,23 @@
                 version_added: "2.4"
     virtual_network_name:
         description:
-            - Virtual Network name
+            - Virtual Network name.
         aliases:
             - virtual_network
     subnet_name:
         description:
-            - Subnet name
+            - Subnet name.
         aliases:
             - subnet
+    load_balancer:
+        description:
+            - Load balancer name.
+        version_added: "2.5"
     remove_on_absent:
         description:
-            - When removing a VM using state 'absent', also remove associated resources
-            - "It can be 'all' or a list with any of the following: ['network_interfaces', 'virtual_storage', 'public_ips']"
-            - Any other input will be ignored
+            - When removing a VM using state 'absent', also remove associated resources.
+            - "It can be 'all' or a list with any of the following: ['network_interfaces', 'virtual_storage', 'public_ips']."
+            - Any other input will be ignored.
         default: ['all']
 
 extends_documentation_fragment:
@@ -296,6 +300,7 @@
 
 try:
     from msrestazure.azure_exceptions import CloudError
+    from msrestazure.tools import parse_resource_id
     from azure.mgmt.compute.models import VirtualMachineScaleSet, \
         VirtualMachineScaleSetStorageProfile, \
         VirtualMachineScaleSetOSProfile, \
@@ -309,7 +314,7 @@
         VirtualMachineScaleSetPublicIPAddressConfigurationDnsSettings, \
         VirtualMachineScaleSetPublicIPAddressConfiguration, Sku, \
         UpgradePolicy, VirtualMachineScaleSetNetworkConfiguration, \
-        ApiEntityReference, ImageReference
+        ApiEntityReference, ImageReference, SubResource
 
     from azure.mgmt.network.models import PublicIPAddress, \
         NetworkSecurityGroup, NetworkInterface, \
@@ -352,6 +357,7 @@ def __init__(self):
             managed_disk_type=dict(type='str', choices=['Standard_LRS', 'Premium_LRS']),
             data_disks=dict(type='list'),
             subnet_name=dict(type='str', aliases=['subnet']),
+            load_balancer=dict(type='str'),
             virtual_network_name=dict(type='str', aliases=['virtual_network']),
             remove_on_absent=dict(type='list', default=['all']),
         )
@@ -378,6 +384,7 @@ def __init__(self):
         self.virtual_network_name = None
         self.tags = None
         self.differences = None
+        self.load_balancer = None
 
         self.results = dict(
             changed=False,
@@ -511,12 +518,24 @@ def exec_module(self, **kwargs):
                         if disable_ssh_password and not self.ssh_public_keys:
                             self.fail("Parameter error: ssh_public_keys required when disabling SSH password.")
 
-                    if self.subnet_name:
-                        subnet = self.get_subnet(self.virtual_network_name, self.subnet_name)
-
                     if not self.virtual_network_name:
                         default_vnet = self.create_default_vnet()
                         virtual_network = default_vnet.id
+                        self.virtual_network_name = default_vnet.name
+
+                    if self.subnet_name:
+                        subnet = self.get_subnet(self.virtual_network_name, self.subnet_name)
+
+                    load_balancer_backend_address_pools = None
+                    load_balancer_inbound_nat_pools = None
+                    if self.load_balancer:
+                        load_balancer = self.get_load_balancer(self.load_balancer)
+                        load_balancer_backend_address_pools = ([SubResource(resource.id)
+                                                                for resource in load_balancer.backend_address_pools]
+                                                               if load_balancer.backend_address_pools else None)
+                        load_balancer_inbound_nat_pools = ([SubResource(resource.id)
+                                                            for resource in load_balancer.inbound_nat_pools]
+                                                           if load_balancer.inbound_nat_pools else None)
 
                     if not self.short_hostname:
                         self.short_hostname = self.name
@@ -562,7 +581,10 @@ def exec_module(self, **kwargs):
                                                 name='default',
                                                 subnet=ApiEntityReference(
                                                     id=subnet.id
-                                                )
+                                                ),
+                                                primary=True,
+                                                load_balancer_backend_address_pools=load_balancer_backend_address_pools,
+                                                load_balancer_inbound_nat_pools=load_balancer_inbound_nat_pools
                                             )
                                         ]
                                     )
@@ -657,27 +679,34 @@ def get_vmss(self):
         try:
             vmss = self.compute_client.virtual_machine_scale_sets.get(self.resource_group, self.name)
             return vmss
-        except Exception as exc:
+        except CloudError as exc:
             self.fail("Error getting virtual machine scale set {0} - {1}".format(self.name, str(exc)))
 
     def get_virtual_network(self, name):
         try:
             vnet = self.network_client.virtual_networks.get(self.resource_group, name)
             return vnet
-        except Exception as exc:
+        except CloudError as exc:
             self.fail("Error fetching virtual network {0} - {1}".format(name, str(exc)))
 
     def get_subnet(self, vnet_name, subnet_name):
         self.log("Fetching subnet {0} in virtual network {1}".format(subnet_name, vnet_name))
         try:
             subnet = self.network_client.subnets.get(self.resource_group, vnet_name, subnet_name)
-        except Exception as exc:
+        except CloudError as exc:
             self.fail("Error: fetching subnet {0} in virtual network {1} - {2}".format(
                 subnet_name,
                 vnet_name,
                 str(exc)))
         return subnet
 
+    def get_load_balancer(self, id):
+        id_dict = parse_resource_id(id)
+        try:
+            return self.network_client.load_balancers.get(id_dict.get('resource_group', self.resource_group), id_dict.get('name'))
+        except CloudError as exc:
+            self.fail("Error fetching load balancer {0} - {1}".format(id, str(exc)))
+
     def serialize_vmss(self, vmss):
         '''
         Convert a VirtualMachineScaleSet object to dict.
@@ -702,7 +731,7 @@ def delete_vmss(self, vmss):
             poller = self.compute_client.virtual_machine_scale_sets.delete(self.resource_group, self.name)
             # wait for the poller to finish
             self.get_poller_result(poller)
-        except Exception as exc:
+        except CloudError as exc:
             self.fail("Error deleting virtual machine scale set {0} - {1}".format(self.name, str(exc)))
 
         return True
@@ -713,7 +742,7 @@ def get_image_version(self):
                                                                        self.image['publisher'],
                                                                        self.image['offer'],
                                                                        self.image['sku'])
-        except Exception as exc:
+        except CloudError as exc:
             self.fail("Error fetching image {0} {1} {2} - {3}".format(self.image['publisher'],
                                                                       self.image['offer'],
                                                                       self.image['sku'],
@@ -734,7 +763,7 @@ def create_or_update_vmss(self, params):
         try:
             poller = self.compute_client.virtual_machine_scale_sets.create_or_update(self.resource_group, self.name, params)
             self.get_poller_result(poller)
-        except Exception as exc:
+        except CloudError as exc:
             self.fail("Error creating or updating virtual machine {0} - {1}".format(self.name, str(exc)))
 
     def vm_size_is_valid(self):
@@ -745,7 +774,7 @@ def vm_size_is_valid(self):
         '''
         try:
             sizes = self.compute_client.virtual_machine_sizes.list(self.location)
-        except Exception as exc:
+        except CloudError as exc:
             self.fail("Error retrieving available machine sizes - {0}".format(str(exc)))
         for size in sizes:
             if size.name == self.vm_size:


Test patch:
diff --git a/test/integration/targets/azure_rm_virtualmachine_scaleset/tasks/main.yml b/test/integration/targets/azure_rm_virtualmachine_scaleset/tasks/main.yml
--- a/test/integration/targets/azure_rm_virtualmachine_scaleset/tasks/main.yml
+++ b/test/integration/targets/azure_rm_virtualmachine_scaleset/tasks/main.yml
@@ -14,9 +14,15 @@
 - name: Create public IP address
   azure_rm_publicipaddress:
     resource_group: "{{ resource_group }}"
-    allocation_method: Dynamic
+    allocation_method: Static
     name: testPublicIP
 
+- name: Create load balancer
+  azure_rm_loadbalancer:
+    resource_group: "{{ resource_group }}"
+    name: testLB
+    public_ip_address_name: testPublicIP
+
 - name: Create VMSS
   azure_rm_virtualmachine_scaleset:
     resource_group: "{{ resource_group }}"
@@ -73,10 +79,105 @@
         caching: ReadWrite
         managed_disk_type: Standard_LRS
 
+- name: Create VMSS (check mode)
+  azure_rm_virtualmachine_scaleset:
+    resource_group: "{{ resource_group }}"
+    name: testVMSS1
+    vm_size: Standard_DS1_v2
+    admin_username: testuser
+    ssh_password_enabled: true
+    admin_password: "Password1234!"
+    capacity: 2
+    virtual_network_name: testVnet
+    subnet_name: testSubnet
+    load_balancer: testLB
+    upgrade_policy: Manual
+    tier: Standard
+    managed_disk_type: Standard_LRS
+    os_disk_caching: ReadWrite
+    image:
+      offer: CoreOS
+      publisher: CoreOS
+      sku: Stable
+      version: latest
+    data_disks:
+      - lun: 0
+        disk_size_gb: 64
+        caching: ReadWrite
+        managed_disk_type: Standard_LRS
+  register: results
+  check_mode: yes
+
+- name: Assert that VMSS can be created
+  assert:
+    that: results.changed
+
+- name: Create VMSS (check mode)
+  azure_rm_virtualmachine_scaleset:
+    resource_group: "{{ resource_group }}"
+    name: testVMSS1
+    vm_size: Standard_DS1_v2
+    admin_username: testuser
+    ssh_password_enabled: true
+    admin_password: "Password1234!"
+    capacity: 2
+    virtual_network_name: testVnet
+    subnet_name: testSubnet
+    load_balancer: testLB
+    upgrade_policy: Manual
+    tier: Standard
+    managed_disk_type: Standard_LRS
+    os_disk_caching: ReadWrite
+    image:
+      offer: CoreOS
+      publisher: CoreOS
+      sku: Stable
+      version: latest
+    data_disks:
+      - lun: 0
+        disk_size_gb: 64
+        caching: ReadWrite
+        managed_disk_type: Standard_LRS
+  register: results
+
+- name: Assert that VMSS ran
+  assert:
+    that: results.changed
+
+- name: Delete VMSS
+  azure_rm_virtualmachine_scaleset:
+    resource_group: "{{ resource_group }}"
+    name: testVMSS1
+    state: absent
+    remove_on_absent: ['all']
+    vm_size: Standard_DS1_v2
+    admin_username: testuser
+    capacity: 2
+    virtual_network_name: testVnet
+    subnet_name: testSubnet
+    upgrade_policy: Manual
+    tier: Standard
+    os_disk_caching: ReadWrite
+    image:
+      offer: CoreOS
+      publisher: CoreOS
+      sku: Stable
+      version: latest
+    data_disks:
+      - lun: 0
+        disk_size_gb: 64
+        caching: ReadWrite
+        managed_disk_type: Standard_LRS
+
+- name: Delete load balancer
+  azure_rm_loadbalancer:
+    resource_group: "{{ resource_group }}"
+    name: testLB
+    state: absent
+
 - name: Delete public IP address
   azure_rm_publicipaddress:
     resource_group: "{{ resource_group }}"
-    allocation_method: Dynamic
     state: absent
     name: testPublicIP