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