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