1#!/usr/bin/python 2 3# (c) 2016, NetApp, Inc 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 6from __future__ import absolute_import, division, print_function 7__metaclass__ = type 8 9 10ANSIBLE_METADATA = {'metadata_version': '1.1', 11 'status': ['preview'], 12 'supported_by': 'community'} 13 14DOCUMENTATION = """ 15--- 16module: netapp_e_volume 17version_added: "2.2" 18short_description: NetApp E-Series manage storage volumes (standard and thin) 19description: 20 - Create or remove volumes (standard and thin) for NetApp E/EF-series storage arrays. 21author: 22 - Kevin Hulquest (@hulquest) 23 - Nathan Swartz (@ndswartz) 24extends_documentation_fragment: 25 - netapp.eseries 26options: 27 state: 28 description: 29 - Whether the specified volume should exist 30 required: true 31 choices: ['present', 'absent'] 32 name: 33 description: 34 - The name of the volume to manage. 35 required: true 36 storage_pool_name: 37 description: 38 - Required only when requested I(state=='present'). 39 - Name of the storage pool wherein the volume should reside. 40 required: false 41 size_unit: 42 description: 43 - The unit used to interpret the size parameter 44 choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'] 45 default: 'gb' 46 size: 47 description: 48 - Required only when I(state=='present'). 49 - Size of the volume in I(size_unit). 50 - Size of the virtual volume in the case of a thin volume in I(size_unit). 51 - Maximum virtual volume size of a thin provisioned volume is 256tb; however other OS-level restrictions may 52 exist. 53 required: true 54 segment_size_kb: 55 description: 56 - Segment size of the volume 57 - All values are in kibibytes. 58 - Some common choices include '8', '16', '32', '64', '128', '256', and '512' but options are system 59 dependent. 60 - Retrieve the definitive system list from M(netapp_e_facts) under segment_sizes. 61 - When the storage pool is a raidDiskPool then the segment size must be 128kb. 62 - Segment size migrations are not allowed in this module 63 default: '128' 64 thin_provision: 65 description: 66 - Whether the volume should be thin provisioned. 67 - Thin volumes can only be created when I(raid_level=="raidDiskPool"). 68 - Generally, use of thin-provisioning is not recommended due to performance impacts. 69 type: bool 70 default: false 71 thin_volume_repo_size: 72 description: 73 - This value (in size_unit) sets the allocated space for the thin provisioned repository. 74 - Initial value must between or equal to 4gb and 256gb in increments of 4gb. 75 - During expansion operations the increase must be between or equal to 4gb and 256gb in increments of 4gb. 76 - This option has no effect during expansion if I(thin_volume_expansion_policy=="automatic"). 77 - Generally speaking you should almost always use I(thin_volume_expansion_policy=="automatic). 78 required: false 79 thin_volume_max_repo_size: 80 description: 81 - This is the maximum amount the thin volume repository will be allowed to grow. 82 - Only has significance when I(thin_volume_expansion_policy=="automatic"). 83 - When the percentage I(thin_volume_repo_size) of I(thin_volume_max_repo_size) exceeds 84 I(thin_volume_growth_alert_threshold) then a warning will be issued and the storage array will execute 85 the I(thin_volume_expansion_policy) policy. 86 - Expansion operations when I(thin_volume_expansion_policy=="automatic") will increase the maximum 87 repository size. 88 default: same as size (in size_unit) 89 thin_volume_expansion_policy: 90 description: 91 - This is the thin volume expansion policy. 92 - When I(thin_volume_expansion_policy=="automatic") and I(thin_volume_growth_alert_threshold) is exceed the 93 I(thin_volume_max_repo_size) will be automatically expanded. 94 - When I(thin_volume_expansion_policy=="manual") and I(thin_volume_growth_alert_threshold) is exceeded the 95 storage system will wait for manual intervention. 96 - The thin volume_expansion policy can not be modified on existing thin volumes in this module. 97 - Generally speaking you should almost always use I(thin_volume_expansion_policy=="automatic). 98 choices: ["automatic", "manual"] 99 default: "automatic" 100 version_added: 2.8 101 thin_volume_growth_alert_threshold: 102 description: 103 - This is the thin provision repository utilization threshold (in percent). 104 - When the percentage of used storage of the maximum repository size exceeds this value then a alert will 105 be issued and the I(thin_volume_expansion_policy) will be executed. 106 - Values must be between or equal to 10 and 99. 107 default: 95 108 version_added: 2.8 109 owning_controller: 110 description: 111 - Specifies which controller will be the primary owner of the volume 112 - Not specifying will allow the controller to choose ownership. 113 required: false 114 choices: ["A", "B"] 115 version_added: 2.9 116 ssd_cache_enabled: 117 description: 118 - Whether an existing SSD cache should be enabled on the volume (fails if no SSD cache defined) 119 - The default value is to ignore existing SSD cache setting. 120 type: bool 121 default: false 122 data_assurance_enabled: 123 description: 124 - Determines whether data assurance (DA) should be enabled for the volume 125 - Only available when creating a new volume and on a storage pool with drives supporting the DA capability. 126 type: bool 127 default: false 128 read_cache_enable: 129 description: 130 - Indicates whether read caching should be enabled for the volume. 131 type: bool 132 default: true 133 version_added: 2.8 134 read_ahead_enable: 135 description: 136 - Indicates whether or not automatic cache read-ahead is enabled. 137 - This option has no effect on thinly provisioned volumes since the architecture for thin volumes cannot 138 benefit from read ahead caching. 139 type: bool 140 default: true 141 version_added: 2.8 142 write_cache_enable: 143 description: 144 - Indicates whether write-back caching should be enabled for the volume. 145 type: bool 146 default: true 147 version_added: 2.8 148 cache_without_batteries: 149 description: 150 - Indicates whether caching should be used without battery backup. 151 - Warning, M(cache_without_batteries==true) and the storage system looses power and there is no battery backup, data will be lost! 152 type: bool 153 default: false 154 version_added: 2.9 155 workload_name: 156 description: 157 - Label for the workload defined by the metadata. 158 - When I(workload_name) and I(metadata) are specified then the defined workload will be added to the storage 159 array. 160 - When I(workload_name) exists on the storage array but the metadata is different then the workload 161 definition will be updated. (Changes will update all associated volumes!) 162 - Existing workloads can be retrieved using M(netapp_e_facts). 163 required: false 164 version_added: 2.8 165 metadata: 166 description: 167 - Dictionary containing meta data for the use, user, location, etc of the volume (dictionary is arbitrarily 168 defined for whatever the user deems useful) 169 - When I(workload_name) exists on the storage array but the metadata is different then the workload 170 definition will be updated. (Changes will update all associated volumes!) 171 - I(workload_name) must be specified when I(metadata) are defined. 172 type: dict 173 required: false 174 version_added: 2.8 175 wait_for_initialization: 176 description: 177 - Forces the module to wait for expansion operations to complete before continuing. 178 type: bool 179 default: false 180 version_added: 2.8 181 initialization_timeout: 182 description: 183 - Duration in seconds before the wait_for_initialization operation will terminate. 184 - M(wait_for_initialization==True) to have any effect on module's operations. 185 type: int 186 required: false 187 version_added: 2.9 188""" 189EXAMPLES = """ 190- name: Create simple volume with workload tags (volume meta data) 191 netapp_e_volume: 192 ssid: "{{ ssid }}" 193 api_url: "{{ netapp_api_url }}" 194 api_username: "{{ netapp_api_username }}" 195 api_password: "{{ netapp_api_password }}" 196 validate_certs: "{{ netapp_api_validate_certs }}" 197 state: present 198 name: volume 199 storage_pool_name: storage_pool 200 size: 300 201 size_unit: gb 202 workload_name: volume_tag 203 metadata: 204 key1: value1 205 key2: value2 206- name: Create a thin volume 207 netapp_e_volume: 208 ssid: "{{ ssid }}" 209 api_url: "{{ netapp_api_url }}" 210 api_username: "{{ netapp_api_username }}" 211 api_password: "{{ netapp_api_password }}" 212 validate_certs: "{{ netapp_api_validate_certs }}" 213 state: present 214 name: volume1 215 storage_pool_name: storage_pool 216 size: 131072 217 size_unit: gb 218 thin_provision: true 219 thin_volume_repo_size: 32 220 thin_volume_max_repo_size: 1024 221- name: Expand thin volume's virtual size 222 netapp_e_volume: 223 ssid: "{{ ssid }}" 224 api_url: "{{ netapp_api_url }}" 225 api_username: "{{ netapp_api_username }}" 226 api_password: "{{ netapp_api_password }}" 227 validate_certs: "{{ netapp_api_validate_certs }}" 228 state: present 229 name: volume1 230 storage_pool_name: storage_pool 231 size: 262144 232 size_unit: gb 233 thin_provision: true 234 thin_volume_repo_size: 32 235 thin_volume_max_repo_size: 1024 236- name: Expand thin volume's maximum repository size 237 netapp_e_volume: 238 ssid: "{{ ssid }}" 239 api_url: "{{ netapp_api_url }}" 240 api_username: "{{ netapp_api_username }}" 241 api_password: "{{ netapp_api_password }}" 242 validate_certs: "{{ netapp_api_validate_certs }}" 243 state: present 244 name: volume1 245 storage_pool_name: storage_pool 246 size: 262144 247 size_unit: gb 248 thin_provision: true 249 thin_volume_repo_size: 32 250 thin_volume_max_repo_size: 2048 251- name: Delete volume 252 netapp_e_volume: 253 ssid: "{{ ssid }}" 254 api_url: "{{ netapp_api_url }}" 255 api_username: "{{ netapp_api_username }}" 256 api_password: "{{ netapp_api_password }}" 257 validate_certs: "{{ netapp_api_validate_certs }}" 258 state: absent 259 name: volume 260""" 261RETURN = """ 262msg: 263 description: State of volume 264 type: str 265 returned: always 266 sample: "Standard volume [workload_vol_1] has been created." 267""" 268from time import sleep 269from ansible.module_utils.netapp import NetAppESeriesModule 270from ansible.module_utils._text import to_native 271 272 273class NetAppESeriesVolume(NetAppESeriesModule): 274 VOLUME_CREATION_BLOCKING_TIMEOUT_SEC = 300 275 276 def __init__(self): 277 ansible_options = dict( 278 state=dict(required=True, choices=["present", "absent"]), 279 name=dict(required=True, type="str"), 280 storage_pool_name=dict(type="str"), 281 size_unit=dict(default="gb", choices=["bytes", "b", "kb", "mb", "gb", "tb", "pb", "eb", "zb", "yb"], 282 type="str"), 283 size=dict(type="float"), 284 segment_size_kb=dict(type="int", default=128), 285 owning_controller=dict(required=False, choices=['A', 'B']), 286 ssd_cache_enabled=dict(type="bool", default=False), 287 data_assurance_enabled=dict(type="bool", default=False), 288 thin_provision=dict(type="bool", default=False), 289 thin_volume_repo_size=dict(type="int"), 290 thin_volume_max_repo_size=dict(type="float"), 291 thin_volume_expansion_policy=dict(type="str", choices=["automatic", "manual"]), 292 thin_volume_growth_alert_threshold=dict(type="int", default=95), 293 read_cache_enable=dict(type="bool", default=True), 294 read_ahead_enable=dict(type="bool", default=True), 295 write_cache_enable=dict(type="bool", default=True), 296 cache_without_batteries=dict(type="bool", default=False), 297 workload_name=dict(type="str", required=False), 298 metadata=dict(type="dict", required=False), 299 wait_for_initialization=dict(type="bool", default=False), 300 initialization_timeout=dict(type="int", required=False)) 301 302 required_if = [ 303 ["state", "present", ["storage_pool_name", "size"]], 304 ["thin_provision", "true", ["thin_volume_repo_size"]] 305 ] 306 307 super(NetAppESeriesVolume, self).__init__(ansible_options=ansible_options, 308 web_services_version="02.00.0000.0000", 309 supports_check_mode=True, 310 required_if=required_if) 311 312 args = self.module.params 313 self.state = args["state"] 314 self.name = args["name"] 315 self.storage_pool_name = args["storage_pool_name"] 316 self.size_unit = args["size_unit"] 317 self.segment_size_kb = args["segment_size_kb"] 318 if args["size"]: 319 self.size_b = self.convert_to_aligned_bytes(args["size"]) 320 321 self.owning_controller_id = None 322 if args["owning_controller"]: 323 self.owning_controller_id = "070000000000000000000001" if args["owning_controller"] == "A" else "070000000000000000000002" 324 325 self.read_cache_enable = args["read_cache_enable"] 326 self.read_ahead_enable = args["read_ahead_enable"] 327 self.write_cache_enable = args["write_cache_enable"] 328 self.ssd_cache_enabled = args["ssd_cache_enabled"] 329 self.cache_without_batteries = args["cache_without_batteries"] 330 self.data_assurance_enabled = args["data_assurance_enabled"] 331 332 self.thin_provision = args["thin_provision"] 333 self.thin_volume_expansion_policy = args["thin_volume_expansion_policy"] 334 self.thin_volume_growth_alert_threshold = int(args["thin_volume_growth_alert_threshold"]) 335 self.thin_volume_repo_size_b = None 336 self.thin_volume_max_repo_size_b = None 337 338 if args["thin_volume_repo_size"]: 339 self.thin_volume_repo_size_b = self.convert_to_aligned_bytes(args["thin_volume_repo_size"]) 340 if args["thin_volume_max_repo_size"]: 341 self.thin_volume_max_repo_size_b = self.convert_to_aligned_bytes(args["thin_volume_max_repo_size"]) 342 343 self.workload_name = args["workload_name"] 344 self.metadata = args["metadata"] 345 self.wait_for_initialization = args["wait_for_initialization"] 346 self.initialization_timeout = args["initialization_timeout"] 347 348 # convert metadata to a list of dictionaries containing the keys "key" and "value" corresponding to 349 # each of the workload attributes dictionary entries 350 metadata = [] 351 if self.metadata: 352 if not self.workload_name: 353 self.module.fail_json(msg="When metadata is specified then the name for the workload must be specified." 354 " Array [%s]." % self.ssid) 355 for key in self.metadata.keys(): 356 metadata.append(dict(key=key, value=self.metadata[key])) 357 self.metadata = metadata 358 359 if self.thin_provision: 360 if not self.thin_volume_max_repo_size_b: 361 self.thin_volume_max_repo_size_b = self.size_b 362 363 if not self.thin_volume_expansion_policy: 364 self.thin_volume_expansion_policy = "automatic" 365 366 if self.size_b > 256 * 1024 ** 4: 367 self.module.fail_json(msg="Thin provisioned volumes must be less than or equal to 256tb is size." 368 " Attempted size [%sg]" % (self.size_b * 1024 ** 3)) 369 370 if (self.thin_volume_repo_size_b and self.thin_volume_max_repo_size_b and 371 self.thin_volume_repo_size_b > self.thin_volume_max_repo_size_b): 372 self.module.fail_json(msg="The initial size of the thin volume must not be larger than the maximum" 373 " repository size. Array [%s]." % self.ssid) 374 375 if self.thin_volume_growth_alert_threshold < 10 or self.thin_volume_growth_alert_threshold > 99: 376 self.module.fail_json(msg="thin_volume_growth_alert_threshold must be between or equal to 10 and 99." 377 "thin_volume_growth_alert_threshold [%s]. Array [%s]." 378 % (self.thin_volume_growth_alert_threshold, self.ssid)) 379 380 self.volume_detail = None 381 self.pool_detail = None 382 self.workload_id = None 383 384 def convert_to_aligned_bytes(self, size): 385 """Convert size to the truncated byte size that aligns on the segment size.""" 386 size_bytes = int(size * self.SIZE_UNIT_MAP[self.size_unit]) 387 segment_size_bytes = int(self.segment_size_kb * self.SIZE_UNIT_MAP["kb"]) 388 segment_count = int(size_bytes / segment_size_bytes) 389 return segment_count * segment_size_bytes 390 391 def get_volume(self): 392 """Retrieve volume details from storage array.""" 393 volumes = list() 394 thin_volumes = list() 395 try: 396 rc, volumes = self.request("storage-systems/%s/volumes" % self.ssid) 397 except Exception as err: 398 self.module.fail_json(msg="Failed to obtain list of thick volumes. Array Id [%s]. Error[%s]." 399 % (self.ssid, to_native(err))) 400 try: 401 rc, thin_volumes = self.request("storage-systems/%s/thin-volumes" % self.ssid) 402 except Exception as err: 403 self.module.fail_json(msg="Failed to obtain list of thin volumes. Array Id [%s]. Error[%s]." 404 % (self.ssid, to_native(err))) 405 406 volume_detail = [volume for volume in volumes + thin_volumes if volume["name"] == self.name] 407 return volume_detail[0] if volume_detail else dict() 408 409 def wait_for_volume_availability(self, retries=VOLUME_CREATION_BLOCKING_TIMEOUT_SEC / 5): 410 """Waits until volume becomes available. 411 412 :raises AnsibleFailJson when retries are exhausted. 413 """ 414 if retries == 0: 415 self.module.fail_json(msg="Timed out waiting for the volume %s to become available. Array [%s]." 416 % (self.name, self.ssid)) 417 if not self.get_volume(): 418 sleep(5) 419 self.wait_for_volume_availability(retries=retries - 1) 420 421 def wait_for_volume_action(self, timeout=None): 422 """Waits until volume action is complete is complete. 423 :param: int timeout: Wait duration measured in seconds. Waits indefinitely when None. 424 """ 425 action = "unknown" 426 percent_complete = None 427 while action != "complete": 428 sleep(5) 429 430 try: 431 rc, operations = self.request("storage-systems/%s/symbol/getLongLivedOpsProgress" % self.ssid) 432 433 # Search long lived operations for volume 434 action = "complete" 435 for operation in operations["longLivedOpsProgress"]: 436 if operation["volAction"] is not None: 437 for key in operation.keys(): 438 if (operation[key] is not None and "volumeRef" in operation[key] and 439 (operation[key]["volumeRef"] == self.volume_detail["id"] or 440 ("storageVolumeRef" in self.volume_detail and operation[key]["volumeRef"] == self.volume_detail["storageVolumeRef"]))): 441 action = operation["volAction"] 442 percent_complete = operation["init"]["percentComplete"] 443 except Exception as err: 444 self.module.fail_json(msg="Failed to get volume expansion progress. Volume [%s]. Array Id [%s]." 445 " Error[%s]." % (self.name, self.ssid, to_native(err))) 446 447 if timeout is not None: 448 if timeout <= 0: 449 self.module.warn("Expansion action, %s, failed to complete during the allotted time. Time remaining" 450 " [%s]. Array Id [%s]." % (action, percent_complete, self.ssid)) 451 self.module.fail_json(msg="Expansion action failed to complete. Time remaining [%s]. Array Id [%s]." % (percent_complete, self.ssid)) 452 if timeout: 453 timeout -= 5 454 455 self.module.log("Expansion action, %s, is %s complete." % (action, percent_complete)) 456 self.module.log("Expansion action is complete.") 457 458 def get_storage_pool(self): 459 """Retrieve storage pool details from the storage array.""" 460 storage_pools = list() 461 try: 462 rc, storage_pools = self.request("storage-systems/%s/storage-pools" % self.ssid) 463 except Exception as err: 464 self.module.fail_json(msg="Failed to obtain list of storage pools. Array Id [%s]. Error[%s]." 465 % (self.ssid, to_native(err))) 466 467 pool_detail = [storage_pool for storage_pool in storage_pools if storage_pool["name"] == self.storage_pool_name] 468 return pool_detail[0] if pool_detail else dict() 469 470 def check_storage_pool_sufficiency(self): 471 """Perform a series of checks as to the sufficiency of the storage pool for the volume.""" 472 if not self.pool_detail: 473 self.module.fail_json(msg='Requested storage pool (%s) not found' % self.storage_pool_name) 474 475 if not self.volume_detail: 476 if self.thin_provision and not self.pool_detail['diskPool']: 477 self.module.fail_json(msg='Thin provisioned volumes can only be created on raid disk pools.') 478 479 if (self.data_assurance_enabled and not 480 (self.pool_detail["protectionInformationCapabilities"]["protectionInformationCapable"] and 481 self.pool_detail["protectionInformationCapabilities"]["protectionType"] == "type2Protection")): 482 self.module.fail_json(msg="Data Assurance (DA) requires the storage pool to be DA-compatible." 483 " Array [%s]." % self.ssid) 484 485 if int(self.pool_detail["freeSpace"]) < self.size_b and not self.thin_provision: 486 self.module.fail_json(msg="Not enough storage pool free space available for the volume's needs." 487 " Array [%s]." % self.ssid) 488 else: 489 # Check for expansion 490 if (int(self.pool_detail["freeSpace"]) < int(self.volume_detail["totalSizeInBytes"]) - self.size_b and 491 not self.thin_provision): 492 self.module.fail_json(msg="Not enough storage pool free space available for the volume's needs." 493 " Array [%s]." % self.ssid) 494 495 def update_workload_tags(self, check_mode=False): 496 """Check the status of the workload tag and update storage array definitions if necessary. 497 498 When the workload attributes are not provided but an existing workload tag name is, then the attributes will be 499 used. 500 501 :return bool: Whether changes were required to be made.""" 502 change_required = False 503 workload_tags = None 504 request_body = None 505 ansible_profile_id = None 506 507 if self.workload_name: 508 try: 509 rc, workload_tags = self.request("storage-systems/%s/workloads" % self.ssid) 510 except Exception as error: 511 self.module.fail_json(msg="Failed to retrieve storage array workload tags. Array [%s]" % self.ssid) 512 513 # Generate common indexed Ansible workload tag 514 current_tag_index_list = [int(pair["value"].replace("ansible_workload_", "")) 515 for tag in workload_tags for pair in tag["workloadAttributes"] 516 if pair["key"] == "profileId" and "ansible_workload_" in pair["value"] and 517 str(pair["value"]).replace("ansible_workload_", "").isdigit()] 518 519 tag_index = 1 520 if current_tag_index_list: 521 tag_index = max(current_tag_index_list) + 1 522 523 ansible_profile_id = "ansible_workload_%d" % tag_index 524 request_body = dict(name=self.workload_name, 525 profileId=ansible_profile_id, 526 workloadInstanceIndex=None, 527 isValid=True) 528 529 # evaluate and update storage array when needed 530 for tag in workload_tags: 531 if tag["name"] == self.workload_name: 532 self.workload_id = tag["id"] 533 534 if not self.metadata: 535 break 536 537 # Determine if core attributes (everything but profileId) is the same 538 metadata_set = set(tuple(sorted(attr.items())) for attr in self.metadata) 539 tag_set = set(tuple(sorted(attr.items())) 540 for attr in tag["workloadAttributes"] if attr["key"] != "profileId") 541 if metadata_set != tag_set: 542 self.module.log("Workload tag change is required!") 543 change_required = True 544 545 # only perform the required action when check_mode==False 546 if change_required and not check_mode: 547 self.metadata.append(dict(key="profileId", value=ansible_profile_id)) 548 request_body.update(dict(isNewWorkloadInstance=False, 549 isWorkloadDataInitialized=True, 550 isWorkloadCardDataToBeReset=True, 551 workloadAttributes=self.metadata)) 552 try: 553 rc, resp = self.request("storage-systems/%s/workloads/%s" % (self.ssid, tag["id"]), 554 data=request_body, method="POST") 555 except Exception as error: 556 self.module.fail_json(msg="Failed to create new workload tag. Array [%s]. Error [%s]" 557 % (self.ssid, to_native(error))) 558 self.module.log("Workload tag [%s] required change." % self.workload_name) 559 break 560 561 # existing workload tag not found so create new workload tag 562 else: 563 change_required = True 564 self.module.log("Workload tag creation is required!") 565 566 if change_required and not check_mode: 567 if self.metadata: 568 self.metadata.append(dict(key="profileId", value=ansible_profile_id)) 569 else: 570 self.metadata = [dict(key="profileId", value=ansible_profile_id)] 571 572 request_body.update(dict(isNewWorkloadInstance=True, 573 isWorkloadDataInitialized=False, 574 isWorkloadCardDataToBeReset=False, 575 workloadAttributes=self.metadata)) 576 try: 577 rc, resp = self.request("storage-systems/%s/workloads" % self.ssid, 578 method="POST", data=request_body) 579 self.workload_id = resp["id"] 580 except Exception as error: 581 self.module.fail_json(msg="Failed to create new workload tag. Array [%s]. Error [%s]" 582 % (self.ssid, to_native(error))) 583 self.module.log("Workload tag [%s] was added." % self.workload_name) 584 585 return change_required 586 587 def get_volume_property_changes(self): 588 """Retrieve the volume update request body when change(s) are required. 589 590 :raise AnsibleFailJson when attempting to change segment size on existing volume. 591 :return dict: request body when change(s) to a volume's properties are required. 592 """ 593 change = False 594 request_body = dict(flashCache=self.ssd_cache_enabled, metaTags=[], 595 cacheSettings=dict(readCacheEnable=self.read_cache_enable, 596 writeCacheEnable=self.write_cache_enable)) 597 598 # check for invalid modifications 599 if self.segment_size_kb * 1024 != int(self.volume_detail["segmentSize"]): 600 self.module.fail_json(msg="Existing volume segment size is %s and cannot be modified." 601 % self.volume_detail["segmentSize"]) 602 603 # common thick/thin volume properties 604 if (self.read_cache_enable != self.volume_detail["cacheSettings"]["readCacheEnable"] or 605 self.write_cache_enable != self.volume_detail["cacheSettings"]["writeCacheEnable"] or 606 self.ssd_cache_enabled != self.volume_detail["flashCached"]): 607 change = True 608 609 # controller ownership 610 if self.owning_controller_id and self.owning_controller_id != self.volume_detail["preferredManager"]: 611 change = True 612 request_body.update(dict(owningControllerId=self.owning_controller_id)) 613 614 if self.workload_name: 615 request_body.update(dict(metaTags=[dict(key="workloadId", value=self.workload_id), 616 dict(key="volumeTypeId", value="volume")])) 617 if {"key": "workloadId", "value": self.workload_id} not in self.volume_detail["metadata"]: 618 change = True 619 elif self.volume_detail["metadata"]: 620 change = True 621 622 # thick/thin volume specific properties 623 if self.thin_provision: 624 if self.thin_volume_growth_alert_threshold != int(self.volume_detail["growthAlertThreshold"]): 625 change = True 626 request_body.update(dict(growthAlertThreshold=self.thin_volume_growth_alert_threshold)) 627 if self.thin_volume_expansion_policy != self.volume_detail["expansionPolicy"]: 628 change = True 629 request_body.update(dict(expansionPolicy=self.thin_volume_expansion_policy)) 630 else: 631 if self.read_ahead_enable != (int(self.volume_detail["cacheSettings"]["readAheadMultiplier"]) > 0): 632 change = True 633 request_body["cacheSettings"].update(dict(readAheadEnable=self.read_ahead_enable)) 634 if self.cache_without_batteries != self.volume_detail["cacheSettings"]["cwob"]: 635 change = True 636 request_body["cacheSettings"].update(dict(cacheWithoutBatteries=self.cache_without_batteries)) 637 638 return request_body if change else dict() 639 640 def get_expand_volume_changes(self): 641 """Expand the storage specifications for the existing thick/thin volume. 642 643 :raise AnsibleFailJson when a thick/thin volume expansion request fails. 644 :return dict: dictionary containing all the necessary values for volume expansion request 645 """ 646 request_body = dict() 647 648 if self.size_b < int(self.volume_detail["capacity"]): 649 self.module.fail_json(msg="Reducing the size of volumes is not permitted. Volume [%s]. Array [%s]" 650 % (self.name, self.ssid)) 651 652 if self.volume_detail["thinProvisioned"]: 653 if self.size_b > int(self.volume_detail["capacity"]): 654 request_body.update(dict(sizeUnit="bytes", newVirtualSize=self.size_b)) 655 self.module.log("Thin volume virtual size have been expanded.") 656 657 if self.volume_detail["expansionPolicy"] == "automatic": 658 if self.thin_volume_max_repo_size_b > int(self.volume_detail["provisionedCapacityQuota"]): 659 request_body.update(dict(sizeUnit="bytes", newRepositorySize=self.thin_volume_max_repo_size_b)) 660 self.module.log("Thin volume maximum repository size have been expanded (automatic policy).") 661 662 elif self.volume_detail["expansionPolicy"] == "manual": 663 if self.thin_volume_repo_size_b > int(self.volume_detail["currentProvisionedCapacity"]): 664 change = self.thin_volume_repo_size_b - int(self.volume_detail["currentProvisionedCapacity"]) 665 if change < 4 * 1024 ** 3 or change > 256 * 1024 ** 3 or change % (4 * 1024 ** 3) != 0: 666 self.module.fail_json(msg="The thin volume repository increase must be between or equal to 4gb" 667 " and 256gb in increments of 4gb. Attempted size [%sg]." 668 % (self.thin_volume_repo_size_b * 1024 ** 3)) 669 670 request_body.update(dict(sizeUnit="bytes", newRepositorySize=self.thin_volume_repo_size_b)) 671 self.module.log("Thin volume maximum repository size have been expanded (manual policy).") 672 673 elif self.size_b > int(self.volume_detail["capacity"]): 674 request_body.update(dict(sizeUnit="bytes", expansionSize=self.size_b)) 675 self.module.log("Volume storage capacities have been expanded.") 676 677 return request_body 678 679 def create_volume(self): 680 """Create thick/thin volume according to the specified criteria.""" 681 body = dict(name=self.name, poolId=self.pool_detail["id"], sizeUnit="bytes", 682 dataAssuranceEnabled=self.data_assurance_enabled) 683 684 if self.thin_provision: 685 body.update(dict(virtualSize=self.size_b, 686 repositorySize=self.thin_volume_repo_size_b, 687 maximumRepositorySize=self.thin_volume_max_repo_size_b, 688 expansionPolicy=self.thin_volume_expansion_policy, 689 growthAlertThreshold=self.thin_volume_growth_alert_threshold)) 690 try: 691 rc, volume = self.request("storage-systems/%s/thin-volumes" % self.ssid, data=body, method="POST") 692 except Exception as error: 693 self.module.fail_json(msg="Failed to create thin volume. Volume [%s]. Array Id [%s]. Error[%s]." 694 % (self.name, self.ssid, to_native(error))) 695 696 self.module.log("New thin volume created [%s]." % self.name) 697 698 else: 699 body.update(dict(size=self.size_b, segSize=self.segment_size_kb)) 700 try: 701 rc, volume = self.request("storage-systems/%s/volumes" % self.ssid, data=body, method="POST") 702 except Exception as error: 703 self.module.fail_json(msg="Failed to create volume. Volume [%s]. Array Id [%s]. Error[%s]." 704 % (self.name, self.ssid, to_native(error))) 705 706 self.module.log("New volume created [%s]." % self.name) 707 708 def update_volume_properties(self): 709 """Update existing thin-volume or volume properties. 710 711 :raise AnsibleFailJson when either thick/thin volume update request fails. 712 :return bool: whether update was applied 713 """ 714 self.wait_for_volume_availability() 715 self.volume_detail = self.get_volume() 716 717 request_body = self.get_volume_property_changes() 718 719 if request_body: 720 if self.thin_provision: 721 try: 722 rc, resp = self.request("storage-systems/%s/thin-volumes/%s" 723 % (self.ssid, self.volume_detail["id"]), data=request_body, method="POST") 724 except Exception as error: 725 self.module.fail_json(msg="Failed to update thin volume properties. Volume [%s]. Array Id [%s]." 726 " Error[%s]." % (self.name, self.ssid, to_native(error))) 727 else: 728 try: 729 rc, resp = self.request("storage-systems/%s/volumes/%s" % (self.ssid, self.volume_detail["id"]), 730 data=request_body, method="POST") 731 except Exception as error: 732 self.module.fail_json(msg="Failed to update volume properties. Volume [%s]. Array Id [%s]." 733 " Error[%s]." % (self.name, self.ssid, to_native(error))) 734 return True 735 return False 736 737 def expand_volume(self): 738 """Expand the storage specifications for the existing thick/thin volume. 739 740 :raise AnsibleFailJson when a thick/thin volume expansion request fails. 741 """ 742 request_body = self.get_expand_volume_changes() 743 if request_body: 744 if self.volume_detail["thinProvisioned"]: 745 try: 746 rc, resp = self.request("storage-systems/%s/thin-volumes/%s/expand" 747 % (self.ssid, self.volume_detail["id"]), data=request_body, method="POST") 748 except Exception as err: 749 self.module.fail_json(msg="Failed to expand thin volume. Volume [%s]. Array Id [%s]. Error[%s]." 750 % (self.name, self.ssid, to_native(err))) 751 self.module.log("Thin volume specifications have been expanded.") 752 753 else: 754 try: 755 rc, resp = self.request( 756 "storage-systems/%s/volumes/%s/expand" % (self.ssid, self.volume_detail['id']), 757 data=request_body, method="POST") 758 except Exception as err: 759 self.module.fail_json(msg="Failed to expand volume. Volume [%s]. Array Id [%s]. Error[%s]." 760 % (self.name, self.ssid, to_native(err))) 761 762 self.module.log("Volume storage capacities have been expanded.") 763 764 def delete_volume(self): 765 """Delete existing thin/thick volume.""" 766 if self.thin_provision: 767 try: 768 rc, resp = self.request("storage-systems/%s/thin-volumes/%s" % (self.ssid, self.volume_detail["id"]), 769 method="DELETE") 770 except Exception as error: 771 self.module.fail_json(msg="Failed to delete thin volume. Volume [%s]. Array Id [%s]. Error[%s]." 772 % (self.name, self.ssid, to_native(error))) 773 self.module.log("Thin volume deleted [%s]." % self.name) 774 else: 775 try: 776 rc, resp = self.request("storage-systems/%s/volumes/%s" % (self.ssid, self.volume_detail["id"]), 777 method="DELETE") 778 except Exception as error: 779 self.module.fail_json(msg="Failed to delete volume. Volume [%s]. Array Id [%s]. Error[%s]." 780 % (self.name, self.ssid, to_native(error))) 781 self.module.log("Volume deleted [%s]." % self.name) 782 783 def apply(self): 784 """Determine and apply any changes necessary to satisfy the specified criteria. 785 786 :raise AnsibleExitJson when completes successfully""" 787 change = False 788 msg = None 789 790 self.volume_detail = self.get_volume() 791 self.pool_detail = self.get_storage_pool() 792 793 # Determine whether changes need to be applied to existing workload tags 794 if self.state == 'present' and self.update_workload_tags(check_mode=True): 795 change = True 796 797 # Determine if any changes need to be applied 798 if self.volume_detail: 799 if self.state == 'absent': 800 change = True 801 802 elif self.state == 'present': 803 if self.get_expand_volume_changes() or self.get_volume_property_changes(): 804 change = True 805 806 elif self.state == 'present': 807 if self.thin_provision and (self.thin_volume_repo_size_b < 4 * 1024 ** 3 or 808 self.thin_volume_repo_size_b > 256 * 1024 ** 3 or 809 self.thin_volume_repo_size_b % (4 * 1024 ** 3) != 0): 810 self.module.fail_json(msg="The initial thin volume repository size must be between 4gb and 256gb in" 811 " increments of 4gb. Attempted size [%sg]." 812 % (self.thin_volume_repo_size_b * 1024 ** 3)) 813 change = True 814 815 self.module.log("Update required: [%s]." % change) 816 817 # Apply any necessary changes 818 if change and not self.module.check_mode: 819 if self.state == 'present': 820 if self.update_workload_tags(): 821 msg = "Workload tag change occurred." 822 823 if not self.volume_detail: 824 self.check_storage_pool_sufficiency() 825 self.create_volume() 826 self.update_volume_properties() 827 msg = msg[:-1] + " and volume [%s] was created." if msg else "Volume [%s] has been created." 828 else: 829 if self.update_volume_properties(): 830 msg = "Volume [%s] properties were updated." 831 832 if self.get_expand_volume_changes(): 833 self.expand_volume() 834 msg = msg[:-1] + " and was expanded." if msg else "Volume [%s] was expanded." 835 836 if self.wait_for_initialization: 837 self.module.log("Waiting for volume operation to complete.") 838 self.wait_for_volume_action(timeout=self.initialization_timeout) 839 840 elif self.state == 'absent': 841 self.delete_volume() 842 msg = "Volume [%s] has been deleted." 843 844 else: 845 msg = "Volume [%s] does not exist." if self.state == 'absent' else "Volume [%s] exists." 846 847 self.module.exit_json(msg=(msg % self.name if msg and "%s" in msg else msg), changed=change) 848 849 850def main(): 851 volume = NetAppESeriesVolume() 852 volume.apply() 853 854 855if __name__ == '__main__': 856 main() 857