1#!/usr/local/bin/python3.8 2# 3# Copyright (c) 2017 Bruno Medina Bolanos Cacho <bruno.medina@microsoft.com> 4# 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10 11DOCUMENTATION = ''' 12--- 13module: azure_rm_manageddisk 14 15version_added: "0.1.2" 16 17short_description: Manage Azure Manage Disks 18 19description: 20 - Create, update and delete an Azure Managed Disk. 21 22notes: 23 - This module was called M(azure_rm_managed_disk) before Ansible 2.8. The usage did not change. 24 25options: 26 resource_group: 27 description: 28 - Name of a resource group where the managed disk exists or will be created. 29 required: true 30 name: 31 description: 32 - Name of the managed disk. 33 required: true 34 state: 35 description: 36 - Assert the state of the managed disk. Use C(present) to create or update a managed disk and C(absent) to delete a managed disk. 37 default: present 38 choices: 39 - absent 40 - present 41 location: 42 description: 43 - Valid Azure location. Defaults to location of the resource group. 44 storage_account_type: 45 description: 46 - Type of storage for the managed disk. 47 - If not specified, the disk is created as C(Standard_LRS). 48 - C(Standard_LRS) is for Standard HDD. 49 - C(StandardSSD_LRS) (added in 2.8) is for Standard SSD. 50 - C(Premium_LRS) is for Premium SSD. 51 - C(UltraSSD_LRS) (added in 2.8) is for Ultra SSD, which is in preview mode, and only available on select instance types. 52 - See U(https://docs.microsoft.com/en-us/azure/virtual-machines/windows/disks-types) for more information about disk types. 53 choices: 54 - Standard_LRS 55 - StandardSSD_LRS 56 - Premium_LRS 57 - UltraSSD_LRS 58 create_option: 59 description: 60 - C(import) from a VHD file in I(source_uri) and C(copy) from previous managed disk I(source_uri). 61 choices: 62 - empty 63 - import 64 - copy 65 source_uri: 66 description: 67 - URI to a valid VHD file to be used or the resource ID of the managed disk to copy. 68 aliases: 69 - source_resource_uri 70 os_type: 71 description: 72 - Type of Operating System. 73 - Used when I(create_option=copy) or I(create_option=import) and the source is an OS disk. 74 - If omitted during creation, no value is set. 75 - If omitted during an update, no change is made. 76 - Once set, this value cannot be cleared. 77 choices: 78 - linux 79 - windows 80 disk_size_gb: 81 description: 82 - Size in GB of the managed disk to be created. 83 - If I(create_option=copy) then the value must be greater than or equal to the source's size. 84 managed_by: 85 description: 86 - Name of an existing virtual machine with which the disk is or will be associated, this VM should be in the same resource group. 87 - To detach a disk from a vm, explicitly set to ''. 88 - If this option is unset, the value will not be changed. 89 attach_caching: 90 description: 91 - Disk caching policy controlled by VM. Will be used when attached to the VM defined by C(managed_by). 92 - If this option is different from the current caching policy, the managed disk will be deattached and attached with current caching option again. 93 choices: 94 - '' 95 - read_only 96 - read_write 97 tags: 98 description: 99 - Tags to assign to the managed disk. 100 - Format tags as 'key' or 'key:value'. 101 zone: 102 description: 103 - The Azure managed disk's zone. 104 - Allowed values are C(1), C(2), C(3) and C(' '). 105 choices: 106 - 1 107 - 2 108 - 3 109 - '' 110 lun: 111 description: 112 - The logical unit number for data disk. 113 - This value is used to identify data disks within the VM and therefore must be unique for each data disk attached to a VM. 114 type: int 115 116extends_documentation_fragment: 117 - azure.azcollection.azure 118 - azure.azcollection.azure_tags 119author: 120 - Bruno Medina (@brusMX) 121''' 122 123EXAMPLES = ''' 124 - name: Create managed disk 125 azure_rm_manageddisk: 126 name: mymanageddisk 127 location: eastus 128 resource_group: myResourceGroup 129 disk_size_gb: 4 130 131 - name: Create managed operating system disk from page blob 132 azure_rm_manageddisk: 133 name: mymanageddisk 134 location: eastus2 135 resource_group: myResourceGroup 136 create_option: import 137 source_uri: https://storageaccountname.blob.core.windows.net/containername/blob-name.vhd 138 os_type: windows 139 storage_account_type: Premium_LRS 140 141 - name: Mount the managed disk to VM 142 azure_rm_manageddisk: 143 name: mymanageddisk 144 location: eastus 145 resource_group: myResourceGroup 146 disk_size_gb: 4 147 managed_by: testvm001 148 attach_caching: read_only 149 150 - name: Unmount the managed disk to VM 151 azure_rm_manageddisk: 152 name: mymanageddisk 153 location: eastus 154 resource_group: myResourceGroup 155 disk_size_gb: 4 156 157 - name: Delete managed disk 158 azure_rm_manageddisk: 159 name: mymanageddisk 160 location: eastus 161 resource_group: myResourceGroup 162 state: absent 163''' 164 165RETURN = ''' 166id: 167 description: 168 - The managed disk resource ID. 169 returned: always 170 type: dict 171state: 172 description: 173 - Current state of the managed disk. 174 returned: always 175 type: dict 176changed: 177 description: 178 - Whether or not the resource has changed. 179 returned: always 180 type: bool 181''' 182 183import re 184 185 186from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase 187try: 188 from msrestazure.tools import parse_resource_id 189 from msrestazure.azure_exceptions import CloudError 190except ImportError: 191 # This is handled in azure_rm_common 192 pass 193 194 195# duplicated in azure_rm_manageddisk_facts 196def managed_disk_to_dict(managed_disk): 197 create_data = managed_disk.creation_data 198 return dict( 199 id=managed_disk.id, 200 name=managed_disk.name, 201 location=managed_disk.location, 202 tags=managed_disk.tags, 203 create_option=create_data.create_option.lower(), 204 source_uri=create_data.source_uri or create_data.source_resource_id, 205 disk_size_gb=managed_disk.disk_size_gb, 206 os_type=managed_disk.os_type.lower() if managed_disk.os_type else None, 207 storage_account_type=managed_disk.sku.name if managed_disk.sku else None, 208 managed_by=managed_disk.managed_by, 209 zone=managed_disk.zones[0] if managed_disk.zones and len(managed_disk.zones) > 0 else '' 210 ) 211 212 213class AzureRMManagedDisk(AzureRMModuleBase): 214 """Configuration class for an Azure RM Managed Disk resource""" 215 216 def __init__(self): 217 self.module_arg_spec = dict( 218 resource_group=dict( 219 type='str', 220 required=True 221 ), 222 name=dict( 223 type='str', 224 required=True 225 ), 226 state=dict( 227 type='str', 228 default='present', 229 choices=['present', 'absent'] 230 ), 231 location=dict( 232 type='str' 233 ), 234 storage_account_type=dict( 235 type='str', 236 choices=['Standard_LRS', 'StandardSSD_LRS', 'Premium_LRS', 'UltraSSD_LRS'] 237 ), 238 create_option=dict( 239 type='str', 240 choices=['empty', 'import', 'copy'] 241 ), 242 source_uri=dict( 243 type='str', 244 aliases=['source_resource_uri'] 245 ), 246 os_type=dict( 247 type='str', 248 choices=['linux', 'windows'] 249 ), 250 disk_size_gb=dict( 251 type='int' 252 ), 253 managed_by=dict( 254 type='str' 255 ), 256 zone=dict( 257 type='str', 258 choices=['', '1', '2', '3'] 259 ), 260 attach_caching=dict( 261 type='str', 262 choices=['', 'read_only', 'read_write'] 263 ), 264 lun=dict( 265 type='int' 266 ) 267 ) 268 required_if = [ 269 ('create_option', 'import', ['source_uri']), 270 ('create_option', 'copy', ['source_uri']), 271 ('create_option', 'empty', ['disk_size_gb']) 272 ] 273 self.results = dict( 274 changed=False, 275 state=dict()) 276 277 self.resource_group = None 278 self.name = None 279 self.location = None 280 self.storage_account_type = None 281 self.create_option = None 282 self.source_uri = None 283 self.os_type = None 284 self.disk_size_gb = None 285 self.tags = None 286 self.zone = None 287 self.managed_by = None 288 self.attach_caching = None 289 self.lun = None 290 super(AzureRMManagedDisk, self).__init__( 291 derived_arg_spec=self.module_arg_spec, 292 required_if=required_if, 293 supports_check_mode=True, 294 supports_tags=True) 295 296 def exec_module(self, **kwargs): 297 """Main module execution method""" 298 for key in list(self.module_arg_spec.keys()) + ['tags']: 299 setattr(self, key, kwargs[key]) 300 301 result = None 302 changed = False 303 304 resource_group = self.get_resource_group(self.resource_group) 305 if not self.location: 306 self.location = resource_group.location 307 308 disk_instance = self.get_managed_disk() 309 result = disk_instance 310 311 # need create or update 312 if self.state == 'present': 313 parameter = self.generate_managed_disk_property() 314 if not disk_instance or self.is_different(disk_instance, parameter): 315 changed = True 316 if not self.check_mode: 317 result = self.create_or_update_managed_disk(parameter) 318 else: 319 result = True 320 321 # unmount from the old virtual machine and mount to the new virtual machine 322 if self.managed_by or self.managed_by == '': 323 vm_name = parse_resource_id(disk_instance.get('managed_by', '')).get('name') if disk_instance else None 324 vm_name = vm_name or '' 325 vm_old = None 326 if self.managed_by != vm_name or self.is_attach_caching_option_different(vm_name, result): 327 changed = True 328 if not self.check_mode: 329 if vm_name: 330 vm_old = self._get_vm(vm_name) 331 self.detach(vm_name, result) 332 if self.managed_by: 333 self.attach(self.managed_by, result, vm_old) 334 result = self.get_managed_disk() 335 336 if self.state == 'absent' and disk_instance: 337 changed = True 338 if not self.check_mode: 339 self.delete_managed_disk() 340 result = True 341 342 self.results['changed'] = changed 343 self.results['state'] = result 344 return self.results 345 346 def attach(self, vm_name, disk, vm_old): 347 vm = self._get_vm(vm_name) 348 # find the lun 349 if self.lun: 350 lun = self.lun 351 else: 352 luns = ([d.lun for d in vm.storage_profile.data_disks] 353 if vm.storage_profile.data_disks else []) 354 lun = 0 355 while True: 356 if lun not in luns: 357 break 358 lun = lun + 1 359 if vm_old is not None: 360 for item in vm_old.storage_profile.data_disks: 361 if item.name == self.name: 362 lun = item.lun 363 364 # prepare the data disk 365 params = self.compute_models.ManagedDiskParameters(id=disk.get('id'), storage_account_type=disk.get('storage_account_type')) 366 caching_options = self.compute_models.CachingTypes[self.attach_caching] if self.attach_caching and self.attach_caching != '' else None 367 # pylint: disable=missing-kwoa 368 data_disk = self.compute_models.DataDisk(lun=lun, 369 create_option=self.compute_models.DiskCreateOptionTypes.attach, 370 managed_disk=params, 371 caching=caching_options) 372 vm.storage_profile.data_disks.append(data_disk) 373 self._update_vm(vm_name, vm) 374 375 def detach(self, vm_name, disk): 376 vm = self._get_vm(vm_name) 377 leftovers = [d for d in vm.storage_profile.data_disks if d.name.lower() != disk.get('name').lower()] 378 if len(vm.storage_profile.data_disks) == len(leftovers): 379 self.fail("No disk with the name '{0}' was found".format(disk.get('name'))) 380 vm.storage_profile.data_disks = leftovers 381 self._update_vm(vm_name, vm) 382 383 def _update_vm(self, name, params): 384 try: 385 poller = self.compute_client.virtual_machines.create_or_update(self.resource_group, name, params) 386 self.get_poller_result(poller) 387 except Exception as exc: 388 self.fail("Error updating virtual machine {0} - {1}".format(name, str(exc))) 389 390 def _get_vm(self, name): 391 try: 392 return self.compute_client.virtual_machines.get(self.resource_group, name, expand='instanceview') 393 except Exception as exc: 394 self.fail("Error getting virtual machine {0} - {1}".format(name, str(exc))) 395 396 def generate_managed_disk_property(self): 397 # TODO: Add support for EncryptionSettings, DiskIOPSReadWrite, DiskMBpsReadWrite 398 disk_params = {} 399 creation_data = {} 400 disk_params['location'] = self.location 401 disk_params['tags'] = self.tags 402 if self.zone: 403 disk_params['zones'] = [self.zone] 404 if self.storage_account_type: 405 storage_account_type = self.compute_models.DiskSku(name=self.storage_account_type) 406 disk_params['sku'] = storage_account_type 407 disk_params['disk_size_gb'] = self.disk_size_gb 408 creation_data['create_option'] = self.compute_models.DiskCreateOption.empty 409 if self.create_option == 'import': 410 creation_data['create_option'] = self.compute_models.DiskCreateOption.import_enum 411 creation_data['source_uri'] = self.source_uri 412 elif self.create_option == 'copy': 413 creation_data['create_option'] = self.compute_models.DiskCreateOption.copy 414 creation_data['source_resource_id'] = self.source_uri 415 if self.os_type: 416 typecon = { 417 'linux': self.compute_models.OperatingSystemTypes.linux, 418 'windows': self.compute_models.OperatingSystemTypes.windows 419 } 420 disk_params['os_type'] = typecon[self.os_type] 421 else: 422 disk_params['os_type'] = None 423 disk_params['creation_data'] = creation_data 424 return disk_params 425 426 def create_or_update_managed_disk(self, parameter): 427 try: 428 poller = self.compute_client.disks.create_or_update( 429 self.resource_group, 430 self.name, 431 parameter) 432 aux = self.get_poller_result(poller) 433 return managed_disk_to_dict(aux) 434 except CloudError as e: 435 self.fail("Error creating the managed disk: {0}".format(str(e))) 436 437 # This method accounts for the difference in structure between the 438 # Azure retrieved disk and the parameters for the new disk to be created. 439 def is_different(self, found_disk, new_disk): 440 resp = False 441 if new_disk.get('disk_size_gb'): 442 if not found_disk['disk_size_gb'] == new_disk['disk_size_gb']: 443 resp = True 444 if new_disk.get('os_type'): 445 if not found_disk['os_type'] == new_disk['os_type']: 446 resp = True 447 if new_disk.get('sku'): 448 if not found_disk['storage_account_type'] == new_disk['sku'].name: 449 resp = True 450 # Check how to implement tags 451 if new_disk.get('tags') is not None: 452 if not found_disk['tags'] == new_disk['tags']: 453 resp = True 454 if self.zone is not None: 455 if not found_disk['zone'] == self.zone: 456 resp = True 457 return resp 458 459 def delete_managed_disk(self): 460 try: 461 poller = self.compute_client.disks.delete( 462 self.resource_group, 463 self.name) 464 return self.get_poller_result(poller) 465 except CloudError as e: 466 self.fail("Error deleting the managed disk: {0}".format(str(e))) 467 468 def get_managed_disk(self): 469 try: 470 resp = self.compute_client.disks.get( 471 self.resource_group, 472 self.name) 473 return managed_disk_to_dict(resp) 474 except CloudError as e: 475 self.log('Did not find managed disk') 476 477 def is_attach_caching_option_different(self, vm_name, disk): 478 resp = False 479 if vm_name: 480 vm = self._get_vm(vm_name) 481 correspondence = next((d for d in vm.storage_profile.data_disks if d.name.lower() == disk.get('name').lower()), None) 482 if correspondence and correspondence.caching.name != self.attach_caching: 483 resp = True 484 if correspondence.caching.name == 'none' and self.attach_caching == '': 485 resp = False 486 return resp 487 488 489def main(): 490 """Main execution""" 491 AzureRMManagedDisk() 492 493 494if __name__ == '__main__': 495 main() 496