From 4d6c28f8c3ec9fe7e63a5046f447fb7efd6d1a98 Mon Sep 17 00:00:00 2001 From: Yuwei Zhou Date: Thu, 21 Dec 2017 05:53:14 +0800 Subject: [PATCH] #30142:Enable attach data disk to existing VM or detach it from VM (#32711) * disk can be mounted to vm * add version added * fix lint * Fix some mirrors * fix lint * remove trailing space * fix as review comment * fix yaml * fix * fix * Minor update to doc on unmounting disk from VM --- .../cloud/azure/azure_rm_managed_disk.py | 201 +++++++++----- .../azure/azure_rm_managed_disk_facts.py | 7 +- .../azure_rm_managed_disk/tasks/main.yml | 258 +++++++++++++++--- 3 files changed, 355 insertions(+), 111 deletions(-) diff --git a/lib/ansible/modules/cloud/azure/azure_rm_managed_disk.py b/lib/ansible/modules/cloud/azure/azure_rm_managed_disk.py index 13f027d176..5345ed3cf0 100644 --- a/lib/ansible/modules/cloud/azure/azure_rm_managed_disk.py +++ b/lib/ansible/modules/cloud/azure/azure_rm_managed_disk.py @@ -27,64 +27,59 @@ description: options: resource_group: description: - - "Name of a resource group where the managed disk exists or will be created." + - Name of a resource group where the managed disk exists or will be created. required: true name: description: - - Name of the managed disk + - Name of the managed disk. required: true state: description: - - Assert the state of the managed disk. Use 'present' to create or update a managed disk and - 'absent' to delete a managed disk. + - Assert the state of the managed disk. Use C(present) to create or update a managed disk and 'absent' to delete a managed disk. default: present choices: - absent - present - required: false location: description: - Valid Azure location. Defaults to location of the resource group. default: resource_group location - required: false storage_account_type: description: - - "Type of storage for the managed disk: 'Standard_LRS' or 'Premium_LRS'. If not specified the disk is created 'Standard_LRS'" + - "Type of storage for the managed disk: C(Standard_LRS) or C(Premium_LRS). If not specified the disk is created C(Standard_LRS)." choices: - Standard_LRS - Premium_LRS - required: false create_option: description: - - "Allowed values: empty, import, copy. 'import' from a VHD file in 'source_uri' and 'copy' from previous managed disk 'source_resource_uri'." + - "Allowed values: empty, import, copy. C(import) from a VHD file in I(source_uri) and C(copy) from previous managed disk I(source_resource_uri)." choices: - empty - import - copy - required: false source_uri: description: - - URI to a valid VHD file to be used when 'create_option' is 'import'. - required: false + - URI to a valid VHD file to be used when I(create_option) is C(import). source_resource_uri: description: - - The resource ID of the managed disk to copy when 'create_option' is 'copy'. - required: false + - The resource ID of the managed disk to copy when I(create_option) is C(copy). os_type: description: - - "Type of Operating System: 'linux' or 'windows'. Used when 'create_option' is either 'copy' or 'import' and the source is an OS disk." + - "Type of Operating System: C(linux) or C(windows). Used when I(create_option) is either C(copy) or C(import) and the source is an OS disk." choices: - linux - windows - required: false disk_size_gb: description: - -Size in GB of the managed disk to be created. If 'create_option' is 'copy' then the value must be greater than or equal to the source's size. - required: true + - Size in GB of the managed disk to be created. If I(create_option) is C(copy) then the value must be greater than or equal to the source's size. + managed_by: + description: + - Name of an existing virtual machine with which the disk is or will be associated, this VM should be in the same resource group. + - To detach a disk from a vm, keep undefined. + version_added: 2.5 tags: description: - Tags to assign to the managed disk. - required: false extends_documentation_fragment: - azure @@ -101,6 +96,21 @@ EXAMPLES = ''' resource_group: Testing disk_size_gb: 4 + - name: Mount the managed disk to VM + azure_rm_managed_disk: + name: mymanageddisk + location: eastus + resource_group: Testing + disk_size_gb: 4 + managed_by: testvm001 + + - name: Unmount the managed disk to VM + azure_rm_managed_disk: + name: mymanageddisk + location: eastus + resource_group: Testing + disk_size_gb: 4 + - name: Delete managed disk azure_rm_manage_disk: name: mymanageddisk @@ -129,9 +139,9 @@ import re from ansible.module_utils.azure_rm_common import AzureRMModuleBase try: + from msrestazure.tools import parse_resource_id from msrestazure.azure_exceptions import CloudError - from azure.mgmt.compute.models import DiskCreateOption - from azure.mgmt.compute.models import DiskSku + from azure.mgmt.compute.models import DiskCreateOption, DiskCreateOptionTypes, ManagedDiskParameters, DiskSku, DataDisk except ImportError: # This is handled in azure_rm_common pass @@ -148,7 +158,8 @@ def managed_disk_to_dict(managed_disk): tags=managed_disk.tags, disk_size_gb=managed_disk.disk_size_gb, os_type=os_type, - storage_account_type='Premium_LRS' if managed_disk.sku.tier == 'Premium' else 'Standard_LRS' + storage_account_type=managed_disk.sku.name.value, + managed_by=managed_disk.managed_by ) @@ -167,46 +178,41 @@ class AzureRMManagedDisk(AzureRMModuleBase): ), state=dict( type='str', - required=False, default='present', choices=['present', 'absent'] ), location=dict( - type='str', - required=False + type='str' ), storage_account_type=dict( type='str', - required=False, choices=['Standard_LRS', 'Premium_LRS'] ), create_option=dict( type='str', - required=False, choices=['empty', 'import', 'copy'] ), source_uri=dict( - type='str', - required=False + type='str' ), source_resource_uri=dict( - type='str', - required=False + type='str' ), os_type=dict( type='str', - required=False, choices=['linux', 'windows'] ), disk_size_gb=dict( - type='int', - required=False + type='int' + ), + managed_by=dict( + type='str' ) ) required_if = [ ('create_option', 'import', ['source_uri']), ('create_option', 'copy', ['source_resource_uri']), - ('state', 'present', ['disk_size_gb']) + ('create_option', 'empty', ['disk_size_gb']) ] self.results = dict( changed=False, @@ -222,6 +228,7 @@ class AzureRMManagedDisk(AzureRMModuleBase): self.os_type = None self.disk_size_gb = None self.tags = None + self.managed_by = None super(AzureRMManagedDisk, self).__init__( derived_arg_spec=self.module_arg_spec, required_if=required_if, @@ -232,21 +239,83 @@ class AzureRMManagedDisk(AzureRMModuleBase): """Main module execution method""" for key in list(self.module_arg_spec.keys()) + ['tags']: setattr(self, key, kwargs[key]) - results = dict() - resource_group = None - response = None + + result = None + changed = False resource_group = self.get_resource_group(self.resource_group) if not self.location: self.location = resource_group.location + + disk_instance = self.get_managed_disk() + result = disk_instance + + # need create or update if self.state == 'present': - self.results['state'] = self.create_or_update_managed_disk() - elif self.state == 'absent': - self.delete_managed_disk() + parameter = self.generate_managed_disk_property() + if not disk_instance or self.is_different(disk_instance, parameter): + changed = True + if not self.check_mode: + result = self.create_or_update_managed_disk(parameter) + else: + result = True + + # unmount from the old virtual machine and mount to the new virtual machine + vm_name = parse_resource_id(disk_instance.get('managed_by', '')).get('name') if disk_instance else None + if self.managed_by != vm_name: + changed = True + if not self.check_mode: + if vm_name: + self.detach(vm_name, result) + if self.managed_by: + self.attach(self.managed_by, result) + result = self.get_managed_disk() + + if self.state == 'absent' and disk_instance: + changed = True + if not self.check_mode: + self.delete_managed_disk() + result = True + + self.results['changed'] = changed + self.results['state'] = result return self.results - def create_or_update_managed_disk(self): - # Scaffolding empty managed disk + def attach(self, vm_name, disk): + vm = self._get_vm(vm_name) + # find the lun + luns = ([d.lun for d in vm.storage_profile.data_disks] + if vm.storage_profile.data_disks else []) + lun = max(luns) + 1 if luns else 0 + + # prepare the data disk + params = ManagedDiskParameters(id=disk.get('id'), storage_account_type=disk.get('storage_account_type')) + data_disk = DataDisk(lun, DiskCreateOptionTypes.attach, managed_disk=params) + vm.storage_profile.data_disks.append(data_disk) + self._update_vm(vm_name, vm) + + def detach(self, vm_name, disk): + vm = self._get_vm(vm_name) + leftovers = [d for d in vm.storage_profile.data_disks if d.name.lower() != disk.get('name').lower()] + if len(vm.storage_profile.data_disks) == len(leftovers): + self.fail("No disk with the name '{0}' was found".format(disk.get('name'))) + vm.storage_profile.data_disks = leftovers + self._update_vm(vm_name, vm) + + def _update_vm(self, name, params): + try: + poller = self.compute_client.virtual_machines.create_or_update(self.resource_group, name, params) + self.get_poller_result(poller) + except Exception as exc: + self.fail("Error updating virtual machine {0} - {1}".format(name, str(exc))) + + def _get_vm(self, name): + try: + return self.compute_client.virtual_machines.get(self.resource_group, name, expand='instanceview') + except Exception as exc: + self.fail("Error getting virtual machine {0} - {1}".format(name, str(exc))) + + def generate_managed_disk_property(self): disk_params = {} creation_data = {} disk_params['location'] = self.location @@ -263,26 +332,19 @@ class AzureRMManagedDisk(AzureRMModuleBase): elif self.create_option == 'copy': creation_data['create_option'] = DiskCreateOption.copy creation_data['source_resource_id'] = self.source_resource_uri + disk_params['creation_data'] = creation_data + return disk_params + + def create_or_update_managed_disk(self, parameter): try: - # CreationData cannot be changed after creation - disk_params['creation_data'] = creation_data - found_prev_disk = self.get_managed_disk() - if found_prev_disk: - if not self.is_different(found_prev_disk, disk_params): - return found_prev_disk - if not self.check_mode: - poller = self.compute_client.disks.create_or_update( - self.resource_group, - self.name, - disk_params) - aux = self.get_poller_result(poller) - result = managed_disk_to_dict(aux) - else: - result = True - self.results['changed'] = True + poller = self.compute_client.disks.create_or_update( + self.resource_group, + self.name, + parameter) + aux = self.get_poller_result(poller) + return managed_disk_to_dict(aux) except CloudError as e: self.fail("Error creating the managed disk: {0}".format(str(e))) - return result # This method accounts for the difference in structure between the # Azure retrieved disk and the parameters for the new disk to be created. @@ -302,30 +364,21 @@ class AzureRMManagedDisk(AzureRMModuleBase): def delete_managed_disk(self): try: - if not self.check_mode: - poller = self.compute_client.disks.delete( - self.resource_group, - self.name) - result = self.get_poller_result(poller) - else: - result = True - self.results['changed'] = True + poller = self.compute_client.disks.delete( + self.resource_group, + self.name) + return self.get_poller_result(poller) except CloudError as e: self.fail("Error deleting the managed disk: {0}".format(str(e))) - return result def get_managed_disk(self): - resp = False try: resp = self.compute_client.disks.get( self.resource_group, self.name) + return managed_disk_to_dict(resp) except CloudError as e: self.log('Did not find managed disk') - if resp: - resp = managed_disk_to_dict( - resp) - return resp def main(): diff --git a/lib/ansible/modules/cloud/azure/azure_rm_managed_disk_facts.py b/lib/ansible/modules/cloud/azure/azure_rm_managed_disk_facts.py index dc89e5af05..0fb40c30cc 100644 --- a/lib/ansible/modules/cloud/azure/azure_rm_managed_disk_facts.py +++ b/lib/ansible/modules/cloud/azure/azure_rm_managed_disk_facts.py @@ -100,7 +100,8 @@ def managed_disk_to_dict(managed_disk): tags=managed_disk.tags, disk_size_gb=managed_disk.disk_size_gb, os_type=os_type, - storage_account_type='Premium_LRS' if managed_disk.sku.tier == 'Premium' else 'Standard_LRS' + storage_account_type=managed_disk.sku.name.value, + managed_by=managed_disk.managed_by ) @@ -141,6 +142,10 @@ class AzureRMManagedDiskFacts(AzureRMModuleBase): type='int', required=False ), + managed_by=dict( + type='str', + required=False + ), tags=dict( type='str', required=False diff --git a/test/integration/targets/azure_rm_managed_disk/tasks/main.yml b/test/integration/targets/azure_rm_managed_disk/tasks/main.yml index f13ae65f52..cfc3d73329 100644 --- a/test/integration/targets/azure_rm_managed_disk/tasks/main.yml +++ b/test/integration/targets/azure_rm_managed_disk/tasks/main.yml @@ -3,21 +3,18 @@ managed_disk1: "{{ resource_group | hash('md5') | truncate(24, True, '') }}" managed_disk2: "{{ resource_group | hash('md5') | truncate(18, True, '') }}" - - name: Clearing (if) previous disks were created (1/2) + - name: Clearing (if) previous disks were created azure_rm_managed_disk: - resource_group: "{{ resource_group }}" - name: "{{ managed_disk2 }}" + resource_group: "{{ resource_group }}" + name: "{{item }}" state: absent + with_items: + - "{{ managed_disk2 }}" + - "{{ managed_disk1 }}" - - name: Clearing (if) previous disks were created (2/2) - azure_rm_managed_disk: - resource_group: "{{ resource_group }}" - name: "{{ managed_disk1 }}" - state: absent - - name: Create managed disk (Check Mode) azure_rm_managed_disk: - resource_group: "{{ resource_group }}" + resource_group: "{{ resource_group }}" name: "{{ managed_disk1 }}" disk_size_gb: 1 tags: @@ -34,20 +31,20 @@ - name: Test invalid account name (should give error) azure_rm_managed_disk: - resource_group: "{{ resource_group }}" + resource_group: "{{ resource_group }}" name: "invalid_char$" disk_size_gb: 1 state: present - register: output - ignore_errors: yes + register: output + ignore_errors: yes check_mode: no - name: Assert task failed assert: { that: "output['failed'] == True" } - - name: Create new managed disk succesfully + - name: Create new managed disk succesfully azure_rm_managed_disk: - resource_group: "{{ resource_group }}" + resource_group: "{{ resource_group }}" name: "{{ managed_disk1 }}" storage_account_type: "Standard_LRS" disk_size_gb: 1 @@ -56,7 +53,7 @@ delete: never register: output - - name: Assert status succeeded and results include an Id value + - name: Assert status succeeded and results include an Id value assert: that: - output.changed @@ -64,14 +61,14 @@ - name: Copy disk to a new managed disk azure_rm_managed_disk: - resource_group: "{{ resource_group }}" + resource_group: "{{ resource_group }}" name: "{{ managed_disk2 }}" create_option: "copy" source_resource_uri: "{{ output.state.id }}" disk_size_gb: 1 register: copy - - name: Assert status succeeded and results include an Id value + - name: Assert status succeeded and results include an Id value assert: that: - copy.changed @@ -79,13 +76,13 @@ - name: Update a new disk without changes azure_rm_managed_disk: - resource_group: "{{ resource_group }}" + resource_group: "{{ resource_group }}" name: "{{ managed_disk1 }}" storage_account_type: "Standard_LRS" disk_size_gb: 1 register: output - - name: Assert status succeeded and results include an Id value + - name: Assert status succeeded and results include an Id value assert: that: - not output.changed @@ -93,17 +90,17 @@ - name: Change storage account type to an invalid type azure_rm_managed_disk: - resource_group: "{{ resource_group }}" + resource_group: "{{ resource_group }}" name: "{{ managed_disk1 }}" storage_account_type: "PremiumL" disk_size_gb: 1 register: output - ignore_errors: yes + ignore_errors: yes - - name: Assert storage account type change failed + - name: Assert storage account type change failed assert: { that: "output['failed'] == True" } - - name: Change disk size to incompatible size + - name: Change disk size to incompatible size azure_rm_managed_disk: resource_group: "{{ resource_group }}" name: "{{ managed_disk1 }}" @@ -116,7 +113,7 @@ - name: Change disk to bigger size azure_rm_managed_disk: - resource_group: "{{ resource_group }}" + resource_group: "{{ resource_group }}" name: "{{ managed_disk1 }}" disk_size_gb: 2 register: output @@ -128,7 +125,7 @@ - name: Change disk to Premium azure_rm_managed_disk: - resource_group: "{{ resource_group }}" + resource_group: "{{ resource_group }}" name: "{{ managed_disk1 }}" storage_account_type: "Premium_LRS" disk_size_gb: 2 @@ -190,32 +187,221 @@ that: - "azure_managed_disk | length > 0" - - name: Delete managed disk (Check Mode) + - name: Create virtual network + azure_rm_virtualnetwork: + resource_group: "{{ resource_group }}" + name: testvm001 + address_prefixes: "10.10.0.0/16" + + - name: Add subnet + azure_rm_subnet: + resource_group: "{{ resource_group }}" + name: testvm001 + address_prefix: "10.10.0.0/24" + virtual_network: testvm001 + + - name: Create public ip + azure_rm_publicipaddress: + resource_group: "{{ resource_group }}" + allocation_method: Static + name: testvm001 + + - name: Create security group + azure_rm_securitygroup: + resource_group: "{{ resource_group }}" + name: testvm001 + + - name: Create NIC + azure_rm_networkinterface: + resource_group: "{{ resource_group }}" + name: testvm001 + virtual_network: testvm001 + subnet: testvm001 + public_ip_name: testvm001 + security_group: testvm001 + + - name: Create virtual machine + azure_rm_virtualmachine: + resource_group: "{{ resource_group }}" + name: testvm001 + admin_username: adminuser + admin_password: Password123! + os_type: Linux + managed_disk_type: Premium_LRS + vm_size: Standard_DS1_v2 + network_interfaces: testvm001 + image: + offer: UbuntuServer + publisher: Canonical + sku: 16.04-LTS + version: latest + + - name: Mount the disk to virtual machine (check mode) azure_rm_managed_disk: - resource_group: "{{ resource_group }}" + resource_group: "{{ resource_group }}" name: "{{ managed_disk1 }}" - state: absent disk_size_gb: 2 + managed_by: testvm001 + tags: + testing: testing + delete: never + register: mounted check_mode: yes - - name: Assert status succeeded + - assert: + that: + - not mounted.state.managed_by + + - name: Mount the disk to virtual machine + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 2 + managed_by: testvm001 + tags: + testing: testing + delete: never + register: mounted + + - assert: + that: + - "'testvm001' in mounted.state.managed_by" + + - name: Mount the disk to virtual machine (idempotent) + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 2 + managed_by: testvm001 + tags: + testing: testing + delete: never + register: mounted + + - assert: + that: + - not mounted.changed + - "'testvm001' in mounted.state.managed_by" + + - name: Unmount the disk to virtual machine (check mode) + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 2 + tags: + testing: testing + delete: never + check_mode: yes + register: mounted + + - assert: + that: + - mounted.changed + + - name: Unmount the disk to virtual machine + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 2 + tags: + testing: testing + delete: never + register: mounted + + - assert: + that: + - mounted.changed + - not mounted.state.managed_by + + - name: Unmount the disk to virtual machine (idempotent) + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 2 + tags: + testing: testing + delete: never + register: mounted + + - assert: + that: + - not mounted.changed + - not mounted.state.managed_by + + - name: Update disk size + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 4 + tags: + testing: testing + delete: never + register: output + + - assert: + that: + - output.state.disk_size_gb == 4 + + - name: Attach the disk to virtual machine again + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + disk_size_gb: 4 + managed_by: testvm001 + tags: + testing: testing + delete: never + register: mounted + + - assert: + that: + - mounted.changed + - "'testvm001' in mounted.state.managed_by" + + - name: Change disk size to incompatible size + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + state: absent + managed_by: testvm001 + register: output + ignore_errors: yes + + - name: Assert delete failed since disk is attached to VM + assert: { that: "output['failed'] == True" } + + - name: Delete managed disk (Check Mode) + azure_rm_managed_disk: + resource_group: "{{ resource_group }}" + name: "{{ managed_disk1 }}" + state: absent + register: output + check_mode: yes + + - name: Assert status succeeded assert: that: - output.changed - output.state - - name: Delete managed disk + - name: Delete managed disk azure_rm_managed_disk: - resource_group: "{{ resource_group }}" + resource_group: "{{ resource_group }}" name: "{{ managed_disk2 }}" - disk_size_gb: 1 state: absent check_mode: no - - name: Delete copied managed disk + - name: Delete copied managed disk azure_rm_managed_disk: - resource_group: "{{ resource_group }}" + resource_group: "{{ resource_group }}" name: "{{ managed_disk1 }}" disk_size_gb: 2 state: absent - check_mode: no \ No newline at end of file + check_mode: no + + - name: Delete virtual machine + azure_rm_virtualmachine: + resource_group: "{{ resource_group }}" + name: testvm001 + state: absent + vm_size: Standard_DS1_v2