Dre4m Shell
Server IP : 85.214.239.14  /  Your IP : 3.145.156.17
Web Server : Apache/2.4.62 (Debian)
System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64
User : www-data ( 33)
PHP Version : 7.4.18
Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
MySQL : OFF  |  cURL : OFF  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : ON  |  Pkexec : OFF
Directory :  /lib/python3/dist-packages/ansible_collections/netapp/ontap/plugins/modules/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ HOME SHELL ]     

Current File : /lib/python3/dist-packages/ansible_collections/netapp/ontap/plugins/modules/na_ontap_aggregate.py
#!/usr/bin/python

# (c) 2018-2023, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

'''
na_ontap_aggregate
'''

from __future__ import absolute_import, division, print_function
__metaclass__ = type

DOCUMENTATION = '''

module: na_ontap_aggregate
short_description: NetApp ONTAP manage aggregates.
extends_documentation_fragment:
    - netapp.ontap.netapp.na_ontap
version_added: 2.6.0
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>

description:
  - Create, delete, or manage aggregates on ONTAP.

options:

  state:
    description:
      - Whether the specified aggregate should exist or not.
    choices: ['present', 'absent']
    default: 'present'
    type: str

  service_state:
    description:
      - Whether the specified aggregate should be enabled or disabled. Creates aggregate if doesnt exist.
      - Supported from 9.11.1 or later in REST.
    choices: ['online', 'offline']
    type: str

  name:
    description:
      - The name of the aggregate to manage.
    required: true
    type: str

  from_name:
    description:
      - Name of the aggregate to be renamed.
    type: str
    version_added: 2.7.0

  nodes:
    description:
      - Node(s) for the aggregate to be created on.  If no node specified, mgmt lif home will be used.
      - ZAPI only - if multiple nodes specified an aggr stripe will be made.
      - With REST, only one node can be specified.  If disk_count is present, node name is required.
    type: list
    elements: str

  disk_type:
    description:
      - Type of disk to use to build aggregate.
      - Not supported with REST - see C(disk_class).
      - SSD-NVM, SSD-CAP were added with ONTAP 9.6.
      - VMLUN was added with ONTAP 9.9.
    choices: ['ATA', 'BSAS', 'FCAL', 'FSAS', 'LUN', 'MSATA', 'SAS', 'SSD', 'SSD-CAP', 'SSD-NVM', 'VMDISK', 'VMLUN', 'VMLUN-SSD']
    type: str
    version_added: 2.7.0

  disk_class:
    description:
      - Class of disk to use to build aggregate.
      - C(capacity_flash) is listed in swagger, but rejected as invalid by ONTAP.
    choices: ['capacity', 'performance', 'archive', 'solid_state', 'array', 'virtual', 'data_center', 'capacity_flash']
    type: str
    version_added: 21.16.0

  disk_count:
    description:
      - Number of disks to place into the aggregate, including parity disks.
      - The disks in this newly-created aggregate come from the spare disk pool.
      - The smallest disks in this pool join the aggregate first, unless the C(disk-size) argument is provided.
      - Either C(disk-count) or C(disks) must be supplied. Range [0..2^31-1].
      - Required when C(state=present).
      - Modifiable only if specified disk_count is larger than current disk_count.
      - Cannot create raidgroup with 1 disk when using raid type raid4.
      - If the disk_count % raid_size == 1, only disk_count/raid_size * raid_size will be added.
      - If disk_count is 6, raid_type is raid4, raid_size 4, all 6 disks will be added.
      - If disk_count is 5, raid_type is raid4, raid_size 4, 5/4 * 4 = 4 will be added. 1 will not be added.
      - With REST, C(nodes) is required if C(disk_count) is present.
    type: int

  disk_size:
    description:
      - Disk size to use in 4K block size.  Disks within 10% of specified size will be used.
      - With REST, this is converted to bytes using 4096.  Use C(disk_size_with_unit) to skip the conversion.
    type: int
    version_added: 2.7.0

  disk_size_with_unit:
    description:
      - Disk size to use in the specified unit.
      - It is a positive integer number followed by unit of T/G/M/K. For example, 72G, 1T and 32M.
      - Or the unit can be omitted for bytes (REST also accepts B).
      - This option is ignored if a specific list of disks is specified through the "disks" parameter.
      - You must only use one of either "disk-size" or "disk-size-with-unit" parameters.
      - With REST, this is converted to bytes, assuming K=1024.
    type: str

  raid_size:
    description:
      - Sets the maximum number of drives per raid group.
    type: int
    version_added: 2.7.0

  raid_type:
    description:
      - Specifies the type of RAID groups to use in the new aggregate.
      - raid_0 is only available on ONTAP Select.
    choices: ['raid4', 'raid_dp', 'raid_tec', 'raid_0']
    type: str
    version_added: 2.7.0

  unmount_volumes:
    description:
      - If set to "true", this option specifies that all of the volumes hosted by the given aggregate are to be unmounted
        before the offline operation is executed.
      - By default, the system will reject any attempt to offline an aggregate that hosts one or more online volumes.
      - Not supported with REST, by default REST unmount volumes when trying to offline aggregate.
    type: bool

  disks:
    description:
      - Specific list of disks to use for the new aggregate.
      - To create a "mirrored" aggregate with a specific list of disks, both 'disks' and 'mirror_disks' options must be supplied.
        Additionally, the same number of disks must be supplied in both lists.
      - Not supported with REST.
    type: list
    elements: str
    version_added: 2.8.0

  is_mirrored:
    description:
      - Specifies that the new aggregate be mirrored (have two plexes).
      - If set to true, then the indicated disks will be split across the two plexes. By default, the new aggregate will not be mirrored.
      - This option cannot be used when a specific list of disks is supplied with either the 'disks' or 'mirror_disks' options.
    type: bool
    version_added: 2.8.0

  mirror_disks:
    description:
      - List of mirror disks to use. It must contain the same number of disks specified in 'disks'.
      - Not supported with REST.
    type: list
    elements: str
    version_added: 2.8.0

  spare_pool:
    description:
      - Specifies the spare pool from which to select spare disks to use in creation of a new aggregate.
      - Not supported with REST.
    choices: ['Pool0', 'Pool1']
    type: str
    version_added: 2.8.0

  wait_for_online:
    description:
      - Set this parameter to 'true' for synchronous execution during create (wait until aggregate status is online).
      - Set this parameter to 'false' for asynchronous execution.
      - For asynchronous, execution exits as soon as the request is sent, without checking aggregate status.
      - Ignored with REST (always wait).
    type: bool
    default: false
    version_added: 2.8.0

  time_out:
    description:
      - time to wait for aggregate creation in seconds.
      - default is set to 100 seconds.
    type: int
    default: 100
    version_added: 2.8.0

  object_store_name:
    description:
      - Name of the object store configuration attached to the aggregate.
    type: str
    version_added: 2.9.0

  allow_flexgroups:
    description:
      - This optional parameter allows attaching object store to an aggregate containing FlexGroup constituents. The default value is false.
      - Mixing FabricPools and non-FabricPools within a FlexGroup is not recommended.
      - All aggregates hosting constituents of a FlexGroup should be attached to the object store.
    type: bool
    version_added: 22.3.0

  snaplock_type:
    description:
      - Type of snaplock for the aggregate being created.
    choices: ['compliance', 'enterprise', 'non_snaplock']
    type: str
    version_added: 20.1.0

  ignore_pool_checks:
    description:
      - only valid when I(disks) option is used.
      - disks in a plex should belong to the same spare pool, and mirror disks to another spare pool.
      - when set to true, these checks are ignored.
      - Ignored with REST as I(disks) is not supported.
    type: bool
    version_added: 20.8.0

  encryption:
    description:
      - whether to enable software encryption.
      - this is equivalent to -encrypt-with-aggr-key when using the CLI.
      - requires a VE license.
    type: bool
    version_added: 21.14.0

  tags:
    description:
      - Tags are an optional way to track the uses of a resource.
      - Tag values must be formatted as key:value strings, example ["team:csi", "environment:test"]
    type: list
    elements: str
    version_added: 22.6.0

notes:
  - supports check_mode.
  - support ZAPI and REST.

'''

EXAMPLES = """
- name: Create Aggregates and wait 5 minutes until aggregate is online in ZAPI.
  netapp.ontap.na_ontap_aggregate:
    state: present
    service_state: online
    name: ansibleAggr
    disk_count: 10
    wait_for_online: True
    time_out: 300
    snaplock_type: non_snaplock
    use_rest: never
    hostname: "{{ netapp_hostname }}"
    username: "{{ netapp_username }}"
    password: "{{ netapp_password }}"

- name: Create Aggregates in REST.
  netapp.ontap.na_ontap_aggregate:
    state: present
    service_state: online
    name: ansibleAggr
    disk_count: 10
    nodes: ontap-node
    snaplock_type: non_snaplock
    use_rest: always
    hostname: "{{ netapp_hostname }}"
    username: "{{ netapp_username }}"
    password: "{{ netapp_password }}"

- name: Manage Aggregates in ZAPI, modify service state.
  netapp.ontap.na_ontap_aggregate:
    state: present
    service_state: offline
    unmount_volumes: true
    name: ansibleAggr
    disk_count: 10
    use_rest: never
    hostname: "{{ netapp_hostname }}"
    username: "{{ netapp_username }}"
    password: "{{ netapp_password }}"

- name: Manage Aggregates in REST, increase disk count.
  netapp.ontap.na_ontap_aggregate:
    state: present
    name: ansibleAggr
    disk_count: 20
    nodes: ontap-node
    hostname: "{{ netapp_hostname }}"
    username: "{{ netapp_username }}"
    password: "{{ netapp_password }}"

- name: Attach object store
  netapp.ontap.na_ontap_aggregate:
    state: present
    name: aggr4
    object_store_name: sgws_305
    hostname: "{{ netapp_hostname }}"
    username: "{{ netapp_username }}"
    password: "{{ netapp_password }}"

- name: Rename Aggregates
  netapp.ontap.na_ontap_aggregate:
    state: present
    service_state: online
    from_name: ansibleAggr
    name: ansibleAggr2
    disk_count: 20
    hostname: "{{ netapp_hostname }}"
    username: "{{ netapp_username }}"
    password: "{{ netapp_password }}"

- name: Delete Aggregates
  netapp.ontap.na_ontap_aggregate:
    state: absent
    service_state: offline
    unmount_volumes: true
    name: ansibleAggr
    hostname: "{{ netapp_hostname }}"
    username: "{{ netapp_username }}"
    password: "{{ netapp_password }}"
"""

RETURN = """

"""
import re
import time
import traceback

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule
from ansible_collections.netapp.ontap.plugins.module_utils.netapp import OntapRestAPI
from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic
import ansible_collections.netapp.ontap.plugins.module_utils.rest_response_helpers as rrh


class NetAppOntapAggregate:
    ''' object initialize and class methods '''

    def __init__(self):
        self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
        self.argument_spec.update(dict(
            name=dict(required=True, type='str'),
            disks=dict(required=False, type='list', elements='str'),
            disk_count=dict(required=False, type='int', default=None),
            disk_size=dict(required=False, type='int'),
            disk_size_with_unit=dict(required=False, type='str'),
            disk_class=dict(required=False,
                            choices=['capacity', 'performance', 'archive', 'solid_state', 'array', 'virtual', 'data_center', 'capacity_flash']),
            disk_type=dict(required=False,
                           choices=['ATA', 'BSAS', 'FCAL', 'FSAS', 'LUN', 'MSATA', 'SAS', 'SSD', 'SSD-CAP', 'SSD-NVM', 'VMDISK', 'VMLUN', 'VMLUN-SSD']),
            from_name=dict(required=False, type='str'),
            mirror_disks=dict(required=False, type='list', elements='str'),
            nodes=dict(required=False, type='list', elements='str'),
            is_mirrored=dict(required=False, type='bool'),
            raid_size=dict(required=False, type='int'),
            raid_type=dict(required=False, choices=['raid4', 'raid_dp', 'raid_tec', 'raid_0']),
            service_state=dict(required=False, choices=['online', 'offline']),
            spare_pool=dict(required=False, choices=['Pool0', 'Pool1']),
            state=dict(required=False, choices=['present', 'absent'], default='present'),
            unmount_volumes=dict(required=False, type='bool'),
            wait_for_online=dict(required=False, type='bool', default=False),
            time_out=dict(required=False, type='int', default=100),
            object_store_name=dict(required=False, type='str'),
            allow_flexgroups=dict(required=False, type='bool'),
            snaplock_type=dict(required=False, type='str', choices=['compliance', 'enterprise', 'non_snaplock']),
            ignore_pool_checks=dict(required=False, type='bool'),
            encryption=dict(required=False, type='bool'),
            tags=dict(required=False, type='list', elements='str')
        ))

        self.module = AnsibleModule(
            argument_spec=self.argument_spec,
            mutually_exclusive=[
                ('is_mirrored', 'disks'),
                ('is_mirrored', 'mirror_disks'),
                ('is_mirrored', 'spare_pool'),
                ('spare_pool', 'disks'),
                ('disk_count', 'disks'),
                ('disk_size', 'disk_size_with_unit'),
                ('disk_class', 'disk_type'),
            ],
            supports_check_mode=True
        )
        self.na_helper = NetAppModule()
        self.parameters = self.na_helper.set_parameters(self.module.params)
        self.rest_api = OntapRestAPI(self.module)
        self.uuid = None
        # some attributes are not supported in earlier REST implementation
        unsupported_rest_properties = ['disks', 'disk_type', 'mirror_disks', 'spare_pool', 'unmount_volumes']
        partially_supported_rest_properties = [['service_state', (9, 11, 1)], ['tags', (9, 13, 1)]]
        self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, unsupported_rest_properties, partially_supported_rest_properties)
        if not self.use_rest:
            if not netapp_utils.has_netapp_lib():
                self.module.fail_json(msg=netapp_utils.netapp_lib_is_required())
            if 'tags' in self.parameters:
                self.module.fail_json(msg="Error: tags only supported with REST.")
            self.server = netapp_utils.setup_na_ontap_zapi(module=self.module)

        if self.parameters['state'] == 'present':
            self.validate_options()

    def validate_options(self):
        errors = []
        if self.use_rest:
            if len(self.parameters.get('nodes', [])) > 1:
                errors.append('only one node can be specified when using rest, found %s' % self.parameters['nodes'])
            if 'disk_count' in self.parameters and 'nodes' not in self.parameters:
                errors.append('nodes is required when disk_count is present')
        else:
            if self.parameters.get('mirror_disks') is not None and self.parameters.get('disks') is None:
                errors.append('mirror_disks require disks options to be set')
        if errors:
            plural = 's' if len(errors) > 1 else ''
            self.module.fail_json(msg='Error%s when validating options: %s.' % (plural, '; '.join(errors)))

    def aggr_get_iter(self, name):
        """
        Return aggr-get-iter query results
        :param name: Name of the aggregate
        :return: NaElement if aggregate found, None otherwise
        """

        aggr_get_iter = netapp_utils.zapi.NaElement('aggr-get-iter')
        query_details = netapp_utils.zapi.NaElement.create_node_with_children(
            'aggr-attributes', **{'aggregate-name': name})
        query = netapp_utils.zapi.NaElement('query')
        query.add_child_elem(query_details)
        aggr_get_iter.add_child_elem(query)
        result = None
        try:
            result = self.server.invoke_successfully(aggr_get_iter, enable_tunneling=False)
        except netapp_utils.zapi.NaApiError as error:
            if to_native(error.code) != '13040':
                self.module.fail_json(msg='Error getting aggregate: %s' % to_native(error), exception=traceback.format_exc())
        return result

    def get_aggr(self, name=None):
        """
        Fetch details if aggregate exists.
        :param name: Name of the aggregate to be fetched
        :return:
            Dictionary of current details if aggregate found
            None if aggregate is not found
        """
        if name is None:
            name = self.parameters.get('name')
        if self.use_rest:
            return self.get_aggr_rest(name)
        aggr_get = self.aggr_get_iter(name)
        if aggr_get and aggr_get.get_child_by_name('num-records') and int(aggr_get.get_child_content('num-records')) >= 1:
            attr = aggr_get.get_child_by_name('attributes-list').get_child_by_name('aggr-attributes')
            current_aggr = {'service_state': attr.get_child_by_name('aggr-raid-attributes').get_child_content('state')}
            if attr.get_child_by_name('aggr-raid-attributes').get_child_content('disk-count'):
                current_aggr['disk_count'] = int(attr.get_child_by_name('aggr-raid-attributes').get_child_content('disk-count'))
            if attr.get_child_by_name('aggr-raid-attributes').get_child_content('encrypt-with-aggr-key'):
                current_aggr['encryption'] = attr.get_child_by_name('aggr-raid-attributes').get_child_content('encrypt-with-aggr-key') == 'true'
            snaplock_type = self.na_helper.safe_get(attr, ['aggr-snaplock-attributes', 'snaplock-type'])
            if snaplock_type:
                current_aggr['snaplock_type'] = snaplock_type
            return current_aggr
        return None

    def disk_get_iter(self, name):
        """
        Return storage-disk-get-iter query results
        Filter disk list by aggregate name, and only reports disk-name and plex-name
        :param name: Name of the aggregate
        :return: NaElement
        """

        disk_get_iter = netapp_utils.zapi.NaElement('storage-disk-get-iter')
        query_details = {
            'query': {
                'storage-disk-info': {
                    'disk-raid-info': {
                        'disk-aggregate-info': {
                            'aggregate-name': name
                        }
                    }
                }
            }
        }
        disk_get_iter.translate_struct(query_details)
        attributes = {
            'desired-attributes': {
                'storage-disk-info': {
                    'disk-name': None,
                    'disk-raid-info': {
                        'disk_aggregate_info': {
                            'plex-name': None
                        }
                    }
                }
            }
        }
        disk_get_iter.translate_struct(attributes)

        result = None
        try:
            result = self.server.invoke_successfully(disk_get_iter, enable_tunneling=False)
        except netapp_utils.zapi.NaApiError as error:
            self.module.fail_json(msg='Error getting disks: %s' % to_native(error), exception=traceback.format_exc())
        return result

    def get_aggr_disks(self, name):
        """
        Fetch disks that are used for this aggregate.
        :param name: Name of the aggregate to be fetched
        :return:
            list of tuples (disk-name, plex-name)
            empty list if aggregate is not found
        """
        disks = []
        aggr_get = self.disk_get_iter(name)
        if aggr_get and aggr_get.get_child_by_name('num-records') and int(aggr_get.get_child_content('num-records')) >= 1:
            attr = aggr_get.get_child_by_name('attributes-list')
            disks = [(disk_info.get_child_content('disk-name'),
                      disk_info.get_child_by_name('disk-raid-info').get_child_by_name('disk-aggregate-info').get_child_content('plex-name'))
                     for disk_info in attr.get_children()]
        return disks

    def object_store_get_iter(self, name):
        """
        Return aggr-object-store-get query results
        :return: NaElement if object-store for given aggregate found, None otherwise
        """

        object_store_get_iter = netapp_utils.zapi.NaElement('aggr-object-store-get-iter')
        query_details = netapp_utils.zapi.NaElement.create_node_with_children(
            'object-store-information', **{'object-store-name': self.parameters.get('object_store_name'),
                                           'aggregate': name})
        query = netapp_utils.zapi.NaElement('query')
        query.add_child_elem(query_details)
        object_store_get_iter.add_child_elem(query)
        result = None
        try:
            result = self.server.invoke_successfully(object_store_get_iter, enable_tunneling=False)
        except netapp_utils.zapi.NaApiError as error:
            self.module.fail_json(msg='Error getting object store: %s' % to_native(error), exception=traceback.format_exc())
        return result

    def get_object_store(self, name):
        """
        Fetch details if object store attached to the given aggregate exists.
        :return:
            Dictionary of current details if object store attached to the given aggregate is found
            None if object store is not found
        """
        if self.use_rest:
            return self.get_object_store_rest()
        object_store_get = self.object_store_get_iter(name)
        if object_store_get and object_store_get.get_child_by_name('num-records') and int(object_store_get.get_child_content('num-records')) >= 1:
            attr = object_store_get.get_child_by_name('attributes-list').get_child_by_name('object-store-information')
            return {'object_store_name': attr.get_child_content('object-store-name')}
        return None

    def aggregate_online(self):
        """
        Set state of an offline aggregate to online
        :return: None
        """
        if self.use_rest:
            return self.patch_aggr_rest('make service state online for', {'state': 'online'})
        online_aggr = netapp_utils.zapi.NaElement.create_node_with_children(
            'aggr-online', **{'aggregate': self.parameters['name'],
                              'force-online': 'true'})
        try:
            self.server.invoke_successfully(online_aggr,
                                            enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as error:
            self.module.fail_json(msg='Error changing the state of aggregate %s to %s: %s' %
                                  (self.parameters['name'], self.parameters['service_state'], to_native(error)),
                                  exception=traceback.format_exc())

    def aggregate_offline(self):
        """
        Set state of an online aggregate to offline
        :return: None
        """
        if self.use_rest:
            return self.patch_aggr_rest('make service state offline for', {'state': 'offline'})
        offline_aggr = netapp_utils.zapi.NaElement.create_node_with_children(
            'aggr-offline', **{'aggregate': self.parameters['name'],
                               'force-offline': 'false',
                               'unmount-volumes': str(self.parameters.get('unmount_volumes', False))})

        # if disk add operation is in progress, cannot offline aggregate, retry few times.
        retry = 10
        while retry > 0:
            try:
                self.server.invoke_successfully(offline_aggr, enable_tunneling=True)
                break
            except netapp_utils.zapi.NaApiError as error:
                if 'disk add operation is in progress' in to_native(error):
                    retry -= 1
                    if retry > 0:
                        continue
                self.module.fail_json(msg='Error changing the state of aggregate %s to %s: %s' %
                                      (self.parameters['name'], self.parameters['service_state'], to_native(error)),
                                      exception=traceback.format_exc())

    @staticmethod
    def get_disks_or_mirror_disks_object(name, disks):
        '''
        create ZAPI object for disks or mirror_disks
        '''
        disks_obj = netapp_utils.zapi.NaElement(name)
        for disk in disks:
            disk_info_obj = netapp_utils.zapi.NaElement('disk-info')
            disk_info_obj.add_new_child('name', disk)
            disks_obj.add_child_elem(disk_info_obj)
        return disks_obj

    def create_aggr(self):
        """
        Create aggregate
        :return: None
        """
        if self.use_rest:
            return self.create_aggr_rest()
        options = {'aggregate': self.parameters['name']}
        if self.parameters.get('disk_class'):
            options['disk-class'] = self.parameters['disk_class']
        if self.parameters.get('disk_type'):
            options['disk-type'] = self.parameters['disk_type']
        if self.parameters.get('raid_type'):
            options['raid-type'] = self.parameters['raid_type']
        if self.parameters.get('snaplock_type'):
            options['snaplock-type'] = self.parameters['snaplock_type']
        if self.parameters.get('spare_pool'):
            options['spare-pool'] = self.parameters['spare_pool']
        # int to str
        if self.parameters.get('disk_count'):
            options['disk-count'] = str(self.parameters['disk_count'])
        if self.parameters.get('disk_size'):
            options['disk-size'] = str(self.parameters['disk_size'])
        if self.parameters.get('disk_size_with_unit'):
            options['disk-size-with-unit'] = str(self.parameters['disk_size_with_unit'])
        if self.parameters.get('raid_size'):
            options['raid-size'] = str(self.parameters['raid_size'])
        # boolean to str
        if self.parameters.get('is_mirrored'):
            options['is-mirrored'] = str(self.parameters['is_mirrored']).lower()
        if self.parameters.get('ignore_pool_checks'):
            options['ignore-pool-checks'] = str(self.parameters['ignore_pool_checks']).lower()
        if self.parameters.get('encryption'):
            options['encrypt-with-aggr-key'] = str(self.parameters['encryption']).lower()
        aggr_create = netapp_utils.zapi.NaElement.create_node_with_children('aggr-create', **options)
        if self.parameters.get('nodes'):
            nodes_obj = netapp_utils.zapi.NaElement('nodes')
            aggr_create.add_child_elem(nodes_obj)
            for node in self.parameters['nodes']:
                nodes_obj.add_new_child('node-name', node)
        if self.parameters.get('disks'):
            aggr_create.add_child_elem(self.get_disks_or_mirror_disks_object('disks', self.parameters.get('disks')))
        if self.parameters.get('mirror_disks'):
            aggr_create.add_child_elem(self.get_disks_or_mirror_disks_object('mirror-disks', self.parameters.get('mirror_disks')))

        try:
            self.server.invoke_successfully(aggr_create, enable_tunneling=False)
            if self.parameters.get('wait_for_online'):
                # round off time_out
                retries = (self.parameters['time_out'] + 5) / 10
                current = self.get_aggr()
                status = None if current is None else current['service_state']
                while status != 'online' and retries > 0:
                    time.sleep(10)
                    retries = retries - 1
                    current = self.get_aggr()
                    status = None if current is None else current['service_state']
            else:
                current = self.get_aggr()
            if current is not None and current.get('disk_count') != self.parameters.get('disk_count'):
                self.module.warn("Aggregate created with mismatched disk_count: created %s not %s"
                                 % (current.get('disk_count'), self.parameters.get('disk_count')))
        except netapp_utils.zapi.NaApiError as error:
            self.module.fail_json(msg="Error provisioning aggregate %s: %s"
                                  % (self.parameters['name'], to_native(error)),
                                  exception=traceback.format_exc())

    def delete_aggr(self):
        """
        Delete aggregate.
        :return: None
        """
        if self.use_rest:
            return self.delete_aggr_rest()
        aggr_destroy = netapp_utils.zapi.NaElement.create_node_with_children(
            'aggr-destroy', **{'aggregate': self.parameters['name']})

        try:
            self.server.invoke_successfully(aggr_destroy,
                                            enable_tunneling=False)
        except netapp_utils.zapi.NaApiError as error:
            self.module.fail_json(msg="Error removing aggregate %s: %s" % (self.parameters['name'], to_native(error)),
                                  exception=traceback.format_exc())

    def rename_aggregate(self):
        """
        Rename aggregate.
        """
        if self.use_rest:
            return self.rename_aggr_rest()
        aggr_rename = netapp_utils.zapi.NaElement.create_node_with_children(
            'aggr-rename', **{'aggregate': self.parameters['from_name'],
                              'new-aggregate-name': self.parameters['name']})

        try:
            self.server.invoke_successfully(aggr_rename, enable_tunneling=False)
        except netapp_utils.zapi.NaApiError as error:
            self.module.fail_json(msg="Error renaming aggregate %s: %s"
                                  % (self.parameters['from_name'], to_native(error)),
                                  exception=traceback.format_exc())

    def modify_aggr(self, modify):
        """
        Modify state of the aggregate
        :param modify: dictionary of parameters to be modified
        :return: None
        """
        # online aggregate first, so disk can be added after online.
        if modify.get('service_state') == 'online':
            self.aggregate_online()
        # modify tags
        if modify.get('tags') is not None:
            self.patch_aggr_rest('modify tags for', {'_tags': modify['tags']})
        # add disk before taking aggregate offline.
        disk_size = self.parameters.get('disk_size', 0)
        disk_size_with_unit = self.parameters.get('disk_size_with_unit')
        if modify.get('disk_count'):
            self.add_disks(modify['disk_count'], disk_size=disk_size, disk_size_with_unit=disk_size_with_unit)
        if modify.get('disks_to_add') or modify.get('mirror_disks_to_add'):
            self.add_disks(0, modify.get('disks_to_add'), modify.get('mirror_disks_to_add'))
        # offline aggregate after adding additional disks.
        if modify.get('service_state') == 'offline':
            self.aggregate_offline()

    def attach_object_store_to_aggr(self):
        """
        Attach object store to aggregate.
        :return: None
        """
        if self.use_rest:
            return self.attach_object_store_to_aggr_rest()
        store_obj = {'aggregate': self.parameters['name'], 'object-store-name': self.parameters['object_store_name']}
        if 'allow_flexgroups' in self.parameters:
            store_obj['allow-flexgroup'] = self.na_helper.get_value_for_bool(False, self.parameters['allow_flexgroups'])
        attach_object_store = netapp_utils.zapi.NaElement.create_node_with_children('aggr-object-store-attach', **store_obj)

        try:
            self.server.invoke_successfully(attach_object_store,
                                            enable_tunneling=False)
        except netapp_utils.zapi.NaApiError as error:
            self.module.fail_json(msg="Error attaching object store %s to aggregate %s: %s" %
                                  (self.parameters['object_store_name'], self.parameters['name'], to_native(error)),
                                  exception=traceback.format_exc())

    def add_disks(self, count=0, disks=None, mirror_disks=None, disk_size=0, disk_size_with_unit=None):
        """
        Add additional disks to aggregate.
        :return: None
        """
        if self.use_rest:
            return self.add_disks_rest(count, disks, mirror_disks, disk_size, disk_size_with_unit)
        options = {'aggregate': self.parameters['name']}
        if count:
            options['disk-count'] = str(count)
        if disks and self.parameters.get('ignore_pool_checks'):
            options['ignore-pool-checks'] = str(self.parameters['ignore_pool_checks'])
        if disk_size:
            options['disk-size'] = str(disk_size)
        if disk_size_with_unit:
            options['disk-size-with-unit'] = disk_size_with_unit
        if self.parameters.get('disk_class'):
            options['disk-class'] = self.parameters['disk_class']
        if self.parameters.get('disk_type'):
            options['disk-type'] = self.parameters['disk_type']
        aggr_add = netapp_utils.zapi.NaElement.create_node_with_children(
            'aggr-add', **options)
        if disks:
            aggr_add.add_child_elem(self.get_disks_or_mirror_disks_object('disks', disks))
        if mirror_disks:
            aggr_add.add_child_elem(self.get_disks_or_mirror_disks_object('mirror-disks', mirror_disks))

        try:
            self.server.invoke_successfully(aggr_add,
                                            enable_tunneling=True)
        except netapp_utils.zapi.NaApiError as error:
            self.module.fail_json(msg='Error adding additional disks to aggregate %s: %s' %
                                  (self.parameters['name'], to_native(error)),
                                  exception=traceback.format_exc())

    def map_plex_to_primary_and_mirror(self, plex_disks, disks, mirror_disks):
        '''
        we have N plexes, and disks, and maybe mirror_disks
        we're trying to find which plex is used for disks, and which one, if applicable, for mirror_disks
        :return: a tuple with the names of the two plexes (disks_plex, mirror_disks_plex)
        the second one can be None
        '''
        disks_plex = None
        mirror_disks_plex = None
        error = ''
        for plex in plex_disks:
            common = set(plex_disks[plex]).intersection(set(disks))
            if common:
                if disks_plex is None:
                    disks_plex = plex
                else:
                    error = 'found overlapping plexes: %s and %s' % (disks_plex, plex)
            if mirror_disks is not None:
                common = set(plex_disks[plex]).intersection(set(mirror_disks))
                if common:
                    if mirror_disks_plex is None:
                        mirror_disks_plex = plex
                    else:
                        error = 'found overlapping mirror plexes: %s and %s' % (mirror_disks_plex, plex)
        if not error:
            # make sure we found a match
            if disks_plex is None:
                error = 'cannot match disks with current aggregate disks'
            if mirror_disks is not None and mirror_disks_plex is None:
                if error:
                    error += ', and '
                error += 'cannot match mirror_disks with current aggregate disks'
        if error:
            self.module.fail_json(msg="Error mapping disks for aggregate %s: %s.  Found: %s" %
                                  (self.parameters['name'], error, str(plex_disks)))
        return disks_plex, mirror_disks_plex

    def get_disks_to_add(self, aggr_name, disks, mirror_disks):
        '''
        Get list of disks used by the aggregate, as primary and mirror.
        Report error if:
          the plexes in use cannot be matched with user inputs (we expect some overlap)
          the user request requires some disks to be removed (not supported)
        : return: a tuple of two lists of disks: disks_to_add, mirror_disks_to_add
        '''
        # let's see if we need to add disks
        disks_in_use = self.get_aggr_disks(aggr_name)
        # we expect a list of tuples (disk_name, plex_name), if there is a mirror, we should have 2 plexes
        # let's get a list of disks for each plex
        plex_disks = {}
        for disk_name, plex_name in disks_in_use:
            plex_disks.setdefault(plex_name, []).append(disk_name)
        # find who is who
        disks_plex, mirror_disks_plex = self.map_plex_to_primary_and_mirror(plex_disks, disks, mirror_disks)
        # Now that we know what is which, find what needs to be removed (error), and what needs to be added
        disks_to_remove = [disk for disk in plex_disks[disks_plex] if disk not in disks]
        if mirror_disks_plex:
            disks_to_remove.extend([disk for disk in plex_disks[mirror_disks_plex] if disk not in mirror_disks])
        if disks_to_remove:
            error = 'these disks cannot be removed: %s' % str(disks_to_remove)
            self.module.fail_json(msg="Error removing disks is not supported.  Aggregate %s: %s.  In use: %s" %
                                  (aggr_name, error, str(plex_disks)))
        # finally, what's to be added
        disks_to_add = [disk for disk in disks if disk not in plex_disks[disks_plex]]
        mirror_disks_to_add = []
        if mirror_disks_plex:
            mirror_disks_to_add = [disk for disk in mirror_disks if disk not in plex_disks[mirror_disks_plex]]
        if mirror_disks_to_add and not disks_to_add:
            self.module.fail_json(msg="Error cannot add mirror disks %s without adding disks for aggregate %s.  In use: %s" %
                                  (str(mirror_disks_to_add), aggr_name, str(plex_disks)))
        if disks_to_add or mirror_disks_to_add:
            self.na_helper.changed = True

        return disks_to_add, mirror_disks_to_add

    def set_disk_count(self, current, modify):
        if modify.get('disk_count'):
            if int(modify['disk_count']) < int(current['disk_count']):
                self.module.fail_json(msg="Error: specified disk_count is less than current disk_count. Only adding disks is allowed.")
            else:
                modify['disk_count'] = modify['disk_count'] - current['disk_count']

    def get_aggr_actions(self):
        aggr_name = self.parameters.get('name')
        rename, cd_action, modify = None, None, {}
        current = self.get_aggr()
        cd_action = self.na_helper.get_cd_action(current, self.parameters)
        if cd_action == 'create' and self.parameters.get('from_name'):
            # create by renaming existing aggregate
            old_aggregate = self.get_aggr(self.parameters['from_name'])
            rename = self.na_helper.is_rename_action(old_aggregate, current)
            if rename is None:
                self.module.fail_json(msg='Error renaming aggregate %s: no aggregate with from_name %s.'
                                      % (self.parameters['name'], self.parameters['from_name']))
            if rename:
                current = old_aggregate
                aggr_name = self.parameters['from_name']
                cd_action = None
        if cd_action is None and self.parameters['state'] == 'present':
            modify = self.na_helper.get_modified_attributes(current, self.parameters)
            if 'encryption' in modify and not self.use_rest:
                self.module.fail_json(msg='Error: modifying encryption is not supported with ZAPI.')
            if 'snaplock_type' in modify:
                self.module.fail_json(msg='Error: snaplock_type is not modifiable.  Cannot change to: %s.' % modify['snaplock_type'])
            if self.parameters.get('disks'):
                modify['disks_to_add'], modify['mirror_disks_to_add'] = \
                    self.get_disks_to_add(aggr_name, self.parameters['disks'], self.parameters.get('mirror_disks'))
            self.set_disk_count(current, modify)

        return current, cd_action, rename, modify

    def get_object_store_action(self, current, rename):
        object_store_cd_action = None
        if self.parameters.get('object_store_name'):
            aggr_name = self.parameters['from_name'] if rename else self.parameters['name']
            object_store_current = self.get_object_store(aggr_name) if current else None
            object_store_cd_action = self.na_helper.get_cd_action(object_store_current, self.parameters.get('object_store_name'))
            if object_store_cd_action is None and object_store_current is not None\
                    and object_store_current['object_store_name'] != self.parameters.get('object_store_name'):
                self.module.fail_json(msg='Error: object store %s is already associated with aggregate %s.' %
                                      (object_store_current['object_store_name'], aggr_name))
        return object_store_cd_action

    def get_aggr_rest(self, name):
        if not name:
            return None
        api = 'storage/aggregates'
        query = {'name': name}
        fields = 'uuid,state,block_storage.primary.disk_count,data_encryption,snaplock_type'
        if 'tags' in self.parameters:
            fields += ',_tags'
        record, error = rest_generic.get_one_record(self.rest_api, api, query, fields)
        if error:
            self.module.fail_json(msg='Error: failed to get aggregate %s: %s' % (name, error))
        if record:
            return {
                'tags': record.get('_tags', []),
                'disk_count': self.na_helper.safe_get(record, ['block_storage', 'primary', 'disk_count']),
                'encryption': self.na_helper.safe_get(record, ['data_encryption', 'software_encryption_enabled']),
                'service_state': record['state'],
                'snaplock_type': record['snaplock_type'],
                'uuid': record['uuid'],
            }
        return None

    def get_multiplier(self, unit):
        if not unit:
            return 1
        try:
            return netapp_utils.POW2_BYTE_MAP[unit[0].lower()]
        except KeyError:
            self.module.fail_json(msg='Error: unexpected unit in disk_size_with_unit: %s' % self.parameters['disk_size_with_unit'])

    def get_disk_size(self):
        if 'disk_size' in self.parameters:
            return self.parameters['disk_size'] * 4 * 1024
        if 'disk_size_with_unit' in self.parameters:
            match = re.match(r'([\d.]+)(.*)', self.parameters['disk_size_with_unit'])
            if match:
                size, unit = match.groups()
                mul = self.get_multiplier(unit)
                return int(float(size) * mul)
            self.module.fail_json(msg='Error: unexpected value in disk_size_with_unit: %s' % self.parameters['disk_size_with_unit'])
        return None

    def create_aggr_rest(self):
        api = 'storage/aggregates'

        disk_size = self.get_disk_size()
        # Interestingly, REST expects True/False in body, but 'true'/'false' in query
        # I guess it's because we're using json in the body
        query = {'return_records': 'true'}    # in order to capture UUID
        if disk_size:
            query['disk_size'] = disk_size
        # query = {'disk_size': disk_size} if disk_size else None

        body = {'name': self.parameters['name']} if 'name' in self.parameters else {}
        block_storage = {}
        primary = {}
        if self.parameters.get('nodes'):
            body['node.name'] = self.parameters['nodes'][0]
        if self.parameters.get('disk_class'):
            primary['disk_class'] = self.parameters['disk_class']
        if self.parameters.get('disk_count'):
            primary['disk_count'] = self.parameters['disk_count']
        if self.parameters.get('raid_size'):
            primary['raid_size'] = self.parameters['raid_size']
        if self.parameters.get('raid_type'):
            primary['raid_type'] = self.parameters['raid_type']
        if primary:
            block_storage['primary'] = primary
        mirror = {}
        if self.parameters.get('is_mirrored'):
            mirror['enabled'] = self.parameters['is_mirrored']
        if mirror:
            block_storage['mirror'] = mirror
        if block_storage:
            body['block_storage'] = block_storage
        if self.parameters.get('encryption'):
            body['data_encryption'] = {'software_encryption_enabled': True}
        if self.parameters.get('snaplock_type'):
            body['snaplock_type'] = self.parameters['snaplock_type']
        if self.parameters.get('tags') is not None:
            body['_tags'] = self.parameters['tags']
        response, error = rest_generic.post_async(self.rest_api, api, body or None, query, job_timeout=self.parameters['time_out'])
        if error:
            self.module.fail_json(msg='Error: failed to create aggregate: %s' % error)
        if response:
            record, error = rrh.check_for_0_or_1_records(api, response, error, query)
            if not error and record and 'uuid' not in record:
                error = 'uuid key not present in %s:' % record
            if error:
                self.module.fail_json(msg='Error: failed to parse create aggregate response: %s' % error)
            if record:
                self.uuid = record['uuid']

    def delete_aggr_rest(self):
        api = 'storage/aggregates'
        dummy, error = rest_generic.delete_async(self.rest_api, api, self.uuid)
        if error:
            self.module.fail_json(msg='Error: failed to delete aggregate: %s' % error)

    def patch_aggr_rest(self, action, body, query=None):
        api = 'storage/aggregates'
        dummy, error = rest_generic.patch_async(self.rest_api, api, self.uuid, body, query)
        if error:
            self.module.fail_json(msg='Error: failed to %s aggregate: %s' % (action, error))

    def add_disks_rest(self, count=0, disks=None, mirror_disks=None, disk_size=0, disk_size_with_unit=None):
        """
        Add additional disks to aggregate.
        :return: None
        """
        if disks or mirror_disks:
            self.module.fail_json(msg='Error: disks or mirror disks are mot supported with rest: %s, %s.' % (disks, mirror_disks))
        if self.parameters.get('disk_class'):
            self.module.warn('disk_class is ignored when adding disks to an exiting aggregate')
        primary = {'disk_count': self.parameters['disk_count']} if count else None
        body = {'block_storage': {'primary': primary}} if primary else None
        if body:
            disk_size = self.get_disk_size()
            query = {'disk_size': disk_size} if disk_size else None
            self.patch_aggr_rest('increase disk count for', body, query)

    def rename_aggr_rest(self):
        body = {'name': self.parameters['name']}
        self.patch_aggr_rest('rename', body)

    def get_object_store_rest(self):
        '''TODO: support mirror in addition to primary'''
        api = 'storage/aggregates/%s/cloud-stores' % self.uuid
        record, error = rest_generic.get_one_record(self.rest_api, api, query={'primary': True})
        if error:
            self.module.fail_json(msg='Error: failed to get cloud stores for aggregate: %s' % error)
        return record

    def get_cloud_target_uuid_rest(self):
        api = 'cloud/targets'
        query = {'name': self.parameters['object_store_name']}
        record, error = rest_generic.get_one_record(self.rest_api, api, query)
        if error or not record:
            self.module.fail_json(msg='Error: failed to find cloud store with name %s: %s' % (self.parameters['object_store_name'], error))
        return record['uuid']

    def attach_object_store_to_aggr_rest(self):
        '''TODO: support mirror in addition to primary'''

        if self.uuid is None:
            error = 'aggregate UUID is not set.'
            self.module.fail_json(msg='Error: cannot attach cloud store with name %s: %s' % (self.parameters['object_store_name'], error))
        body = {'target': {'uuid': self.get_cloud_target_uuid_rest()}}
        api = 'storage/aggregates/%s/cloud-stores' % self.uuid
        query = None
        if 'allow_flexgroups' in self.parameters:
            query = {'allow_flexgroups': 'true' if self.parameters['allow_flexgroups'] else 'false'}
        record, error = rest_generic.post_async(self.rest_api, api, body, query)
        if error:
            self.module.fail_json(msg='Error: failed to attach cloud store with name %s: %s' % (self.parameters['object_store_name'], error))
        return record

    def validate_expensive_options(self, cd_action, modify):
        if cd_action == 'create' or (modify and 'disk_count' in modify):
            # report an error if disk_size_with_unit is not valid
            self.get_disk_size()

    def apply(self):
        """
        Apply action to the aggregate
        :return: None
        """
        current, cd_action, rename, modify = self.get_aggr_actions()
        if current:
            self.uuid = current.get('uuid')
        object_store_cd_action = self.get_object_store_action(current, rename)

        if self.na_helper.changed and self.module.check_mode:
            # additional validations that are done at runtime
            self.validate_expensive_options(cd_action, modify)

        if self.na_helper.changed and not self.module.check_mode:
            if cd_action == 'create':
                self.create_aggr()
                # offine aggregate after create.
                if self.parameters.get('service_state') == 'offline':
                    self.modify_aggr({'service_state': 'offline'})
            elif cd_action == 'delete':
                self.delete_aggr()
            else:
                if rename:
                    self.rename_aggregate()
                if modify:
                    self.modify_aggr(modify)
            if object_store_cd_action == 'create':
                self.attach_object_store_to_aggr()
        if rename:
            modify['name'] = self.parameters['name']
        result = netapp_utils.generate_result(self.na_helper.changed, cd_action, modify)
        self.module.exit_json(**result)


def main():
    """
    Create Aggregate class instance and invoke apply
    :return: None
    """
    obj_aggr = NetAppOntapAggregate()
    obj_aggr.apply()


if __name__ == '__main__':
    main()

Anon7 - 2022
AnonSec Team