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